From 585414a312b293e99c4b76da63493b936579993b Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 23 Jan 2024 15:09:20 -0500 Subject: [PATCH 001/686] Merge master into devel (#3167) From bc00a0a3b0949f388ed4589d805423372db38e73 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 09:38:15 +0800 Subject: [PATCH 002/686] [pre-commit.ci] pre-commit autoupdate (#3163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.13 → v0.1.14](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.13...v0.1.14) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d4e89f1129..0fd2d1b40f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^source/3rdparty - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.1.13 + rev: v0.1.14 hooks: - id: ruff args: ["--fix"] From dd53e0716655c499b61bd1a3328c1d4b8f800b12 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 23 Jan 2024 22:22:18 -0500 Subject: [PATCH 003/686] setup PyTorch C++ interface build environement (#3169) See #3120. - CMake: add `ENABLE_TENSORFLOW` and `ENABLE_PYTORCH`. `BUILD_TENSORFLOW` will be enabled when `TENSORFLOW_ROOT` is not empty or `USE_TF_PYTHON_LIBS` is on. - api_cc: add `BUILD_TENSORFLOW` and `BUILD_PYTORCH` defination. Move several functions from `common.h` to `commonTF.h` to prevent exposing them to header files. - CI: download libtorch in the build/test CC actions. --------- Signed-off-by: Jinzhe Zeng --- .github/workflows/build_cc.yml | 12 +- .github/workflows/codeql.yml | 6 +- .github/workflows/test_cc.yml | 8 +- doc/conf.py | 5 +- source/CMakeLists.txt | 30 ++++- source/api_cc/CMakeLists.txt | 23 +++- source/api_cc/include/DataModifierTF.h | 1 + source/api_cc/include/DeepPotTF.h | 1 + source/api_cc/include/DeepTensorTF.h | 1 + source/api_cc/include/common.h | 141 ------------------------ source/api_cc/include/commonTF.h | 147 +++++++++++++++++++++++++ source/api_cc/include/version.h.in | 1 + source/api_cc/src/DataModifier.cc | 7 +- source/api_cc/src/DataModifierTF.cc | 2 + source/api_cc/src/DeepPot.cc | 7 +- source/api_cc/src/DeepPotPT.cc | 8 ++ source/api_cc/src/DeepPotTF.cc | 2 + source/api_cc/src/DeepTensor.cc | 7 +- source/api_cc/src/DeepTensorTF.cc | 2 + source/api_cc/src/common.cc | 33 ++++++ source/install/build_cc.sh | 8 +- 21 files changed, 298 insertions(+), 154 deletions(-) create mode 100644 source/api_cc/include/commonTF.h create mode 100644 source/api_cc/src/DeepPotPT.cc diff --git a/.github/workflows/build_cc.yml b/.github/workflows/build_cc.yml index f029517d80..991be798aa 100644 --- a/.github/workflows/build_cc.yml +++ b/.github/workflows/build_cc.yml @@ -27,6 +27,10 @@ jobs: cache: 'pip' - uses: lukka/get-cmake@latest - run: python -m pip install tensorflow + - name: Download libtorch + run: | + wget https://download.pytorch.org/libtorch/cpu/libtorch-cxx11-abi-shared-with-deps-2.1.2%2Bcpu.zip -O libtorch.zip + unzip libtorch.zip - run: | wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.0-1_all.deb \ && sudo dpkg -i cuda-keyring_1.0-1_all.deb \ @@ -48,13 +52,17 @@ jobs: && sudo apt-get update \ && sudo apt-get install -y rocm-dev hipcub-dev if: matrix.variant == 'rocm' - - run: source/install/build_cc.sh + - run: | + export CMAKE_PREFIX_PATH=$GITHUB_WORKSPACE/libtorch + source/install/build_cc.sh env: DP_VARIANT: ${{ matrix.dp_variant }} DOWNLOAD_TENSORFLOW: "FALSE" CMAKE_GENERATOR: Ninja if: matrix.variant != 'clang' - - run: source/install/build_cc.sh + - run: | + export CMAKE_PREFIX_PATH=$GITHUB_WORKSPACE/libtorch + source/install/build_cc.sh env: DP_VARIANT: cpu DOWNLOAD_TENSORFLOW: "FALSE" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a9a162432c..c5460109f4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,6 +37,8 @@ jobs: && sudo apt-get update \ && sudo apt-get -y install cuda-cudart-dev-12-2 cuda-nvcc-12-2 python -m pip install tensorflow + wget https://download.pytorch.org/libtorch/cpu/libtorch-cxx11-abi-shared-with-deps-2.1.2%2Bcpu.zip -O libtorch.zip + unzip libtorch.zip env: DEBIAN_FRONTEND: noninteractive # Initializes the CodeQL tools for scanning. @@ -46,7 +48,9 @@ jobs: languages: ${{ matrix.language }} queries: security-extended,security-and-quality - name: "Run, Build Application using script" - run: source/install/build_cc.sh + run: | + export CMAKE_PREFIX_PATH=$GITHUB_WORKSPACE/libtorch + source/install/build_cc.sh env: DP_VARIANT: cuda DOWNLOAD_TENSORFLOW: "FALSE" diff --git a/.github/workflows/test_cc.yml b/.github/workflows/test_cc.yml index ef6fade8e5..1ded666070 100644 --- a/.github/workflows/test_cc.yml +++ b/.github/workflows/test_cc.yml @@ -18,7 +18,13 @@ jobs: mpi: mpich - uses: lukka/get-cmake@latest - run: python -m pip install tensorflow - - run: source/install/test_cc_local.sh + - name: Download libtorch + run: | + wget https://download.pytorch.org/libtorch/cpu/libtorch-cxx11-abi-shared-with-deps-2.1.2%2Bcpu.zip -O libtorch.zip + unzip libtorch.zip + - run: | + export CMAKE_PREFIX_PATH=$GITHUB_WORKSPACE/libtorch + source/install/test_cc_local.sh env: OMP_NUM_THREADS: 1 TF_INTRA_OP_PARALLELISM_THREADS: 1 diff --git a/doc/conf.py b/doc/conf.py index 63af974a86..261c105d9b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -213,7 +213,10 @@ def setup(app): exhale_projects_args = { "cc": { "containmentFolder": "./API_CC", - "exhaleDoxygenStdin": "INPUT = ../source/api_cc/include/", + "exhaleDoxygenStdin": """INPUT = ../source/api_cc/include/ + PREDEFINED += BUILD_TENSORFLOW + BUILD_PYTORCH + """, "rootFileTitle": "C++ API", "rootFileName": "api_cc.rst", }, diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index c1c9b8e7fe..c273bc9263 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.16) project(DeePMD) +option(ENABLE_TENSORFLOW "Enable TensorFlow interface" OFF) +option(ENABLE_PYTORCH "Enable PyTorch interface" OFF) option(BUILD_TESTING "Build test and enable converage" OFF) set(DEEPMD_C_ROOT "" @@ -131,6 +133,7 @@ if(INSTALL_TENSORFLOW) set(USE_TF_PYTHON_LIBS TRUE) endif(INSTALL_TENSORFLOW) if(USE_TF_PYTHON_LIBS) + set(ENABLE_TENSORFLOW TRUE) if(NOT "$ENV{CIBUILDWHEEL}" STREQUAL "1") find_package( Python @@ -141,11 +144,31 @@ if(USE_TF_PYTHON_LIBS) set(PYTHON_INCLUDE_DIRS ${PYTHON_INCLUDE_DIR}) endif() endif(USE_TF_PYTHON_LIBS) +if(TENSORFLOW_ROOT) + set(ENABLE_TENSORFLOW TRUE) +endif() # find tensorflow, I need tf abi info -if(NOT DEEPMD_C_ROOT) +if(ENABLE_TENSORFLOW AND NOT DEEPMD_C_ROOT) find_package(tensorflow REQUIRED) endif() +if(ENABLE_PYTORCH AND NOT DEEPMD_C_ROOT) + find_package(Torch REQUIRED) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}") +endif() +# log enabled backends +if(NOT DEEPMD_C_ROOT) + message(STATUS "Enabled backends:") + if(ENABLE_TENSORFLOW) + message(STATUS "- TensorFlow") + endif() + if(ENABLE_PYTORCH) + message(STATUS "- PyTorch") + endif() + if(NOT ENABLE_TENSORFLOW AND NOT ENABLE_PYTORCH) + message(FATAL_ERROR "No backend is enabled.") + endif() +endif() # find threads find_package(Threads) @@ -233,10 +256,13 @@ if(DEEPMD_C_ROOT) # use variable for TF path to set deepmd_c path set(TensorFlow_LIBRARY_PATH "${DEEPMD_C_ROOT}/lib") set(TENSORFLOW_INCLUDE_DIRS "${DEEPMD_C_ROOT}/include") + set(TORCH_LIBRARIES "${DEEPMD_C_ROOT}/lib/libtorch.so") endif() if(NOT DEEPMD_C_ROOT) - add_subdirectory(op/) + if(ENABLE_TENSORFLOW) + add_subdirectory(op/) + endif() add_subdirectory(lib/) endif() if(BUILD_PY_IF) diff --git a/source/api_cc/CMakeLists.txt b/source/api_cc/CMakeLists.txt index 2f296e3dfd..cd42594f1e 100644 --- a/source/api_cc/CMakeLists.txt +++ b/source/api_cc/CMakeLists.txt @@ -11,8 +11,16 @@ add_library(${libname} SHARED ${LIB_SRC}) # link: libdeepmd libdeepmd_op libtensorflow_cc libtensorflow_framework target_link_libraries(${libname} PUBLIC ${LIB_DEEPMD}) -target_link_libraries(${libname} PRIVATE TensorFlow::tensorflow_cc - TensorFlow::tensorflow_framework) +if(ENABLE_TENSORFLOW) + target_link_libraries(${libname} PRIVATE TensorFlow::tensorflow_cc + TensorFlow::tensorflow_framework) + target_compile_definitions(${libname} PRIVATE BUILD_TENSORFLOW) +endif() +if(ENABLE_PYTORCH) + target_link_libraries(${libname} PRIVATE "${TORCH_LIBRARIES}") + target_compile_definitions(${libname} PRIVATE BUILD_PYTORCH) +endif() + target_include_directories( ${libname} PUBLIC $ @@ -55,3 +63,14 @@ ${CMAKE_INSTALL_PREFIX}/lib/${CMAKE_SHARED_LIBRARY_PREFIX}${libname}${LOW_PREC_V add_subdirectory(tests) endif() endif(BUILD_PY_IF) + +if(BUILD_TESTING) + # A compilation test to make sure api_cc can compile without any backend + add_library(deepmd_cc_test_no_backend SHARED ${LIB_SRC}) + target_link_libraries(deepmd_cc_test_no_backend PUBLIC ${LIB_DEEPMD}) + target_include_directories( + deepmd_cc_test_no_backend + PUBLIC $ + $ + $) +endif() diff --git a/source/api_cc/include/DataModifierTF.h b/source/api_cc/include/DataModifierTF.h index 2ca3729525..c0021c6947 100644 --- a/source/api_cc/include/DataModifierTF.h +++ b/source/api_cc/include/DataModifierTF.h @@ -3,6 +3,7 @@ #include "DataModifier.h" #include "common.h" +#include "commonTF.h" namespace deepmd { /** diff --git a/source/api_cc/include/DeepPotTF.h b/source/api_cc/include/DeepPotTF.h index 0580c61da5..699b0ff7fe 100644 --- a/source/api_cc/include/DeepPotTF.h +++ b/source/api_cc/include/DeepPotTF.h @@ -3,6 +3,7 @@ #include "DeepPot.h" #include "common.h" +#include "commonTF.h" #include "neighbor_list.h" namespace deepmd { diff --git a/source/api_cc/include/DeepTensorTF.h b/source/api_cc/include/DeepTensorTF.h index 3c724dce88..3ca316a29f 100644 --- a/source/api_cc/include/DeepTensorTF.h +++ b/source/api_cc/include/DeepTensorTF.h @@ -3,6 +3,7 @@ #include "DeepTensor.h" #include "common.h" +#include "commonTF.h" #include "neighbor_list.h" namespace deepmd { diff --git a/source/api_cc/include/common.h b/source/api_cc/include/common.h index 7982c4f89d..0392747979 100644 --- a/source/api_cc/include/common.h +++ b/source/api_cc/include/common.h @@ -10,12 +10,6 @@ #include "neighbor_list.h" #include "version.h" -#ifdef TF_PRIVATE -#include "tf_private.h" -#else -#include "tf_public.h" -#endif - namespace deepmd { typedef double ENERGYTYPE; @@ -175,143 +169,8 @@ struct tf_exception : public deepmd::deepmd_exception { : deepmd::deepmd_exception(std::string("TensorFlow Error: ") + msg){}; }; -/** - * @brief Check TensorFlow status. Exit if not OK. - * @param[in] status TensorFlow status. - **/ -void check_status(const tensorflow::Status& status); - std::string name_prefix(const std::string& name_scope); -/** - * @brief Get the value of a tensor. - * @param[in] session TensorFlow session. - * @param[in] name The name of the tensor. - * @param[in] scope The scope of the tensor. - * @return The value of the tensor. - **/ -template -VT session_get_scalar(tensorflow::Session* session, - const std::string name, - const std::string scope = ""); - -/** - * @brief Get the vector of a tensor. - * @param[out] o_vec The output vector. - * @param[in] session TensorFlow session. - * @param[in] name The name of the tensor. - * @param[in] scope The scope of the tensor. - **/ -template -void session_get_vector(std::vector& o_vec, - tensorflow::Session* session, - const std::string name_, - const std::string scope = ""); - -/** - * @brief Get the type of a tensor. - * @param[in] session TensorFlow session. - * @param[in] name The name of the tensor. - * @param[in] scope The scope of the tensor. - * @return The type of the tensor as int. - **/ -int session_get_dtype(tensorflow::Session* session, - const std::string name, - const std::string scope = ""); - -/** - * @brief Get input tensors. - * @param[out] input_tensors Input tensors. - * @param[in] dcoord_ Coordinates of atoms. - * @param[in] ntypes Number of atom types. - * @param[in] datype_ Atom types. - * @param[in] dbox Box matrix. - * @param[in] cell_size Cell size. - * @param[in] fparam_ Frame parameters. - * @param[in] aparam_ Atom parameters. - * @param[in] atommap Atom map. - * @param[in] scope The scope of the tensors. - * @param[in] aparam_nall Whether the atomic dimesion of atomic parameters is - * nall. - */ -template -int session_input_tensors( - std::vector>& input_tensors, - const std::vector& dcoord_, - const int& ntypes, - const std::vector& datype_, - const std::vector& dbox, - const double& cell_size, - const std::vector& fparam_, - const std::vector& aparam_, - const deepmd::AtomMap& atommap, - const std::string scope = "", - const bool aparam_nall = false); - -/** - * @brief Get input tensors. - * @param[out] input_tensors Input tensors. - * @param[in] dcoord_ Coordinates of atoms. - * @param[in] ntypes Number of atom types. - * @param[in] datype_ Atom types. - * @param[in] dlist Neighbor list. - * @param[in] fparam_ Frame parameters. - * @param[in] aparam_ Atom parameters. - * @param[in] atommap Atom map. - * @param[in] nghost Number of ghost atoms. - * @param[in] ago Update the internal neighbour list if ago is 0. - * @param[in] scope The scope of the tensors. - * @param[in] aparam_nall Whether the atomic dimesion of atomic parameters is - * nall. - */ -template -int session_input_tensors( - std::vector>& input_tensors, - const std::vector& dcoord_, - const int& ntypes, - const std::vector& datype_, - const std::vector& dbox, - InputNlist& dlist, - const std::vector& fparam_, - const std::vector& aparam_, - const deepmd::AtomMap& atommap, - const int nghost, - const int ago, - const std::string scope = "", - const bool aparam_nall = false); - -/** - * @brief Get input tensors for mixed type. - * @param[out] input_tensors Input tensors. - * @param[in] nframes Number of frames. - * @param[in] dcoord_ Coordinates of atoms. - * @param[in] ntypes Number of atom types. - * @param[in] datype_ Atom types. - * @param[in] dlist Neighbor list. - * @param[in] fparam_ Frame parameters. - * @param[in] aparam_ Atom parameters. - * @param[in] atommap Atom map. - * @param[in] nghost Number of ghost atoms. - * @param[in] ago Update the internal neighbour list if ago is 0. - * @param[in] scope The scope of the tensors. - * @param[in] aparam_nall Whether the atomic dimesion of atomic parameters is - * nall. - */ -template -int session_input_tensors_mixed_type( - std::vector>& input_tensors, - const int& nframes, - const std::vector& dcoord_, - const int& ntypes, - const std::vector& datype_, - const std::vector& dbox, - const double& cell_size, - const std::vector& fparam_, - const std::vector& aparam_, - const deepmd::AtomMap& atommap, - const std::string scope = "", - const bool aparam_nall = false); - /** * @brief Read model file to a string. * @param[in] model Path to the model. diff --git a/source/api_cc/include/commonTF.h b/source/api_cc/include/commonTF.h new file mode 100644 index 0000000000..0c14597e30 --- /dev/null +++ b/source/api_cc/include/commonTF.h @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#include +#include + +#ifdef TF_PRIVATE +#include "tf_private.h" +#else +#include "tf_public.h" +#endif + +namespace deepmd { +/** + * @brief Check TensorFlow status. Exit if not OK. + * @param[in] status TensorFlow status. + **/ +void check_status(const tensorflow::Status& status); + +/** + * @brief Get the value of a tensor. + * @param[in] session TensorFlow session. + * @param[in] name The name of the tensor. + * @param[in] scope The scope of the tensor. + * @return The value of the tensor. + **/ +template +VT session_get_scalar(tensorflow::Session* session, + const std::string name, + const std::string scope = ""); + +/** + * @brief Get the vector of a tensor. + * @param[out] o_vec The output vector. + * @param[in] session TensorFlow session. + * @param[in] name The name of the tensor. + * @param[in] scope The scope of the tensor. + **/ +template +void session_get_vector(std::vector& o_vec, + tensorflow::Session* session, + const std::string name_, + const std::string scope = ""); + +/** + * @brief Get the type of a tensor. + * @param[in] session TensorFlow session. + * @param[in] name The name of the tensor. + * @param[in] scope The scope of the tensor. + * @return The type of the tensor as int. + **/ +int session_get_dtype(tensorflow::Session* session, + const std::string name, + const std::string scope = ""); + +/** + * @brief Get input tensors. + * @param[out] input_tensors Input tensors. + * @param[in] dcoord_ Coordinates of atoms. + * @param[in] ntypes Number of atom types. + * @param[in] datype_ Atom types. + * @param[in] dbox Box matrix. + * @param[in] cell_size Cell size. + * @param[in] fparam_ Frame parameters. + * @param[in] aparam_ Atom parameters. + * @param[in] atommap Atom map. + * @param[in] scope The scope of the tensors. + * @param[in] aparam_nall Whether the atomic dimesion of atomic parameters is + * nall. + */ +template +int session_input_tensors( + std::vector>& input_tensors, + const std::vector& dcoord_, + const int& ntypes, + const std::vector& datype_, + const std::vector& dbox, + const double& cell_size, + const std::vector& fparam_, + const std::vector& aparam_, + const deepmd::AtomMap& atommap, + const std::string scope = "", + const bool aparam_nall = false); + +/** + * @brief Get input tensors. + * @param[out] input_tensors Input tensors. + * @param[in] dcoord_ Coordinates of atoms. + * @param[in] ntypes Number of atom types. + * @param[in] datype_ Atom types. + * @param[in] dlist Neighbor list. + * @param[in] fparam_ Frame parameters. + * @param[in] aparam_ Atom parameters. + * @param[in] atommap Atom map. + * @param[in] nghost Number of ghost atoms. + * @param[in] ago Update the internal neighbour list if ago is 0. + * @param[in] scope The scope of the tensors. + * @param[in] aparam_nall Whether the atomic dimesion of atomic parameters is + * nall. + */ +template +int session_input_tensors( + std::vector>& input_tensors, + const std::vector& dcoord_, + const int& ntypes, + const std::vector& datype_, + const std::vector& dbox, + InputNlist& dlist, + const std::vector& fparam_, + const std::vector& aparam_, + const deepmd::AtomMap& atommap, + const int nghost, + const int ago, + const std::string scope = "", + const bool aparam_nall = false); + +/** + * @brief Get input tensors for mixed type. + * @param[out] input_tensors Input tensors. + * @param[in] nframes Number of frames. + * @param[in] dcoord_ Coordinates of atoms. + * @param[in] ntypes Number of atom types. + * @param[in] datype_ Atom types. + * @param[in] dlist Neighbor list. + * @param[in] fparam_ Frame parameters. + * @param[in] aparam_ Atom parameters. + * @param[in] atommap Atom map. + * @param[in] nghost Number of ghost atoms. + * @param[in] ago Update the internal neighbour list if ago is 0. + * @param[in] scope The scope of the tensors. + * @param[in] aparam_nall Whether the atomic dimesion of atomic parameters is + * nall. + */ +template +int session_input_tensors_mixed_type( + std::vector>& input_tensors, + const int& nframes, + const std::vector& dcoord_, + const int& ntypes, + const std::vector& datype_, + const std::vector& dbox, + const double& cell_size, + const std::vector& fparam_, + const std::vector& aparam_, + const deepmd::AtomMap& atommap, + const std::string scope = "", + const bool aparam_nall = false); + +} // namespace deepmd diff --git a/source/api_cc/include/version.h.in b/source/api_cc/include/version.h.in index c6bf6cf491..26b0c1be48 100644 --- a/source/api_cc/include/version.h.in +++ b/source/api_cc/include/version.h.in @@ -9,4 +9,5 @@ const std::string global_git_date="@GIT_DATE@"; const std::string global_git_branch="@GIT_BRANCH@"; const std::string global_tf_include_dir="@TensorFlow_INCLUDE_DIRS@"; const std::string global_tf_lib="@TensorFlow_LIBRARY@"; +const std::string global_pt_lib="@TORCH_LIBRARIES@"; const std::string global_model_version="@MODEL_VERSION@"; diff --git a/source/api_cc/src/DataModifier.cc b/source/api_cc/src/DataModifier.cc index 954c969c13..38d1fc879a 100644 --- a/source/api_cc/src/DataModifier.cc +++ b/source/api_cc/src/DataModifier.cc @@ -1,7 +1,9 @@ // SPDX-License-Identifier: LGPL-3.0-or-later #include "DataModifier.h" +#ifdef BUILD_TENSORFLOW #include "DataModifierTF.h" +#endif #include "common.h" using namespace deepmd; @@ -29,9 +31,12 @@ void DipoleChargeModifier::init(const std::string& model, // TODO: To implement detect_backend DPBackend backend = deepmd::DPBackend::TensorFlow; if (deepmd::DPBackend::TensorFlow == backend) { - // TODO: throw errors if TF backend is not built, without mentioning TF +#ifdef BUILD_TENSORFLOW dcm = std::make_shared(model, gpu_rank, name_scope_); +#else + throw deepmd::deepmd_exception("TensorFlow backend is not built"); +#endif } else if (deepmd::DPBackend::PyTorch == backend) { throw deepmd::deepmd_exception("PyTorch backend is not supported yet"); } else if (deepmd::DPBackend::Paddle == backend) { diff --git a/source/api_cc/src/DataModifierTF.cc b/source/api_cc/src/DataModifierTF.cc index 219139cf89..324cb14098 100644 --- a/source/api_cc/src/DataModifierTF.cc +++ b/source/api_cc/src/DataModifierTF.cc @@ -1,4 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later +#ifdef BUILD_TENSORFLOW #include "DataModifierTF.h" #include "common.h" @@ -361,3 +362,4 @@ void DipoleChargeModifierTF::computew( compute(dfcorr_, dvcorr_, dcoord_, datype_, dbox, pairs, delef_, nghost, lmp_list); } +#endif // BUILD_TENSORFLOW diff --git a/source/api_cc/src/DeepPot.cc b/source/api_cc/src/DeepPot.cc index 083e9b091f..c598549844 100644 --- a/source/api_cc/src/DeepPot.cc +++ b/source/api_cc/src/DeepPot.cc @@ -7,7 +7,9 @@ #include #include "AtomMap.h" +#ifdef BUILD_TENSORFLOW #include "DeepPotTF.h" +#endif #include "device.h" using namespace deepmd; @@ -35,8 +37,11 @@ void DeepPot::init(const std::string& model, // TODO: To implement detect_backend DPBackend backend = deepmd::DPBackend::TensorFlow; if (deepmd::DPBackend::TensorFlow == backend) { - // TODO: throw errors if TF backend is not built, without mentioning TF +#ifdef BUILD_TENSORFLOW dp = std::make_shared(model, gpu_rank, file_content); +#else + throw deepmd::deepmd_exception("TensorFlow backend is not built"); +#endif } else if (deepmd::DPBackend::PyTorch == backend) { throw deepmd::deepmd_exception("PyTorch backend is not supported yet"); } else if (deepmd::DPBackend::Paddle == backend) { diff --git a/source/api_cc/src/DeepPotPT.cc b/source/api_cc/src/DeepPotPT.cc new file mode 100644 index 0000000000..c94fb4247b --- /dev/null +++ b/source/api_cc/src/DeepPotPT.cc @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#ifdef BUILD_PYTORCH +#include + +void test_function_please_remove_after_torch_is_actually_used() { + torch::Tensor tensor = torch::rand({2, 3}); +} +#endif diff --git a/source/api_cc/src/DeepPotTF.cc b/source/api_cc/src/DeepPotTF.cc index ef348fe14c..7bf2bebce4 100644 --- a/source/api_cc/src/DeepPotTF.cc +++ b/source/api_cc/src/DeepPotTF.cc @@ -1,4 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later +#ifdef BUILD_TENSORFLOW #include "DeepPotTF.h" #include @@ -1051,3 +1052,4 @@ void DeepPotTF::computew_mixed_type(std::vector& ener, compute_mixed_type(ener, force, virial, atom_energy, atom_virial, nframes, coord, atype, box, fparam, aparam); } +#endif diff --git a/source/api_cc/src/DeepTensor.cc b/source/api_cc/src/DeepTensor.cc index 2c88ab2f4b..a0596e046f 100644 --- a/source/api_cc/src/DeepTensor.cc +++ b/source/api_cc/src/DeepTensor.cc @@ -3,7 +3,9 @@ #include +#ifdef BUILD_TENSORFLOW #include "DeepTensorTF.h" +#endif #include "common.h" using namespace deepmd; @@ -31,8 +33,11 @@ void DeepTensor::init(const std::string &model, // TODO: To implement detect_backend DPBackend backend = deepmd::DPBackend::TensorFlow; if (deepmd::DPBackend::TensorFlow == backend) { - // TODO: throw errors if TF backend is not built, without mentioning TF +#ifdef BUILD_TENSORFLOW dt = std::make_shared(model, gpu_rank, name_scope_); +#else + throw deepmd::deepmd_exception("TensorFlow backend is not built."); +#endif } else if (deepmd::DPBackend::PyTorch == backend) { throw deepmd::deepmd_exception("PyTorch backend is not supported yet"); } else if (deepmd::DPBackend::Paddle == backend) { diff --git a/source/api_cc/src/DeepTensorTF.cc b/source/api_cc/src/DeepTensorTF.cc index 436e389ad2..34a47bc6f3 100644 --- a/source/api_cc/src/DeepTensorTF.cc +++ b/source/api_cc/src/DeepTensorTF.cc @@ -1,4 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later +#ifdef BUILD_TENSORFLOW #include "DeepTensorTF.h" using namespace deepmd; @@ -844,3 +845,4 @@ void DeepTensorTF::computew(std::vector &global_tensor, atom_virial.clear(); } } +#endif diff --git a/source/api_cc/src/common.cc b/source/api_cc/src/common.cc index 2f75aaa291..a552f646f1 100644 --- a/source/api_cc/src/common.cc +++ b/source/api_cc/src/common.cc @@ -3,6 +3,8 @@ #include +#include + #include "AtomMap.h" #include "device.h" #if defined(_WIN32) @@ -20,10 +22,13 @@ // not windows #include #endif +#ifdef BUILD_TENSORFLOW +#include "commonTF.h" #include "google/protobuf/io/zero_copy_stream_impl.h" #include "google/protobuf/text_format.h" using namespace tensorflow; +#endif static std::vector split(const std::string& input_, const std::string& delimiter) { @@ -300,12 +305,14 @@ void deepmd::NeighborListData::make_inlist(InputNlist& inlist) { inlist.firstneigh = &firstneigh[0]; } +#ifdef BUILD_TENSORFLOW void deepmd::check_status(const tensorflow::Status& status) { if (!status.ok()) { std::cout << status.ToString() << std::endl; throw deepmd::tf_exception(status.ToString()); } } +#endif void throw_env_not_set_warning(std::string env_name) { std::cerr << "DeePMD-kit WARNING: Environmental variable " << env_name @@ -345,6 +352,7 @@ void deepmd::get_env_nthreads(int& num_intra_nthreads, } void deepmd::load_op_library() { +#ifdef BUILD_TENSORFLOW tensorflow::Env* env = tensorflow::Env::Default(); #if defined(_WIN32) std::string dso_path = "deepmd_op.dll"; @@ -358,6 +366,7 @@ void deepmd::load_op_library() { dso_path + " is not found! You can add the library directory to LD_LIBRARY_PATH"); } +#endif } std::string deepmd::name_prefix(const std::string& scope) { @@ -368,6 +377,7 @@ std::string deepmd::name_prefix(const std::string& scope) { return prefix; } +#ifdef BUILD_TENSORFLOW template int deepmd::session_input_tensors( std::vector>& input_tensors, @@ -850,6 +860,7 @@ int deepmd::session_get_dtype(tensorflow::Session* session, // cast enum to int return (int)output_rc.dtype(); } +#endif template void deepmd::select_map(std::vector& out, @@ -940,6 +951,7 @@ void deepmd::select_map_inv(typename std::vector::iterator out, } } +#ifdef BUILD_TENSORFLOW template int deepmd::session_get_scalar(Session*, const std::string, const std::string); @@ -989,6 +1001,7 @@ template void deepmd::session_get_vector(std::vector&, Session*, const std::string, const std::string); +#endif template void deepmd::select_map(std::vector& out, const std::vector& in, @@ -1018,6 +1031,7 @@ template void deepmd::select_map_inv( const std::vector& idx_map, const int& stride); +#ifdef BUILD_TENSORFLOW template double deepmd::session_get_scalar(Session*, const std::string, const std::string); @@ -1026,6 +1040,7 @@ template void deepmd::session_get_vector(std::vector&, Session*, const std::string, const std::string); +#endif template void deepmd::select_map(std::vector& out, const std::vector& in, @@ -1055,6 +1070,7 @@ template void deepmd::select_map_inv( const std::vector& idx_map, const int& stride); +#ifdef BUILD_TENSORFLOW template deepmd::STRINGTYPE deepmd::session_get_scalar( Session*, const std::string, const std::string); @@ -1093,13 +1109,19 @@ template void deepmd::select_map_inv( const typename std::vector::const_iterator in, const std::vector& idx_map, const int& stride); +#endif void deepmd::read_file_to_string(std::string model, std::string& file_content) { +#ifdef BUILD_TENSORFLOW deepmd::check_status(tensorflow::ReadFileToString(tensorflow::Env::Default(), model, &file_content)); +#else + throw deepmd::deepmd_exception("TODO: read_file_to_string only support TF"); +#endif } void deepmd::convert_pbtxt_to_pb(std::string fn_pb_txt, std::string fn_pb) { +#ifdef BUILD_TENSORFLOW int fd = open(fn_pb_txt.c_str(), O_RDONLY); tensorflow::protobuf::io::ZeroCopyInputStream* input = new tensorflow::protobuf::io::FileInputStream(fd); @@ -1109,8 +1131,13 @@ void deepmd::convert_pbtxt_to_pb(std::string fn_pb_txt, std::string fn_pb) { std::fstream output(fn_pb, std::ios::out | std::ios::trunc | std::ios::binary); graph_def.SerializeToOstream(&output); +#else + throw deepmd::deepmd_exception( + "convert_pbtxt_to_pb: TensorFlow backend is not enabled."); +#endif } +#ifdef BUILD_TENSORFLOW template int deepmd::session_input_tensors( std::vector>& input_tensors, const std::vector& dcoord_, @@ -1272,6 +1299,7 @@ template int deepmd::session_input_tensors_mixed_type( const deepmd::AtomMap& atommap, const std::string scope, const bool aparam_nall); +#endif void deepmd::print_summary(const std::string& pre) { int num_intra_nthreads, num_inter_nthreads; @@ -1292,8 +1320,13 @@ void deepmd::print_summary(const std::string& pre) { std::cout << pre << "build variant: cpu" << "\n"; #endif +#ifdef BUILD_TENSORFLOW std::cout << pre << "build with tf inc: " + global_tf_include_dir << "\n"; std::cout << pre << "build with tf lib: " + global_tf_lib << "\n"; +#endif +#ifdef BUILD_PYTORCH + std::cout << pre << "build with pt lib: " + global_pt_lib << "\n"; +#endif std::cout << pre << "set tf intra_op_parallelism_threads: " << num_intra_nthreads << "\n"; diff --git a/source/install/build_cc.sh b/source/install/build_cc.sh index fef9e82ebc..83a586049d 100755 --- a/source/install/build_cc.sh +++ b/source/install/build_cc.sh @@ -20,7 +20,13 @@ NPROC=$(nproc --all) BUILD_TMP_DIR=${SCRIPT_PATH}/../build mkdir -p ${BUILD_TMP_DIR} cd ${BUILD_TMP_DIR} -cmake -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} -DUSE_TF_PYTHON_LIBS=TRUE ${CUDA_ARGS} -DLAMMPS_VERSION=stable_2Aug2023_update2 .. +cmake -D ENABLE_TENSORFLOW=ON \ + -D ENABLE_PYTORCH=ON \ + -D CMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} \ + -D USE_TF_PYTHON_LIBS=TRUE \ + ${CUDA_ARGS} \ + -D LAMMPS_VERSION=stable_2Aug2023_update2 \ + .. cmake --build . -j${NPROC} cmake --install . From 0f9c6eb539c6889d68d5bf543094f25067aafd81 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 23 Jan 2024 22:23:25 -0500 Subject: [PATCH 004/686] docs: add TF icons to platform-specific features (#3171) Fix #3121. The PyTorch icon can be added when a feature implemented by PyTorch is added. However, I can't find a way to add an icon to TOC. ![image](https://github.com/deepmodeling/deepmd-kit/assets/9496702/7f29da27-af81-4850-9da0-79310d216b2d) Signed-off-by: Jinzhe Zeng --- doc/_static/css/custom.css | 6 ++++++ doc/_static/pytorch.svg | 1 + doc/_static/tensorflow.svg | 1 + doc/conf.py | 7 +++++++ doc/freeze/compress.md | 6 +++++- doc/model/dplr.md | 6 +++++- doc/model/dprc.md | 6 +++++- doc/model/linear.md | 6 +++++- doc/model/pairtab.md | 6 +++++- doc/model/train-energy-spin.md | 6 +++++- doc/model/train-energy.md | 6 +++++- doc/model/train-fitting-dos.md | 6 +++++- doc/model/train-fitting-tensor.md | 6 +++++- doc/model/train-hybrid.md | 6 +++++- doc/model/train-se-a-mask.md | 6 +++++- doc/model/train-se-atten.md | 6 +++++- doc/model/train-se-e2-a-tebd.md | 6 +++++- doc/model/train-se-e2-a.md | 6 +++++- doc/model/train-se-e2-r.md | 6 +++++- doc/model/train-se-e3.md | 6 +++++- doc/nvnmd/nvnmd.md | 6 +++++- doc/train/finetuning.md | 6 +++++- doc/train/gpu-limitations.md | 3 ++- doc/train/multi-task-training.md | 6 +++++- doc/train/parallel-training.md | 6 +++++- doc/train/tensorboard.md | 6 +++++- 26 files changed, 122 insertions(+), 22 deletions(-) create mode 100644 doc/_static/pytorch.svg create mode 100644 doc/_static/tensorflow.svg diff --git a/doc/_static/css/custom.css b/doc/_static/css/custom.css index 1569dc4a38..8894f47813 100644 --- a/doc/_static/css/custom.css +++ b/doc/_static/css/custom.css @@ -7,8 +7,14 @@ pre{ .wy-side-nav-search .wy-dropdown > a img.logo, .wy-side-nav-search > a img.logo { width: 275px; } +img.platform-icon { + height: 2ex; +} @media (prefers-color-scheme: dark) { .wy-side-nav-search .wy-dropdown > a img.logo, .wy-side-nav-search > a img.logo { content: url("../logo-dark.svg"); } + img.platform-icon { + filter: invert(1); + } } diff --git a/doc/_static/pytorch.svg b/doc/_static/pytorch.svg new file mode 100644 index 0000000000..04aae0c2a3 --- /dev/null +++ b/doc/_static/pytorch.svg @@ -0,0 +1 @@ +PyTorch icon diff --git a/doc/_static/tensorflow.svg b/doc/_static/tensorflow.svg new file mode 100644 index 0000000000..48746104ec --- /dev/null +++ b/doc/_static/tensorflow.svg @@ -0,0 +1 @@ +TensorFlow icon diff --git a/doc/conf.py b/doc/conf.py index 261c105d9b..e6bb4b6ba2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -278,6 +278,11 @@ def setup(app): .. |PRECISION| replace:: {list_to_doc(PRECISION_DICT.keys())} """ +myst_substitutions = { + "tensorflow_icon": """![TensorFlow](/_static/tensorflow.svg){class=platform-icon}""", + "pytorch_icon": """![PyTorch](/_static/pytorch.svg){class=platform-icon}""", +} + # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for @@ -301,6 +306,8 @@ def setup(app): myst_enable_extensions = [ "dollarmath", "colon_fence", + "substitution", + "attrs_inline", ] myst_fence_as_directive = ("math",) # fix emoji issue in pdf diff --git a/doc/freeze/compress.md b/doc/freeze/compress.md index 7394f77143..b6c8966c60 100644 --- a/doc/freeze/compress.md +++ b/doc/freeze/compress.md @@ -1,4 +1,8 @@ -# Compress a model +# Compress a model {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: ## Theory diff --git a/doc/model/dplr.md b/doc/model/dplr.md index feea84e562..317630ebe5 100644 --- a/doc/model/dplr.md +++ b/doc/model/dplr.md @@ -1,4 +1,8 @@ -# Deep potential long-range (DPLR) +# Deep potential long-range (DPLR) {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: Notice: **The interfaces of DPLR are not stable and subject to change** diff --git a/doc/model/dprc.md b/doc/model/dprc.md index c7547a769f..48e18e8d89 100644 --- a/doc/model/dprc.md +++ b/doc/model/dprc.md @@ -1,4 +1,8 @@ -# Deep Potential - Range Correction (DPRc) +# Deep Potential - Range Correction (DPRc) {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: Deep Potential - Range Correction (DPRc) is designed to combine with QM/MM method, and corrects energies from a low-level QM/MM method to a high-level QM/MM method: diff --git a/doc/model/linear.md b/doc/model/linear.md index b5e7c5c76a..3891559d90 100644 --- a/doc/model/linear.md +++ b/doc/model/linear.md @@ -1,4 +1,8 @@ -## Linear model +## Linear model {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: One can linearly combine existing models with arbitrary coefficients: diff --git a/doc/model/pairtab.md b/doc/model/pairtab.md index 115345796a..719bb95004 100644 --- a/doc/model/pairtab.md +++ b/doc/model/pairtab.md @@ -1,4 +1,8 @@ -# Interpolation or combination with a pairwise potential +# Interpolation or combination with a pairwise potential {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: ## Theory In applications like the radiation damage simulation, the interatomic distance may become too close, so that the DFT calculations fail. diff --git a/doc/model/train-energy-spin.md b/doc/model/train-energy-spin.md index d155ec977d..e0b3968c09 100644 --- a/doc/model/train-energy-spin.md +++ b/doc/model/train-energy-spin.md @@ -1,4 +1,8 @@ -# Fit spin energy +# Fit spin energy {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: In this section, we will take `$deepmd_source_dir/examples/NiO/se_e2_a/input.json` as an example of the input file. diff --git a/doc/model/train-energy.md b/doc/model/train-energy.md index 90e027d7a0..74a933c79c 100644 --- a/doc/model/train-energy.md +++ b/doc/model/train-energy.md @@ -1,4 +1,8 @@ -# Fit energy +# Fit energy {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: In this section, we will take `$deepmd_source_dir/examples/water/se_e2_a/input.json` as an example of the input file. diff --git a/doc/model/train-fitting-dos.md b/doc/model/train-fitting-dos.md index bbe5b50690..b74ab3acf7 100644 --- a/doc/model/train-fitting-dos.md +++ b/doc/model/train-fitting-dos.md @@ -1,4 +1,8 @@ -# Fit electronic density of states (DOS) +# Fit electronic density of states (DOS) {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: Here we present an API to DeepDOS model, which can be used to fit electronic density of state (DOS) (which is a vector). diff --git a/doc/model/train-fitting-tensor.md b/doc/model/train-fitting-tensor.md index 90370adfcf..3272418a7c 100644 --- a/doc/model/train-fitting-tensor.md +++ b/doc/model/train-fitting-tensor.md @@ -1,4 +1,8 @@ -# Fit `tensor` like `Dipole` and `Polarizability` +# Fit `tensor` like `Dipole` and `Polarizability` {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: Unlike `energy`, which is a scalar, one may want to fit some high dimensional physical quantity, like `dipole` (vector) and `polarizability` (matrix, shorted as `polar`). Deep Potential has provided different APIs to do this. In this example, we will show you how to train a model to fit a water system. A complete training input script of the examples can be found in diff --git a/doc/model/train-hybrid.md b/doc/model/train-hybrid.md index 58b66f25e0..1db3f49a1f 100644 --- a/doc/model/train-hybrid.md +++ b/doc/model/train-hybrid.md @@ -1,4 +1,8 @@ -# Descriptor `"hybrid"` +# Descriptor `"hybrid"` {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: This descriptor hybridizes multiple descriptors to form a new descriptor. For example, we have a list of descriptors denoted by $\mathcal D_1$, $\mathcal D_2$, ..., $\mathcal D_N$, the hybrid descriptor this the concatenation of the list, i.e. $\mathcal D = (\mathcal D_1, \mathcal D_2, \cdots, \mathcal D_N)$. diff --git a/doc/model/train-se-a-mask.md b/doc/model/train-se-a-mask.md index 17c211ec73..6d0e2e0320 100644 --- a/doc/model/train-se-a-mask.md +++ b/doc/model/train-se-a-mask.md @@ -1,4 +1,8 @@ -# Descriptor `"se_a_mask"` +# Descriptor `"se_a_mask"` {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: Descriptor `se_a_mask` is a concise implementation of the descriptor `se_e2_a`, diff --git a/doc/model/train-se-atten.md b/doc/model/train-se-atten.md index 7480ddbc12..b4e346327d 100644 --- a/doc/model/train-se-atten.md +++ b/doc/model/train-se-atten.md @@ -1,4 +1,8 @@ -# Descriptor `"se_atten"` +# Descriptor `"se_atten"` {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: ## DPA-1: Pretraining of Attention-based Deep Potential Model for Molecular Simulation diff --git a/doc/model/train-se-e2-a-tebd.md b/doc/model/train-se-e2-a-tebd.md index cb6ce6674f..7797a8f3c0 100644 --- a/doc/model/train-se-e2-a-tebd.md +++ b/doc/model/train-se-e2-a-tebd.md @@ -1,4 +1,8 @@ -# Type embedding approach +# Type embedding approach {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: We generate specific a type embedding vector for each atom type so that we can share one descriptor embedding net and one fitting net in total, which decline training complexity largely. diff --git a/doc/model/train-se-e2-a.md b/doc/model/train-se-e2-a.md index 537253a6d9..d40bb513ea 100644 --- a/doc/model/train-se-e2-a.md +++ b/doc/model/train-se-e2-a.md @@ -1,4 +1,8 @@ -# Descriptor `"se_e2_a"` +# Descriptor `"se_e2_a"` {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: The notation of `se_e2_a` is short for the Deep Potential Smooth Edition (DeepPot-SE) constructed from all information (both angular and radial) of atomic configurations. The `e2` stands for the embedding with two-atoms information. This descriptor was described in detail in [the DeepPot-SE paper](https://arxiv.org/abs/1805.09003). diff --git a/doc/model/train-se-e2-r.md b/doc/model/train-se-e2-r.md index f2f990b16a..c2c5fcfcd9 100644 --- a/doc/model/train-se-e2-r.md +++ b/doc/model/train-se-e2-r.md @@ -1,4 +1,8 @@ -# Descriptor `"se_e2_r"` +# Descriptor `"se_e2_r"` {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: The notation of `se_e2_r` is short for the Deep Potential Smooth Edition (DeepPot-SE) constructed from the radial information of atomic configurations. The `e2` stands for the embedding with two-atom information. diff --git a/doc/model/train-se-e3.md b/doc/model/train-se-e3.md index 5b0710a389..4eb35357a0 100644 --- a/doc/model/train-se-e3.md +++ b/doc/model/train-se-e3.md @@ -1,4 +1,8 @@ -# Descriptor `"se_e3"` +# Descriptor `"se_e3"` {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: The notation of `se_e3` is short for the Deep Potential Smooth Edition (DeepPot-SE) constructed from all information (both angular and radial) of atomic configurations. The embedding takes bond angles between a central atom and its two neighboring atoms as input (denoted by `e3`). diff --git a/doc/nvnmd/nvnmd.md b/doc/nvnmd/nvnmd.md index c11fee0bc9..7c00baad27 100644 --- a/doc/nvnmd/nvnmd.md +++ b/doc/nvnmd/nvnmd.md @@ -1,4 +1,8 @@ -# Introduction +# Introduction {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: NVNMD stands for non-von Neumann molecular dynamics. diff --git a/doc/train/finetuning.md b/doc/train/finetuning.md index ebc7cda2c9..bbab74f41e 100644 --- a/doc/train/finetuning.md +++ b/doc/train/finetuning.md @@ -1,4 +1,8 @@ -# Finetune the pretrained model +# Finetune the pretrained model {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: Pretraining-and-finetuning is a widely used approach in other fields such as Computer Vision (CV) or Natural Language Processing (NLP) to vastly reduce the training cost, while it's not trivial in potential models. diff --git a/doc/train/gpu-limitations.md b/doc/train/gpu-limitations.md index 5df76d28c9..dee606c2a3 100644 --- a/doc/train/gpu-limitations.md +++ b/doc/train/gpu-limitations.md @@ -1,4 +1,5 @@ -# Known limitations of using GPUs +# Known limitations of using GPUs {{ tensorflow_icon }} + If you use DeePMD-kit in a GPU environment, the acceptable value range of some variables is additionally restricted compared to the CPU environment due to the software's GPU implementations: 1. The number of atom types of a given system must be less than 128. 2. The maximum distance between an atom and its neighbors must be less than 128. It can be controlled by setting the rcut value of training parameters. diff --git a/doc/train/multi-task-training.md b/doc/train/multi-task-training.md index c647e6905e..76f404ab88 100644 --- a/doc/train/multi-task-training.md +++ b/doc/train/multi-task-training.md @@ -1,4 +1,8 @@ -# Multi-task training +# Multi-task training {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: ## Theory diff --git a/doc/train/parallel-training.md b/doc/train/parallel-training.md index 98d12f2b9b..4c707e5607 100644 --- a/doc/train/parallel-training.md +++ b/doc/train/parallel-training.md @@ -1,4 +1,8 @@ -# Parallel training +# Parallel training {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: Currently, parallel training is enabled in a synchronized way with help of [Horovod](https://github.com/horovod/horovod). Depending on the number of training processes (according to MPI context) and the number of GPU cards available, DeePMD-kit will decide whether to launch the training in parallel (distributed) mode or in serial mode. Therefore, no additional options are specified in your JSON/YAML input file. diff --git a/doc/train/tensorboard.md b/doc/train/tensorboard.md index 4846005216..1d6c5f0d68 100644 --- a/doc/train/tensorboard.md +++ b/doc/train/tensorboard.md @@ -1,4 +1,8 @@ -# TensorBoard Usage +# TensorBoard Usage {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: TensorBoard provides the visualization and tooling needed for machine learning experimentation. Full instructions for TensorBoard can be found From 04c414a57dafabbbb2b37fcc634d2b86d96423bf Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 23 Jan 2024 22:30:31 -0500 Subject: [PATCH 005/686] add universal Python inference interface DeepPot (#3164) Need discussion for other classes. --------- Signed-off-by: Jinzhe Zeng --- deepmd/infer/deep_pot.py | 3 +- deepmd_utils/infer/__init__.py | 6 ++ deepmd_utils/infer/backend.py | 33 +++++++++ deepmd_utils/infer/deep_pot.py | 126 +++++++++++++++++++++++++++++++++ source/tests/test_uni_infer.py | 27 +++++++ 5 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 deepmd_utils/infer/__init__.py create mode 100644 deepmd_utils/infer/backend.py create mode 100644 deepmd_utils/infer/deep_pot.py create mode 100644 source/tests/test_uni_infer.py diff --git a/deepmd/infer/deep_pot.py b/deepmd/infer/deep_pot.py index 81cfdde7a8..45db3fcb0c 100644 --- a/deepmd/infer/deep_pot.py +++ b/deepmd/infer/deep_pot.py @@ -26,6 +26,7 @@ from deepmd.utils.sess import ( run_sess, ) +from deepmd_utils.infer.deep_pot import DeepPot as DeepPotBase if TYPE_CHECKING: from pathlib import ( @@ -35,7 +36,7 @@ log = logging.getLogger(__name__) -class DeepPot(DeepEval): +class DeepPot(DeepEval, DeepPotBase): """Constructor. Parameters diff --git a/deepmd_utils/infer/__init__.py b/deepmd_utils/infer/__init__.py new file mode 100644 index 0000000000..644f5e1f43 --- /dev/null +++ b/deepmd_utils/infer/__init__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from .deep_pot import ( + DeepPot, +) + +__all__ = ["DeepPot"] diff --git a/deepmd_utils/infer/backend.py b/deepmd_utils/infer/backend.py new file mode 100644 index 0000000000..809e19466b --- /dev/null +++ b/deepmd_utils/infer/backend.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from enum import ( + Enum, +) + + +class DPBackend(Enum): + """DeePMD-kit backend.""" + + TensorFlow = 1 + PyTorch = 2 + Paddle = 3 + Unknown = 4 + + +def detect_backend(filename: str) -> DPBackend: + """Detect the backend of the given model file. + + Parameters + ---------- + filename : str + The model file name + """ + if filename.endswith(".pb"): + return DPBackend.TensorFlow + elif filename.endswith(".pth") or filename.endswith(".pt"): + return DPBackend.PyTorch + elif filename.endswith(".pdmodel"): + return DPBackend.Paddle + return DPBackend.Unknown + + +__all__ = ["DPBackend", "detect_backend"] diff --git a/deepmd_utils/infer/deep_pot.py b/deepmd_utils/infer/deep_pot.py new file mode 100644 index 0000000000..dec0a7c47c --- /dev/null +++ b/deepmd_utils/infer/deep_pot.py @@ -0,0 +1,126 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from abc import ( + ABC, + abstractmethod, +) +from typing import ( + List, + Optional, + Tuple, + Union, +) + +import numpy as np + +from deepmd_utils.utils.batch_size import ( + AutoBatchSize, +) + +from .backend import ( + DPBackend, + detect_backend, +) + + +class DeepPot(ABC): + """Potential energy model. + + Parameters + ---------- + model_file : Path + The name of the frozen model file. + auto_batch_size : bool or int or AutoBatchSize, default: True + If True, automatic batch size will be used. If int, it will be used + as the initial batch size. + neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional + The ASE neighbor list class to produce the neighbor list. If None, the + neighbor list will be built natively in the model. + """ + + @abstractmethod + def __init__( + self, + model_file, + *args, + auto_batch_size: Union[bool, int, AutoBatchSize] = True, + neighbor_list=None, + **kwargs, + ) -> None: + pass + + def __new__(cls, model_file: str, *args, **kwargs): + if cls is DeepPot: + backend = detect_backend(model_file) + if backend == DPBackend.TensorFlow: + from deepmd.infer.deep_pot import DeepPot as DeepPotTF + + return super().__new__(DeepPotTF) + elif backend == DPBackend.PyTorch: + from deepmd_pt.infer.deep_eval import DeepPot as DeepPotPT + + return super().__new__(DeepPotPT) + else: + raise NotImplementedError("Unsupported backend: " + str(backend)) + return super().__new__(cls) + + @abstractmethod + def eval( + self, + coords: np.ndarray, + cells: np.ndarray, + atom_types: List[int], + atomic: bool = False, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + efield: Optional[np.ndarray] = None, + mixed_type: bool = False, + ) -> Tuple[np.ndarray, ...]: + """Evaluate energy, force, and virial. If atomic is True, + also return atomic energy and atomic virial. + + Parameters + ---------- + coords : np.ndarray + The coordinates of the atoms, in shape (nframes, natoms, 3). + cells : np.ndarray + The cell vectors of the system, in shape (nframes, 9). If the system + is not periodic, set it to None. + atom_types : List[int] + The types of the atoms. If mixed_type is False, the shape is (natoms,); + otherwise, the shape is (nframes, natoms). + atomic : bool, optional + Whether to return atomic energy and atomic virial, by default False. + fparam : np.ndarray, optional + The frame parameters, by default None. + aparam : np.ndarray, optional + The atomic parameters, by default None. + efield : np.ndarray, optional + The electric field, by default None. + mixed_type : bool, optional + Whether the system contains mixed atom types, by default False. + + Returns + ------- + energy + The energy of the system, in shape (nframes,). + force + The force of the system, in shape (nframes, natoms, 3). + virial + The virial of the system, in shape (nframes, 9). + atomic_energy + The atomic energy of the system, in shape (nframes, natoms). Only returned + when atomic is True. + atomic_virial + The atomic virial of the system, in shape (nframes, natoms, 9). Only returned + when atomic is True. + """ + # This method has been used by: + # documentation python.md + # dp model_devi: +fparam, +aparam, +mixed_type + # dp test: +atomic, +fparam, +aparam, +efield, +mixed_type + # finetune: +mixed_type + # dpdata + # ase + + +__all__ = ["DeepPot"] diff --git a/source/tests/test_uni_infer.py b/source/tests/test_uni_infer.py new file mode 100644 index 0000000000..6b70d17f7e --- /dev/null +++ b/source/tests/test_uni_infer.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Unit tests for the universal Python inference interface.""" + +import os +import unittest + +from common import ( + tests_path, +) + +from deepmd.infer.deep_pot import DeepPot as DeepPotTF +from deepmd.utils.convert import ( + convert_pbtxt_to_pb, +) +from deepmd_utils.infer.deep_pot import DeepPot as DeepPot + + +class TestUniversalInfer(unittest.TestCase): + @classmethod + def setUpClass(cls): + convert_pbtxt_to_pb( + str(tests_path / os.path.join("infer", "deeppot-r.pbtxt")), "deeppot.pb" + ) + + def test_deep_pot(self): + dp = DeepPot("deeppot.pb") + self.assertIsInstance(dp, DeepPotTF) From 5dfbb5503fd2e661a2b1c585d68dae695703f9f7 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 23 Jan 2024 22:37:56 -0500 Subject: [PATCH 006/686] detect version in advance before building deepmd-kit-cu11 (#3172) Fix #3168. See: https://github.com/pypa/setuptools_scm/issues/1006#issuecomment-1905563223 --------- Signed-off-by: Jinzhe Zeng --- .github/workflows/build_wheel.yml | 11 +++++++++++ pyproject.toml | 1 + 2 files changed, 12 insertions(+) diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 23076e9bf5..fa109cac5e 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -68,6 +68,17 @@ jobs: - uses: docker/setup-qemu-action@v3 name: Setup QEMU if: matrix.platform_id == 'manylinux_aarch64' && matrix.os == 'ubuntu-latest' + # detect version in advance. See #3168 + - uses: actions/setup-python@v5 + name: Install Python + with: + python-version: '3.11' + cache: 'pip' + if: matrix.dp_pkg_name == 'deepmd-kit-cu11' + - run: | + python -m pip install setuptools_scm + python -c "from setuptools_scm import get_version;print('SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DEEPMD-KIT-CU11='+get_version())" >> $GITHUB_ENV + if: matrix.dp_pkg_name == 'deepmd-kit-cu11' - name: Build wheels uses: pypa/cibuildwheel@v2.16 env: diff --git a/pyproject.toml b/pyproject.toml index e91fd320f3..550fbc4b54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -155,6 +155,7 @@ environment-pass = [ "DP_VARIANT", "CUDA_VERSION", "DP_PKG_NAME", + "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DEEPMD-KIT-CU11", ] environment = { PIP_PREFER_BINARY="1", DP_LAMMPS_VERSION="stable_2Aug2023_update2", DP_ENABLE_IPI="1", MPI_HOME="/usr/lib64/mpich", PATH="/usr/lib64/mpich/bin:$PATH" } before-all = [ From 4e112335a6c4d09557c8220bc3dd6242e2cf595c Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 24 Jan 2024 10:11:11 -0500 Subject: [PATCH 007/686] add dpdata driver (#3174) Add a dpdata driver via the plugin mechanism (override that in the dpdata package) so it can benefit from the multiple-backend DeepPot. Currently, the driver in the dpdata package has to support both v1 and v2 for backward compatibility. When shipped within the deepmd-kit package, it only needs to support the current deepmd-kit version. --------- Signed-off-by: Jinzhe Zeng --- README.md | 1 + backend/dynamic_metadata.py | 2 +- deepmd_utils/driver.py | 73 ++++++++++++++++++++++++++++ doc/third-party/dpdata.md | 12 +++++ doc/third-party/index.md | 1 + doc/third-party/index.rst | 1 + doc/third-party/out-of-deepmd-kit.md | 13 ----- pyproject.toml | 3 ++ source/tests/test_deeppot_a.py | 52 ++++++++++++++++++++ 9 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 deepmd_utils/driver.py create mode 100644 doc/third-party/dpdata.md diff --git a/README.md b/README.md index 81fdead098..27c8dab4bc 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,7 @@ A full [document](doc/train/train-input-auto.rst) on options in the training inp - [C++ interface](doc/inference/cxx.md) - [Node.js interface](doc/inference/nodejs.md) - [Integrate with third-party packages](doc/third-party/index.rst) + - [Use deep potential with dpdata](doc/third-party/dpdata.md) - [Use deep potential with ASE](doc/third-party/ase.md) - [Run MD with LAMMPS](doc/third-party/lammps-command.md) - [Run path-integral MD with i-PI](doc/third-party/ipi.md) diff --git a/backend/dynamic_metadata.py b/backend/dynamic_metadata.py index ab955c3cf8..210c04235e 100644 --- a/backend/dynamic_metadata.py +++ b/backend/dynamic_metadata.py @@ -33,7 +33,7 @@ def dynamic_metadata( elif field == "optional-dependencies": return { "test": [ - "dpdata>=0.1.9", + "dpdata>=0.2.7", "ase", "pytest", "pytest-cov", diff --git a/deepmd_utils/driver.py b/deepmd_utils/driver.py new file mode 100644 index 0000000000..b9e70f15e4 --- /dev/null +++ b/deepmd_utils/driver.py @@ -0,0 +1,73 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""dpdata driver.""" +# Derived from https://github.com/deepmodeling/dpdata/blob/18a0ed5ebced8b1f6887038883d46f31ae9990a4/dpdata/plugins/deepmd.py#L361-L443 +# under LGPL-3.0-or-later license. +# The original deepmd driver maintained in the dpdata package will be overriden. +# The class in the dpdata package needs to handle different situations for v1 and v2 interface, +# which is too complex with the development of deepmd-kit. +# So, it will be a good idea to ship it with DeePMD-kit itself. +import dpdata +from dpdata.utils import ( + sort_atom_names, +) + + +@dpdata.driver.Driver.register("dp") +@dpdata.driver.Driver.register("deepmd") +@dpdata.driver.Driver.register("deepmd-kit") +class DPDriver(dpdata.driver.Driver): + """DeePMD-kit driver. + + Parameters + ---------- + dp : deepmd.DeepPot or str + The deepmd-kit potential class or the filename of the model. + + Examples + -------- + >>> DPDriver("frozen_model.pb") + """ + + def __init__(self, dp: str) -> None: + from deepmd_utils.infer.deep_pot import ( + DeepPot, + ) + + if not isinstance(dp, DeepPot): + self.dp = DeepPot(dp, auto_batch_size=True) + else: + self.dp = dp + + def label(self, data: dict) -> dict: + """Label a system data by deepmd-kit. Returns new data with energy, forces, and virials. + + Parameters + ---------- + data : dict + data with coordinates and atom types + + Returns + ------- + dict + labeled data with energies and forces + """ + nframes = data["coords"].shape[0] + natoms = data["coords"].shape[1] + type_map = self.dp.get_type_map() + # important: dpdata type_map may not be the same as the model type_map + # note: while we want to change the type_map when feeding to DeepPot, + # we don't want to change the type_map in the returned data + sorted_data = sort_atom_names(data.copy(), type_map=type_map) + atype = sorted_data["atom_types"] + + coord = data["coords"].reshape((nframes, natoms * 3)) + if "nopbc" not in data: + cell = data["cells"].reshape((nframes, 9)) + else: + cell = None + e, f, v = self.dp.eval(coord, cell, atype) + data = data.copy() + data["energies"] = e.reshape((nframes,)) + data["forces"] = f.reshape((nframes, natoms, 3)) + data["virials"] = v.reshape((nframes, 3, 3)) + return data diff --git a/doc/third-party/dpdata.md b/doc/third-party/dpdata.md new file mode 100644 index 0000000000..05e0f6fb40 --- /dev/null +++ b/doc/third-party/dpdata.md @@ -0,0 +1,12 @@ +# Use deep potential with dpdata + +DeePMD-kit provides a driver for [dpdata](https://github.com/deepmodeling/dpdata) >=0.2.7 via the plugin mechanism, making it possible to call the `predict` method for `System` class: + +```py +import dpdata + +dsys = dpdata.LabeledSystem("OUTCAR") +dp_sys = dsys.predict("frozen_model_compressed.pb", driver="dp") +``` + +By inferring with the DP model `frozen_model_compressed.pb`, dpdata will generate a new labeled system `dp_sys` with inferred energies, forces, and virials. diff --git a/doc/third-party/index.md b/doc/third-party/index.md index 235337974c..419f1fbb5c 100644 --- a/doc/third-party/index.md +++ b/doc/third-party/index.md @@ -2,6 +2,7 @@ Note that the model for inference is required to be compatible with the DeePMD-kit package. See [Model compatibility](../troubleshooting/model-compatability.html) for details. +- [Use deep potential with dpdata](dpdata.md) - [Use deep potential with ASE](ase.md) - [Run MD with LAMMPS](lammps-command.md) - [Run path-integral MD with i-PI](ipi.md) diff --git a/doc/third-party/index.rst b/doc/third-party/index.rst index f88a477fc7..cd0726a4bb 100644 --- a/doc/third-party/index.rst +++ b/doc/third-party/index.rst @@ -6,6 +6,7 @@ Note that the model for inference is required to be compatible with the DeePMD-k .. toctree:: :maxdepth: 1 + dpdata ase lammps-command ipi diff --git a/doc/third-party/out-of-deepmd-kit.md b/doc/third-party/out-of-deepmd-kit.md index 71dc9adb23..2ebed6fb46 100644 --- a/doc/third-party/out-of-deepmd-kit.md +++ b/doc/third-party/out-of-deepmd-kit.md @@ -2,19 +2,6 @@ The codes of the following interfaces are not a part of the DeePMD-kit package and maintained by other repositories. We list these interfaces here for user convenience. -## dpdata - -[dpdata](https://github.com/deepmodeling/dpdata) provides the `predict` method for `System` class: - -```py -import dpdata - -dsys = dpdata.LabeledSystem("OUTCAR") -dp_sys = dsys.predict("frozen_model_compressed.pb") -``` - -By inferring with the DP model `frozen_model_compressed.pb`, dpdata will generate a new labeled system `dp_sys` with inferred energies, forces, and virials. - ## OpenMM plugin for DeePMD-kit An [OpenMM](https://github.com/openmm/openmm) plugin is provided from [JingHuangLab/openmm_deepmd_plugin](https://github.com/JingHuangLab/openmm_deepmd_plugin), written by the [Huang Lab](http://www.compbiophysics.org/) at Westlake University. diff --git a/pyproject.toml b/pyproject.toml index 550fbc4b54..e5515984fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,9 @@ deepmd = "deepmd.lmp:get_op_dir" [project.entry-points."dpgui"] "DeePMD-kit" = "deepmd_utils.utils.argcheck:gen_args" +[project.entry-points."dpdata.plugins"] +deepmd_driver = "deepmd_utils.driver:DPDriver" + [project.urls] Homepage = "https://github.com/deepmodeling/deepmd-kit" documentation = "https://docs.deepmodeling.com/projects/deepmd" diff --git a/source/tests/test_deeppot_a.py b/source/tests/test_deeppot_a.py index c229b4302c..ba3655b7f9 100644 --- a/source/tests/test_deeppot_a.py +++ b/source/tests/test_deeppot_a.py @@ -4,6 +4,7 @@ import unittest import ase.neighborlist +import dpdata import numpy as np from common import ( run_dp, @@ -518,6 +519,32 @@ def test_2frame_atm(self): expected_sv = np.sum(expected_v.reshape([nframes, -1, 9]), axis=1) np.testing.assert_almost_equal(vv.ravel(), expected_sv.ravel(), default_places) + def test_dpdata_driver(self): + nframes = 1 + system = dpdata.System( + data={ + "coords": self.coords.reshape((nframes, -1, 3)), + "cells": np.zeros((nframes, 3, 3)), + "atom_types": np.array(self.atype), + "orig": np.zeros((3,)), + "atom_names": ["O", "H"], + "atom_numbs": [2, 4], + "nopbc": True, + } + ) + system_predicted = system.predict(self.dp, driver="dp") + np.testing.assert_almost_equal( + system_predicted["forces"].ravel(), self.expected_f.ravel(), default_places + ) + expected_se = np.sum(self.expected_e.reshape([nframes, -1]), axis=1) + np.testing.assert_almost_equal( + system_predicted["energies"].ravel(), expected_se.ravel(), default_places + ) + expected_sv = np.sum(self.expected_v.reshape([nframes, -1, 9]), axis=1) + np.testing.assert_almost_equal( + system_predicted["virials"].ravel(), expected_sv.ravel(), default_places + ) + class TestDeepPotALargeBoxNoPBC(unittest.TestCase): @classmethod @@ -716,6 +743,31 @@ def test_ase(self): expected_se = np.sum(self.expected_e.reshape([nframes, -1]), axis=1) np.testing.assert_almost_equal(ee.ravel(), expected_se.ravel(), default_places) + def test_dpdata_driver(self): + nframes = 1 + system = dpdata.System( + data={ + "coords": self.coords.reshape((nframes, -1, 3)), + "cells": self.box.reshape((nframes, 3, 3)), + "atom_types": np.array(self.atype), + "orig": np.zeros((3,)), + "atom_names": ["O", "H"], + "atom_numbs": [2, 4], + } + ) + system_predicted = system.predict("deeppot.pb", driver="dp") + np.testing.assert_almost_equal( + system_predicted["forces"].ravel(), self.expected_f.ravel(), default_places + ) + expected_se = np.sum(self.expected_e.reshape([nframes, -1]), axis=1) + np.testing.assert_almost_equal( + system_predicted["energies"].ravel(), expected_se.ravel(), default_places + ) + expected_sv = np.sum(self.expected_v.reshape([nframes, -1, 9]), axis=1) + np.testing.assert_almost_equal( + system_predicted["virials"].ravel(), expected_sv.ravel(), default_places + ) + class TestModelConvert(unittest.TestCase): def setUp(self): From 2a32c87335900284382619220d8cc1ebd1a750ee Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 24 Jan 2024 10:24:32 -0500 Subject: [PATCH 008/686] Move model deviation and ase calculator to `deepmd_utils` (#3173) ..., so they can benifit from multiple-backend DeepPot. Update docs. --------- Signed-off-by: Jinzhe Zeng --- deepmd/calculator.py | 146 +-------- deepmd/infer/model_devi.py | 523 +------------------------------ deepmd_utils/calculator.py | 144 +++++++++ deepmd_utils/infer/__init__.py | 5 +- deepmd_utils/infer/model_devi.py | 506 ++++++++++++++++++++++++++++++ doc/inference/python.md | 10 +- doc/third-party/ase.md | 2 +- 7 files changed, 680 insertions(+), 656 deletions(-) create mode 100644 deepmd_utils/calculator.py create mode 100644 deepmd_utils/infer/model_devi.py diff --git a/deepmd/calculator.py b/deepmd/calculator.py index b9c0a81006..4dbed51fac 100644 --- a/deepmd/calculator.py +++ b/deepmd/calculator.py @@ -1,144 +1,8 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -"""ASE calculator interface module.""" - -from pathlib import ( - Path, -) -from typing import ( - TYPE_CHECKING, - ClassVar, - Dict, - List, - Optional, - Union, -) - -from ase.calculators.calculator import ( - Calculator, - PropertyNotImplementedError, - all_changes, -) - -from deepmd import ( - DeepPotential, +from deepmd_utils.calculator import ( + DP, ) -if TYPE_CHECKING: - from ase import ( - Atoms, - ) - -__all__ = ["DP"] - - -class DP(Calculator): - """Implementation of ASE deepmd calculator. - - Implemented propertie are `energy`, `forces` and `stress` - - Parameters - ---------- - model : Union[str, Path] - path to the model - label : str, optional - calculator label, by default "DP" - type_dict : Dict[str, int], optional - mapping of element types and their numbers, best left None and the calculator - will infer this information from model, by default None - neighbor_list : ase.neighborlist.NeighborList, optional - The neighbor list object. If None, then build the native neighbor list. - - Examples - -------- - Compute potential energy - - >>> from ase import Atoms - >>> from deepmd.calculator import DP - >>> water = Atoms('H2O', - >>> positions=[(0.7601, 1.9270, 1), - >>> (1.9575, 1, 1), - >>> (1., 1., 1.)], - >>> cell=[100, 100, 100], - >>> calculator=DP(model="frozen_model.pb")) - >>> print(water.get_potential_energy()) - >>> print(water.get_forces()) - - Run BFGS structure optimization - - >>> from ase.optimize import BFGS - >>> dyn = BFGS(water) - >>> dyn.run(fmax=1e-6) - >>> print(water.get_positions()) - """ - - name = "DP" - implemented_properties: ClassVar[List[str]] = [ - "energy", - "free_energy", - "forces", - "virial", - "stress", - ] - - def __init__( - self, - model: Union[str, "Path"], - label: str = "DP", - type_dict: Optional[Dict[str, int]] = None, - neighbor_list=None, - **kwargs, - ) -> None: - Calculator.__init__(self, label=label, **kwargs) - self.dp = DeepPotential(str(Path(model).resolve()), neighbor_list=neighbor_list) - if type_dict: - self.type_dict = type_dict - else: - self.type_dict = dict( - zip(self.dp.get_type_map(), range(self.dp.get_ntypes())) - ) - - def calculate( - self, - atoms: Optional["Atoms"] = None, - properties: List[str] = ["energy", "forces", "virial"], - system_changes: List[str] = all_changes, - ): - """Run calculation with deepmd model. - - Parameters - ---------- - atoms : Optional[Atoms], optional - atoms object to run the calculation on, by default None - properties : List[str], optional - unused, only for function signature compatibility, - by default ["energy", "forces", "stress"] - system_changes : List[str], optional - unused, only for function signature compatibility, by default all_changes - """ - if atoms is not None: - self.atoms = atoms.copy() - - coord = self.atoms.get_positions().reshape([1, -1]) - if sum(self.atoms.get_pbc()) > 0: - cell = self.atoms.get_cell().reshape([1, -1]) - else: - cell = None - symbols = self.atoms.get_chemical_symbols() - atype = [self.type_dict[k] for k in symbols] - e, f, v = self.dp.eval(coords=coord, cells=cell, atom_types=atype) - self.results["energy"] = e[0][0] - # see https://gitlab.com/ase/ase/-/merge_requests/2485 - self.results["free_energy"] = e[0][0] - self.results["forces"] = f[0] - self.results["virial"] = v[0].reshape(3, 3) - - # convert virial into stress for lattice relaxation - if "stress" in properties: - if sum(atoms.get_pbc()) > 0: - # the usual convention (tensile stress is positive) - # stress = -virial / volume - stress = -0.5 * (v[0].copy() + v[0].copy().T) / atoms.get_volume() - # Voigt notation - self.results["stress"] = stress.flat[[0, 4, 8, 5, 2, 1]] - else: - raise PropertyNotImplementedError +__all__ = [ + "DP", +] diff --git a/deepmd/infer/model_devi.py b/deepmd/infer/model_devi.py index 8c329a0845..802e0ae401 100644 --- a/deepmd/infer/model_devi.py +++ b/deepmd/infer/model_devi.py @@ -1,511 +1,18 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from typing import ( - Optional, - Tuple, - overload, +from deepmd_utils.infer.model_devi import ( + calc_model_devi, + calc_model_devi_e, + calc_model_devi_f, + calc_model_devi_v, + make_model_devi, + write_model_devi_out, ) -import numpy as np - -from deepmd.common import ( - expand_sys_str, -) - -from ..utils.batch_size import ( - AutoBatchSize, -) -from ..utils.data import ( - DeepmdData, -) -from .deep_pot import ( - DeepPot, -) - -try: - from typing import Literal # python >=3.8 -except ImportError: - from typing_extensions import Literal # type: ignore - - -@overload -def calc_model_devi_f( - fs: np.ndarray, - real_f: Optional[np.ndarray] = None, - relative: Optional[float] = None, - atomic: Literal[False] = False, -) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - ... - - -@overload -def calc_model_devi_f( - fs: np.ndarray, - real_f: Optional[np.ndarray] = None, - relative: Optional[float] = None, - *, - atomic: Literal[True], -) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - ... - - -def calc_model_devi_f( - fs: np.ndarray, - real_f: Optional[np.ndarray] = None, - relative: Optional[float] = None, - atomic: bool = False, -) -> Tuple[np.ndarray, ...]: - """Calculate model deviation of force. - - Parameters - ---------- - fs : numpy.ndarray - size of `n_models x n_frames x n_atoms x 3` - real_f : numpy.ndarray or None - real force, size of `n_frames x n_atoms x 3`. If given, - the RMS real error is calculated instead. - relative : float, default: None - If given, calculate the relative model deviation of force. The - value is the level parameter for computing the relative model - deviation of the force. - atomic : bool, default: False - Whether return deviation of force in all atoms - - Returns - ------- - max_devi_f : numpy.ndarray - maximum deviation of force in all atoms - min_devi_f : numpy.ndarray - minimum deviation of force in all atoms - avg_devi_f : numpy.ndarray - average deviation of force in all atoms - fs_devi : numpy.ndarray - deviation of force in all atoms, returned if atomic=True - """ - if real_f is None: - fs_devi = np.linalg.norm(np.std(fs, axis=0), axis=-1) - else: - fs_devi = np.linalg.norm( - np.sqrt(np.mean(np.square(fs - real_f), axis=0)), axis=-1 - ) - if relative is not None: - if real_f is None: - # if real force is not given, the magnitude is calculated from mean value of four models - # See DeepPotModelDevi::compute_relative_std_f - # See also Eq. 71 in DeePMD-kit v2 paepr - magnitude = np.linalg.norm(np.mean(fs, axis=0), axis=-1) - else: - # otherwise, the magnitude is calculated from the real force - magnitude = np.linalg.norm(real_f, axis=-1) - fs_devi /= magnitude + relative - max_devi_f = np.max(fs_devi, axis=-1) - min_devi_f = np.min(fs_devi, axis=-1) - avg_devi_f = np.mean(fs_devi, axis=-1) - if atomic: - return max_devi_f, min_devi_f, avg_devi_f, fs_devi - return max_devi_f, min_devi_f, avg_devi_f - - -def calc_model_devi_e( - es: np.ndarray, real_e: Optional[np.ndarray] = None -) -> np.ndarray: - """Calculate model deviation of total energy per atom. - - Here we don't use the atomic energy, as the decomposition - of energy is arbitrary and not unique. There is no fitting - target for atomic energy. - - Parameters - ---------- - es : numpy.ndarray - size of `n_models x n_frames x 1 - real_e : numpy.ndarray - real energy, size of `n_frames x 1`. If given, - the RMS real error is calculated instead. - - Returns - ------- - max_devi_e : numpy.ndarray - maximum deviation of energy - """ - if real_e is None: - es_devi = np.std(es, axis=0) - else: - es_devi = np.sqrt(np.mean(np.square(es - real_e), axis=0)) - es_devi = np.squeeze(es_devi, axis=-1) - return es_devi - - -def calc_model_devi_v( - vs: np.ndarray, - real_v: Optional[np.ndarray] = None, - relative: Optional[float] = None, -) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - """Calculate model deviation of virial. - - Parameters - ---------- - vs : numpy.ndarray - size of `n_models x n_frames x 9` - real_v : numpy.ndarray - real virial, size of `n_frames x 9`. If given, - the RMS real error is calculated instead. - relative : float, default: None - If given, calculate the relative model deviation of virial. The - value is the level parameter for computing the relative model - deviation of the virial. - - Returns - ------- - max_devi_v : numpy.ndarray - maximum deviation of virial in 9 elements - min_devi_v : numpy.ndarray - minimum deviation of virial in 9 elements - avg_devi_v : numpy.ndarray - average deviation of virial in 9 elements - """ - if real_v is None: - vs_devi = np.std(vs, axis=0) - else: - vs_devi = np.sqrt(np.mean(np.square(vs - real_v), axis=0)) - if relative is not None: - if real_v is None: - # if real virial is not given, the magnitude is calculated from mean value of four models - # See DeepPotModelDevi::compute_relative_std_v - # See also Eq. 72 in DeePMD-kit v2 paepr - magnitude = np.linalg.norm(np.mean(vs, axis=0), axis=-1) - else: - # otherwise, the magnitude is calculated from the real virial - magnitude = np.linalg.norm(real_v, axis=-1) - vs_devi /= magnitude + relative - max_devi_v = np.max(vs_devi, axis=-1) - min_devi_v = np.min(vs_devi, axis=-1) - avg_devi_v = np.linalg.norm(vs_devi, axis=-1) / 3 - return max_devi_v, min_devi_v, avg_devi_v - - -def write_model_devi_out( - devi: np.ndarray, fname: str, header: str = "", atomic: bool = False -): - """Write output of model deviation. - - Parameters - ---------- - devi : numpy.ndarray - the first column is the steps index - fname : str - the file name to dump - header : str, default="" - the header to dump - atomic : bool, default: False - whether atomic model deviation is printed - """ - if not atomic: - assert devi.shape[1] == 8 - else: - assert devi.shape[1] > 8 - header = "%s\n%10s" % (header, "step") - for item in "vf": - header += "%19s%19s%19s" % ( - f"max_devi_{item}", - f"min_devi_{item}", - f"avg_devi_{item}", - ) - header += "%19s" % "devi_e" - if atomic: - header += "%19s" % "atm_devi_f(N)" - with open(fname, "ab") as fp: - np.savetxt( - fp, - devi, - fmt=["%12d"] + ["%19.6e" for _ in range(devi.shape[1] - 1)], - delimiter="", - header=header, - ) - return devi - - -def _check_tmaps(tmaps, ref_tmap=None): - """Check whether type maps are identical.""" - assert isinstance(tmaps, list) - if ref_tmap is None: - ref_tmap = tmaps[0] - assert isinstance(ref_tmap, list) - - flag = True - for tmap in tmaps: - if tmap != ref_tmap: - flag = False - break - return flag - - -def calc_model_devi( - coord, - box, - atype, - models, - fname=None, - frequency=1, - mixed_type=False, - fparam: Optional[np.ndarray] = None, - aparam: Optional[np.ndarray] = None, - real_data: Optional[dict] = None, - atomic: bool = False, - relative: Optional[float] = None, - relative_v: Optional[float] = None, -): - """Python interface to calculate model deviation. - - Parameters - ---------- - coord : numpy.ndarray, `n_frames x n_atoms x 3` - Coordinates of system to calculate - box : numpy.ndarray or None, `n_frames x 3 x 3` - Box to specify periodic boundary condition. If None, no pbc will be used - atype : numpy.ndarray, `n_atoms x 1` - Atom types - models : list of DeepPot models - Models used to evaluate deviation - fname : str or None - File to dump results, default None - frequency : int - Steps between frames (if the system is given by molecular dynamics engine), default 1 - mixed_type : bool - Whether the input atype is in mixed_type format or not - fparam : numpy.ndarray - frame specific parameters - aparam : numpy.ndarray - atomic specific parameters - real_data : dict, optional - real data to calculate RMS real error - atomic : bool, default: False - If True, calculate the force model deviation of each atom. - relative : float, default: None - If given, calculate the relative model deviation of force. The - value is the level parameter for computing the relative model - deviation of the force. - relative_v : float, default: None - If given, calculate the relative model deviation of virial. The - value is the level parameter for computing the relative model - deviation of the virial. - - Returns - ------- - model_devi : numpy.ndarray, `n_frames x 8` - Model deviation results. The first column is index of steps, the other 7 columns are - max_devi_v, min_devi_v, avg_devi_v, max_devi_f, min_devi_f, avg_devi_f, devi_e. - - Examples - -------- - >>> from deepmd.infer import calc_model_devi - >>> from deepmd.infer import DeepPot as DP - >>> import numpy as np - >>> coord = np.array([[1,0,0], [0,0,1.5], [1,0,3]]).reshape([1, -1]) - >>> cell = np.diag(10 * np.ones(3)).reshape([1, -1]) - >>> atype = [1,0,1] - >>> graphs = [DP("graph.000.pb"), DP("graph.001.pb")] - >>> model_devi = calc_model_devi(coord, cell, atype, graphs) - """ - energies = [] - forces = [] - virials = [] - natom = atype.shape[-1] - for dp in models: - ret = dp.eval( - coord, - box, - atype, - fparam=fparam, - aparam=aparam, - mixed_type=mixed_type, - ) - energies.append(ret[0] / natom) - forces.append(ret[1]) - virials.append(ret[2] / natom) - - energies = np.array(energies) - forces = np.array(forces) - virials = np.array(virials) - - devi = [np.arange(coord.shape[0]) * frequency] - if real_data is None: - devi += list(calc_model_devi_v(virials, relative=relative_v)) - devi_f = list(calc_model_devi_f(forces, relative=relative, atomic=atomic)) - devi += devi_f[:3] - devi.append(calc_model_devi_e(energies)) - else: - devi += list( - calc_model_devi_v(virials, real_data["virial"], relative=relative_v) - ) - devi_f = list( - calc_model_devi_f( - forces, real_data["force"], relative=relative, atomic=atomic - ) - ) - devi += devi_f[:3] - devi.append(calc_model_devi_e(energies, real_data["energy"])) - devi = np.vstack(devi).T - if atomic: - devi = np.concatenate([devi, devi_f[3]], axis=1) - if fname: - write_model_devi_out(devi, fname, atomic=atomic) - return devi - - -def make_model_devi( - *, - models: list, - system: str, - set_prefix: str, - output: str, - frequency: int, - real_error: bool = False, - atomic: bool = False, - relative: Optional[float] = None, - relative_v: Optional[float] = None, - **kwargs, -): - """Make model deviation calculation. - - Parameters - ---------- - models : list - A list of paths of models to use for making model deviation - system : str - The path of system to make model deviation calculation - set_prefix : str - The set prefix of the system - output : str - The output file for model deviation results - frequency : int - The number of steps that elapse between writing coordinates - in a trajectory by a MD engine (such as Gromacs / Lammps). - This paramter is used to determine the index in the output file. - real_error : bool, default: False - If True, calculate the RMS real error instead of model deviation. - atomic : bool, default: False - If True, calculate the force model deviation of each atom. - relative : float, default: None - If given, calculate the relative model deviation of force. The - value is the level parameter for computing the relative model - deviation of the force. - relative_v : float, default: None - If given, calculate the relative model deviation of virial. The - value is the level parameter for computing the relative model - deviation of the virial. - **kwargs - Arbitrary keyword arguments. - """ - auto_batch_size = AutoBatchSize() - # init models - dp_models = [DeepPot(model, auto_batch_size=auto_batch_size) for model in models] - - # check type maps - tmaps = [dp.get_type_map() for dp in dp_models] - if _check_tmaps(tmaps): - tmap = tmaps[0] - else: - raise RuntimeError("The models does not have the same type map.") - - all_sys = expand_sys_str(system) - if len(all_sys) == 0: - raise RuntimeError("Did not find valid system") - devis_coll = [] - - first_dp = dp_models[0] - - for system in all_sys: - # create data-system - dp_data = DeepmdData( - system, set_prefix, shuffle_test=False, type_map=tmap, sort_atoms=False - ) - if first_dp.get_dim_fparam() > 0: - dp_data.add( - "fparam", - first_dp.get_dim_fparam(), - atomic=False, - must=True, - high_prec=False, - ) - if first_dp.get_dim_aparam() > 0: - dp_data.add( - "aparam", - first_dp.get_dim_aparam(), - atomic=True, - must=True, - high_prec=False, - ) - if real_error: - dp_data.add( - "energy", - 1, - atomic=False, - must=False, - high_prec=True, - ) - dp_data.add( - "force", - 3, - atomic=True, - must=False, - high_prec=False, - ) - dp_data.add( - "virial", - 9, - atomic=False, - must=False, - high_prec=False, - ) - - mixed_type = dp_data.mixed_type - - data_sets = [dp_data._load_set(set_name) for set_name in dp_data.dirs] - nframes_tot = 0 - devis = [] - for data in data_sets: - coord = data["coord"] - box = data["box"] - if mixed_type: - atype = data["type"] - else: - atype = data["type"][0] - if not dp_data.pbc: - box = None - if first_dp.get_dim_fparam() > 0: - fparam = data["fparam"] - else: - fparam = None - if first_dp.get_dim_aparam() > 0: - aparam = data["aparam"] - else: - aparam = None - if real_error: - natoms = atype.shape[-1] - real_data = { - "energy": data["energy"] / natoms, - "force": data["force"].reshape([-1, natoms, 3]), - "virial": data["virial"] / natoms, - } - else: - real_data = None - devi = calc_model_devi( - coord, - box, - atype, - dp_models, - mixed_type=mixed_type, - fparam=fparam, - aparam=aparam, - real_data=real_data, - atomic=atomic, - relative=relative, - relative_v=relative_v, - ) - nframes_tot += coord.shape[0] - devis.append(devi) - devis = np.vstack(devis) - devis[:, 0] = np.arange(nframes_tot) * frequency - write_model_devi_out(devis, output, header=system, atomic=atomic) - devis_coll.append(devis) - return devis_coll +__all__ = [ + "make_model_devi", + "calc_model_devi", + "write_model_devi_out", + "calc_model_devi_e", + "calc_model_devi_f", + "calc_model_devi_v", +] diff --git a/deepmd_utils/calculator.py b/deepmd_utils/calculator.py new file mode 100644 index 0000000000..5b45aa49b3 --- /dev/null +++ b/deepmd_utils/calculator.py @@ -0,0 +1,144 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""ASE calculator interface module.""" + +from pathlib import ( + Path, +) +from typing import ( + TYPE_CHECKING, + ClassVar, + Dict, + List, + Optional, + Union, +) + +from ase.calculators.calculator import ( + Calculator, + PropertyNotImplementedError, + all_changes, +) + +from deepmd_utils.infer import ( + DeepPot, +) + +if TYPE_CHECKING: + from ase import ( + Atoms, + ) + +__all__ = ["DP"] + + +class DP(Calculator): + """Implementation of ASE deepmd calculator. + + Implemented propertie are `energy`, `forces` and `stress` + + Parameters + ---------- + model : Union[str, Path] + path to the model + label : str, optional + calculator label, by default "DP" + type_dict : Dict[str, int], optional + mapping of element types and their numbers, best left None and the calculator + will infer this information from model, by default None + neighbor_list : ase.neighborlist.NeighborList, optional + The neighbor list object. If None, then build the native neighbor list. + + Examples + -------- + Compute potential energy + + >>> from ase import Atoms + >>> from deepmd.calculator import DP + >>> water = Atoms('H2O', + >>> positions=[(0.7601, 1.9270, 1), + >>> (1.9575, 1, 1), + >>> (1., 1., 1.)], + >>> cell=[100, 100, 100], + >>> calculator=DP(model="frozen_model.pb")) + >>> print(water.get_potential_energy()) + >>> print(water.get_forces()) + + Run BFGS structure optimization + + >>> from ase.optimize import BFGS + >>> dyn = BFGS(water) + >>> dyn.run(fmax=1e-6) + >>> print(water.get_positions()) + """ + + name = "DP" + implemented_properties: ClassVar[List[str]] = [ + "energy", + "free_energy", + "forces", + "virial", + "stress", + ] + + def __init__( + self, + model: Union[str, "Path"], + label: str = "DP", + type_dict: Optional[Dict[str, int]] = None, + neighbor_list=None, + **kwargs, + ) -> None: + Calculator.__init__(self, label=label, **kwargs) + self.dp = DeepPot(str(Path(model).resolve()), neighbor_list=neighbor_list) + if type_dict: + self.type_dict = type_dict + else: + self.type_dict = dict( + zip(self.dp.get_type_map(), range(self.dp.get_ntypes())) + ) + + def calculate( + self, + atoms: Optional["Atoms"] = None, + properties: List[str] = ["energy", "forces", "virial"], + system_changes: List[str] = all_changes, + ): + """Run calculation with deepmd model. + + Parameters + ---------- + atoms : Optional[Atoms], optional + atoms object to run the calculation on, by default None + properties : List[str], optional + unused, only for function signature compatibility, + by default ["energy", "forces", "stress"] + system_changes : List[str], optional + unused, only for function signature compatibility, by default all_changes + """ + if atoms is not None: + self.atoms = atoms.copy() + + coord = self.atoms.get_positions().reshape([1, -1]) + if sum(self.atoms.get_pbc()) > 0: + cell = self.atoms.get_cell().reshape([1, -1]) + else: + cell = None + symbols = self.atoms.get_chemical_symbols() + atype = [self.type_dict[k] for k in symbols] + e, f, v = self.dp.eval(coords=coord, cells=cell, atom_types=atype) + self.results["energy"] = e[0][0] + # see https://gitlab.com/ase/ase/-/merge_requests/2485 + self.results["free_energy"] = e[0][0] + self.results["forces"] = f[0] + self.results["virial"] = v[0].reshape(3, 3) + + # convert virial into stress for lattice relaxation + if "stress" in properties: + if sum(atoms.get_pbc()) > 0: + # the usual convention (tensile stress is positive) + # stress = -virial / volume + stress = -0.5 * (v[0].copy() + v[0].copy().T) / atoms.get_volume() + # Voigt notation + self.results["stress"] = stress.flat[[0, 4, 8, 5, 2, 1]] + else: + raise PropertyNotImplementedError diff --git a/deepmd_utils/infer/__init__.py b/deepmd_utils/infer/__init__.py index 644f5e1f43..b76262882e 100644 --- a/deepmd_utils/infer/__init__.py +++ b/deepmd_utils/infer/__init__.py @@ -2,5 +2,8 @@ from .deep_pot import ( DeepPot, ) +from .model_devi import ( + calc_model_devi, +) -__all__ = ["DeepPot"] +__all__ = ["DeepPot", "calc_model_devi"] diff --git a/deepmd_utils/infer/model_devi.py b/deepmd_utils/infer/model_devi.py new file mode 100644 index 0000000000..b0693f5823 --- /dev/null +++ b/deepmd_utils/infer/model_devi.py @@ -0,0 +1,506 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Optional, + Tuple, + overload, +) + +import numpy as np + +from deepmd_utils.common import ( + expand_sys_str, +) +from deepmd_utils.infer.deep_pot import ( + DeepPot, +) +from deepmd_utils.utils.data import ( + DeepmdData, +) + +try: + from typing import Literal # python >=3.8 +except ImportError: + from typing_extensions import Literal # type: ignore + + +@overload +def calc_model_devi_f( + fs: np.ndarray, + real_f: Optional[np.ndarray] = None, + relative: Optional[float] = None, + atomic: Literal[False] = False, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + ... + + +@overload +def calc_model_devi_f( + fs: np.ndarray, + real_f: Optional[np.ndarray] = None, + relative: Optional[float] = None, + *, + atomic: Literal[True], +) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + ... + + +def calc_model_devi_f( + fs: np.ndarray, + real_f: Optional[np.ndarray] = None, + relative: Optional[float] = None, + atomic: bool = False, +) -> Tuple[np.ndarray, ...]: + """Calculate model deviation of force. + + Parameters + ---------- + fs : numpy.ndarray + size of `n_models x n_frames x n_atoms x 3` + real_f : numpy.ndarray or None + real force, size of `n_frames x n_atoms x 3`. If given, + the RMS real error is calculated instead. + relative : float, default: None + If given, calculate the relative model deviation of force. The + value is the level parameter for computing the relative model + deviation of the force. + atomic : bool, default: False + Whether return deviation of force in all atoms + + Returns + ------- + max_devi_f : numpy.ndarray + maximum deviation of force in all atoms + min_devi_f : numpy.ndarray + minimum deviation of force in all atoms + avg_devi_f : numpy.ndarray + average deviation of force in all atoms + fs_devi : numpy.ndarray + deviation of force in all atoms, returned if atomic=True + """ + if real_f is None: + fs_devi = np.linalg.norm(np.std(fs, axis=0), axis=-1) + else: + fs_devi = np.linalg.norm( + np.sqrt(np.mean(np.square(fs - real_f), axis=0)), axis=-1 + ) + if relative is not None: + if real_f is None: + # if real force is not given, the magnitude is calculated from mean value of four models + # See DeepPotModelDevi::compute_relative_std_f + # See also Eq. 71 in DeePMD-kit v2 paepr + magnitude = np.linalg.norm(np.mean(fs, axis=0), axis=-1) + else: + # otherwise, the magnitude is calculated from the real force + magnitude = np.linalg.norm(real_f, axis=-1) + fs_devi /= magnitude + relative + max_devi_f = np.max(fs_devi, axis=-1) + min_devi_f = np.min(fs_devi, axis=-1) + avg_devi_f = np.mean(fs_devi, axis=-1) + if atomic: + return max_devi_f, min_devi_f, avg_devi_f, fs_devi + return max_devi_f, min_devi_f, avg_devi_f + + +def calc_model_devi_e( + es: np.ndarray, real_e: Optional[np.ndarray] = None +) -> np.ndarray: + """Calculate model deviation of total energy per atom. + + Here we don't use the atomic energy, as the decomposition + of energy is arbitrary and not unique. There is no fitting + target for atomic energy. + + Parameters + ---------- + es : numpy.ndarray + size of `n_models x n_frames x 1 + real_e : numpy.ndarray + real energy, size of `n_frames x 1`. If given, + the RMS real error is calculated instead. + + Returns + ------- + max_devi_e : numpy.ndarray + maximum deviation of energy + """ + if real_e is None: + es_devi = np.std(es, axis=0) + else: + es_devi = np.sqrt(np.mean(np.square(es - real_e), axis=0)) + es_devi = np.squeeze(es_devi, axis=-1) + return es_devi + + +def calc_model_devi_v( + vs: np.ndarray, + real_v: Optional[np.ndarray] = None, + relative: Optional[float] = None, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Calculate model deviation of virial. + + Parameters + ---------- + vs : numpy.ndarray + size of `n_models x n_frames x 9` + real_v : numpy.ndarray + real virial, size of `n_frames x 9`. If given, + the RMS real error is calculated instead. + relative : float, default: None + If given, calculate the relative model deviation of virial. The + value is the level parameter for computing the relative model + deviation of the virial. + + Returns + ------- + max_devi_v : numpy.ndarray + maximum deviation of virial in 9 elements + min_devi_v : numpy.ndarray + minimum deviation of virial in 9 elements + avg_devi_v : numpy.ndarray + average deviation of virial in 9 elements + """ + if real_v is None: + vs_devi = np.std(vs, axis=0) + else: + vs_devi = np.sqrt(np.mean(np.square(vs - real_v), axis=0)) + if relative is not None: + if real_v is None: + # if real virial is not given, the magnitude is calculated from mean value of four models + # See DeepPotModelDevi::compute_relative_std_v + # See also Eq. 72 in DeePMD-kit v2 paepr + magnitude = np.linalg.norm(np.mean(vs, axis=0), axis=-1) + else: + # otherwise, the magnitude is calculated from the real virial + magnitude = np.linalg.norm(real_v, axis=-1) + vs_devi /= magnitude + relative + max_devi_v = np.max(vs_devi, axis=-1) + min_devi_v = np.min(vs_devi, axis=-1) + avg_devi_v = np.linalg.norm(vs_devi, axis=-1) / 3 + return max_devi_v, min_devi_v, avg_devi_v + + +def write_model_devi_out( + devi: np.ndarray, fname: str, header: str = "", atomic: bool = False +): + """Write output of model deviation. + + Parameters + ---------- + devi : numpy.ndarray + the first column is the steps index + fname : str + the file name to dump + header : str, default="" + the header to dump + atomic : bool, default: False + whether atomic model deviation is printed + """ + if not atomic: + assert devi.shape[1] == 8 + else: + assert devi.shape[1] > 8 + header = "%s\n%10s" % (header, "step") + for item in "vf": + header += "%19s%19s%19s" % ( + f"max_devi_{item}", + f"min_devi_{item}", + f"avg_devi_{item}", + ) + header += "%19s" % "devi_e" + if atomic: + header += "%19s" % "atm_devi_f(N)" + with open(fname, "ab") as fp: + np.savetxt( + fp, + devi, + fmt=["%12d"] + ["%19.6e" for _ in range(devi.shape[1] - 1)], + delimiter="", + header=header, + ) + return devi + + +def _check_tmaps(tmaps, ref_tmap=None): + """Check whether type maps are identical.""" + assert isinstance(tmaps, list) + if ref_tmap is None: + ref_tmap = tmaps[0] + assert isinstance(ref_tmap, list) + + flag = True + for tmap in tmaps: + if tmap != ref_tmap: + flag = False + break + return flag + + +def calc_model_devi( + coord, + box, + atype, + models, + fname=None, + frequency=1, + mixed_type=False, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + real_data: Optional[dict] = None, + atomic: bool = False, + relative: Optional[float] = None, + relative_v: Optional[float] = None, +): + """Python interface to calculate model deviation. + + Parameters + ---------- + coord : numpy.ndarray, `n_frames x n_atoms x 3` + Coordinates of system to calculate + box : numpy.ndarray or None, `n_frames x 3 x 3` + Box to specify periodic boundary condition. If None, no pbc will be used + atype : numpy.ndarray, `n_atoms x 1` + Atom types + models : list of DeepPot models + Models used to evaluate deviation + fname : str or None + File to dump results, default None + frequency : int + Steps between frames (if the system is given by molecular dynamics engine), default 1 + mixed_type : bool + Whether the input atype is in mixed_type format or not + fparam : numpy.ndarray + frame specific parameters + aparam : numpy.ndarray + atomic specific parameters + real_data : dict, optional + real data to calculate RMS real error + atomic : bool, default: False + If True, calculate the force model deviation of each atom. + relative : float, default: None + If given, calculate the relative model deviation of force. The + value is the level parameter for computing the relative model + deviation of the force. + relative_v : float, default: None + If given, calculate the relative model deviation of virial. The + value is the level parameter for computing the relative model + deviation of the virial. + + Returns + ------- + model_devi : numpy.ndarray, `n_frames x 8` + Model deviation results. The first column is index of steps, the other 7 columns are + max_devi_v, min_devi_v, avg_devi_v, max_devi_f, min_devi_f, avg_devi_f, devi_e. + + Examples + -------- + >>> from deepmd.infer import calc_model_devi + >>> from deepmd.infer import DeepPot as DP + >>> import numpy as np + >>> coord = np.array([[1,0,0], [0,0,1.5], [1,0,3]]).reshape([1, -1]) + >>> cell = np.diag(10 * np.ones(3)).reshape([1, -1]) + >>> atype = [1,0,1] + >>> graphs = [DP("graph.000.pb"), DP("graph.001.pb")] + >>> model_devi = calc_model_devi(coord, cell, atype, graphs) + """ + energies = [] + forces = [] + virials = [] + natom = atype.shape[-1] + for dp in models: + ret = dp.eval( + coord, + box, + atype, + fparam=fparam, + aparam=aparam, + mixed_type=mixed_type, + ) + energies.append(ret[0] / natom) + forces.append(ret[1]) + virials.append(ret[2] / natom) + + energies = np.array(energies) + forces = np.array(forces) + virials = np.array(virials) + + devi = [np.arange(coord.shape[0]) * frequency] + if real_data is None: + devi += list(calc_model_devi_v(virials, relative=relative_v)) + devi_f = list(calc_model_devi_f(forces, relative=relative, atomic=atomic)) + devi += devi_f[:3] + devi.append(calc_model_devi_e(energies)) + else: + devi += list( + calc_model_devi_v(virials, real_data["virial"], relative=relative_v) + ) + devi_f = list( + calc_model_devi_f( + forces, real_data["force"], relative=relative, atomic=atomic + ) + ) + devi += devi_f[:3] + devi.append(calc_model_devi_e(energies, real_data["energy"])) + devi = np.vstack(devi).T + if atomic: + devi = np.concatenate([devi, devi_f[3]], axis=1) + if fname: + write_model_devi_out(devi, fname, atomic=atomic) + return devi + + +def make_model_devi( + *, + models: list, + system: str, + set_prefix: str, + output: str, + frequency: int, + real_error: bool = False, + atomic: bool = False, + relative: Optional[float] = None, + relative_v: Optional[float] = None, + **kwargs, +): + """Make model deviation calculation. + + Parameters + ---------- + models : list + A list of paths of models to use for making model deviation + system : str + The path of system to make model deviation calculation + set_prefix : str + The set prefix of the system + output : str + The output file for model deviation results + frequency : int + The number of steps that elapse between writing coordinates + in a trajectory by a MD engine (such as Gromacs / Lammps). + This paramter is used to determine the index in the output file. + real_error : bool, default: False + If True, calculate the RMS real error instead of model deviation. + atomic : bool, default: False + If True, calculate the force model deviation of each atom. + relative : float, default: None + If given, calculate the relative model deviation of force. The + value is the level parameter for computing the relative model + deviation of the force. + relative_v : float, default: None + If given, calculate the relative model deviation of virial. The + value is the level parameter for computing the relative model + deviation of the virial. + **kwargs + Arbitrary keyword arguments. + """ + # init models + dp_models = [DeepPot(model, auto_batch_size=True) for model in models] + + # check type maps + tmaps = [dp.get_type_map() for dp in dp_models] + if _check_tmaps(tmaps): + tmap = tmaps[0] + else: + raise RuntimeError("The models does not have the same type map.") + + all_sys = expand_sys_str(system) + if len(all_sys) == 0: + raise RuntimeError("Did not find valid system") + devis_coll = [] + + first_dp = dp_models[0] + + for system in all_sys: + # create data-system + dp_data = DeepmdData( + system, set_prefix, shuffle_test=False, type_map=tmap, sort_atoms=False + ) + if first_dp.get_dim_fparam() > 0: + dp_data.add( + "fparam", + first_dp.get_dim_fparam(), + atomic=False, + must=True, + high_prec=False, + ) + if first_dp.get_dim_aparam() > 0: + dp_data.add( + "aparam", + first_dp.get_dim_aparam(), + atomic=True, + must=True, + high_prec=False, + ) + if real_error: + dp_data.add( + "energy", + 1, + atomic=False, + must=False, + high_prec=True, + ) + dp_data.add( + "force", + 3, + atomic=True, + must=False, + high_prec=False, + ) + dp_data.add( + "virial", + 9, + atomic=False, + must=False, + high_prec=False, + ) + + mixed_type = dp_data.mixed_type + + data_sets = [dp_data._load_set(set_name) for set_name in dp_data.dirs] + nframes_tot = 0 + devis = [] + for data in data_sets: + coord = data["coord"] + box = data["box"] + if mixed_type: + atype = data["type"] + else: + atype = data["type"][0] + if not dp_data.pbc: + box = None + if first_dp.get_dim_fparam() > 0: + fparam = data["fparam"] + else: + fparam = None + if first_dp.get_dim_aparam() > 0: + aparam = data["aparam"] + else: + aparam = None + if real_error: + natoms = atype.shape[-1] + real_data = { + "energy": data["energy"] / natoms, + "force": data["force"].reshape([-1, natoms, 3]), + "virial": data["virial"] / natoms, + } + else: + real_data = None + devi = calc_model_devi( + coord, + box, + atype, + dp_models, + mixed_type=mixed_type, + fparam=fparam, + aparam=aparam, + real_data=real_data, + atomic=atomic, + relative=relative, + relative_v=relative_v, + ) + nframes_tot += coord.shape[0] + devis.append(devi) + devis = np.vstack(devis) + devis[:, 0] = np.arange(nframes_tot) * frequency + write_model_devi_out(devis, output, header=system, atomic=atomic) + devis_coll.append(devis) + return devis_coll diff --git a/doc/inference/python.md b/doc/inference/python.md index b5d3ca1efc..b01f371356 100644 --- a/doc/inference/python.md +++ b/doc/inference/python.md @@ -2,7 +2,7 @@ One may use the python interface of DeePMD-kit for model inference, an example is given as follows ```python -from deepmd.infer import DeepPot +from deepmd_utils.infer import DeepPot import numpy as np dp = DeepPot("graph.pb") @@ -15,8 +15,8 @@ where `e`, `f` and `v` are predicted energy, force and virial of the system, res Furthermore, one can use the python interface to calculate model deviation. ```python -from deepmd.infer import calc_model_devi -from deepmd.infer import DeepPot as DP +from deepmd_utils.infer import calc_model_devi +from deepmd_utils.infer import DeepPot as DP import numpy as np coord = np.array([[1, 0, 0], [0, 0, 1.5], [1, 0, 3]]).reshape([1, -1]) @@ -32,7 +32,7 @@ Note that if the model inference or model deviation is performed cyclically, one The native neighbor list algorithm of the DeePMD-kit is in $O(N^2)$ complexity ($N$ is the number of atoms). While this is not a problem for small systems that quantum methods can afford, the large systems for molecular dynamics have slow performance. -In this case, one may pass an external neighbor list that has lower complexity to {class}`DeepPot `, once it is compatible with {class}`ase.neighborlist.NewPrimitiveNeighborList`. +In this case, one may pass an external neighbor list that has lower complexity to {class}`DeepPot `, once it is compatible with {class}`ase.neighborlist.NewPrimitiveNeighborList`. ```py import ase.neighborlist @@ -43,4 +43,4 @@ neighbor_list = ase.neighborlist.NewPrimitiveNeighborList( dp = DeepPot("graph.pb", neighbor_list=neighbor_list) ``` -The `update` and `build` methods will be called by {class}`DeepPot `, and `first_neigh`, `pair_second`, and `offset_vec` properties will be used. +The `update` and `build` methods will be called by {class}`DeepPot `, and `first_neigh`, `pair_second`, and `offset_vec` properties will be used. diff --git a/doc/third-party/ase.md b/doc/third-party/ase.md index ac65fc926e..d9ab67ae3d 100644 --- a/doc/third-party/ase.md +++ b/doc/third-party/ase.md @@ -3,7 +3,7 @@ Deep potential can be set up as a calculator with ASE to obtain potential energies and forces. ```python from ase import Atoms -from deepmd.calculator import DP +from deepmd_utils.calculator import DP water = Atoms( "H2O", From 3ee3f4ce0df5834ec0db5bed3b9820a122d287f8 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 24 Jan 2024 10:25:13 -0500 Subject: [PATCH 009/686] add more abstractmethods to universal `DeepPot` (#3175) They are used by the downstream APIs, so must be implemented. --------- Signed-off-by: Jinzhe Zeng --- deepmd_utils/infer/deep_pot.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/deepmd_utils/infer/deep_pot.py b/deepmd_utils/infer/deep_pot.py index dec0a7c47c..66510ea349 100644 --- a/deepmd_utils/infer/deep_pot.py +++ b/deepmd_utils/infer/deep_pot.py @@ -122,5 +122,21 @@ def eval( # dpdata # ase + @abstractmethod + def get_ntypes(self) -> int: + """Get the number of atom types of this model.""" + + @abstractmethod + def get_type_map(self) -> List[str]: + """Get the type map (element name of the atom types) of this model.""" + + @abstractmethod + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this DP.""" + + @abstractmethod + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this DP.""" + __all__ = ["DeepPot"] From 663e4a8bcb38db7717f54de4f1b907ace02cec47 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 24 Jan 2024 23:50:51 -0500 Subject: [PATCH 010/686] cc: reimplement read_file_to_string without calling TensorFlow (#3176) LAMMPS is using it Signed-off-by: Jinzhe Zeng --- source/api_cc/src/common.cc | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/source/api_cc/src/common.cc b/source/api_cc/src/common.cc index a552f646f1..2923534fb7 100644 --- a/source/api_cc/src/common.cc +++ b/source/api_cc/src/common.cc @@ -4,6 +4,9 @@ #include #include +#include +#include +#include #include "AtomMap.h" #include "device.h" @@ -1112,12 +1115,16 @@ template void deepmd::select_map_inv( #endif void deepmd::read_file_to_string(std::string model, std::string& file_content) { -#ifdef BUILD_TENSORFLOW - deepmd::check_status(tensorflow::ReadFileToString(tensorflow::Env::Default(), - model, &file_content)); -#else - throw deepmd::deepmd_exception("TODO: read_file_to_string only support TF"); -#endif + // generated by GitHub Copilot + std::ifstream file(model); + if (file.is_open()) { + std::stringstream buffer; + buffer << file.rdbuf(); + file_content = buffer.str(); + file.close(); + } else { + throw deepmd::deepmd_exception("Failed to open file: " + model); + } } void deepmd::convert_pbtxt_to_pb(std::string fn_pb_txt, std::string fn_pb) { From 5b9dd3d23fb711f2ee7ce093df14536e66cc3968 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 25 Jan 2024 00:05:16 -0500 Subject: [PATCH 011/686] breaking: move deepmd to deepmd.tf (#3177) Signed-off-by: Jinzhe Zeng --- backend/read_env.py | 2 +- deepmd/__init__.py | 50 ------------ deepmd/{ => tf}/__about__.py | 0 deepmd/tf/__init__.py | 61 ++++++++++++++ deepmd/{ => tf}/__main__.py | 0 deepmd/{ => tf}/calculator.py | 0 deepmd/{ => tf}/cluster/__init__.py | 0 deepmd/{ => tf}/cluster/local.py | 2 +- deepmd/{ => tf}/cluster/slurm.py | 2 +- deepmd/{ => tf}/common.py | 2 +- deepmd/{ => tf}/descriptor/__init__.py | 0 deepmd/{ => tf}/descriptor/descriptor.py | 16 ++-- deepmd/{ => tf}/descriptor/hybrid.py | 26 +++--- deepmd/{ => tf}/descriptor/loc_frame.py | 16 ++-- deepmd/{ => tf}/descriptor/se.py | 8 +- deepmd/{ => tf}/descriptor/se_a.py | 36 ++++----- deepmd/{ => tf}/descriptor/se_a_ebd.py | 6 +- deepmd/{ => tf}/descriptor/se_a_ebd_v2.py | 2 +- deepmd/{ => tf}/descriptor/se_a_ef.py | 16 ++-- deepmd/{ => tf}/descriptor/se_a_mask.py | 18 ++--- deepmd/{ => tf}/descriptor/se_atten.py | 32 ++++---- deepmd/{ => tf}/descriptor/se_atten_v2.py | 0 deepmd/{ => tf}/descriptor/se_r.py | 24 +++--- deepmd/{ => tf}/descriptor/se_t.py | 22 ++--- deepmd/{ => tf}/entrypoints/__init__.py | 0 deepmd/{ => tf}/entrypoints/compress.py | 12 +-- deepmd/{ => tf}/entrypoints/convert.py | 2 +- deepmd/{ => tf}/entrypoints/doc.py | 0 deepmd/{ => tf}/entrypoints/freeze.py | 12 +-- deepmd/{ => tf}/entrypoints/gui.py | 0 deepmd/{ => tf}/entrypoints/ipi.py | 2 +- deepmd/{ => tf}/entrypoints/main.py | 8 +- deepmd/{ => tf}/entrypoints/neighbor_stat.py | 6 +- deepmd/{ => tf}/entrypoints/test.py | 14 ++-- deepmd/{ => tf}/entrypoints/train.py | 28 +++---- deepmd/{ => tf}/entrypoints/transfer.py | 2 +- deepmd/{ => tf}/env.py | 0 deepmd/{ => tf}/fit/__init__.py | 0 deepmd/{ => tf}/fit/dipole.py | 14 ++-- deepmd/{ => tf}/fit/dos.py | 22 ++--- deepmd/{ => tf}/fit/ener.py | 26 +++--- deepmd/{ => tf}/fit/fitting.py | 6 +- deepmd/{ => tf}/fit/polar.py | 16 ++-- deepmd/{ => tf}/infer/__init__.py | 0 deepmd/{ => tf}/infer/data_modifier.py | 14 ++-- deepmd/{ => tf}/infer/deep_dipole.py | 2 +- deepmd/{ => tf}/infer/deep_dos.py | 8 +- deepmd/{ => tf}/infer/deep_eval.py | 10 +-- deepmd/{ => tf}/infer/deep_polar.py | 2 +- deepmd/{ => tf}/infer/deep_pot.py | 12 +-- deepmd/{ => tf}/infer/deep_tensor.py | 6 +- deepmd/{ => tf}/infer/deep_wfc.py | 2 +- deepmd/{ => tf}/infer/ewald_recp.py | 4 +- deepmd/{ => tf}/infer/model_devi.py | 0 deepmd/{ => tf}/lmp.py | 2 +- deepmd/{ => tf}/loggers/__init__.py | 0 deepmd/{ => tf}/loggers/loggers.py | 0 deepmd/{ => tf}/loss/__init__.py | 0 deepmd/{ => tf}/loss/dos.py | 6 +- deepmd/{ => tf}/loss/ener.py | 6 +- deepmd/{ => tf}/loss/loss.py | 2 +- deepmd/{ => tf}/loss/tensor.py | 6 +- deepmd/{ => tf}/model/__init__.py | 0 deepmd/{ => tf}/model/dos.py | 4 +- deepmd/{ => tf}/model/ener.py | 8 +- deepmd/{ => tf}/model/frozen.py | 8 +- deepmd/{ => tf}/model/linear.py | 6 +- deepmd/{ => tf}/model/model.py | 32 ++++---- deepmd/{ => tf}/model/model_stat.py | 0 deepmd/{ => tf}/model/multi.py | 20 ++--- deepmd/{ => tf}/model/pairtab.py | 12 +-- deepmd/{ => tf}/model/pairwise_dprc.py | 18 ++--- deepmd/{ => tf}/model/tensor.py | 4 +- deepmd/{ => tf}/nvnmd/__init__.py | 0 deepmd/{ => tf}/nvnmd/data/__init__.py | 0 deepmd/{ => tf}/nvnmd/data/data.py | 0 deepmd/{ => tf}/nvnmd/descriptor/__init__.py | 0 deepmd/{ => tf}/nvnmd/descriptor/se_a.py | 10 +-- deepmd/{ => tf}/nvnmd/descriptor/se_atten.py | 8 +- deepmd/{ => tf}/nvnmd/entrypoints/__init__.py | 0 deepmd/{ => tf}/nvnmd/entrypoints/freeze.py | 6 +- deepmd/{ => tf}/nvnmd/entrypoints/mapt.py | 14 ++-- deepmd/{ => tf}/nvnmd/entrypoints/train.py | 16 ++-- deepmd/{ => tf}/nvnmd/entrypoints/wrap.py | 16 ++-- deepmd/{ => tf}/nvnmd/fit/__init__.py | 0 deepmd/{ => tf}/nvnmd/fit/ener.py | 6 +- deepmd/{ => tf}/nvnmd/utils/__init__.py | 0 deepmd/{ => tf}/nvnmd/utils/argcheck.py | 0 deepmd/{ => tf}/nvnmd/utils/config.py | 6 +- deepmd/{ => tf}/nvnmd/utils/encode.py | 2 +- deepmd/{ => tf}/nvnmd/utils/fio.py | 0 deepmd/{ => tf}/nvnmd/utils/network.py | 8 +- deepmd/{ => tf}/nvnmd/utils/op.py | 0 deepmd/{ => tf}/nvnmd/utils/weight.py | 4 +- deepmd/{ => tf}/op/__init__.py | 2 +- deepmd/{ => tf}/op/_add_flt_nvnmd_grad.py | 2 +- deepmd/{ => tf}/op/_copy_flt_nvnmd_grad.py | 2 +- deepmd/{ => tf}/op/_dotmul_flt_nvnmd_grad.py | 2 +- deepmd/{ => tf}/op/_flt_nvnmd_grad.py | 2 +- deepmd/{ => tf}/op/_gelu.py | 2 +- deepmd/{ => tf}/op/_map_flt_nvnmd_grad.py | 2 +- .../{ => tf}/op/_matmul_fitnet_nvnmd_grad.py | 2 +- deepmd/{ => tf}/op/_matmul_flt2fix_nvnmd.py | 2 +- deepmd/{ => tf}/op/_matmul_flt_nvnmd_grad.py | 2 +- deepmd/{ => tf}/op/_mul_flt_nvnmd_grad.py | 2 +- deepmd/{ => tf}/op/_prod_force_grad.py | 2 +- deepmd/{ => tf}/op/_prod_force_se_a_grad.py | 2 +- .../{ => tf}/op/_prod_force_se_a_mask_grad.py | 2 +- deepmd/{ => tf}/op/_prod_force_se_r_grad.py | 2 +- deepmd/{ => tf}/op/_prod_virial_grad.py | 2 +- deepmd/{ => tf}/op/_prod_virial_se_a_grad.py | 2 +- deepmd/{ => tf}/op/_prod_virial_se_r_grad.py | 2 +- deepmd/{ => tf}/op/_quantize_nvnmd_grad.py | 2 +- deepmd/{ => tf}/op/_soft_min_force_grad.py | 2 +- deepmd/{ => tf}/op/_soft_min_virial_grad.py | 2 +- deepmd/{ => tf}/op/_tabulate_grad.py | 4 +- deepmd/{ => tf}/op/_tanh4_flt_nvnmd_grad.py | 2 +- deepmd/{ => tf}/train/__init__.py | 0 deepmd/{ => tf}/train/run_options.py | 6 +- deepmd/{ => tf}/train/trainer.py | 26 +++--- deepmd/{ => tf}/utils/__init__.py | 0 deepmd/{ => tf}/utils/argcheck.py | 0 deepmd/{ => tf}/utils/batch_size.py | 4 +- deepmd/{ => tf}/utils/compat.py | 0 deepmd/{ => tf}/utils/compress.py | 4 +- deepmd/{ => tf}/utils/convert.py | 4 +- deepmd/{ => tf}/utils/data.py | 0 deepmd/{ => tf}/utils/data_system.py | 0 deepmd/{ => tf}/utils/errors.py | 0 deepmd/{ => tf}/utils/finetune.py | 4 +- deepmd/{ => tf}/utils/graph.py | 6 +- deepmd/{ => tf}/utils/learning_rate.py | 2 +- deepmd/{ => tf}/utils/multi_init.py | 4 +- deepmd/{ => tf}/utils/neighbor_stat.py | 6 +- deepmd/{ => tf}/utils/network.py | 4 +- deepmd/{ => tf}/utils/pair_tab.py | 0 deepmd/{ => tf}/utils/parallel_op.py | 8 +- deepmd/{ => tf}/utils/path.py | 0 deepmd/{ => tf}/utils/plugin.py | 0 deepmd/{ => tf}/utils/random.py | 0 deepmd/{ => tf}/utils/sess.py | 4 +- deepmd/{ => tf}/utils/spin.py | 2 +- deepmd/{ => tf}/utils/tabulate.py | 80 +++++++++---------- deepmd/{ => tf}/utils/type_embed.py | 10 +-- deepmd/{ => tf}/utils/weight_avg.py | 0 deepmd_utils/calculator.py | 2 +- deepmd_utils/infer/deep_pot.py | 2 +- deepmd_utils/infer/model_devi.py | 4 +- deepmd_utils/main.py | 2 +- deepmd_utils/model_format/se_e2_a.py | 2 +- deepmd_utils/utils/argcheck.py | 2 +- deepmd_utils/utils/compat.py | 2 +- doc/api_op.rst | 4 +- doc/cli.rst | 2 +- doc/conf.py | 4 +- doc/development/create-a-model.md | 6 +- doc/getting-started/quick_start.ipynb | 2 +- doc/train/train-input.rst | 2 +- pyproject.toml | 6 +- source/install/docker/Dockerfile | 2 +- source/ipi/tests/test_driver.py | 2 +- source/lmp/tests/test_deeptensor.py | 4 +- source/lmp/tests/test_dplr.py | 2 +- source/lmp/tests/test_lammps.py | 4 +- source/lmp/tests/test_lammps_3types.py | 4 +- source/lmp/tests/test_lammps_faparam.py | 2 +- source/tests/common.py | 8 +- source/tests/test_activation_fn_gelu.py | 6 +- source/tests/test_adjust_sel.py | 6 +- source/tests/test_argument_parser.py | 2 +- source/tests/test_auto_batch_size.py | 4 +- source/tests/test_cluster.py | 2 +- source/tests/test_common.py | 6 +- source/tests/test_compat_input.py | 2 +- source/tests/test_compressed_training.py | 4 +- source/tests/test_data_large_batch.py | 14 ++-- source/tests/test_data_modifier.py | 12 +-- source/tests/test_data_modifier_shuffle.py | 14 ++-- source/tests/test_data_requirement.py | 2 +- source/tests/test_deepdipole.py | 6 +- source/tests/test_deepdos.py | 6 +- source/tests/test_deepmd_data.py | 4 +- source/tests/test_deepmd_data_sys.py | 6 +- source/tests/test_deeppolar.py | 6 +- source/tests/test_deeppot_a.py | 8 +- source/tests/test_deeppot_r.py | 6 +- source/tests/test_deeppot_spin.py | 6 +- source/tests/test_descrpt_hybrid.py | 8 +- source/tests/test_descrpt_nonsmth.py | 2 +- source/tests/test_descrpt_se_a_mask.py | 10 +-- source/tests/test_descrpt_se_a_type.py | 8 +- source/tests/test_descrpt_se_atten.py | 8 +- source/tests/test_descrpt_se_r.py | 2 +- source/tests/test_descrpt_sea_ef.py | 2 +- source/tests/test_descrpt_sea_ef_para.py | 2 +- source/tests/test_descrpt_sea_ef_rot.py | 4 +- source/tests/test_descrpt_sea_ef_vert.py | 2 +- source/tests/test_descrpt_smooth.py | 2 +- source/tests/test_dipole_se_a.py | 10 +-- source/tests/test_dipole_se_a_tebd.py | 12 +-- source/tests/test_dipolecharge.py | 6 +- source/tests/test_dp_test.py | 4 +- source/tests/test_embedding_net.py | 4 +- source/tests/test_env.py | 4 +- source/tests/test_ewald.py | 4 +- source/tests/test_examples.py | 4 +- source/tests/test_finetune_se_atten.py | 12 +-- source/tests/test_fitting_dos.py | 8 +- source/tests/test_fitting_ener_type.py | 8 +- source/tests/test_fitting_stat.py | 4 +- source/tests/test_gen_stat_data.py | 10 +-- source/tests/test_get_potential.py | 4 +- source/tests/test_init_frz_model_multi.py | 14 ++-- source/tests/test_init_frz_model_se_a.py | 12 +-- source/tests/test_init_frz_model_se_a_tebd.py | 12 +-- source/tests/test_init_frz_model_se_a_type.py | 12 +-- source/tests/test_init_frz_model_se_atten.py | 12 +-- source/tests/test_init_frz_model_se_r.py | 12 +-- source/tests/test_init_frz_model_spin.py | 12 +-- source/tests/test_lammps.py | 2 +- source/tests/test_layer_name.py | 10 +-- source/tests/test_linear_model.py | 8 +- source/tests/test_loss_gf.py | 2 +- source/tests/test_mixed_prec_training.py | 4 +- source/tests/test_model_compression_se_a.py | 8 +- .../tests/test_model_compression_se_a_ebd.py | 8 +- ...odel_compression_se_a_ebd_type_one_side.py | 8 +- ...ession_se_a_type_one_side_exclude_types.py | 6 +- .../tests/test_model_compression_se_atten.py | 8 +- source/tests/test_model_compression_se_r.py | 8 +- source/tests/test_model_compression_se_t.py | 8 +- source/tests/test_model_devi.py | 6 +- source/tests/test_model_devi_mix.py | 8 +- source/tests/test_model_dos.py | 10 +-- source/tests/test_model_loc_frame.py | 10 +-- source/tests/test_model_multi.py | 10 +-- source/tests/test_model_pairtab.py | 6 +- source/tests/test_model_se_a.py | 12 +-- source/tests/test_model_se_a_aparam.py | 10 +-- source/tests/test_model_se_a_ebd.py | 10 +-- source/tests/test_model_se_a_ebd_v2.py | 12 +-- source/tests/test_model_se_a_fparam.py | 10 +-- source/tests/test_model_se_a_srtab.py | 10 +-- source/tests/test_model_se_a_type.py | 12 +-- source/tests/test_model_se_atten.py | 12 +-- source/tests/test_model_se_r.py | 10 +-- source/tests/test_model_se_t.py | 10 +-- source/tests/test_model_spin.py | 12 +-- source/tests/test_neighbor_stat.py | 2 +- source/tests/test_nvnmd_entrypoints.py | 22 ++--- source/tests/test_nvnmd_op.py | 2 +- source/tests/test_pairwise_dprc.py | 14 ++-- source/tests/test_parallel_training.py | 2 +- source/tests/test_polar_se_a.py | 10 +-- source/tests/test_polar_se_a_tebd.py | 12 +-- source/tests/test_prod_env_mat.py | 2 +- source/tests/test_prod_force.py | 2 +- source/tests/test_prod_force_grad.py | 2 +- source/tests/test_prod_virial.py | 2 +- source/tests/test_prod_virial_grad.py | 2 +- source/tests/test_sel_idx.py | 2 +- source/tests/test_tab_nonsmth.py | 6 +- source/tests/test_tab_smooth.py | 4 +- source/tests/test_tabulate.py | 4 +- source/tests/test_train.py | 16 ++-- source/tests/test_transfer.py | 6 +- source/tests/test_type_embed.py | 4 +- source/tests/test_type_one_side.py | 6 +- source/tests/test_uni_infer.py | 4 +- source/tests/test_virtual_type.py | 10 +-- 270 files changed, 921 insertions(+), 910 deletions(-) rename deepmd/{ => tf}/__about__.py (100%) create mode 100644 deepmd/tf/__init__.py rename deepmd/{ => tf}/__main__.py (100%) rename deepmd/{ => tf}/calculator.py (100%) rename deepmd/{ => tf}/cluster/__init__.py (100%) rename deepmd/{ => tf}/cluster/local.py (98%) rename deepmd/{ => tf}/cluster/slurm.py (97%) rename deepmd/{ => tf}/common.py (99%) rename deepmd/{ => tf}/descriptor/__init__.py (100%) rename deepmd/{ => tf}/descriptor/descriptor.py (97%) rename deepmd/{ => tf}/descriptor/hybrid.py (95%) rename deepmd/{ => tf}/descriptor/loc_frame.py (97%) rename deepmd/{ => tf}/descriptor/se.py (95%) rename deepmd/{ => tf}/descriptor/se_a.py (98%) rename deepmd/{ => tf}/descriptor/se_a_ebd.py (99%) rename deepmd/{ => tf}/descriptor/se_a_ebd_v2.py (98%) rename deepmd/{ => tf}/descriptor/se_a_ef.py (97%) rename deepmd/{ => tf}/descriptor/se_a_mask.py (96%) rename deepmd/{ => tf}/descriptor/se_atten.py (98%) rename deepmd/{ => tf}/descriptor/se_atten_v2.py (100%) rename deepmd/{ => tf}/descriptor/se_r.py (97%) rename deepmd/{ => tf}/descriptor/se_t.py (97%) rename deepmd/{ => tf}/entrypoints/__init__.py (100%) rename deepmd/{ => tf}/entrypoints/compress.py (96%) rename deepmd/{ => tf}/entrypoints/convert.py (97%) rename deepmd/{ => tf}/entrypoints/doc.py (100%) rename deepmd/{ => tf}/entrypoints/freeze.py (98%) rename deepmd/{ => tf}/entrypoints/gui.py (100%) rename deepmd/{ => tf}/entrypoints/ipi.py (95%) rename deepmd/{ => tf}/entrypoints/main.py (94%) rename deepmd/{ => tf}/entrypoints/neighbor_stat.py (92%) rename deepmd/{ => tf}/entrypoints/test.py (99%) rename deepmd/{ => tf}/entrypoints/train.py (96%) rename deepmd/{ => tf}/entrypoints/transfer.py (99%) rename deepmd/{ => tf}/env.py (100%) rename deepmd/{ => tf}/fit/__init__.py (100%) rename deepmd/{ => tf}/fit/dipole.py (97%) rename deepmd/{ => tf}/fit/dos.py (98%) rename deepmd/{ => tf}/fit/ener.py (98%) rename deepmd/{ => tf}/fit/fitting.py (96%) rename deepmd/{ => tf}/fit/polar.py (98%) rename deepmd/{ => tf}/infer/__init__.py (100%) rename deepmd/{ => tf}/infer/data_modifier.py (98%) rename deepmd/{ => tf}/infer/deep_dipole.py (97%) rename deepmd/{ => tf}/infer/deep_dos.py (99%) rename deepmd/{ => tf}/infer/deep_eval.py (98%) rename deepmd/{ => tf}/infer/deep_polar.py (99%) rename deepmd/{ => tf}/infer/deep_pot.py (99%) rename deepmd/{ => tf}/infer/deep_tensor.py (99%) rename deepmd/{ => tf}/infer/deep_wfc.py (97%) rename deepmd/{ => tf}/infer/ewald_recp.py (97%) rename deepmd/{ => tf}/infer/model_devi.py (100%) rename deepmd/{ => tf}/lmp.py (99%) rename deepmd/{ => tf}/loggers/__init__.py (100%) rename deepmd/{ => tf}/loggers/loggers.py (100%) rename deepmd/{ => tf}/loss/__init__.py (100%) rename deepmd/{ => tf}/loss/dos.py (98%) rename deepmd/{ => tf}/loss/ener.py (99%) rename deepmd/{ => tf}/loss/loss.py (98%) rename deepmd/{ => tf}/loss/tensor.py (98%) rename deepmd/{ => tf}/model/__init__.py (100%) rename deepmd/{ => tf}/model/dos.py (99%) rename deepmd/{ => tf}/model/ener.py (99%) rename deepmd/{ => tf}/model/frozen.py (97%) rename deepmd/{ => tf}/model/linear.py (98%) rename deepmd/{ => tf}/model/model.py (97%) rename deepmd/{ => tf}/model/model_stat.py (100%) rename deepmd/{ => tf}/model/multi.py (98%) rename deepmd/{ => tf}/model/pairtab.py (97%) rename deepmd/{ => tf}/model/pairwise_dprc.py (97%) rename deepmd/{ => tf}/model/tensor.py (99%) rename deepmd/{ => tf}/nvnmd/__init__.py (100%) rename deepmd/{ => tf}/nvnmd/data/__init__.py (100%) rename deepmd/{ => tf}/nvnmd/data/data.py (100%) rename deepmd/{ => tf}/nvnmd/descriptor/__init__.py (100%) rename deepmd/{ => tf}/nvnmd/descriptor/se_a.py (98%) rename deepmd/{ => tf}/nvnmd/descriptor/se_atten.py (98%) rename deepmd/{ => tf}/nvnmd/entrypoints/__init__.py (100%) rename deepmd/{ => tf}/nvnmd/entrypoints/freeze.py (96%) rename deepmd/{ => tf}/nvnmd/entrypoints/mapt.py (98%) rename deepmd/{ => tf}/nvnmd/entrypoints/train.py (94%) rename deepmd/{ => tf}/nvnmd/entrypoints/wrap.py (98%) rename deepmd/{ => tf}/nvnmd/fit/__init__.py (100%) rename deepmd/{ => tf}/nvnmd/fit/ener.py (58%) rename deepmd/{ => tf}/nvnmd/utils/__init__.py (100%) rename deepmd/{ => tf}/nvnmd/utils/argcheck.py (100%) rename deepmd/{ => tf}/nvnmd/utils/config.py (99%) rename deepmd/{ => tf}/nvnmd/utils/encode.py (99%) rename deepmd/{ => tf}/nvnmd/utils/fio.py (100%) rename deepmd/{ => tf}/nvnmd/utils/network.py (98%) rename deepmd/{ => tf}/nvnmd/utils/op.py (100%) rename deepmd/{ => tf}/nvnmd/utils/weight.py (98%) rename deepmd/{ => tf}/op/__init__.py (96%) rename deepmd/{ => tf}/op/_add_flt_nvnmd_grad.py (90%) rename deepmd/{ => tf}/op/_copy_flt_nvnmd_grad.py (91%) rename deepmd/{ => tf}/op/_dotmul_flt_nvnmd_grad.py (95%) rename deepmd/{ => tf}/op/_flt_nvnmd_grad.py (90%) rename deepmd/{ => tf}/op/_gelu.py (97%) rename deepmd/{ => tf}/op/_map_flt_nvnmd_grad.py (97%) rename deepmd/{ => tf}/op/_matmul_fitnet_nvnmd_grad.py (94%) rename deepmd/{ => tf}/op/_matmul_flt2fix_nvnmd.py (97%) rename deepmd/{ => tf}/op/_matmul_flt_nvnmd_grad.py (97%) rename deepmd/{ => tf}/op/_mul_flt_nvnmd_grad.py (96%) rename deepmd/{ => tf}/op/_prod_force_grad.py (95%) rename deepmd/{ => tf}/op/_prod_force_se_a_grad.py (95%) rename deepmd/{ => tf}/op/_prod_force_se_a_mask_grad.py (95%) rename deepmd/{ => tf}/op/_prod_force_se_r_grad.py (93%) rename deepmd/{ => tf}/op/_prod_virial_grad.py (95%) rename deepmd/{ => tf}/op/_prod_virial_se_a_grad.py (95%) rename deepmd/{ => tf}/op/_prod_virial_se_r_grad.py (94%) rename deepmd/{ => tf}/op/_quantize_nvnmd_grad.py (93%) rename deepmd/{ => tf}/op/_soft_min_force_grad.py (95%) rename deepmd/{ => tf}/op/_soft_min_virial_grad.py (95%) rename deepmd/{ => tf}/op/_tabulate_grad.py (97%) rename deepmd/{ => tf}/op/_tanh4_flt_nvnmd_grad.py (97%) rename deepmd/{ => tf}/train/__init__.py (100%) rename deepmd/{ => tf}/train/run_options.py (98%) rename deepmd/{ => tf}/train/trainer.py (99%) rename deepmd/{ => tf}/utils/__init__.py (100%) rename deepmd/{ => tf}/utils/argcheck.py (100%) rename deepmd/{ => tf}/utils/batch_size.py (94%) rename deepmd/{ => tf}/utils/compat.py (100%) rename deepmd/{ => tf}/utils/compress.py (98%) rename deepmd/{ => tf}/utils/convert.py (99%) rename deepmd/{ => tf}/utils/data.py (100%) rename deepmd/{ => tf}/utils/data_system.py (100%) rename deepmd/{ => tf}/utils/errors.py (100%) rename deepmd/{ => tf}/utils/finetune.py (98%) rename deepmd/{ => tf}/utils/graph.py (99%) rename deepmd/{ => tf}/utils/learning_rate.py (99%) rename deepmd/{ => tf}/utils/multi_init.py (98%) rename deepmd/{ => tf}/utils/neighbor_stat.py (98%) rename deepmd/{ => tf}/utils/network.py (99%) rename deepmd/{ => tf}/utils/pair_tab.py (100%) rename deepmd/{ => tf}/utils/parallel_op.py (94%) rename deepmd/{ => tf}/utils/path.py (100%) rename deepmd/{ => tf}/utils/plugin.py (100%) rename deepmd/{ => tf}/utils/random.py (100%) rename deepmd/{ => tf}/utils/sess.py (95%) rename deepmd/{ => tf}/utils/spin.py (98%) rename deepmd/{ => tf}/utils/tabulate.py (92%) rename deepmd/{ => tf}/utils/type_embed.py (97%) rename deepmd/{ => tf}/utils/weight_avg.py (100%) diff --git a/backend/read_env.py b/backend/read_env.py index 079211d4d7..ba6bf5f9f3 100644 --- a/backend/read_env.py +++ b/backend/read_env.py @@ -78,7 +78,7 @@ def get_argument_from_env() -> Tuple[str, list, list, dict, str]: cmake_args.append(f"-DLAMMPS_VERSION={dp_lammps_version}") if dp_ipi == "1": cmake_args.append("-DENABLE_IPI:BOOL=TRUE") - extra_scripts["dp_ipi"] = "deepmd.entrypoints.ipi:dp_ipi" + extra_scripts["dp_ipi"] = "deepmd.tf.entrypoints.ipi:dp_ipi" tf_install_dir, _ = find_tensorflow() tf_version = get_tf_version(tf_install_dir) diff --git a/deepmd/__init__.py b/deepmd/__init__.py index 0190bbc124..5fc690b94f 100644 --- a/deepmd/__init__.py +++ b/deepmd/__init__.py @@ -1,36 +1,4 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -"""Root of the deepmd package, exposes all public classes and submodules.""" - -try: - from importlib import ( - metadata, - ) -except ImportError: # for Python<3.8 - import importlib_metadata as metadata - -import deepmd.utils.network as network - -from . import ( - cluster, - descriptor, - fit, - loss, - nvnmd, - utils, -) -from .env import ( - set_mkl, -) -from .infer import ( - DeepEval, - DeepPotential, -) -from .infer.data_modifier import ( - DipoleChargeModifier, -) - -set_mkl() - try: from deepmd_utils._version import version as __version__ except ImportError: @@ -38,24 +6,6 @@ __version__, ) -# load third-party plugins -try: - eps = metadata.entry_points(group="deepmd") -except TypeError: - eps = metadata.entry_points().get("deepmd", []) -for ep in eps: - ep.load() - __all__ = [ "__version__", - "descriptor", - "fit", - "loss", - "utils", - "cluster", - "network", - "DeepEval", - "DeepPotential", - "DipoleChargeModifier", - "nvnmd", ] diff --git a/deepmd/__about__.py b/deepmd/tf/__about__.py similarity index 100% rename from deepmd/__about__.py rename to deepmd/tf/__about__.py diff --git a/deepmd/tf/__init__.py b/deepmd/tf/__init__.py new file mode 100644 index 0000000000..faa0b20bab --- /dev/null +++ b/deepmd/tf/__init__.py @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Root of the deepmd package, exposes all public classes and submodules.""" + +try: + from importlib import ( + metadata, + ) +except ImportError: # for Python<3.8 + import importlib_metadata as metadata + +import deepmd.tf.utils.network as network + +from . import ( + cluster, + descriptor, + fit, + loss, + nvnmd, + utils, +) +from .env import ( + set_mkl, +) +from .infer import ( + DeepEval, + DeepPotential, +) +from .infer.data_modifier import ( + DipoleChargeModifier, +) + +set_mkl() + +try: + from deepmd_utils._version import version as __version__ +except ImportError: + from .__about__ import ( + __version__, + ) + +# load third-party plugins +try: + eps = metadata.entry_points(group="deepmd") +except TypeError: + eps = metadata.entry_points().get("deepmd", []) +for ep in eps: + ep.load() + +__all__ = [ + "__version__", + "descriptor", + "fit", + "loss", + "utils", + "cluster", + "network", + "DeepEval", + "DeepPotential", + "DipoleChargeModifier", + "nvnmd", +] diff --git a/deepmd/__main__.py b/deepmd/tf/__main__.py similarity index 100% rename from deepmd/__main__.py rename to deepmd/tf/__main__.py diff --git a/deepmd/calculator.py b/deepmd/tf/calculator.py similarity index 100% rename from deepmd/calculator.py rename to deepmd/tf/calculator.py diff --git a/deepmd/cluster/__init__.py b/deepmd/tf/cluster/__init__.py similarity index 100% rename from deepmd/cluster/__init__.py rename to deepmd/tf/cluster/__init__.py diff --git a/deepmd/cluster/local.py b/deepmd/tf/cluster/local.py similarity index 98% rename from deepmd/cluster/local.py rename to deepmd/tf/cluster/local.py index 3c12c9dc85..bd0e4c86aa 100644 --- a/deepmd/cluster/local.py +++ b/deepmd/tf/cluster/local.py @@ -10,7 +10,7 @@ Tuple, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) diff --git a/deepmd/cluster/slurm.py b/deepmd/tf/cluster/slurm.py similarity index 97% rename from deepmd/cluster/slurm.py rename to deepmd/tf/cluster/slurm.py index 5264622232..7a7ebcee3e 100644 --- a/deepmd/cluster/slurm.py +++ b/deepmd/tf/cluster/slurm.py @@ -15,7 +15,7 @@ import hostlist -from deepmd.cluster import ( +from deepmd.tf.cluster import ( local, ) diff --git a/deepmd/common.py b/deepmd/tf/common.py similarity index 99% rename from deepmd/common.py rename to deepmd/tf/common.py index 54e3d0a6f8..9860f82017 100644 --- a/deepmd/common.py +++ b/deepmd/tf/common.py @@ -17,7 +17,7 @@ tensor_util, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, op_module, tf, diff --git a/deepmd/descriptor/__init__.py b/deepmd/tf/descriptor/__init__.py similarity index 100% rename from deepmd/descriptor/__init__.py rename to deepmd/tf/descriptor/__init__.py diff --git a/deepmd/descriptor/descriptor.py b/deepmd/tf/descriptor/descriptor.py similarity index 97% rename from deepmd/descriptor/descriptor.py rename to deepmd/tf/descriptor/descriptor.py index bd731004cb..9bdda3ec37 100644 --- a/deepmd/descriptor/descriptor.py +++ b/deepmd/tf/descriptor/descriptor.py @@ -13,11 +13,11 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, tf, ) -from deepmd.utils import ( +from deepmd.tf.utils import ( Plugin, PluginVariant, ) @@ -34,7 +34,7 @@ class Descriptor(PluginVariant): -------- >>> descript = Descriptor(type="se_e2_a", rcut=6., rcut_smth=0.5, sel=[50]) >>> type(descript) - + Notes ----- @@ -174,18 +174,18 @@ def compute_input_stats( ---------- data_coord : list[np.ndarray] The coordinates. Can be generated by - :meth:`deepmd.model.model_stat.make_stat_input` + :meth:`deepmd.tf.model.model_stat.make_stat_input` data_box : list[np.ndarray] The box. Can be generated by - :meth:`deepmd.model.model_stat.make_stat_input` + :meth:`deepmd.tf.model.model_stat.make_stat_input` data_atype : list[np.ndarray] - The atom types. Can be generated by :meth:`deepmd.model.model_stat.make_stat_input` + The atom types. Can be generated by :meth:`deepmd.tf.model.model_stat.make_stat_input` natoms_vec : list[np.ndarray] The vector for the number of atoms of the system and different types of - atoms. Can be generated by :meth:`deepmd.model.model_stat.make_stat_input` + atoms. Can be generated by :meth:`deepmd.tf.model.model_stat.make_stat_input` mesh : list[np.ndarray] The mesh for neighbor searching. Can be generated by - :meth:`deepmd.model.model_stat.make_stat_input` + :meth:`deepmd.tf.model.model_stat.make_stat_input` input_dict : dict[str, list[np.ndarray]] Dictionary for additional input **kwargs diff --git a/deepmd/descriptor/hybrid.py b/deepmd/tf/descriptor/hybrid.py similarity index 95% rename from deepmd/descriptor/hybrid.py rename to deepmd/tf/descriptor/hybrid.py index 5ee5ec884b..8ce8acc4db 100644 --- a/deepmd/descriptor/hybrid.py +++ b/deepmd/tf/descriptor/hybrid.py @@ -7,20 +7,20 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, tf, ) -from deepmd.utils.spin import ( +from deepmd.tf.utils.spin import ( Spin, ) -# from deepmd.descriptor import DescrptLocFrame -# from deepmd.descriptor import DescrptSeA -# from deepmd.descriptor import DescrptSeT -# from deepmd.descriptor import DescrptSeAEbd -# from deepmd.descriptor import DescrptSeAEf -# from deepmd.descriptor import DescrptSeR +# from deepmd.tf.descriptor import DescrptLocFrame +# from deepmd.tf.descriptor import DescrptSeA +# from deepmd.tf.descriptor import DescrptSeT +# from deepmd.tf.descriptor import DescrptSeAEbd +# from deepmd.tf.descriptor import DescrptSeAEf +# from deepmd.tf.descriptor import DescrptSeR from .descriptor import ( Descriptor, ) @@ -146,15 +146,15 @@ def compute_input_stats( Parameters ---------- data_coord - The coordinates. Can be generated by deepmd.model.make_stat_input + The coordinates. Can be generated by deepmd.tf.model.make_stat_input data_box - The box. Can be generated by deepmd.model.make_stat_input + The box. Can be generated by deepmd.tf.model.make_stat_input data_atype - The atom types. Can be generated by deepmd.model.make_stat_input + The atom types. Can be generated by deepmd.tf.model.make_stat_input natoms_vec - The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.model.make_stat_input + The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.tf.model.make_stat_input mesh - The mesh for neighbor searching. Can be generated by deepmd.model.make_stat_input + The mesh for neighbor searching. Can be generated by deepmd.tf.model.make_stat_input input_dict Dictionary for additional input mixed_type diff --git a/deepmd/descriptor/loc_frame.py b/deepmd/tf/descriptor/loc_frame.py similarity index 97% rename from deepmd/descriptor/loc_frame.py rename to deepmd/tf/descriptor/loc_frame.py index 0765be55f8..b43678c381 100644 --- a/deepmd/descriptor/loc_frame.py +++ b/deepmd/tf/descriptor/loc_frame.py @@ -7,17 +7,17 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, default_tf_session_config, op_module, tf, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_tensor_by_name_from_graph, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) @@ -168,15 +168,15 @@ def compute_input_stats( Parameters ---------- data_coord - The coordinates. Can be generated by deepmd.model.make_stat_input + The coordinates. Can be generated by deepmd.tf.model.make_stat_input data_box - The box. Can be generated by deepmd.model.make_stat_input + The box. Can be generated by deepmd.tf.model.make_stat_input data_atype - The atom types. Can be generated by deepmd.model.make_stat_input + The atom types. Can be generated by deepmd.tf.model.make_stat_input natoms_vec - The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.model.make_stat_input + The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.tf.model.make_stat_input mesh - The mesh for neighbor searching. Can be generated by deepmd.model.make_stat_input + The mesh for neighbor searching. Can be generated by deepmd.tf.model.make_stat_input input_dict Dictionary for additional input **kwargs diff --git a/deepmd/descriptor/se.py b/deepmd/tf/descriptor/se.py similarity index 95% rename from deepmd/descriptor/se.py rename to deepmd/tf/descriptor/se.py index 598f6f9ff8..4f49a8800f 100644 --- a/deepmd/descriptor/se.py +++ b/deepmd/tf/descriptor/se.py @@ -3,10 +3,10 @@ Tuple, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_embedding_net_variables_from_graph_def, get_tensor_by_name_from_graph, ) @@ -22,7 +22,7 @@ class DescrptSe(Descriptor): Notes ----- All of these descriptors have an environmental matrix and an - embedding network (:meth:`deepmd.utils.network.embedding_net`), so + embedding network (:meth:`deepmd.tf.utils.network.embedding_net`), so they can share some similiar methods without defining them twice. Attributes @@ -153,7 +153,7 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): local_jdata : dict The local data refer to the current class """ - from deepmd.entrypoints.train import ( + from deepmd.tf.entrypoints.train import ( update_one_sel, ) diff --git a/deepmd/descriptor/se_a.py b/deepmd/tf/descriptor/se_a.py similarity index 98% rename from deepmd/descriptor/se_a.py rename to deepmd/tf/descriptor/se_a.py index 721bb0d534..f1f90451fc 100644 --- a/deepmd/descriptor/se_a.py +++ b/deepmd/tf/descriptor/se_a.py @@ -7,20 +7,20 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( cast_precision, get_activation_func, get_np_precision, get_precision, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, default_tf_session_config, op_module, tf, ) -from deepmd.nvnmd.descriptor.se_a import ( +from deepmd.tf.nvnmd.descriptor.se_a import ( build_davg_dstd, build_op_descriptor, check_switch_range, @@ -28,38 +28,38 @@ filter_GR2D, filter_lower_R42GR, ) -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) -from deepmd.utils.compress import ( +from deepmd.tf.utils.compress import ( get_extra_side_embedding_net_variable, get_two_side_type_embedding, get_type_embedding, make_data, ) -from deepmd.utils.errors import ( +from deepmd.tf.utils.errors import ( GraphWithoutTensorError, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_extra_embedding_net_suffix, get_extra_embedding_net_variables_from_graph_def, get_pattern_nodes_from_graph_def, get_tensor_by_name_from_graph, ) -from deepmd.utils.network import ( +from deepmd.tf.utils.network import ( embedding_net, embedding_net_rand_seed_shift, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) -from deepmd.utils.spin import ( +from deepmd.tf.utils.spin import ( Spin, ) -from deepmd.utils.tabulate import ( +from deepmd.tf.utils.tabulate import ( DPTabulate, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( embed_atom_type, ) @@ -112,7 +112,7 @@ class DescrptSeA(DescrptSe): :math:`\mathcal{G}^i_< \in \mathbb{R}^{N \times M_2}` takes first :math:`M_2` columns of :math:`\mathcal{G}^i`. The equation of embedding network :math:`\mathcal{N}` can be found at - :meth:`deepmd.utils.network.embedding_net`. + :meth:`deepmd.tf.utils.network.embedding_net`. Parameters ---------- @@ -333,15 +333,15 @@ def compute_input_stats( Parameters ---------- data_coord - The coordinates. Can be generated by deepmd.model.make_stat_input + The coordinates. Can be generated by deepmd.tf.model.make_stat_input data_box - The box. Can be generated by deepmd.model.make_stat_input + The box. Can be generated by deepmd.tf.model.make_stat_input data_atype - The atom types. Can be generated by deepmd.model.make_stat_input + The atom types. Can be generated by deepmd.tf.model.make_stat_input natoms_vec - The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.model.make_stat_input + The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.tf.model.make_stat_input mesh - The mesh for neighbor searching. Can be generated by deepmd.model.make_stat_input + The mesh for neighbor searching. Can be generated by deepmd.tf.model.make_stat_input input_dict Dictionary for additional input **kwargs diff --git a/deepmd/descriptor/se_a_ebd.py b/deepmd/tf/descriptor/se_a_ebd.py similarity index 99% rename from deepmd/descriptor/se_a_ebd.py rename to deepmd/tf/descriptor/se_a_ebd.py index 4816ec1569..92b194b37a 100644 --- a/deepmd/descriptor/se_a_ebd.py +++ b/deepmd/tf/descriptor/se_a_ebd.py @@ -6,15 +6,15 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( add_data_requirement, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, op_module, tf, ) -from deepmd.utils.network import ( +from deepmd.tf.utils.network import ( embedding_net, one_layer, ) diff --git a/deepmd/descriptor/se_a_ebd_v2.py b/deepmd/tf/descriptor/se_a_ebd_v2.py similarity index 98% rename from deepmd/descriptor/se_a_ebd_v2.py rename to deepmd/tf/descriptor/se_a_ebd_v2.py index c6e3cebc71..54790fbfed 100644 --- a/deepmd/descriptor/se_a_ebd_v2.py +++ b/deepmd/tf/descriptor/se_a_ebd_v2.py @@ -5,7 +5,7 @@ Optional, ) -from deepmd.utils.spin import ( +from deepmd.tf.utils.spin import ( Spin, ) diff --git a/deepmd/descriptor/se_a_ef.py b/deepmd/tf/descriptor/se_a_ef.py similarity index 97% rename from deepmd/descriptor/se_a_ef.py rename to deepmd/tf/descriptor/se_a_ef.py index 32a62b48f3..0c2b78162b 100644 --- a/deepmd/descriptor/se_a_ef.py +++ b/deepmd/tf/descriptor/se_a_ef.py @@ -7,17 +7,17 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( add_data_requirement, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, default_tf_session_config, op_module, tf, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) @@ -180,15 +180,15 @@ def compute_input_stats( Parameters ---------- data_coord - The coordinates. Can be generated by deepmd.model.make_stat_input + The coordinates. Can be generated by deepmd.tf.model.make_stat_input data_box - The box. Can be generated by deepmd.model.make_stat_input + The box. Can be generated by deepmd.tf.model.make_stat_input data_atype - The atom types. Can be generated by deepmd.model.make_stat_input + The atom types. Can be generated by deepmd.tf.model.make_stat_input natoms_vec - The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.model.make_stat_input + The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.tf.model.make_stat_input mesh - The mesh for neighbor searching. Can be generated by deepmd.model.make_stat_input + The mesh for neighbor searching. Can be generated by deepmd.tf.model.make_stat_input input_dict Dictionary for additional input **kwargs diff --git a/deepmd/descriptor/se_a_mask.py b/deepmd/tf/descriptor/se_a_mask.py similarity index 96% rename from deepmd/descriptor/se_a_mask.py rename to deepmd/tf/descriptor/se_a_mask.py index cc2e6b4fc8..55b34adf48 100644 --- a/deepmd/descriptor/se_a_mask.py +++ b/deepmd/tf/descriptor/se_a_mask.py @@ -10,18 +10,18 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( get_activation_func, get_precision, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, default_tf_session_config, op_module, tf, ) -from deepmd.utils.network import ( +from deepmd.tf.utils.network import ( embedding_net_rand_seed_shift, ) @@ -73,7 +73,7 @@ class DescrptSeAMask(DescrptSeA): :math:`\mathcal{G}^i_< \in \mathbb{R}^{N \times M_2}` takes first :math:`M_2` columns of :math:`\mathcal{G}^i`. The equation of embedding network :math:`\mathcal{N}` can be found at - :meth:`deepmd.utils.network.embedding_net`. + :meth:`deepmd.tf.utils.network.embedding_net`. Specially for descriptor se_a_mask is a concise implementation of se_a. The difference is that se_a_mask only considered a non-pbc system. And accept a mask matrix to indicate the atom i in frame j is a real atom or not. @@ -235,15 +235,15 @@ def compute_input_stats( Parameters ---------- data_coord - The coordinates. Can be generated by deepmd.model.make_stat_input + The coordinates. Can be generated by deepmd.tf.model.make_stat_input data_box - The box. Can be generated by deepmd.model.make_stat_input + The box. Can be generated by deepmd.tf.model.make_stat_input data_atype - The atom types. Can be generated by deepmd.model.make_stat_input + The atom types. Can be generated by deepmd.tf.model.make_stat_input natoms_vec - The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.model.make_stat_input + The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.tf.model.make_stat_input mesh - The mesh for neighbor searching. Can be generated by deepmd.model.make_stat_input + The mesh for neighbor searching. Can be generated by deepmd.tf.model.make_stat_input input_dict Dictionary for additional input **kwargs diff --git a/deepmd/descriptor/se_atten.py b/deepmd/tf/descriptor/se_atten.py similarity index 98% rename from deepmd/descriptor/se_atten.py rename to deepmd/tf/descriptor/se_atten.py index 1ceda23065..327c3c1d3d 100644 --- a/deepmd/descriptor/se_atten.py +++ b/deepmd/tf/descriptor/se_atten.py @@ -12,11 +12,11 @@ Version, ) -from deepmd.common import ( +from deepmd.tf.common import ( cast_precision, get_np_precision, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, TF_VERSION, @@ -24,7 +24,7 @@ op_module, tf, ) -from deepmd.nvnmd.descriptor.se_atten import ( +from deepmd.tf.nvnmd.descriptor.se_atten import ( build_davg_dstd, build_op_descriptor, check_switch_range, @@ -32,29 +32,29 @@ filter_GR2D, filter_lower_R42GR, ) -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) -from deepmd.utils.compress import ( +from deepmd.tf.utils.compress import ( get_extra_side_embedding_net_variable, get_two_side_type_embedding, make_data, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_attention_layer_variables_from_graph_def, get_extra_embedding_net_suffix, get_extra_embedding_net_variables_from_graph_def, get_pattern_nodes_from_graph_def, get_tensor_by_name_from_graph, ) -from deepmd.utils.network import ( +from deepmd.tf.utils.network import ( embedding_net, one_layer, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) -from deepmd.utils.tabulate import ( +from deepmd.tf.utils.tabulate import ( DPTabulate, ) @@ -269,16 +269,16 @@ def compute_input_stats( Parameters ---------- data_coord - The coordinates. Can be generated by deepmd.model.make_stat_input + The coordinates. Can be generated by deepmd.tf.model.make_stat_input data_box - The box. Can be generated by deepmd.model.make_stat_input + The box. Can be generated by deepmd.tf.model.make_stat_input data_atype - The atom types. Can be generated by deepmd.model.make_stat_input + The atom types. Can be generated by deepmd.tf.model.make_stat_input natoms_vec The vector for the number of atoms of the system and different types of atoms. If mixed_type is True, this para is blank. See real_natoms_vec. mesh - The mesh for neighbor searching. Can be generated by deepmd.model.make_stat_input + The mesh for neighbor searching. Can be generated by deepmd.tf.model.make_stat_input input_dict Dictionary for additional input mixed_type @@ -1339,7 +1339,7 @@ def build_type_exclude_mask( Notes ----- This method has the similiar way to build the type exclude mask as - :meth:`deepmd.descriptor.descriptor.Descriptor.build_type_exclude_mask`. + :meth:`deepmd.tf.descriptor.descriptor.Descriptor.build_type_exclude_mask`. The mathmatical expression has been explained in that method. The difference is that the attention descriptor has provided the type of the neighbors (idx_j) that is not in order, so we use it from an extra @@ -1373,7 +1373,7 @@ def build_type_exclude_mask( See Also -------- - deepmd.descriptor.descriptor.Descriptor.build_type_exclude_mask + deepmd.tf.descriptor.descriptor.Descriptor.build_type_exclude_mask """ # generate a mask # op returns ntypes when the neighbor doesn't exist, so we need to add 1 @@ -1424,7 +1424,7 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): local_jdata : dict The local data refer to the current class """ - from deepmd.entrypoints.train import ( + from deepmd.tf.entrypoints.train import ( update_one_sel, ) diff --git a/deepmd/descriptor/se_atten_v2.py b/deepmd/tf/descriptor/se_atten_v2.py similarity index 100% rename from deepmd/descriptor/se_atten_v2.py rename to deepmd/tf/descriptor/se_atten_v2.py diff --git a/deepmd/descriptor/se_r.py b/deepmd/tf/descriptor/se_r.py similarity index 97% rename from deepmd/descriptor/se_r.py rename to deepmd/tf/descriptor/se_r.py index ae926c339f..ac94ec0614 100644 --- a/deepmd/descriptor/se_r.py +++ b/deepmd/tf/descriptor/se_r.py @@ -7,32 +7,32 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( cast_precision, get_activation_func, get_precision, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, default_tf_session_config, op_module, tf, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_tensor_by_name_from_graph, ) -from deepmd.utils.network import ( +from deepmd.tf.utils.network import ( embedding_net, embedding_net_rand_seed_shift, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) -from deepmd.utils.spin import ( +from deepmd.tf.utils.spin import ( Spin, ) -from deepmd.utils.tabulate import ( +from deepmd.tf.utils.tabulate import ( DPTabulate, ) @@ -235,15 +235,15 @@ def compute_input_stats( Parameters ---------- data_coord - The coordinates. Can be generated by deepmd.model.make_stat_input + The coordinates. Can be generated by deepmd.tf.model.make_stat_input data_box - The box. Can be generated by deepmd.model.make_stat_input + The box. Can be generated by deepmd.tf.model.make_stat_input data_atype - The atom types. Can be generated by deepmd.model.make_stat_input + The atom types. Can be generated by deepmd.tf.model.make_stat_input natoms_vec - The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.model.make_stat_input + The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.tf.model.make_stat_input mesh - The mesh for neighbor searching. Can be generated by deepmd.model.make_stat_input + The mesh for neighbor searching. Can be generated by deepmd.tf.model.make_stat_input input_dict Dictionary for additional input **kwargs diff --git a/deepmd/descriptor/se_t.py b/deepmd/tf/descriptor/se_t.py similarity index 97% rename from deepmd/descriptor/se_t.py rename to deepmd/tf/descriptor/se_t.py index d0c9fcbc2e..98f4cf8212 100644 --- a/deepmd/descriptor/se_t.py +++ b/deepmd/tf/descriptor/se_t.py @@ -7,29 +7,29 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( cast_precision, get_activation_func, get_precision, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, default_tf_session_config, op_module, tf, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_tensor_by_name_from_graph, ) -from deepmd.utils.network import ( +from deepmd.tf.utils.network import ( embedding_net, embedding_net_rand_seed_shift, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) -from deepmd.utils.tabulate import ( +from deepmd.tf.utils.tabulate import ( DPTabulate, ) @@ -225,15 +225,15 @@ def compute_input_stats( Parameters ---------- data_coord - The coordinates. Can be generated by deepmd.model.make_stat_input + The coordinates. Can be generated by deepmd.tf.model.make_stat_input data_box - The box. Can be generated by deepmd.model.make_stat_input + The box. Can be generated by deepmd.tf.model.make_stat_input data_atype - The atom types. Can be generated by deepmd.model.make_stat_input + The atom types. Can be generated by deepmd.tf.model.make_stat_input natoms_vec - The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.model.make_stat_input + The vector for the number of atoms of the system and different types of atoms. Can be generated by deepmd.tf.model.make_stat_input mesh - The mesh for neighbor searching. Can be generated by deepmd.model.make_stat_input + The mesh for neighbor searching. Can be generated by deepmd.tf.model.make_stat_input input_dict Dictionary for additional input **kwargs diff --git a/deepmd/entrypoints/__init__.py b/deepmd/tf/entrypoints/__init__.py similarity index 100% rename from deepmd/entrypoints/__init__.py rename to deepmd/tf/entrypoints/__init__.py diff --git a/deepmd/entrypoints/compress.py b/deepmd/tf/entrypoints/compress.py similarity index 96% rename from deepmd/entrypoints/compress.py rename to deepmd/tf/entrypoints/compress.py index 61d6dfcb44..b1273b92e1 100644 --- a/deepmd/entrypoints/compress.py +++ b/deepmd/tf/entrypoints/compress.py @@ -8,24 +8,24 @@ Optional, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_loader, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_ENER_FLOAT_PRECISION, tf, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( normalize, ) -from deepmd.utils.compat import ( +from deepmd.tf.utils.compat import ( update_deepmd_input, ) -from deepmd.utils.errors import ( +from deepmd.tf.utils.errors import ( GraphTooLargeError, GraphWithoutTensorError, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_tensor_by_name_from_graph, load_graph_def, ) diff --git a/deepmd/entrypoints/convert.py b/deepmd/tf/entrypoints/convert.py similarity index 97% rename from deepmd/entrypoints/convert.py rename to deepmd/tf/entrypoints/convert.py index bea047ba72..17c8667362 100644 --- a/deepmd/entrypoints/convert.py +++ b/deepmd/tf/entrypoints/convert.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_10_to_21, convert_012_to_21, convert_12_to_21, diff --git a/deepmd/entrypoints/doc.py b/deepmd/tf/entrypoints/doc.py similarity index 100% rename from deepmd/entrypoints/doc.py rename to deepmd/tf/entrypoints/doc.py diff --git a/deepmd/entrypoints/freeze.py b/deepmd/tf/entrypoints/freeze.py similarity index 98% rename from deepmd/entrypoints/freeze.py rename to deepmd/tf/entrypoints/freeze.py index 22f3cb80b4..9cb59f4c9d 100755 --- a/deepmd/entrypoints/freeze.py +++ b/deepmd/tf/entrypoints/freeze.py @@ -21,22 +21,22 @@ import google.protobuf.message # load grad of force module -import deepmd.op # noqa: F401 -from deepmd.env import ( +import deepmd.tf.op # noqa: F401 +from deepmd.tf.env import ( FITTING_NET_PATTERN, REMOVE_SUFFIX_DICT, tf, ) -from deepmd.nvnmd.entrypoints.freeze import ( +from deepmd.tf.nvnmd.entrypoints.freeze import ( save_weight, ) -from deepmd.utils.errors import ( +from deepmd.tf.utils.errors import ( GraphTooLargeError, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_pattern_nodes_from_graph_def, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) diff --git a/deepmd/entrypoints/gui.py b/deepmd/tf/entrypoints/gui.py similarity index 100% rename from deepmd/entrypoints/gui.py rename to deepmd/tf/entrypoints/gui.py diff --git a/deepmd/entrypoints/ipi.py b/deepmd/tf/entrypoints/ipi.py similarity index 95% rename from deepmd/entrypoints/ipi.py rename to deepmd/tf/entrypoints/ipi.py index da287ff3de..49f72434f3 100644 --- a/deepmd/entrypoints/ipi.py +++ b/deepmd/tf/entrypoints/ipi.py @@ -7,7 +7,7 @@ List, ) -from deepmd.lmp import ( +from deepmd.tf.lmp import ( get_op_dir, ) diff --git a/deepmd/entrypoints/main.py b/deepmd/tf/entrypoints/main.py similarity index 94% rename from deepmd/entrypoints/main.py rename to deepmd/tf/entrypoints/main.py index 2c6ac26a7f..21f5e10aa9 100644 --- a/deepmd/entrypoints/main.py +++ b/deepmd/tf/entrypoints/main.py @@ -11,10 +11,10 @@ Union, ) -from deepmd.common import ( +from deepmd.tf.common import ( clear_session, ) -from deepmd.entrypoints import ( +from deepmd.tf.entrypoints import ( compress, convert, doc_train_input, @@ -26,10 +26,10 @@ train_dp, transfer, ) -from deepmd.loggers import ( +from deepmd.tf.loggers import ( set_log_handles, ) -from deepmd.nvnmd.entrypoints.train import ( +from deepmd.tf.nvnmd.entrypoints.train import ( train_nvnmd, ) from deepmd_utils.main import ( diff --git a/deepmd/entrypoints/neighbor_stat.py b/deepmd/tf/entrypoints/neighbor_stat.py similarity index 92% rename from deepmd/entrypoints/neighbor_stat.py rename to deepmd/tf/entrypoints/neighbor_stat.py index 28cab00ad2..d2e8a01e82 100644 --- a/deepmd/entrypoints/neighbor_stat.py +++ b/deepmd/tf/entrypoints/neighbor_stat.py @@ -4,13 +4,13 @@ List, ) -from deepmd.common import ( +from deepmd.tf.common import ( expand_sys_str, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) -from deepmd.utils.neighbor_stat import ( +from deepmd.tf.utils.neighbor_stat import ( NeighborStat, ) diff --git a/deepmd/entrypoints/test.py b/deepmd/tf/entrypoints/test.py similarity index 99% rename from deepmd/entrypoints/test.py rename to deepmd/tf/entrypoints/test.py index 4658b16e7c..1b917bc276 100644 --- a/deepmd/entrypoints/test.py +++ b/deepmd/tf/entrypoints/test.py @@ -14,29 +14,29 @@ import numpy as np -from deepmd import ( +from deepmd.tf import ( DeepPotential, ) -from deepmd.common import ( +from deepmd.tf.common import ( expand_sys_str, ) -from deepmd.utils import random as dp_random -from deepmd.utils.data import ( +from deepmd.tf.utils import random as dp_random +from deepmd.tf.utils.data import ( DeepmdData, ) -from deepmd.utils.weight_avg import ( +from deepmd.tf.utils.weight_avg import ( weighted_average, ) if TYPE_CHECKING: - from deepmd.infer import ( + from deepmd.tf.infer import ( DeepDipole, DeepDOS, DeepPolar, DeepPot, DeepWFC, ) - from deepmd.infer.deep_tensor import ( + from deepmd.tf.infer.deep_tensor import ( DeepTensor, ) diff --git a/deepmd/entrypoints/train.py b/deepmd/tf/entrypoints/train.py similarity index 96% rename from deepmd/entrypoints/train.py rename to deepmd/tf/entrypoints/train.py index 227aa13644..17063d2bac 100755 --- a/deepmd/entrypoints/train.py +++ b/deepmd/tf/entrypoints/train.py @@ -13,52 +13,52 @@ Optional, ) -from deepmd.common import ( +from deepmd.tf.common import ( data_requirement, expand_sys_str, j_loader, j_must_have, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_ENER_FLOAT_PRECISION, reset_default_tf_session_config, tf, ) -from deepmd.infer.data_modifier import ( +from deepmd.tf.infer.data_modifier import ( DipoleChargeModifier, ) -from deepmd.model.model import ( +from deepmd.tf.model.model import ( Model, ) -from deepmd.train.run_options import ( +from deepmd.tf.train.run_options import ( BUILD, CITATION, WELCOME, RunOptions, ) -from deepmd.train.trainer import ( +from deepmd.tf.train.trainer import ( DPTrainer, ) -from deepmd.utils import random as dp_random -from deepmd.utils.argcheck import ( +from deepmd.tf.utils import random as dp_random +from deepmd.tf.utils.argcheck import ( normalize, ) -from deepmd.utils.compat import ( +from deepmd.tf.utils.compat import ( update_deepmd_input, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) -from deepmd.utils.finetune import ( +from deepmd.tf.utils.finetune import ( replace_model_params_with_pretrained_model, ) -from deepmd.utils.multi_init import ( +from deepmd.tf.utils.multi_init import ( replace_model_params_with_frz_multi_model, ) -from deepmd.utils.neighbor_stat import ( +from deepmd.tf.utils.neighbor_stat import ( NeighborStat, ) -from deepmd.utils.path import ( +from deepmd.tf.utils.path import ( DPPath, ) diff --git a/deepmd/entrypoints/transfer.py b/deepmd/tf/entrypoints/transfer.py similarity index 99% rename from deepmd/entrypoints/transfer.py rename to deepmd/tf/entrypoints/transfer.py index 535b32ec09..7c90c77de8 100644 --- a/deepmd/entrypoints/transfer.py +++ b/deepmd/tf/entrypoints/transfer.py @@ -11,7 +11,7 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( TRANSFER_PATTERN, tf, ) diff --git a/deepmd/env.py b/deepmd/tf/env.py similarity index 100% rename from deepmd/env.py rename to deepmd/tf/env.py diff --git a/deepmd/fit/__init__.py b/deepmd/tf/fit/__init__.py similarity index 100% rename from deepmd/fit/__init__.py rename to deepmd/tf/fit/__init__.py diff --git a/deepmd/fit/dipole.py b/deepmd/tf/fit/dipole.py similarity index 97% rename from deepmd/fit/dipole.py rename to deepmd/tf/fit/dipole.py index 312bcc9bf1..55da62d69b 100644 --- a/deepmd/fit/dipole.py +++ b/deepmd/tf/fit/dipole.py @@ -6,27 +6,27 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( cast_precision, get_activation_func, get_precision, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit.fitting import ( +from deepmd.tf.fit.fitting import ( Fitting, ) -from deepmd.loss.loss import ( +from deepmd.tf.loss.loss import ( Loss, ) -from deepmd.loss.tensor import ( +from deepmd.tf.loss.tensor import ( TensorLoss, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_fitting_net_variables_from_graph_def, ) -from deepmd.utils.network import ( +from deepmd.tf.utils.network import ( one_layer, one_layer_rand_seed_shift, ) diff --git a/deepmd/fit/dos.py b/deepmd/tf/fit/dos.py similarity index 98% rename from deepmd/fit/dos.py rename to deepmd/tf/fit/dos.py index bbf7d39a09..e8681f47ea 100644 --- a/deepmd/fit/dos.py +++ b/deepmd/tf/fit/dos.py @@ -7,40 +7,40 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( add_data_requirement, cast_precision, get_activation_func, get_precision, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, tf, ) -from deepmd.fit.fitting import ( +from deepmd.tf.fit.fitting import ( Fitting, ) -from deepmd.loss.dos import ( +from deepmd.tf.loss.dos import ( DOSLoss, ) -from deepmd.loss.loss import ( +from deepmd.tf.loss.loss import ( Loss, ) -from deepmd.nvnmd.fit.ener import ( +from deepmd.tf.nvnmd.fit.ener import ( one_layer_nvnmd, ) -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) -from deepmd.utils.errors import ( +from deepmd.tf.utils.errors import ( GraphWithoutTensorError, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_fitting_net_variables_from_graph_def, get_tensor_by_name_from_graph, ) -from deepmd.utils.network import one_layer as one_layer_deepmd -from deepmd.utils.network import ( +from deepmd.tf.utils.network import one_layer as one_layer_deepmd +from deepmd.tf.utils.network import ( one_layer_rand_seed_shift, ) diff --git a/deepmd/fit/ener.py b/deepmd/tf/fit/ener.py similarity index 98% rename from deepmd/fit/ener.py rename to deepmd/tf/fit/ener.py index 4c15e57124..751e5091bd 100644 --- a/deepmd/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -7,49 +7,49 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( add_data_requirement, cast_precision, get_activation_func, get_precision, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, global_cvt_2_tf_float, tf, ) -from deepmd.fit.fitting import ( +from deepmd.tf.fit.fitting import ( Fitting, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPotential, ) -from deepmd.loss.ener import ( +from deepmd.tf.loss.ener import ( EnerDipoleLoss, EnerSpinLoss, EnerStdLoss, ) -from deepmd.loss.loss import ( +from deepmd.tf.loss.loss import ( Loss, ) -from deepmd.nvnmd.fit.ener import ( +from deepmd.tf.nvnmd.fit.ener import ( one_layer_nvnmd, ) -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) -from deepmd.utils.errors import ( +from deepmd.tf.utils.errors import ( GraphWithoutTensorError, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_fitting_net_variables_from_graph_def, get_tensor_by_name_from_graph, ) -from deepmd.utils.network import one_layer as one_layer_deepmd -from deepmd.utils.network import ( +from deepmd.tf.utils.network import one_layer as one_layer_deepmd +from deepmd.tf.utils.network import ( one_layer_rand_seed_shift, ) -from deepmd.utils.spin import ( +from deepmd.tf.utils.spin import ( Spin, ) diff --git a/deepmd/fit/fitting.py b/deepmd/tf/fit/fitting.py similarity index 96% rename from deepmd/fit/fitting.py rename to deepmd/tf/fit/fitting.py index a467ec1201..5d666a19f7 100644 --- a/deepmd/fit/fitting.py +++ b/deepmd/tf/fit/fitting.py @@ -6,13 +6,13 @@ Callable, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.loss.loss import ( +from deepmd.tf.loss.loss import ( Loss, ) -from deepmd.utils import ( +from deepmd.tf.utils import ( Plugin, PluginVariant, ) diff --git a/deepmd/fit/polar.py b/deepmd/tf/fit/polar.py similarity index 98% rename from deepmd/fit/polar.py rename to deepmd/tf/fit/polar.py index 8f6631866c..ae02c02064 100644 --- a/deepmd/fit/polar.py +++ b/deepmd/tf/fit/polar.py @@ -7,30 +7,30 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( cast_precision, get_activation_func, get_precision, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit.fitting import ( +from deepmd.tf.fit.fitting import ( Fitting, ) -from deepmd.loss.loss import ( +from deepmd.tf.loss.loss import ( Loss, ) -from deepmd.loss.tensor import ( +from deepmd.tf.loss.tensor import ( TensorLoss, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_fitting_net_variables_from_graph_def, ) -from deepmd.utils.network import ( +from deepmd.tf.utils.network import ( one_layer, one_layer_rand_seed_shift, ) diff --git a/deepmd/infer/__init__.py b/deepmd/tf/infer/__init__.py similarity index 100% rename from deepmd/infer/__init__.py rename to deepmd/tf/infer/__init__.py diff --git a/deepmd/infer/data_modifier.py b/deepmd/tf/infer/data_modifier.py similarity index 98% rename from deepmd/infer/data_modifier.py rename to deepmd/tf/infer/data_modifier.py index 62c4b879e9..e53151d80a 100644 --- a/deepmd/infer/data_modifier.py +++ b/deepmd/tf/infer/data_modifier.py @@ -7,26 +7,26 @@ import numpy as np -import deepmd.op # noqa: F401 -from deepmd.common import ( +import deepmd.tf.op # noqa: F401 +from deepmd.tf.common import ( make_default_mesh, select_idx_map, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, op_module, tf, ) -from deepmd.infer.deep_dipole import ( +from deepmd.tf.infer.deep_dipole import ( DeepDipole, ) -from deepmd.infer.ewald_recp import ( +from deepmd.tf.infer.ewald_recp import ( EwaldRecp, ) -from deepmd.utils.data import ( +from deepmd.tf.utils.data import ( DeepmdData, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) diff --git a/deepmd/infer/deep_dipole.py b/deepmd/tf/infer/deep_dipole.py similarity index 97% rename from deepmd/infer/deep_dipole.py rename to deepmd/tf/infer/deep_dipole.py index aba098a9f3..a0a4a0f78e 100644 --- a/deepmd/infer/deep_dipole.py +++ b/deepmd/tf/infer/deep_dipole.py @@ -4,7 +4,7 @@ Optional, ) -from deepmd.infer.deep_tensor import ( +from deepmd.tf.infer.deep_tensor import ( DeepTensor, ) diff --git a/deepmd/infer/deep_dos.py b/deepmd/tf/infer/deep_dos.py similarity index 99% rename from deepmd/infer/deep_dos.py rename to deepmd/tf/infer/deep_dos.py index 5f181bd336..9b2830161d 100644 --- a/deepmd/infer/deep_dos.py +++ b/deepmd/tf/infer/deep_dos.py @@ -11,16 +11,16 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( make_default_mesh, ) -from deepmd.infer.deep_eval import ( +from deepmd.tf.infer.deep_eval import ( DeepEval, ) -from deepmd.utils.batch_size import ( +from deepmd.tf.utils.batch_size import ( AutoBatchSize, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) diff --git a/deepmd/infer/deep_eval.py b/deepmd/tf/infer/deep_eval.py similarity index 98% rename from deepmd/infer/deep_eval.py rename to deepmd/tf/infer/deep_eval.py index 0ca9f21a77..9e3106f4ad 100644 --- a/deepmd/infer/deep_eval.py +++ b/deepmd/tf/infer/deep_eval.py @@ -11,15 +11,15 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( MODEL_VERSION, default_tf_session_config, tf, ) -from deepmd.utils.batch_size import ( +from deepmd.tf.utils.batch_size import ( AutoBatchSize, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) @@ -353,13 +353,13 @@ def eval_typeebd(self) -> np.ndarray: See Also -------- - deepmd.utils.type_embed.TypeEmbedNet : The type embedding network. + deepmd.tf.utils.type_embed.TypeEmbedNet : The type embedding network. Examples -------- Get the output of type embedding network of `graph.pb`: - >>> from deepmd.infer import DeepPotential + >>> from deepmd.tf.infer import DeepPotential >>> dp = DeepPotential('graph.pb') >>> dp.eval_typeebd() """ diff --git a/deepmd/infer/deep_polar.py b/deepmd/tf/infer/deep_polar.py similarity index 99% rename from deepmd/infer/deep_polar.py rename to deepmd/tf/infer/deep_polar.py index c1f981ef86..e0b73da2a2 100644 --- a/deepmd/infer/deep_polar.py +++ b/deepmd/tf/infer/deep_polar.py @@ -7,7 +7,7 @@ import numpy as np -from deepmd.infer.deep_tensor import ( +from deepmd.tf.infer.deep_tensor import ( DeepTensor, ) diff --git a/deepmd/infer/deep_pot.py b/deepmd/tf/infer/deep_pot.py similarity index 99% rename from deepmd/infer/deep_pot.py rename to deepmd/tf/infer/deep_pot.py index 45db3fcb0c..df1f8a477f 100644 --- a/deepmd/infer/deep_pot.py +++ b/deepmd/tf/infer/deep_pot.py @@ -11,19 +11,19 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( make_default_mesh, ) -from deepmd.infer.data_modifier import ( +from deepmd.tf.infer.data_modifier import ( DipoleChargeModifier, ) -from deepmd.infer.deep_eval import ( +from deepmd.tf.infer.deep_eval import ( DeepEval, ) -from deepmd.utils.batch_size import ( +from deepmd.tf.utils.batch_size import ( AutoBatchSize, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) from deepmd_utils.infer.deep_pot import DeepPot as DeepPotBase @@ -58,7 +58,7 @@ class DeepPot(DeepEval, DeepPotBase): Examples -------- - >>> from deepmd.infer import DeepPot + >>> from deepmd.tf.infer import DeepPot >>> import numpy as np >>> dp = DeepPot('graph.pb') >>> coord = np.array([[1,0,0], [0,0,1.5], [1,0,3]]).reshape([1, -1]) diff --git a/deepmd/infer/deep_tensor.py b/deepmd/tf/infer/deep_tensor.py similarity index 99% rename from deepmd/infer/deep_tensor.py rename to deepmd/tf/infer/deep_tensor.py index a803eb0c6b..9b064114b4 100644 --- a/deepmd/infer/deep_tensor.py +++ b/deepmd/tf/infer/deep_tensor.py @@ -10,13 +10,13 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( make_default_mesh, ) -from deepmd.infer.deep_eval import ( +from deepmd.tf.infer.deep_eval import ( DeepEval, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) diff --git a/deepmd/infer/deep_wfc.py b/deepmd/tf/infer/deep_wfc.py similarity index 97% rename from deepmd/infer/deep_wfc.py rename to deepmd/tf/infer/deep_wfc.py index ed682f642b..aa0dff6f38 100644 --- a/deepmd/infer/deep_wfc.py +++ b/deepmd/tf/infer/deep_wfc.py @@ -4,7 +4,7 @@ Optional, ) -from deepmd.infer.deep_tensor import ( +from deepmd.tf.infer.deep_tensor import ( DeepTensor, ) diff --git a/deepmd/infer/ewald_recp.py b/deepmd/tf/infer/ewald_recp.py similarity index 97% rename from deepmd/infer/ewald_recp.py rename to deepmd/tf/infer/ewald_recp.py index 429a3cdfd6..110188c34f 100644 --- a/deepmd/infer/ewald_recp.py +++ b/deepmd/tf/infer/ewald_recp.py @@ -5,13 +5,13 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, default_tf_session_config, op_module, tf, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) diff --git a/deepmd/infer/model_devi.py b/deepmd/tf/infer/model_devi.py similarity index 100% rename from deepmd/infer/model_devi.py rename to deepmd/tf/infer/model_devi.py diff --git a/deepmd/lmp.py b/deepmd/tf/lmp.py similarity index 99% rename from deepmd/lmp.py rename to deepmd/tf/lmp.py index 5238cd9935..f8497bef59 100644 --- a/deepmd/lmp.py +++ b/deepmd/tf/lmp.py @@ -17,7 +17,7 @@ Version, ) -from deepmd.env import ( +from deepmd.tf.env import ( SHARED_LIB_DIR, TF_VERSION, tf, diff --git a/deepmd/loggers/__init__.py b/deepmd/tf/loggers/__init__.py similarity index 100% rename from deepmd/loggers/__init__.py rename to deepmd/tf/loggers/__init__.py diff --git a/deepmd/loggers/loggers.py b/deepmd/tf/loggers/loggers.py similarity index 100% rename from deepmd/loggers/loggers.py rename to deepmd/tf/loggers/loggers.py diff --git a/deepmd/loss/__init__.py b/deepmd/tf/loss/__init__.py similarity index 100% rename from deepmd/loss/__init__.py rename to deepmd/tf/loss/__init__.py diff --git a/deepmd/loss/dos.py b/deepmd/tf/loss/dos.py similarity index 98% rename from deepmd/loss/dos.py rename to deepmd/tf/loss/dos.py index 7d38f2b17a..763e75638f 100644 --- a/deepmd/loss/dos.py +++ b/deepmd/tf/loss/dos.py @@ -1,15 +1,15 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( add_data_requirement, ) -from deepmd.env import ( +from deepmd.tf.env import ( global_cvt_2_ener_float, global_cvt_2_tf_float, tf, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) diff --git a/deepmd/loss/ener.py b/deepmd/tf/loss/ener.py similarity index 99% rename from deepmd/loss/ener.py rename to deepmd/tf/loss/ener.py index d7f83f09e5..48a13319e4 100644 --- a/deepmd/loss/ener.py +++ b/deepmd/tf/loss/ener.py @@ -5,15 +5,15 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( add_data_requirement, ) -from deepmd.env import ( +from deepmd.tf.env import ( global_cvt_2_ener_float, global_cvt_2_tf_float, tf, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) diff --git a/deepmd/loss/loss.py b/deepmd/tf/loss/loss.py similarity index 98% rename from deepmd/loss/loss.py rename to deepmd/tf/loss/loss.py index a719a08d81..327aea5230 100644 --- a/deepmd/loss/loss.py +++ b/deepmd/tf/loss/loss.py @@ -10,7 +10,7 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) diff --git a/deepmd/loss/tensor.py b/deepmd/tf/loss/tensor.py similarity index 98% rename from deepmd/loss/tensor.py rename to deepmd/tf/loss/tensor.py index a40f95a18e..3be01d3871 100644 --- a/deepmd/loss/tensor.py +++ b/deepmd/tf/loss/tensor.py @@ -1,14 +1,14 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( add_data_requirement, ) -from deepmd.env import ( +from deepmd.tf.env import ( global_cvt_2_tf_float, tf, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) diff --git a/deepmd/model/__init__.py b/deepmd/tf/model/__init__.py similarity index 100% rename from deepmd/model/__init__.py rename to deepmd/tf/model/__init__.py diff --git a/deepmd/model/dos.py b/deepmd/tf/model/dos.py similarity index 99% rename from deepmd/model/dos.py rename to deepmd/tf/model/dos.py index 22e291a0f0..265026b60a 100644 --- a/deepmd/model/dos.py +++ b/deepmd/tf/model/dos.py @@ -5,12 +5,12 @@ Union, ) -from deepmd.env import ( +from deepmd.tf.env import ( MODEL_VERSION, global_cvt_2_ener_float, tf, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/deepmd/model/ener.py b/deepmd/tf/model/ener.py similarity index 99% rename from deepmd/model/ener.py rename to deepmd/tf/model/ener.py index 0d8d66b305..70e0f4d2ba 100644 --- a/deepmd/model/ener.py +++ b/deepmd/tf/model/ener.py @@ -7,19 +7,19 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( MODEL_VERSION, global_cvt_2_ener_float, op_module, tf, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) -from deepmd.utils.spin import ( +from deepmd.tf.utils.spin import ( Spin, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/deepmd/model/frozen.py b/deepmd/tf/model/frozen.py similarity index 97% rename from deepmd/model/frozen.py rename to deepmd/tf/model/frozen.py index 38f342ebec..8732fec8f4 100644 --- a/deepmd/model/frozen.py +++ b/deepmd/tf/model/frozen.py @@ -7,18 +7,18 @@ Union, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, MODEL_VERSION, tf, ) -from deepmd.fit.fitting import ( +from deepmd.tf.fit.fitting import ( Fitting, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPotential, ) -from deepmd.loss.loss import ( +from deepmd.tf.loss.loss import ( Loss, ) diff --git a/deepmd/model/linear.py b/deepmd/tf/model/linear.py similarity index 98% rename from deepmd/model/linear.py rename to deepmd/tf/model/linear.py index 7c527fe9dc..7563e36b3f 100644 --- a/deepmd/model/linear.py +++ b/deepmd/tf/model/linear.py @@ -11,15 +11,15 @@ Union, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, MODEL_VERSION, tf, ) -from deepmd.fit.fitting import ( +from deepmd.tf.fit.fitting import ( Fitting, ) -from deepmd.loss.loss import ( +from deepmd.tf.loss.loss import ( Loss, ) diff --git a/deepmd/model/model.py b/deepmd/tf/model/model.py similarity index 97% rename from deepmd/model/model.py rename to deepmd/tf/model/model.py index 6117b4942d..eee138907f 100644 --- a/deepmd/model/model.py +++ b/deepmd/tf/model/model.py @@ -13,35 +13,35 @@ Union, ) -from deepmd.descriptor.descriptor import ( +from deepmd.tf.descriptor.descriptor import ( Descriptor, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, tf, ) -from deepmd.fit.fitting import ( +from deepmd.tf.fit.fitting import ( Fitting, ) -from deepmd.loss.loss import ( +from deepmd.tf.loss.loss import ( Loss, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( type_embedding_args, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( load_graph_def, ) -from deepmd.utils.pair_tab import ( +from deepmd.tf.utils.pair_tab import ( PairTab, ) -from deepmd.utils.spin import ( +from deepmd.tf.utils.spin import ( Spin, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) @@ -88,19 +88,19 @@ def get_class_by_input(cls, input: dict): The input data """ # infer model type by fitting_type - from deepmd.model.frozen import ( + from deepmd.tf.model.frozen import ( FrozenModel, ) - from deepmd.model.linear import ( + from deepmd.tf.model.linear import ( LinearEnergyModel, ) - from deepmd.model.multi import ( + from deepmd.tf.model.multi import ( MultiModel, ) - from deepmd.model.pairtab import ( + from deepmd.tf.model.pairtab import ( PairTabModel, ) - from deepmd.model.pairwise_dprc import ( + from deepmd.tf.model.pairwise_dprc import ( PairwiseDPRc, ) @@ -515,7 +515,7 @@ def get_feed_dict( natoms[1]: total number of atoms held by this processor natoms[i]: 2 <= i < Ntypes+2, number of type i atoms box : tf.Tensor - The box. Can be generated by deepmd.model.make_stat_input + The box. Can be generated by deepmd.tf.model.make_stat_input mesh : tf.Tensor For historical reasons, only the length of the Tensor matters. if size of mesh == 6, pbc is assumed. diff --git a/deepmd/model/model_stat.py b/deepmd/tf/model/model_stat.py similarity index 100% rename from deepmd/model/model_stat.py rename to deepmd/tf/model/model_stat.py diff --git a/deepmd/model/multi.py b/deepmd/tf/model/multi.py similarity index 98% rename from deepmd/model/multi.py rename to deepmd/tf/model/multi.py index 83b231c0e8..52bbcebf4d 100644 --- a/deepmd/model/multi.py +++ b/deepmd/tf/model/multi.py @@ -8,41 +8,41 @@ import numpy as np -from deepmd.descriptor.descriptor import ( +from deepmd.tf.descriptor.descriptor import ( Descriptor, ) -from deepmd.env import ( +from deepmd.tf.env import ( MODEL_VERSION, global_cvt_2_ener_float, op_module, tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( DipoleFittingSeA, DOSFitting, EnerFitting, GlobalPolarFittingSeA, PolarFittingSeA, ) -from deepmd.fit.fitting import ( +from deepmd.tf.fit.fitting import ( Fitting, ) -from deepmd.loss.loss import ( +from deepmd.tf.loss.loss import ( Loss, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( type_embedding_args, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_tensor_by_name_from_graph, ) -from deepmd.utils.pair_tab import ( +from deepmd.tf.utils.pair_tab import ( PairTab, ) -from deepmd.utils.spin import ( +from deepmd.tf.utils.spin import ( Spin, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/deepmd/model/pairtab.py b/deepmd/tf/model/pairtab.py similarity index 97% rename from deepmd/model/pairtab.py rename to deepmd/tf/model/pairtab.py index 38934818e6..fe94c43f64 100644 --- a/deepmd/model/pairtab.py +++ b/deepmd/tf/model/pairtab.py @@ -10,23 +10,23 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, MODEL_VERSION, global_cvt_2_ener_float, op_module, tf, ) -from deepmd.fit.fitting import ( +from deepmd.tf.fit.fitting import ( Fitting, ) -from deepmd.loss.loss import ( +from deepmd.tf.loss.loss import ( Loss, ) -from deepmd.model.model import ( +from deepmd.tf.model.model import ( Model, ) -from deepmd.utils.pair_tab import ( +from deepmd.tf.utils.pair_tab import ( PairTab, ) @@ -280,7 +280,7 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict) -> dict: dict The updated local data """ - from deepmd.entrypoints.train import ( + from deepmd.tf.entrypoints.train import ( update_one_sel, ) diff --git a/deepmd/model/pairwise_dprc.py b/deepmd/tf/model/pairwise_dprc.py similarity index 97% rename from deepmd/model/pairwise_dprc.py rename to deepmd/tf/model/pairwise_dprc.py index f74571febb..51296a0df9 100644 --- a/deepmd/model/pairwise_dprc.py +++ b/deepmd/tf/model/pairwise_dprc.py @@ -6,29 +6,29 @@ Union, ) -from deepmd.common import ( +from deepmd.tf.common import ( add_data_requirement, make_default_mesh, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, MODEL_VERSION, op_module, tf, ) -from deepmd.loss.loss import ( +from deepmd.tf.loss.loss import ( Loss, ) -from deepmd.model.model import ( +from deepmd.tf.model.model import ( Model, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( load_graph_def, ) -from deepmd.utils.spin import ( +from deepmd.tf.utils.spin import ( Spin, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) @@ -377,7 +377,7 @@ def get_feed_dict( natoms[1]: total number of atoms held by this processor natoms[i]: 2 <= i < Ntypes+2, number of type i atoms box : tf.Tensor - The box. Can be generated by deepmd.model.make_stat_input + The box. Can be generated by deepmd.tf.model.make_stat_input mesh : tf.Tensor For historical reasons, only the length of the Tensor matters. if size of mesh == 6, pbc is assumed. @@ -412,7 +412,7 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): local_jdata : dict The local data refer to the current class """ - from deepmd.entrypoints.train import ( + from deepmd.tf.entrypoints.train import ( get_min_nbor_dist, ) diff --git a/deepmd/model/tensor.py b/deepmd/tf/model/tensor.py similarity index 99% rename from deepmd/model/tensor.py rename to deepmd/tf/model/tensor.py index 6a21e085f3..9c851b6eb0 100644 --- a/deepmd/model/tensor.py +++ b/deepmd/tf/model/tensor.py @@ -5,11 +5,11 @@ Union, ) -from deepmd.env import ( +from deepmd.tf.env import ( MODEL_VERSION, tf, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/deepmd/nvnmd/__init__.py b/deepmd/tf/nvnmd/__init__.py similarity index 100% rename from deepmd/nvnmd/__init__.py rename to deepmd/tf/nvnmd/__init__.py diff --git a/deepmd/nvnmd/data/__init__.py b/deepmd/tf/nvnmd/data/__init__.py similarity index 100% rename from deepmd/nvnmd/data/__init__.py rename to deepmd/tf/nvnmd/data/__init__.py diff --git a/deepmd/nvnmd/data/data.py b/deepmd/tf/nvnmd/data/data.py similarity index 100% rename from deepmd/nvnmd/data/data.py rename to deepmd/tf/nvnmd/data/data.py diff --git a/deepmd/nvnmd/descriptor/__init__.py b/deepmd/tf/nvnmd/descriptor/__init__.py similarity index 100% rename from deepmd/nvnmd/descriptor/__init__.py rename to deepmd/tf/nvnmd/descriptor/__init__.py diff --git a/deepmd/nvnmd/descriptor/se_a.py b/deepmd/tf/nvnmd/descriptor/se_a.py similarity index 98% rename from deepmd/nvnmd/descriptor/se_a.py rename to deepmd/tf/nvnmd/descriptor/se_a.py index 816f17cfa3..cc90df7a5c 100644 --- a/deepmd/nvnmd/descriptor/se_a.py +++ b/deepmd/tf/nvnmd/descriptor/se_a.py @@ -3,7 +3,7 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, op_module, @@ -11,16 +11,16 @@ ) # -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) -from deepmd.nvnmd.utils.weight import ( +from deepmd.tf.nvnmd.utils.weight import ( get_normalize, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_tensor_by_name_from_graph, ) -from deepmd.utils.network import ( +from deepmd.tf.utils.network import ( embedding_net, ) diff --git a/deepmd/nvnmd/descriptor/se_atten.py b/deepmd/tf/nvnmd/descriptor/se_atten.py similarity index 98% rename from deepmd/nvnmd/descriptor/se_atten.py rename to deepmd/tf/nvnmd/descriptor/se_atten.py index cfffb8a90b..474f6995cf 100644 --- a/deepmd/nvnmd/descriptor/se_atten.py +++ b/deepmd/tf/nvnmd/descriptor/se_atten.py @@ -3,20 +3,20 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, op_module, tf, ) # -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) -from deepmd.nvnmd.utils.weight import ( +from deepmd.tf.nvnmd.utils.weight import ( get_normalize, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_tensor_by_name_from_graph, ) diff --git a/deepmd/nvnmd/entrypoints/__init__.py b/deepmd/tf/nvnmd/entrypoints/__init__.py similarity index 100% rename from deepmd/nvnmd/entrypoints/__init__.py rename to deepmd/tf/nvnmd/entrypoints/__init__.py diff --git a/deepmd/nvnmd/entrypoints/freeze.py b/deepmd/tf/nvnmd/entrypoints/freeze.py similarity index 96% rename from deepmd/nvnmd/entrypoints/freeze.py rename to deepmd/tf/nvnmd/entrypoints/freeze.py index e56a0c2130..2a2b8d9179 100644 --- a/deepmd/nvnmd/entrypoints/freeze.py +++ b/deepmd/tf/nvnmd/entrypoints/freeze.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: LGPL-3.0-or-later -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.nvnmd.utils.fio import ( +from deepmd.tf.nvnmd.utils.fio import ( FioDic, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_tensor_by_name_from_graph, ) diff --git a/deepmd/nvnmd/entrypoints/mapt.py b/deepmd/tf/nvnmd/entrypoints/mapt.py similarity index 98% rename from deepmd/nvnmd/entrypoints/mapt.py rename to deepmd/tf/nvnmd/entrypoints/mapt.py index 1299d7a74e..7401234e35 100644 --- a/deepmd/nvnmd/entrypoints/mapt.py +++ b/deepmd/tf/nvnmd/entrypoints/mapt.py @@ -6,30 +6,30 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( op_module, tf, ) -from deepmd.nvnmd.data.data import ( +from deepmd.tf.nvnmd.data.data import ( jdata_deepmd_input_v0, jdata_sys, ) -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) -from deepmd.nvnmd.utils.fio import ( +from deepmd.tf.nvnmd.utils.fio import ( FioDic, ) -from deepmd.nvnmd.utils.network import ( +from deepmd.tf.nvnmd.utils.network import ( get_sess, ) -from deepmd.nvnmd.utils.weight import ( +from deepmd.tf.nvnmd.utils.weight import ( get_filter_type_weight, get_filter_weight, get_normalize, get_type_embedding_weight, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) diff --git a/deepmd/nvnmd/entrypoints/train.py b/deepmd/tf/nvnmd/entrypoints/train.py similarity index 94% rename from deepmd/nvnmd/entrypoints/train.py rename to deepmd/tf/nvnmd/entrypoints/train.py index 6e14b6f865..18c644a7f6 100644 --- a/deepmd/nvnmd/entrypoints/train.py +++ b/deepmd/tf/nvnmd/entrypoints/train.py @@ -5,28 +5,28 @@ Optional, ) -from deepmd.entrypoints.freeze import ( +from deepmd.tf.entrypoints.freeze import ( freeze, ) -from deepmd.entrypoints.train import ( +from deepmd.tf.entrypoints.train import ( train, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.nvnmd.data.data import ( +from deepmd.tf.nvnmd.data.data import ( jdata_deepmd_input_v0, ) -from deepmd.nvnmd.entrypoints.mapt import ( +from deepmd.tf.nvnmd.entrypoints.mapt import ( mapt, ) -from deepmd.nvnmd.entrypoints.wrap import ( +from deepmd.tf.nvnmd.entrypoints.wrap import ( wrap, ) -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) -from deepmd.nvnmd.utils.fio import ( +from deepmd.tf.nvnmd.utils.fio import ( FioDic, ) diff --git a/deepmd/nvnmd/entrypoints/wrap.py b/deepmd/tf/nvnmd/entrypoints/wrap.py similarity index 98% rename from deepmd/nvnmd/entrypoints/wrap.py rename to deepmd/tf/nvnmd/entrypoints/wrap.py index 1ba2ed7384..f2be8352e2 100644 --- a/deepmd/nvnmd/entrypoints/wrap.py +++ b/deepmd/tf/nvnmd/entrypoints/wrap.py @@ -6,32 +6,32 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( op_module, tf, ) -from deepmd.nvnmd.data.data import ( +from deepmd.tf.nvnmd.data.data import ( jdata_deepmd_input_v0, jdata_sys, ) -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) -from deepmd.nvnmd.utils.encode import ( +from deepmd.tf.nvnmd.utils.encode import ( Encode, ) -from deepmd.nvnmd.utils.fio import ( +from deepmd.tf.nvnmd.utils.fio import ( FioBin, FioTxt, ) -from deepmd.nvnmd.utils.network import ( +from deepmd.tf.nvnmd.utils.network import ( get_sess, ) -from deepmd.nvnmd.utils.weight import ( +from deepmd.tf.nvnmd.utils.weight import ( get_fitnet_weight, get_type_weight, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) diff --git a/deepmd/nvnmd/fit/__init__.py b/deepmd/tf/nvnmd/fit/__init__.py similarity index 100% rename from deepmd/nvnmd/fit/__init__.py rename to deepmd/tf/nvnmd/fit/__init__.py diff --git a/deepmd/nvnmd/fit/ener.py b/deepmd/tf/nvnmd/fit/ener.py similarity index 58% rename from deepmd/nvnmd/fit/ener.py rename to deepmd/tf/nvnmd/fit/ener.py index 1f316a2145..20adda395c 100644 --- a/deepmd/nvnmd/fit/ener.py +++ b/deepmd/tf/nvnmd/fit/ener.py @@ -1,12 +1,12 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, tf, ) -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) -from deepmd.nvnmd.utils.network import one_layer as one_layer_nvnmd +from deepmd.tf.nvnmd.utils.network import one_layer as one_layer_nvnmd __all__ = [ "GLOBAL_TF_FLOAT_PRECISION", diff --git a/deepmd/nvnmd/utils/__init__.py b/deepmd/tf/nvnmd/utils/__init__.py similarity index 100% rename from deepmd/nvnmd/utils/__init__.py rename to deepmd/tf/nvnmd/utils/__init__.py diff --git a/deepmd/nvnmd/utils/argcheck.py b/deepmd/tf/nvnmd/utils/argcheck.py similarity index 100% rename from deepmd/nvnmd/utils/argcheck.py rename to deepmd/tf/nvnmd/utils/argcheck.py diff --git a/deepmd/nvnmd/utils/config.py b/deepmd/tf/nvnmd/utils/config.py similarity index 99% rename from deepmd/nvnmd/utils/config.py rename to deepmd/tf/nvnmd/utils/config.py index 5bfd9ea54f..15998069b3 100644 --- a/deepmd/nvnmd/utils/config.py +++ b/deepmd/tf/nvnmd/utils/config.py @@ -3,7 +3,7 @@ import numpy as np -from deepmd.nvnmd.data.data import ( +from deepmd.tf.nvnmd.data.data import ( NVNMD_CITATION, NVNMD_WELCOME, jdata_config_v0, @@ -17,10 +17,10 @@ jdata_deepmd_input_v1_ni128, jdata_deepmd_input_v1_ni256, ) -from deepmd.nvnmd.utils.fio import ( +from deepmd.tf.nvnmd.utils.fio import ( FioDic, ) -from deepmd.nvnmd.utils.op import ( +from deepmd.tf.nvnmd.utils.op import ( r2s, ) diff --git a/deepmd/nvnmd/utils/encode.py b/deepmd/tf/nvnmd/utils/encode.py similarity index 99% rename from deepmd/nvnmd/utils/encode.py rename to deepmd/tf/nvnmd/utils/encode.py index 55f4efd52e..21398fbf23 100644 --- a/deepmd/nvnmd/utils/encode.py +++ b/deepmd/tf/nvnmd/utils/encode.py @@ -3,7 +3,7 @@ import numpy as np -from deepmd.nvnmd.data.data import ( +from deepmd.tf.nvnmd.data.data import ( jdata_sys, ) diff --git a/deepmd/nvnmd/utils/fio.py b/deepmd/tf/nvnmd/utils/fio.py similarity index 100% rename from deepmd/nvnmd/utils/fio.py rename to deepmd/tf/nvnmd/utils/fio.py diff --git a/deepmd/nvnmd/utils/network.py b/deepmd/tf/nvnmd/utils/network.py similarity index 98% rename from deepmd/nvnmd/utils/network.py rename to deepmd/tf/nvnmd/utils/network.py index f0c357eabe..76c80ed4e7 100644 --- a/deepmd/nvnmd/utils/network.py +++ b/deepmd/tf/nvnmd/utils/network.py @@ -3,18 +3,18 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, op_module, tf, ) -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) -from deepmd.nvnmd.utils.weight import ( +from deepmd.tf.nvnmd.utils.weight import ( get_constant_initializer, ) -from deepmd.utils.network import ( +from deepmd.tf.utils.network import ( variable_summaries, ) diff --git a/deepmd/nvnmd/utils/op.py b/deepmd/tf/nvnmd/utils/op.py similarity index 100% rename from deepmd/nvnmd/utils/op.py rename to deepmd/tf/nvnmd/utils/op.py diff --git a/deepmd/nvnmd/utils/weight.py b/deepmd/tf/nvnmd/utils/weight.py similarity index 98% rename from deepmd/nvnmd/utils/weight.py rename to deepmd/tf/nvnmd/utils/weight.py index cc5ab15219..7a60712455 100644 --- a/deepmd/nvnmd/utils/weight.py +++ b/deepmd/tf/nvnmd/utils/weight.py @@ -1,10 +1,10 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) diff --git a/deepmd/op/__init__.py b/deepmd/tf/op/__init__.py similarity index 96% rename from deepmd/op/__init__.py rename to deepmd/tf/op/__init__.py index 9cdfec70cc..421ef0b123 100644 --- a/deepmd/op/__init__.py +++ b/deepmd/tf/op/__init__.py @@ -8,7 +8,7 @@ ) NOT_LOADABLE = ("__init__.py",) -PACKAGE_BASE = "deepmd.op" +PACKAGE_BASE = "deepmd.tf.op" log = logging.getLogger(__name__) diff --git a/deepmd/op/_add_flt_nvnmd_grad.py b/deepmd/tf/op/_add_flt_nvnmd_grad.py similarity index 90% rename from deepmd/op/_add_flt_nvnmd_grad.py rename to deepmd/tf/op/_add_flt_nvnmd_grad.py index 105ec1ec6d..3bea39fcec 100644 --- a/deepmd/op/_add_flt_nvnmd_grad.py +++ b/deepmd/tf/op/_add_flt_nvnmd_grad.py @@ -5,7 +5,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, ) diff --git a/deepmd/op/_copy_flt_nvnmd_grad.py b/deepmd/tf/op/_copy_flt_nvnmd_grad.py similarity index 91% rename from deepmd/op/_copy_flt_nvnmd_grad.py rename to deepmd/tf/op/_copy_flt_nvnmd_grad.py index 09c4a72324..401acba22c 100644 --- a/deepmd/op/_copy_flt_nvnmd_grad.py +++ b/deepmd/tf/op/_copy_flt_nvnmd_grad.py @@ -5,7 +5,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, ) diff --git a/deepmd/op/_dotmul_flt_nvnmd_grad.py b/deepmd/tf/op/_dotmul_flt_nvnmd_grad.py similarity index 95% rename from deepmd/op/_dotmul_flt_nvnmd_grad.py rename to deepmd/tf/op/_dotmul_flt_nvnmd_grad.py index 0f786a6d38..8a4ffb2d0c 100644 --- a/deepmd/op/_dotmul_flt_nvnmd_grad.py +++ b/deepmd/tf/op/_dotmul_flt_nvnmd_grad.py @@ -5,7 +5,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, tf, ) diff --git a/deepmd/op/_flt_nvnmd_grad.py b/deepmd/tf/op/_flt_nvnmd_grad.py similarity index 90% rename from deepmd/op/_flt_nvnmd_grad.py rename to deepmd/tf/op/_flt_nvnmd_grad.py index 0dd67c2c57..b0fbaea11d 100644 --- a/deepmd/op/_flt_nvnmd_grad.py +++ b/deepmd/tf/op/_flt_nvnmd_grad.py @@ -5,7 +5,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, ) diff --git a/deepmd/op/_gelu.py b/deepmd/tf/op/_gelu.py similarity index 97% rename from deepmd/op/_gelu.py rename to deepmd/tf/op/_gelu.py index 6768ac10b3..fcfd2d49fa 100644 --- a/deepmd/op/_gelu.py +++ b/deepmd/tf/op/_gelu.py @@ -6,7 +6,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, ) diff --git a/deepmd/op/_map_flt_nvnmd_grad.py b/deepmd/tf/op/_map_flt_nvnmd_grad.py similarity index 97% rename from deepmd/op/_map_flt_nvnmd_grad.py rename to deepmd/tf/op/_map_flt_nvnmd_grad.py index 3e5749e74c..46f258cafe 100644 --- a/deepmd/op/_map_flt_nvnmd_grad.py +++ b/deepmd/tf/op/_map_flt_nvnmd_grad.py @@ -5,7 +5,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, tf, ) diff --git a/deepmd/op/_matmul_fitnet_nvnmd_grad.py b/deepmd/tf/op/_matmul_fitnet_nvnmd_grad.py similarity index 94% rename from deepmd/op/_matmul_fitnet_nvnmd_grad.py rename to deepmd/tf/op/_matmul_fitnet_nvnmd_grad.py index bab3905c5a..f8d566bd39 100644 --- a/deepmd/op/_matmul_fitnet_nvnmd_grad.py +++ b/deepmd/tf/op/_matmul_fitnet_nvnmd_grad.py @@ -5,7 +5,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, tf, ) diff --git a/deepmd/op/_matmul_flt2fix_nvnmd.py b/deepmd/tf/op/_matmul_flt2fix_nvnmd.py similarity index 97% rename from deepmd/op/_matmul_flt2fix_nvnmd.py rename to deepmd/tf/op/_matmul_flt2fix_nvnmd.py index db9af761de..319fb90ec8 100644 --- a/deepmd/op/_matmul_flt2fix_nvnmd.py +++ b/deepmd/tf/op/_matmul_flt2fix_nvnmd.py @@ -5,7 +5,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, tf, ) diff --git a/deepmd/op/_matmul_flt_nvnmd_grad.py b/deepmd/tf/op/_matmul_flt_nvnmd_grad.py similarity index 97% rename from deepmd/op/_matmul_flt_nvnmd_grad.py rename to deepmd/tf/op/_matmul_flt_nvnmd_grad.py index 1e3ed74c91..6493794b00 100644 --- a/deepmd/op/_matmul_flt_nvnmd_grad.py +++ b/deepmd/tf/op/_matmul_flt_nvnmd_grad.py @@ -5,7 +5,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, tf, ) diff --git a/deepmd/op/_mul_flt_nvnmd_grad.py b/deepmd/tf/op/_mul_flt_nvnmd_grad.py similarity index 96% rename from deepmd/op/_mul_flt_nvnmd_grad.py rename to deepmd/tf/op/_mul_flt_nvnmd_grad.py index c50baf8c12..d05daa7dfa 100644 --- a/deepmd/op/_mul_flt_nvnmd_grad.py +++ b/deepmd/tf/op/_mul_flt_nvnmd_grad.py @@ -5,7 +5,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, tf, ) diff --git a/deepmd/op/_prod_force_grad.py b/deepmd/tf/op/_prod_force_grad.py similarity index 95% rename from deepmd/op/_prod_force_grad.py rename to deepmd/tf/op/_prod_force_grad.py index ffa34a8126..449901c137 100644 --- a/deepmd/op/_prod_force_grad.py +++ b/deepmd/tf/op/_prod_force_grad.py @@ -6,7 +6,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_grads_module, ) diff --git a/deepmd/op/_prod_force_se_a_grad.py b/deepmd/tf/op/_prod_force_se_a_grad.py similarity index 95% rename from deepmd/op/_prod_force_se_a_grad.py rename to deepmd/tf/op/_prod_force_se_a_grad.py index b58b819ee1..d732803bad 100644 --- a/deepmd/op/_prod_force_se_a_grad.py +++ b/deepmd/tf/op/_prod_force_se_a_grad.py @@ -6,7 +6,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_grads_module, ) diff --git a/deepmd/op/_prod_force_se_a_mask_grad.py b/deepmd/tf/op/_prod_force_se_a_mask_grad.py similarity index 95% rename from deepmd/op/_prod_force_se_a_mask_grad.py rename to deepmd/tf/op/_prod_force_se_a_mask_grad.py index d5ef829da2..a7f2d72b16 100644 --- a/deepmd/op/_prod_force_se_a_mask_grad.py +++ b/deepmd/tf/op/_prod_force_se_a_mask_grad.py @@ -6,7 +6,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_grads_module, ) diff --git a/deepmd/op/_prod_force_se_r_grad.py b/deepmd/tf/op/_prod_force_se_r_grad.py similarity index 93% rename from deepmd/op/_prod_force_se_r_grad.py rename to deepmd/tf/op/_prod_force_se_r_grad.py index 254e2e331a..4ec65b31f2 100644 --- a/deepmd/op/_prod_force_se_r_grad.py +++ b/deepmd/tf/op/_prod_force_se_r_grad.py @@ -6,7 +6,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_grads_module, ) diff --git a/deepmd/op/_prod_virial_grad.py b/deepmd/tf/op/_prod_virial_grad.py similarity index 95% rename from deepmd/op/_prod_virial_grad.py rename to deepmd/tf/op/_prod_virial_grad.py index 4a946f3ba8..7fe245ed6b 100644 --- a/deepmd/op/_prod_virial_grad.py +++ b/deepmd/tf/op/_prod_virial_grad.py @@ -6,7 +6,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_grads_module, ) diff --git a/deepmd/op/_prod_virial_se_a_grad.py b/deepmd/tf/op/_prod_virial_se_a_grad.py similarity index 95% rename from deepmd/op/_prod_virial_se_a_grad.py rename to deepmd/tf/op/_prod_virial_se_a_grad.py index 0e738f86b3..c95d3b58e2 100644 --- a/deepmd/op/_prod_virial_se_a_grad.py +++ b/deepmd/tf/op/_prod_virial_se_a_grad.py @@ -6,7 +6,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_grads_module, ) diff --git a/deepmd/op/_prod_virial_se_r_grad.py b/deepmd/tf/op/_prod_virial_se_r_grad.py similarity index 94% rename from deepmd/op/_prod_virial_se_r_grad.py rename to deepmd/tf/op/_prod_virial_se_r_grad.py index a943b35670..8f51310c8c 100644 --- a/deepmd/op/_prod_virial_se_r_grad.py +++ b/deepmd/tf/op/_prod_virial_se_r_grad.py @@ -6,7 +6,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_grads_module, ) diff --git a/deepmd/op/_quantize_nvnmd_grad.py b/deepmd/tf/op/_quantize_nvnmd_grad.py similarity index 93% rename from deepmd/op/_quantize_nvnmd_grad.py rename to deepmd/tf/op/_quantize_nvnmd_grad.py index 2ef282fa78..f1d99dc18d 100644 --- a/deepmd/op/_quantize_nvnmd_grad.py +++ b/deepmd/tf/op/_quantize_nvnmd_grad.py @@ -5,7 +5,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, ) diff --git a/deepmd/op/_soft_min_force_grad.py b/deepmd/tf/op/_soft_min_force_grad.py similarity index 95% rename from deepmd/op/_soft_min_force_grad.py rename to deepmd/tf/op/_soft_min_force_grad.py index ae9cf882c8..cd18f3e186 100644 --- a/deepmd/op/_soft_min_force_grad.py +++ b/deepmd/tf/op/_soft_min_force_grad.py @@ -6,7 +6,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_grads_module, ) diff --git a/deepmd/op/_soft_min_virial_grad.py b/deepmd/tf/op/_soft_min_virial_grad.py similarity index 95% rename from deepmd/op/_soft_min_virial_grad.py rename to deepmd/tf/op/_soft_min_virial_grad.py index 56b828b12c..4d4f4790dd 100644 --- a/deepmd/op/_soft_min_virial_grad.py +++ b/deepmd/tf/op/_soft_min_virial_grad.py @@ -6,7 +6,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_grads_module, ) diff --git a/deepmd/op/_tabulate_grad.py b/deepmd/tf/op/_tabulate_grad.py similarity index 97% rename from deepmd/op/_tabulate_grad.py rename to deepmd/tf/op/_tabulate_grad.py index 8ad8908d7e..667981ef9f 100644 --- a/deepmd/op/_tabulate_grad.py +++ b/deepmd/tf/op/_tabulate_grad.py @@ -6,11 +6,11 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, ) -# from deepmd.DescrptSeATabulate import last_layer_size +# from deepmd.tf.DescrptSeATabulate import last_layer_size @ops.RegisterGradient("TabulateFusion") diff --git a/deepmd/op/_tanh4_flt_nvnmd_grad.py b/deepmd/tf/op/_tanh4_flt_nvnmd_grad.py similarity index 97% rename from deepmd/op/_tanh4_flt_nvnmd_grad.py rename to deepmd/tf/op/_tanh4_flt_nvnmd_grad.py index 45d7366545..04d1724d0b 100644 --- a/deepmd/op/_tanh4_flt_nvnmd_grad.py +++ b/deepmd/tf/op/_tanh4_flt_nvnmd_grad.py @@ -5,7 +5,7 @@ ops, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) diff --git a/deepmd/train/__init__.py b/deepmd/tf/train/__init__.py similarity index 100% rename from deepmd/train/__init__.py rename to deepmd/tf/train/__init__.py diff --git a/deepmd/train/run_options.py b/deepmd/tf/train/run_options.py similarity index 98% rename from deepmd/train/run_options.py rename to deepmd/tf/train/run_options.py index 451632949e..fb9d8beecb 100644 --- a/deepmd/train/run_options.py +++ b/deepmd/tf/train/run_options.py @@ -16,17 +16,17 @@ Version, ) -from deepmd.cluster import ( +from deepmd.tf.cluster import ( get_resource, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_CONFIG, TF_VERSION, get_tf_default_nthreads, global_float_prec, tf, ) -from deepmd.loggers import ( +from deepmd.tf.loggers import ( set_log_handles, ) diff --git a/deepmd/train/trainer.py b/deepmd/tf/train/trainer.py similarity index 99% rename from deepmd/train/trainer.py rename to deepmd/tf/train/trainer.py index 3b81740a93..19b81d7a13 100644 --- a/deepmd/train/trainer.py +++ b/deepmd/tf/train/trainer.py @@ -21,13 +21,13 @@ ) # load grad of force module -import deepmd.op # noqa: F401 -from deepmd.common import ( +import deepmd.tf.op # noqa: F401 +from deepmd.tf.common import ( data_requirement, get_precision, j_must_have, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_ENER_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, TF_VERSION, @@ -35,38 +35,38 @@ tf, tfv2, ) -from deepmd.fit.ener import ( +from deepmd.tf.fit.ener import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( MultiModel, ) -from deepmd.model.model import ( +from deepmd.tf.model.model import ( Model, ) -from deepmd.utils import random as dp_random -from deepmd.utils.data_system import ( +from deepmd.tf.utils import random as dp_random +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) -from deepmd.utils.errors import ( +from deepmd.tf.utils.errors import ( GraphTooLargeError, GraphWithoutTensorError, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_tensor_by_name_from_graph, load_graph_def, ) -from deepmd.utils.learning_rate import ( +from deepmd.tf.utils.learning_rate import ( LearningRateExp, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) log = logging.getLogger(__name__) # nvnmd -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) diff --git a/deepmd/utils/__init__.py b/deepmd/tf/utils/__init__.py similarity index 100% rename from deepmd/utils/__init__.py rename to deepmd/tf/utils/__init__.py diff --git a/deepmd/utils/argcheck.py b/deepmd/tf/utils/argcheck.py similarity index 100% rename from deepmd/utils/argcheck.py rename to deepmd/tf/utils/argcheck.py diff --git a/deepmd/utils/batch_size.py b/deepmd/tf/utils/batch_size.py similarity index 94% rename from deepmd/utils/batch_size.py rename to deepmd/tf/utils/batch_size.py index 863520b3f4..50d02a887b 100644 --- a/deepmd/utils/batch_size.py +++ b/deepmd/tf/utils/batch_size.py @@ -3,11 +3,11 @@ Version, ) -from deepmd.env import ( +from deepmd.tf.env import ( TF_VERSION, tf, ) -from deepmd.utils.errors import ( +from deepmd.tf.utils.errors import ( OutOfMemoryError, ) from deepmd_utils.utils.batch_size import AutoBatchSize as AutoBatchSizeBase diff --git a/deepmd/utils/compat.py b/deepmd/tf/utils/compat.py similarity index 100% rename from deepmd/utils/compat.py rename to deepmd/tf/utils/compat.py diff --git a/deepmd/utils/compress.py b/deepmd/tf/utils/compress.py similarity index 98% rename from deepmd/utils/compress.py rename to deepmd/tf/utils/compress.py index 7a79dec520..0bce633573 100644 --- a/deepmd/utils/compress.py +++ b/deepmd/tf/utils/compress.py @@ -3,10 +3,10 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_pattern_nodes_from_graph_def, get_tensor_by_name_from_graph, ) diff --git a/deepmd/utils/convert.py b/deepmd/tf/utils/convert.py similarity index 99% rename from deepmd/utils/convert.py rename to deepmd/tf/utils/convert.py index 13e07f0885..625f54a9a0 100644 --- a/deepmd/utils/convert.py +++ b/deepmd/tf/utils/convert.py @@ -14,10 +14,10 @@ ) from packaging.version import parse as parse_version -from deepmd import ( +from deepmd.tf import ( __version__, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) diff --git a/deepmd/utils/data.py b/deepmd/tf/utils/data.py similarity index 100% rename from deepmd/utils/data.py rename to deepmd/tf/utils/data.py diff --git a/deepmd/utils/data_system.py b/deepmd/tf/utils/data_system.py similarity index 100% rename from deepmd/utils/data_system.py rename to deepmd/tf/utils/data_system.py diff --git a/deepmd/utils/errors.py b/deepmd/tf/utils/errors.py similarity index 100% rename from deepmd/utils/errors.py rename to deepmd/tf/utils/errors.py diff --git a/deepmd/utils/finetune.py b/deepmd/tf/utils/finetune.py similarity index 98% rename from deepmd/utils/finetune.py rename to deepmd/tf/utils/finetune.py index cc6c0224de..01b5eaaafe 100644 --- a/deepmd/utils/finetune.py +++ b/deepmd/tf/utils/finetune.py @@ -6,10 +6,10 @@ Dict, ) -from deepmd.utils.errors import ( +from deepmd.tf.utils.errors import ( GraphWithoutTensorError, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_tensor_by_name, ) diff --git a/deepmd/utils/graph.py b/deepmd/tf/utils/graph.py similarity index 99% rename from deepmd/utils/graph.py rename to deepmd/tf/utils/graph.py index ad4ee0224a..9d2608e34a 100644 --- a/deepmd/utils/graph.py +++ b/deepmd/tf/utils/graph.py @@ -7,17 +7,17 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( ATTENTION_LAYER_PATTERN, EMBEDDING_NET_PATTERN, FITTING_NET_PATTERN, TYPE_EMBEDDING_PATTERN, tf, ) -from deepmd.utils.errors import ( +from deepmd.tf.utils.errors import ( GraphWithoutTensorError, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) diff --git a/deepmd/utils/learning_rate.py b/deepmd/tf/utils/learning_rate.py similarity index 99% rename from deepmd/utils/learning_rate.py rename to deepmd/tf/utils/learning_rate.py index 5bec5120cd..519bf20bd0 100644 --- a/deepmd/utils/learning_rate.py +++ b/deepmd/tf/utils/learning_rate.py @@ -5,7 +5,7 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) diff --git a/deepmd/utils/multi_init.py b/deepmd/tf/utils/multi_init.py similarity index 98% rename from deepmd/utils/multi_init.py rename to deepmd/tf/utils/multi_init.py index 6c070dc67e..056a6694e8 100644 --- a/deepmd/utils/multi_init.py +++ b/deepmd/tf/utils/multi_init.py @@ -6,10 +6,10 @@ Dict, ) -from deepmd.utils.errors import ( +from deepmd.tf.utils.errors import ( GraphWithoutTensorError, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_tensor_by_name, ) diff --git a/deepmd/utils/neighbor_stat.py b/deepmd/tf/utils/neighbor_stat.py similarity index 98% rename from deepmd/utils/neighbor_stat.py rename to deepmd/tf/utils/neighbor_stat.py index fa9325937e..a240b515db 100644 --- a/deepmd/utils/neighbor_stat.py +++ b/deepmd/tf/utils/neighbor_stat.py @@ -8,16 +8,16 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, default_tf_session_config, op_module, tf, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) -from deepmd.utils.parallel_op import ( +from deepmd.tf.utils.parallel_op import ( ParallelOp, ) diff --git a/deepmd/utils/network.py b/deepmd/tf/utils/network.py similarity index 99% rename from deepmd/utils/network.py rename to deepmd/tf/utils/network.py index 36d8c42f82..fb8e89c737 100644 --- a/deepmd/utils/network.py +++ b/deepmd/tf/utils/network.py @@ -1,10 +1,10 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( get_precision, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, tf, ) diff --git a/deepmd/utils/pair_tab.py b/deepmd/tf/utils/pair_tab.py similarity index 100% rename from deepmd/utils/pair_tab.py rename to deepmd/tf/utils/pair_tab.py diff --git a/deepmd/utils/parallel_op.py b/deepmd/tf/utils/parallel_op.py similarity index 94% rename from deepmd/utils/parallel_op.py rename to deepmd/tf/utils/parallel_op.py index 9ef68bbd84..b7590ed720 100644 --- a/deepmd/utils/parallel_op.py +++ b/deepmd/tf/utils/parallel_op.py @@ -8,10 +8,10 @@ Tuple, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) @@ -30,8 +30,8 @@ class ParallelOp: Examples -------- - >>> from deepmd.env import tf - >>> from deepmd.utils.parallel_op import ParallelOp + >>> from deepmd.tf.env import tf + >>> from deepmd.tf.utils.parallel_op import ParallelOp >>> def builder(): ... x = tf.placeholder(tf.int32, [1]) ... return {"x": x}, (x + 1) diff --git a/deepmd/utils/path.py b/deepmd/tf/utils/path.py similarity index 100% rename from deepmd/utils/path.py rename to deepmd/tf/utils/path.py diff --git a/deepmd/utils/plugin.py b/deepmd/tf/utils/plugin.py similarity index 100% rename from deepmd/utils/plugin.py rename to deepmd/tf/utils/plugin.py diff --git a/deepmd/utils/random.py b/deepmd/tf/utils/random.py similarity index 100% rename from deepmd/utils/random.py rename to deepmd/tf/utils/random.py diff --git a/deepmd/utils/sess.py b/deepmd/tf/utils/sess.py similarity index 95% rename from deepmd/utils/sess.py rename to deepmd/tf/utils/sess.py index a87adffd91..ca98980f89 100644 --- a/deepmd/utils/sess.py +++ b/deepmd/tf/utils/sess.py @@ -1,10 +1,10 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import os -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.utils.errors import ( +from deepmd.tf.utils.errors import ( OutOfMemoryError, ) diff --git a/deepmd/utils/spin.py b/deepmd/tf/utils/spin.py similarity index 98% rename from deepmd/utils/spin.py rename to deepmd/tf/utils/spin.py index 7820627649..c20d4dcc7b 100644 --- a/deepmd/utils/spin.py +++ b/deepmd/tf/utils/spin.py @@ -4,7 +4,7 @@ Optional, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, tf, ) diff --git a/deepmd/utils/tabulate.py b/deepmd/tf/utils/tabulate.py similarity index 92% rename from deepmd/utils/tabulate.py rename to deepmd/tf/utils/tabulate.py index 2b270b1dbc..4ade5962e0 100644 --- a/deepmd/utils/tabulate.py +++ b/deepmd/tf/utils/tabulate.py @@ -16,17 +16,17 @@ ) import deepmd -from deepmd.common import ( +from deepmd.tf.common import ( ACTIVATION_FN_DICT, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( Descriptor, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, tf, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_embedding_net_nodes_from_graph_def, get_tensor_by_name_from_graph, ) @@ -107,15 +107,15 @@ def __init__( self.sub_graph, self.sub_graph_def = self._load_sub_graph() self.sub_sess = tf.Session(graph=self.sub_graph) - if isinstance(self.descrpt, deepmd.descriptor.DescrptSeR): + if isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeR): self.sel_a = self.descrpt.sel_r self.rcut = self.descrpt.rcut self.rcut_smth = self.descrpt.rcut_smth - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeA): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeA): self.sel_a = self.descrpt.sel_a self.rcut = self.descrpt.rcut_r self.rcut_smth = self.descrpt.rcut_r_smth - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeT): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeT): self.sel_a = self.descrpt.sel_a self.rcut = self.descrpt.rcut_r self.rcut_smth = self.descrpt.rcut_r_smth @@ -179,8 +179,8 @@ def build( """ # tabulate range [lower, upper] with stride0 'stride0' lower, upper = self._get_env_mat_range(min_nbor_dist) - if isinstance(self.descrpt, deepmd.descriptor.DescrptSeAtten) or isinstance( - self.descrpt, deepmd.descriptor.DescrptSeAEbdV2 + if isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeAtten) or isinstance( + self.descrpt, deepmd.tf.descriptor.DescrptSeAEbdV2 ): uu = np.max(upper) ll = np.min(lower) @@ -196,7 +196,7 @@ def build( self._build_lower( "filter_net", xx, 0, uu, ll, stride0, stride1, extrapolate, nspline ) - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeA): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeA): for ii in range(self.table_size): if (self.type_one_side and not self._all_excluded(ii)) or ( not self.type_one_side @@ -233,7 +233,7 @@ def build( self._build_lower( net, xx, ii, uu, ll, stride0, stride1, extrapolate, nspline ) - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeT): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeT): xx_all = [] for ii in range(self.ntypes): xx = np.arange( @@ -275,7 +275,7 @@ def build( nspline[ii], ) idx += 1 - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeR): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeR): for ii in range(self.table_size): if (self.type_one_side and not self._all_excluded(ii)) or ( not self.type_one_side @@ -327,10 +327,10 @@ def _build_lower( ) # tt.shape: [nspline, self.last_layer_size] - if isinstance(self.descrpt, deepmd.descriptor.DescrptSeA): + if isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeA): tt = np.full((nspline, self.last_layer_size), stride1) tt[: int((upper - lower) / stride0), :] = stride0 - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeT): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeT): tt = np.full((nspline, self.last_layer_size), stride1) tt[ int((lower - extrapolate * lower) / stride1) + 1 : ( @@ -339,7 +339,7 @@ def _build_lower( ), :, ] = stride0 - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeR): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeR): tt = np.full((nspline, self.last_layer_size), stride1) tt[: int((upper - lower) / stride0), :] = stride0 else: @@ -423,14 +423,14 @@ def _get_bias(self): bias = {} for layer in range(1, self.layer_size + 1): bias["layer_" + str(layer)] = [] - if isinstance(self.descrpt, deepmd.descriptor.DescrptSeAtten) or isinstance( - self.descrpt, deepmd.descriptor.DescrptSeAEbdV2 - ): + if isinstance( + self.descrpt, deepmd.tf.descriptor.DescrptSeAtten + ) or isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeAEbdV2): node = self.embedding_net_nodes[ f"filter_type_all{self.suffix}/bias_{layer}" ] bias["layer_" + str(layer)].append(tf.make_ndarray(node)) - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeA): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeA): if self.type_one_side: for ii in range(0, self.ntypes): if not self._all_excluded(ii): @@ -452,14 +452,14 @@ def _get_bias(self): bias["layer_" + str(layer)].append(tf.make_ndarray(node)) else: bias["layer_" + str(layer)].append(np.array([])) - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeT): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeT): for ii in range(self.ntypes): for jj in range(ii, self.ntypes): node = self.embedding_net_nodes[ f"filter_type_all{self.suffix}/bias_{layer}_{ii}_{jj}" ] bias["layer_" + str(layer)].append(tf.make_ndarray(node)) - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeR): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeR): if self.type_one_side: for ii in range(0, self.ntypes): if not self._all_excluded(ii): @@ -489,14 +489,14 @@ def _get_matrix(self): matrix = {} for layer in range(1, self.layer_size + 1): matrix["layer_" + str(layer)] = [] - if isinstance(self.descrpt, deepmd.descriptor.DescrptSeAtten) or isinstance( - self.descrpt, deepmd.descriptor.DescrptSeAEbdV2 - ): + if isinstance( + self.descrpt, deepmd.tf.descriptor.DescrptSeAtten + ) or isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeAEbdV2): node = self.embedding_net_nodes[ f"filter_type_all{self.suffix}/matrix_{layer}" ] matrix["layer_" + str(layer)].append(tf.make_ndarray(node)) - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeA): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeA): if self.type_one_side: for ii in range(0, self.ntypes): if not self._all_excluded(ii): @@ -518,14 +518,14 @@ def _get_matrix(self): matrix["layer_" + str(layer)].append(tf.make_ndarray(node)) else: matrix["layer_" + str(layer)].append(np.array([])) - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeT): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeT): for ii in range(self.ntypes): for jj in range(ii, self.ntypes): node = self.embedding_net_nodes[ f"filter_type_all{self.suffix}/matrix_{layer}_{ii}_{jj}" ] matrix["layer_" + str(layer)].append(tf.make_ndarray(node)) - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeR): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeR): if self.type_one_side: for ii in range(0, self.ntypes): if not self._all_excluded(ii): @@ -712,14 +712,14 @@ def _layer_1(self, x, w, b): # Change the embedding net range to sw / min_nbor_dist def _get_env_mat_range(self, min_nbor_dist): sw = self._spline5_switch(min_nbor_dist, self.rcut_smth, self.rcut) - if isinstance(self.descrpt, deepmd.descriptor.DescrptSeA): + if isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeA): lower = -self.davg[:, 0] / self.dstd[:, 0] upper = ((1 / min_nbor_dist) * sw - self.davg[:, 0]) / self.dstd[:, 0] - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeT): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeT): var = np.square(sw / (min_nbor_dist * self.dstd[:, 1:4])) lower = np.min(-var, axis=1) upper = np.max(var, axis=1) - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeR): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeR): lower = -self.davg[:, 0] / self.dstd[:, 0] upper = ((1 / min_nbor_dist) * sw - self.davg[:, 0]) / self.dstd[:, 0] else: @@ -741,11 +741,11 @@ def _spline5_switch(self, xx, rmin, rmax): def _get_layer_size(self): layer_size = 0 - if isinstance(self.descrpt, deepmd.descriptor.DescrptSeAtten) or isinstance( - self.descrpt, deepmd.descriptor.DescrptSeAEbdV2 + if isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeAtten) or isinstance( + self.descrpt, deepmd.tf.descriptor.DescrptSeAEbdV2 ): layer_size = len(self.embedding_net_nodes) // 2 - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeA): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeA): layer_size = len(self.embedding_net_nodes) // ( (self.ntypes * self.ntypes - len(self.exclude_types)) * 2 ) @@ -753,11 +753,11 @@ def _get_layer_size(self): layer_size = len(self.embedding_net_nodes) // ( (self.ntypes - self._n_all_excluded) * 2 ) - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeT): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeT): layer_size = len(self.embedding_net_nodes) // int( comb(self.ntypes + 1, 2) * 2 ) - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeR): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeR): layer_size = len(self.embedding_net_nodes) // ( (self.ntypes * self.ntypes - len(self.exclude_types)) * 2 ) @@ -793,17 +793,17 @@ def _all_excluded(self, ii: int) -> bool: def _get_table_size(self): table_size = 0 - if isinstance(self.descrpt, deepmd.descriptor.DescrptSeAtten) or isinstance( - self.descrpt, deepmd.descriptor.DescrptSeAEbdV2 + if isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeAtten) or isinstance( + self.descrpt, deepmd.tf.descriptor.DescrptSeAEbdV2 ): table_size = 1 - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeA): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeA): table_size = self.ntypes * self.ntypes if self.type_one_side: table_size = self.ntypes - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeT): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeT): table_size = int(comb(self.ntypes + 1, 2)) - elif isinstance(self.descrpt, deepmd.descriptor.DescrptSeR): + elif isinstance(self.descrpt, deepmd.tf.descriptor.DescrptSeR): table_size = self.ntypes * self.ntypes if self.type_one_side: table_size = self.ntypes diff --git a/deepmd/utils/type_embed.py b/deepmd/tf/utils/type_embed.py similarity index 97% rename from deepmd/utils/type_embed.py rename to deepmd/tf/utils/type_embed.py index c8ab01f7f5..1cd20814d7 100644 --- a/deepmd/utils/type_embed.py +++ b/deepmd/tf/utils/type_embed.py @@ -5,20 +5,20 @@ Union, ) -from deepmd.common import ( +from deepmd.tf.common import ( get_activation_func, get_precision, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_type_embedding_net_variables_from_graph_def, ) -from deepmd.utils.network import ( +from deepmd.tf.utils.network import ( embedding_net, ) diff --git a/deepmd/utils/weight_avg.py b/deepmd/tf/utils/weight_avg.py similarity index 100% rename from deepmd/utils/weight_avg.py rename to deepmd/tf/utils/weight_avg.py diff --git a/deepmd_utils/calculator.py b/deepmd_utils/calculator.py index 5b45aa49b3..65022d3ccc 100644 --- a/deepmd_utils/calculator.py +++ b/deepmd_utils/calculator.py @@ -53,7 +53,7 @@ class DP(Calculator): Compute potential energy >>> from ase import Atoms - >>> from deepmd.calculator import DP + >>> from deepmd.tf.calculator import DP >>> water = Atoms('H2O', >>> positions=[(0.7601, 1.9270, 1), >>> (1.9575, 1, 1), diff --git a/deepmd_utils/infer/deep_pot.py b/deepmd_utils/infer/deep_pot.py index 66510ea349..cc328356d3 100644 --- a/deepmd_utils/infer/deep_pot.py +++ b/deepmd_utils/infer/deep_pot.py @@ -52,7 +52,7 @@ def __new__(cls, model_file: str, *args, **kwargs): if cls is DeepPot: backend = detect_backend(model_file) if backend == DPBackend.TensorFlow: - from deepmd.infer.deep_pot import DeepPot as DeepPotTF + from deepmd.tf.infer.deep_pot import DeepPot as DeepPotTF return super().__new__(DeepPotTF) elif backend == DPBackend.PyTorch: diff --git a/deepmd_utils/infer/model_devi.py b/deepmd_utils/infer/model_devi.py index b0693f5823..cf303e2245 100644 --- a/deepmd_utils/infer/model_devi.py +++ b/deepmd_utils/infer/model_devi.py @@ -293,8 +293,8 @@ def calc_model_devi( Examples -------- - >>> from deepmd.infer import calc_model_devi - >>> from deepmd.infer import DeepPot as DP + >>> from deepmd.tf.infer import calc_model_devi + >>> from deepmd.tf.infer import DeepPot as DP >>> import numpy as np >>> coord = np.array([[1,0,0], [0,0,1.5], [1,0,3]]).reshape([1, -1]) >>> cell = np.diag(10 * np.ones(3)).reshape([1, -1]) diff --git a/deepmd_utils/main.py b/deepmd_utils/main.py index 19afaeee1f..32c43e17ac 100644 --- a/deepmd_utils/main.py +++ b/deepmd_utils/main.py @@ -651,6 +651,6 @@ def main(): if no command was input """ args = parse_args() - from deepmd.entrypoints.main import main as deepmd_main + from deepmd.tf.entrypoints.main import main as deepmd_main deepmd_main(args) diff --git a/deepmd_utils/model_format/se_e2_a.py b/deepmd_utils/model_format/se_e2_a.py index b9143ee360..37a2deb967 100644 --- a/deepmd_utils/model_format/se_e2_a.py +++ b/deepmd_utils/model_format/se_e2_a.py @@ -65,7 +65,7 @@ class DescrptSeA(NativeOP): :math:`\mathcal{G}^i_< \in \mathbb{R}^{N \times M_2}` takes first :math:`M_2` columns of :math:`\mathcal{G}^i`. The equation of embedding network :math:`\mathcal{N}` can be found at - :meth:`deepmd.utils.network.embedding_net`. + :meth:`deepmd.tf.utils.network.embedding_net`. Parameters ---------- diff --git a/deepmd_utils/utils/argcheck.py b/deepmd_utils/utils/argcheck.py index 6c51a7b859..aaaf8973ab 100644 --- a/deepmd_utils/utils/argcheck.py +++ b/deepmd_utils/utils/argcheck.py @@ -14,7 +14,7 @@ dargs, ) -from deepmd.common import ( +from deepmd.tf.common import ( ACTIVATION_FN_DICT, PRECISION_DICT, ) diff --git a/deepmd_utils/utils/compat.py b/deepmd_utils/utils/compat.py index 5f9c14e6d8..3c48da27c6 100644 --- a/deepmd_utils/utils/compat.py +++ b/deepmd_utils/utils/compat.py @@ -16,7 +16,7 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) diff --git a/doc/api_op.rst b/doc/api_op.rst index 9f4c650497..d620ec6ef5 100644 --- a/doc/api_op.rst +++ b/doc/api_op.rst @@ -4,7 +4,7 @@ OP API op_module --------- -.. automodule:: deepmd.env.op_module +.. automodule:: deepmd.tf.env.op_module :members: :imported-members: :show-inheritance: @@ -13,7 +13,7 @@ op_module op_grads_module --------------- -.. automodule:: deepmd.env.op_grads_module +.. automodule:: deepmd.tf.env.op_grads_module :members: :imported-members: :show-inheritance: diff --git a/doc/cli.rst b/doc/cli.rst index 668a2df2e3..15891369e3 100644 --- a/doc/cli.rst +++ b/doc/cli.rst @@ -4,6 +4,6 @@ Command line interface ====================== .. argparse:: - :module: deepmd.entrypoints.main + :module: deepmd.tf.entrypoints.main :func: main_parser :prog: dp diff --git a/doc/conf.py b/doc/conf.py index e6bb4b6ba2..8138f82ba4 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -17,11 +17,11 @@ date, ) -from deepmd.common import ( +from deepmd.tf.common import ( ACTIVATION_FN_DICT, PRECISION_DICT, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( list_to_doc, ) diff --git a/doc/development/create-a-model.md b/doc/development/create-a-model.md index 6634403021..d71a6e519a 100644 --- a/doc/development/create-a-model.md +++ b/doc/development/create-a-model.md @@ -10,11 +10,11 @@ To incorporate your custom model you'll need to: ## Design a new component -When creating a new component, take descriptor as the example, you should inherit {py:class}`deepmd.descriptor.descriptor.Descriptor` class and override several methods. Abstract methods such as {py:class}`deepmd.descriptor.descriptor.Descriptor.build` must be implemented and others are not. You should keep arguments of these methods unchanged. +When creating a new component, take descriptor as the example, you should inherit {py:class}`deepmd.tf.descriptor.descriptor.Descriptor` class and override several methods. Abstract methods such as {py:class}`deepmd.tf.descriptor.descriptor.Descriptor.build` must be implemented and others are not. You should keep arguments of these methods unchanged. After implementation, you need to register the component with a key: ```py -from deepmd.descriptor import Descriptor +from deepmd.tf.descriptor import Descriptor @Descriptor.register("some_descrpt") @@ -31,7 +31,7 @@ To let someone uses your new component in their input file, you need to create a from typing import List from dargs import Argument -from deepmd.utils.argcheck import descrpt_args_plugin +from deepmd.tf.utils.argcheck import descrpt_args_plugin @descrpt_args_plugin.register("some_descrpt") diff --git a/doc/getting-started/quick_start.ipynb b/doc/getting-started/quick_start.ipynb index ec939265fd..d0f7d8db0b 100644 --- a/doc/getting-started/quick_start.ipynb +++ b/doc/getting-started/quick_start.ipynb @@ -1001,7 +1001,7 @@ "WARNING:tensorflow:From /opt/mamba/lib/python3.10/site-packages/deepmd/utils/batch_size.py:61: is_gpu_available (from tensorflow.python.framework.test_util) is deprecated and will be removed in a future version.\n", "Instructions for updating:\n", "Use `tf.config.list_physical_devices('GPU')` instead.\n", - "WARNING:deepmd.utils.batch_size:You can use the environment variable DP_INFER_BATCH_SIZE tocontrol the inference batch size (nframes * natoms). The default value is 1024.\n" + "WARNING:deepmd.tf.utils.batch_size:You can use the environment variable DP_INFER_BATCH_SIZE tocontrol the inference batch size (nframes * natoms). The default value is 1024.\n" ] } ], diff --git a/doc/train/train-input.rst b/doc/train/train-input.rst index 2a32aeb930..04e82451e4 100644 --- a/doc/train/train-input.rst +++ b/doc/train/train-input.rst @@ -4,5 +4,5 @@ Training Parameters One can load, modify, and export the input file by using our effective web-based tool `DP-GUI `_ online or hosted using the :ref:`command line interface ` :code:`dp gui`. All training parameters below can be set in DP-GUI. By clicking "SAVE JSON", one can download the input file for furthur training. .. dargs:: - :module: deepmd.utils.argcheck + :module: deepmd.tf.utils.argcheck :func: gen_args diff --git a/pyproject.toml b/pyproject.toml index e5515984fb..46632c8ca5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ requires-python = ">=3.7" keywords = ["deepmd"] [project.entry-points."lammps.plugins"] -deepmd = "deepmd.lmp:get_op_dir" +deepmd = "deepmd.tf.lmp:get_op_dir" [project.entry-points."dpgui"] "DeePMD-kit" = "deepmd_utils.utils.argcheck:gen_args" @@ -128,7 +128,7 @@ replacement = '\1="https://github.com/deepmodeling/deepmd-kit/raw/master/\g<2>"' [tool.cibuildwheel] test-command = [ - "python -m deepmd -h", + "python -m deepmd.tf -h", "dp -h", "dp_ipi", "pytest {project}/source/tests/test_lammps.py" @@ -171,7 +171,7 @@ before-all = [ environment = { PIP_PREFER_BINARY="1" } test-extras = ["cpu"] test-command = [ - "python -m deepmd -h", + "python -m deepmd.tf -h", "dp -h", ] diff --git a/source/install/docker/Dockerfile b/source/install/docker/Dockerfile index 26b7be9f19..78a53cb895 100644 --- a/source/install/docker/Dockerfile +++ b/source/install/docker/Dockerfile @@ -10,7 +10,7 @@ RUN pip install "$(ls /dist/deepmd_kit${VARIANT}-*manylinux*_x86_64.whl)[gpu,cu$ && dp -h \ && lmp -h \ && dp_ipi \ - && python -m deepmd -h + && python -m deepmd.tf -h FROM python:3.11 AS build-image COPY --from=compile-image /opt/deepmd-kit /opt/deepmd-kit diff --git a/source/ipi/tests/test_driver.py b/source/ipi/tests/test_driver.py index 9ab6ff53de..78edeac977 100644 --- a/source/ipi/tests/test_driver.py +++ b/source/ipi/tests/test_driver.py @@ -18,7 +18,7 @@ SocketIOCalculator, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/lmp/tests/test_deeptensor.py b/source/lmp/tests/test_deeptensor.py index 3e684b386e..4fcf693482 100644 --- a/source/lmp/tests/test_deeptensor.py +++ b/source/lmp/tests/test_deeptensor.py @@ -57,7 +57,7 @@ sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file.resolve(), pb_file.resolve(), @@ -65,7 +65,7 @@ ) sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file2.resolve(), pb_file2.resolve(), diff --git a/source/lmp/tests/test_dplr.py b/source/lmp/tests/test_dplr.py index 9c8f1c0d4f..ac0acad68f 100644 --- a/source/lmp/tests/test_dplr.py +++ b/source/lmp/tests/test_dplr.py @@ -264,7 +264,7 @@ sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file.resolve(), pb_file.resolve(), diff --git a/source/lmp/tests/test_lammps.py b/source/lmp/tests/test_lammps.py index 028b403abf..7c7172bcb6 100644 --- a/source/lmp/tests/test_lammps.py +++ b/source/lmp/tests/test_lammps.py @@ -219,14 +219,14 @@ sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file.resolve(), pb_file.resolve(), ).split() ) sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file2.resolve(), pb_file2.resolve(), diff --git a/source/lmp/tests/test_lammps_3types.py b/source/lmp/tests/test_lammps_3types.py index 46e1a00c8f..72d3ca2e2e 100644 --- a/source/lmp/tests/test_lammps_3types.py +++ b/source/lmp/tests/test_lammps_3types.py @@ -245,14 +245,14 @@ nktv2p = 1.6021765e6 sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file.resolve(), pb_file.resolve(), ).split() ) sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file2.resolve(), pb_file2.resolve(), diff --git a/source/lmp/tests/test_lammps_faparam.py b/source/lmp/tests/test_lammps_faparam.py index 064928eeb1..8a6d0e01ef 100644 --- a/source/lmp/tests/test_lammps_faparam.py +++ b/source/lmp/tests/test_lammps_faparam.py @@ -134,7 +134,7 @@ sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file.resolve(), pb_file.resolve(), diff --git a/source/tests/common.py b/source/tests/common.py index 9af324896f..cb68e4d46d 100644 --- a/source/tests/common.py +++ b/source/tests/common.py @@ -8,15 +8,15 @@ import dpdata import numpy as np -from deepmd.common import j_loader as dp_j_loader -from deepmd.entrypoints.main import ( +from deepmd.tf.common import j_loader as dp_j_loader +from deepmd.tf.entrypoints.main import ( main, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, tf, ) -from deepmd.utils import random as dp_random +from deepmd.tf.utils import random as dp_random if GLOBAL_NP_FLOAT_PRECISION == np.float32: global_default_fv_hh = 1e-2 diff --git a/source/tests/test_activation_fn_gelu.py b/source/tests/test_activation_fn_gelu.py index b1c30eeefc..9be0885b74 100644 --- a/source/tests/test_activation_fn_gelu.py +++ b/source/tests/test_activation_fn_gelu.py @@ -3,13 +3,13 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( get_activation_func, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.utils.network import ( +from deepmd.tf.utils.network import ( embedding_net, ) diff --git a/source/tests/test_adjust_sel.py b/source/tests/test_adjust_sel.py index 0ff6eb0792..b1cbdc5afc 100644 --- a/source/tests/test_adjust_sel.py +++ b/source/tests/test_adjust_sel.py @@ -6,17 +6,17 @@ import numpy as np -# from deepmd.entrypoints.compress import compress +# from deepmd.tf.entrypoints.compress import compress from common import ( j_loader, run_dp, tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) diff --git a/source/tests/test_argument_parser.py b/source/tests/test_argument_parser.py index bb8dd9ed62..988199d1e4 100644 --- a/source/tests/test_argument_parser.py +++ b/source/tests/test_argument_parser.py @@ -21,7 +21,7 @@ Union, ) -from deepmd.entrypoints.main import ( +from deepmd.tf.entrypoints.main import ( get_ll, parse_args, ) diff --git a/source/tests/test_auto_batch_size.py b/source/tests/test_auto_batch_size.py index 5a349f70b9..3316e186b6 100644 --- a/source/tests/test_auto_batch_size.py +++ b/source/tests/test_auto_batch_size.py @@ -4,10 +4,10 @@ import numpy as np -from deepmd.utils.batch_size import ( +from deepmd.tf.utils.batch_size import ( AutoBatchSize, ) -from deepmd.utils.errors import ( +from deepmd.tf.utils.errors import ( OutOfMemoryError, ) diff --git a/source/tests/test_cluster.py b/source/tests/test_cluster.py index c946177cb5..27526a3ccf 100644 --- a/source/tests/test_cluster.py +++ b/source/tests/test_cluster.py @@ -4,7 +4,7 @@ mock, ) -from deepmd.cluster import ( +from deepmd.tf.cluster import ( local, slurm, ) diff --git a/source/tests/test_common.py b/source/tests/test_common.py index bf68e7056b..95948f29bb 100644 --- a/source/tests/test_common.py +++ b/source/tests/test_common.py @@ -5,12 +5,12 @@ Path, ) -from deepmd.common import ( +from deepmd.tf.common import ( GLOBAL_TF_FLOAT_PRECISION, cast_precision, expand_sys_str, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) @@ -66,7 +66,7 @@ def test_expand(self): class TestCastPrecision(unittest.TestCase): - """This class tests `deepmd.common.cast_precision`.""" + """This class tests `deepmd.tf.common.cast_precision`.""" @property def precision(self): diff --git a/source/tests/test_compat_input.py b/source/tests/test_compat_input.py index 97172be9e7..e8a74e9c48 100644 --- a/source/tests/test_compat_input.py +++ b/source/tests/test_compat_input.py @@ -6,7 +6,7 @@ j_loader, ) -from deepmd.utils.compat import ( +from deepmd.tf.utils.compat import ( convert_input_v0_v1, convert_input_v1_v2, ) diff --git a/source/tests/test_compressed_training.py b/source/tests/test_compressed_training.py index 0a0bbeaadf..c3d07762f8 100644 --- a/source/tests/test_compressed_training.py +++ b/source/tests/test_compressed_training.py @@ -3,7 +3,7 @@ import os import unittest -# from deepmd.entrypoints.compress import compress +# from deepmd.tf.entrypoints.compress import compress from common import ( j_loader, run_dp, @@ -11,7 +11,7 @@ ) from packaging.version import parse as parse_version -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) diff --git a/source/tests/test_data_large_batch.py b/source/tests/test_data_large_batch.py index 5750f956f8..84e99591d5 100644 --- a/source/tests/test_data_large_batch.py +++ b/source/tests/test_data_large_batch.py @@ -9,25 +9,25 @@ ) from packaging.version import parse as parse_version -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeAtten, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( EnerModel, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/source/tests/test_data_modifier.py b/source/tests/test_data_modifier.py index 368a60d68a..01e3cdcb2d 100644 --- a/source/tests/test_data_modifier.py +++ b/source/tests/test_data_modifier.py @@ -8,24 +8,24 @@ tests_path, ) -from deepmd.common import ( +from deepmd.tf.common import ( data_requirement, j_must_have, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, tf, ) -from deepmd.infer.data_modifier import ( +from deepmd.tf.infer.data_modifier import ( DipoleChargeModifier, ) -from deepmd.train.run_options import ( +from deepmd.tf.train.run_options import ( RunOptions, ) -from deepmd.train.trainer import ( +from deepmd.tf.train.trainer import ( DPTrainer, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) diff --git a/source/tests/test_data_modifier_shuffle.py b/source/tests/test_data_modifier_shuffle.py index 9ddbb8ee29..e6985b9e0f 100644 --- a/source/tests/test_data_modifier_shuffle.py +++ b/source/tests/test_data_modifier_shuffle.py @@ -4,27 +4,27 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( data_requirement, j_must_have, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, tf, ) -from deepmd.infer.data_modifier import ( +from deepmd.tf.infer.data_modifier import ( DipoleChargeModifier, ) -from deepmd.infer.deep_dipole import ( +from deepmd.tf.infer.deep_dipole import ( DeepDipole, ) -from deepmd.train.run_options import ( +from deepmd.tf.train.run_options import ( RunOptions, ) -from deepmd.train.trainer import ( +from deepmd.tf.train.trainer import ( DPTrainer, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) diff --git a/source/tests/test_data_requirement.py b/source/tests/test_data_requirement.py index 956cee8ccb..cabea15de1 100644 --- a/source/tests/test_data_requirement.py +++ b/source/tests/test_data_requirement.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import unittest -from deepmd.common import ( +from deepmd.tf.common import ( add_data_requirement, data_requirement, ) diff --git a/source/tests/test_deepdipole.py b/source/tests/test_deepdipole.py index 1d06b5fe92..6dffe59fe5 100644 --- a/source/tests/test_deepdipole.py +++ b/source/tests/test_deepdipole.py @@ -12,13 +12,13 @@ ) from packaging.version import parse as parse_version -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepDipole, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_deepdos.py b/source/tests/test_deepdos.py index c5e100f80e..3f3d0cda7f 100644 --- a/source/tests/test_deepdos.py +++ b/source/tests/test_deepdos.py @@ -7,13 +7,13 @@ tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepDOS, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_deepmd_data.py b/source/tests/test_deepmd_data.py index 92d89665b1..e486446ab8 100644 --- a/source/tests/test_deepmd_data.py +++ b/source/tests/test_deepmd_data.py @@ -9,10 +9,10 @@ tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.utils.data import ( +from deepmd.tf.utils.data import ( DeepmdData, ) diff --git a/source/tests/test_deepmd_data_sys.py b/source/tests/test_deepmd_data_sys.py index abfa7d7e48..49ad8f501c 100644 --- a/source/tests/test_deepmd_data_sys.py +++ b/source/tests/test_deepmd_data_sys.py @@ -5,13 +5,13 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.utils import ( +from deepmd.tf.utils import ( random, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, prob_sys_size_ext, ) diff --git a/source/tests/test_deeppolar.py b/source/tests/test_deeppolar.py index 9627851de4..18d9cb4ad9 100644 --- a/source/tests/test_deeppolar.py +++ b/source/tests/test_deeppolar.py @@ -10,13 +10,13 @@ ) from packaging.version import parse as parse_version -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPolar, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_deeppot_a.py b/source/tests/test_deeppot_a.py index ba3655b7f9..32e92cd8bd 100644 --- a/source/tests/test_deeppot_a.py +++ b/source/tests/test_deeppot_a.py @@ -12,15 +12,15 @@ ) from packaging.version import parse as parse_version -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, MODEL_VERSION, tf, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_dp10_to_dp11, convert_dp012_to_dp10, convert_dp12_to_dp13, @@ -724,7 +724,7 @@ def test_ase(self): Atoms, ) - from deepmd.calculator import ( + from deepmd.tf.calculator import ( DP, ) diff --git a/source/tests/test_deeppot_r.py b/source/tests/test_deeppot_r.py index 44c6e3c167..47f957d2cd 100644 --- a/source/tests/test_deeppot_r.py +++ b/source/tests/test_deeppot_r.py @@ -7,14 +7,14 @@ tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, tf, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_deeppot_spin.py b/source/tests/test_deeppot_spin.py index 9ab119a54e..b390fe6c79 100644 --- a/source/tests/test_deeppot_spin.py +++ b/source/tests/test_deeppot_spin.py @@ -7,13 +7,13 @@ tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_descrpt_hybrid.py b/source/tests/test_descrpt_hybrid.py index 317f6ea5a0..08177f6a08 100644 --- a/source/tests/test_descrpt_hybrid.py +++ b/source/tests/test_descrpt_hybrid.py @@ -9,16 +9,16 @@ ) from packaging.version import parse as parse_version -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptHybrid, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/source/tests/test_descrpt_nonsmth.py b/source/tests/test_descrpt_nonsmth.py index fd3bb0b2f7..31f9da7ff2 100644 --- a/source/tests/test_descrpt_nonsmth.py +++ b/source/tests/test_descrpt_nonsmth.py @@ -11,7 +11,7 @@ ) # load grad of force module -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, op_module, diff --git a/source/tests/test_descrpt_se_a_mask.py b/source/tests/test_descrpt_se_a_mask.py index 85cd1cc2a1..b35bc75b04 100644 --- a/source/tests/test_descrpt_se_a_mask.py +++ b/source/tests/test_descrpt_se_a_mask.py @@ -8,19 +8,19 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeAMask, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_descrpt_se_a_type.py b/source/tests/test_descrpt_se_a_type.py index aeab18f149..f5f294be35 100644 --- a/source/tests/test_descrpt_se_a_type.py +++ b/source/tests/test_descrpt_se_a_type.py @@ -6,16 +6,16 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/source/tests/test_descrpt_se_atten.py b/source/tests/test_descrpt_se_atten.py index 76df651a46..d7e3c31f2c 100644 --- a/source/tests/test_descrpt_se_atten.py +++ b/source/tests/test_descrpt_se_atten.py @@ -10,16 +10,16 @@ ) from packaging.version import parse as parse_version -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeAtten, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/source/tests/test_descrpt_se_r.py b/source/tests/test_descrpt_se_r.py index 779954a545..9e01bc83fd 100644 --- a/source/tests/test_descrpt_se_r.py +++ b/source/tests/test_descrpt_se_r.py @@ -11,7 +11,7 @@ ) # load grad of force module -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, op_module, diff --git a/source/tests/test_descrpt_sea_ef.py b/source/tests/test_descrpt_sea_ef.py index efd86854c7..42f26da887 100644 --- a/source/tests/test_descrpt_sea_ef.py +++ b/source/tests/test_descrpt_sea_ef.py @@ -11,7 +11,7 @@ ) # load grad of force module -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, op_module, diff --git a/source/tests/test_descrpt_sea_ef_para.py b/source/tests/test_descrpt_sea_ef_para.py index 1a109013cb..16c92a5dc7 100644 --- a/source/tests/test_descrpt_sea_ef_para.py +++ b/source/tests/test_descrpt_sea_ef_para.py @@ -11,7 +11,7 @@ ) # load grad of force module -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, op_module, diff --git a/source/tests/test_descrpt_sea_ef_rot.py b/source/tests/test_descrpt_sea_ef_rot.py index 56cdb357b0..8cdbc19ca2 100644 --- a/source/tests/test_descrpt_sea_ef_rot.py +++ b/source/tests/test_descrpt_sea_ef_rot.py @@ -3,11 +3,11 @@ import numpy as np -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, DescrptSeAEfLower, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, op_module, diff --git a/source/tests/test_descrpt_sea_ef_vert.py b/source/tests/test_descrpt_sea_ef_vert.py index 77ffb3150c..bc27a7c933 100644 --- a/source/tests/test_descrpt_sea_ef_vert.py +++ b/source/tests/test_descrpt_sea_ef_vert.py @@ -11,7 +11,7 @@ ) # load grad of force module -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, op_module, diff --git a/source/tests/test_descrpt_smooth.py b/source/tests/test_descrpt_smooth.py index 59076e366e..206aca8d8a 100644 --- a/source/tests/test_descrpt_smooth.py +++ b/source/tests/test_descrpt_smooth.py @@ -11,7 +11,7 @@ ) # load grad of force module -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, op_module, diff --git a/source/tests/test_dipole_se_a.py b/source/tests/test_dipole_se_a.py index 687e68c2be..ca31ce87d5 100644 --- a/source/tests/test_dipole_se_a.py +++ b/source/tests/test_dipole_se_a.py @@ -8,19 +8,19 @@ strerch_box, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( DipoleFittingSeA, ) -from deepmd.model import ( +from deepmd.tf.model import ( DipoleModel, ) diff --git a/source/tests/test_dipole_se_a_tebd.py b/source/tests/test_dipole_se_a_tebd.py index 4b2e6d0688..b211d0eb48 100644 --- a/source/tests/test_dipole_se_a_tebd.py +++ b/source/tests/test_dipole_se_a_tebd.py @@ -11,22 +11,22 @@ ) from packaging.version import parse as parse_version -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( DipoleFittingSeA, ) -from deepmd.model import ( +from deepmd.tf.model import ( DipoleModel, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/source/tests/test_dipolecharge.py b/source/tests/test_dipolecharge.py index 58459d6845..e435eb431a 100644 --- a/source/tests/test_dipolecharge.py +++ b/source/tests/test_dipolecharge.py @@ -7,13 +7,13 @@ tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DipoleChargeModifier, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_dp_test.py b/source/tests/test_dp_test.py index a07706acfe..978cd95804 100644 --- a/source/tests/test_dp_test.py +++ b/source/tests/test_dp_test.py @@ -12,8 +12,8 @@ tests_path, ) -from deepmd.entrypoints.test import test as dp_test -from deepmd.utils.convert import ( +from deepmd.tf.entrypoints.test import test as dp_test +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_embedding_net.py b/source/tests/test_embedding_net.py index 1b8c68c089..f766fff8b3 100644 --- a/source/tests/test_embedding_net.py +++ b/source/tests/test_embedding_net.py @@ -3,10 +3,10 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.utils.network import ( +from deepmd.tf.utils.network import ( embedding_net, ) diff --git a/source/tests/test_env.py b/source/tests/test_env.py index d575c3cf93..eb1b40e707 100644 --- a/source/tests/test_env.py +++ b/source/tests/test_env.py @@ -4,7 +4,7 @@ mock, ) -from deepmd import ( +from deepmd.tf import ( env, ) @@ -35,7 +35,7 @@ def test_default(self): new = env.get_tf_session_config() self.assertNotEqual(id(shared), id(new)) - @mock.patch("deepmd.env.get_tf_default_nthreads") + @mock.patch("deepmd.tf.env.get_tf_default_nthreads") def test_get(self, mock_method): mock_method.return_value = (5, 3) config = env.get_tf_session_config() diff --git a/source/tests/test_ewald.py b/source/tests/test_ewald.py index ef2ace39a4..74b65e9be3 100644 --- a/source/tests/test_ewald.py +++ b/source/tests/test_ewald.py @@ -1,12 +1,12 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, tf, ) -from deepmd.infer.ewald_recp import ( +from deepmd.tf.infer.ewald_recp import ( EwaldRecp, op_module, ) diff --git a/source/tests/test_examples.py b/source/tests/test_examples.py index d50ca5fee1..ea087fbc9d 100644 --- a/source/tests/test_examples.py +++ b/source/tests/test_examples.py @@ -7,10 +7,10 @@ Path, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_loader, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( normalize, ) diff --git a/source/tests/test_finetune_se_atten.py b/source/tests/test_finetune_se_atten.py index f4689aacb3..3614fcb13a 100644 --- a/source/tests/test_finetune_se_atten.py +++ b/source/tests/test_finetune_se_atten.py @@ -12,23 +12,23 @@ ) from packaging.version import parse as parse_version -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, tf, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPotential, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( normalize, ) -from deepmd.utils.compat import ( +from deepmd.tf.utils.compat import ( update_deepmd_input, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) -from deepmd.utils.graph import ( +from deepmd.tf.utils.graph import ( get_tensor_by_name, ) diff --git a/source/tests/test_fitting_dos.py b/source/tests/test_fitting_dos.py index 60a0ee4158..532bcfafbc 100644 --- a/source/tests/test_fitting_dos.py +++ b/source/tests/test_fitting_dos.py @@ -6,16 +6,16 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( DOSFitting, ) diff --git a/source/tests/test_fitting_ener_type.py b/source/tests/test_fitting_ener_type.py index 42190ef557..05a9d053ab 100644 --- a/source/tests/test_fitting_ener_type.py +++ b/source/tests/test_fitting_ener_type.py @@ -6,16 +6,16 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) diff --git a/source/tests/test_fitting_stat.py b/source/tests/test_fitting_stat.py index ad62c89f2a..2b20dd5a4c 100644 --- a/source/tests/test_fitting_stat.py +++ b/source/tests/test_fitting_stat.py @@ -9,10 +9,10 @@ j_loader, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) diff --git a/source/tests/test_gen_stat_data.py b/source/tests/test_gen_stat_data.py index 6667aa15fd..18191eb21d 100644 --- a/source/tests/test_gen_stat_data.py +++ b/source/tests/test_gen_stat_data.py @@ -5,19 +5,19 @@ import dpdata import numpy as np -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model.model_stat import ( +from deepmd.tf.model.model_stat import ( _make_all_stat_ref, make_stat_input, merge_sys_stat, ) -from deepmd.utils import random as dp_random -from deepmd.utils.data_system import ( +from deepmd.tf.utils import random as dp_random +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) diff --git a/source/tests/test_get_potential.py b/source/tests/test_get_potential.py index e2f342537a..feb264afe0 100644 --- a/source/tests/test_get_potential.py +++ b/source/tests/test_get_potential.py @@ -6,13 +6,13 @@ Path, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepDipole, DeepPolar, DeepPot, DeepPotential, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_init_frz_model_multi.py b/source/tests/test_init_frz_model_multi.py index 6696f39319..e5e5733c7d 100644 --- a/source/tests/test_init_frz_model_multi.py +++ b/source/tests/test_init_frz_model_multi.py @@ -10,26 +10,26 @@ tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, tf, ) -from deepmd.train.run_options import ( +from deepmd.tf.train.run_options import ( RunOptions, ) -from deepmd.train.trainer import ( +from deepmd.tf.train.trainer import ( DPTrainer, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( normalize, ) -from deepmd.utils.compat import ( +from deepmd.tf.utils.compat import ( update_deepmd_input, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) -from deepmd.utils.multi_init import ( +from deepmd.tf.utils.multi_init import ( replace_model_params_with_frz_multi_model, ) diff --git a/source/tests/test_init_frz_model_se_a.py b/source/tests/test_init_frz_model_se_a.py index 06532009d1..d98c2bc14f 100644 --- a/source/tests/test_init_frz_model_se_a.py +++ b/source/tests/test_init_frz_model_se_a.py @@ -10,23 +10,23 @@ tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, tf, ) -from deepmd.train.run_options import ( +from deepmd.tf.train.run_options import ( RunOptions, ) -from deepmd.train.trainer import ( +from deepmd.tf.train.trainer import ( DPTrainer, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( normalize, ) -from deepmd.utils.compat import ( +from deepmd.tf.utils.compat import ( update_deepmd_input, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) diff --git a/source/tests/test_init_frz_model_se_a_tebd.py b/source/tests/test_init_frz_model_se_a_tebd.py index e54cae9781..594bf83085 100644 --- a/source/tests/test_init_frz_model_se_a_tebd.py +++ b/source/tests/test_init_frz_model_se_a_tebd.py @@ -10,23 +10,23 @@ tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, tf, ) -from deepmd.train.run_options import ( +from deepmd.tf.train.run_options import ( RunOptions, ) -from deepmd.train.trainer import ( +from deepmd.tf.train.trainer import ( DPTrainer, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( normalize, ) -from deepmd.utils.compat import ( +from deepmd.tf.utils.compat import ( update_deepmd_input, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) diff --git a/source/tests/test_init_frz_model_se_a_type.py b/source/tests/test_init_frz_model_se_a_type.py index 9d2c49579a..3221245065 100644 --- a/source/tests/test_init_frz_model_se_a_type.py +++ b/source/tests/test_init_frz_model_se_a_type.py @@ -10,23 +10,23 @@ tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, tf, ) -from deepmd.train.run_options import ( +from deepmd.tf.train.run_options import ( RunOptions, ) -from deepmd.train.trainer import ( +from deepmd.tf.train.trainer import ( DPTrainer, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( normalize, ) -from deepmd.utils.compat import ( +from deepmd.tf.utils.compat import ( update_deepmd_input, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) diff --git a/source/tests/test_init_frz_model_se_atten.py b/source/tests/test_init_frz_model_se_atten.py index 01956e51c4..5554ae415c 100644 --- a/source/tests/test_init_frz_model_se_atten.py +++ b/source/tests/test_init_frz_model_se_atten.py @@ -11,23 +11,23 @@ ) from packaging.version import parse as parse_version -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, tf, ) -from deepmd.train.run_options import ( +from deepmd.tf.train.run_options import ( RunOptions, ) -from deepmd.train.trainer import ( +from deepmd.tf.train.trainer import ( DPTrainer, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( normalize, ) -from deepmd.utils.compat import ( +from deepmd.tf.utils.compat import ( update_deepmd_input, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) diff --git a/source/tests/test_init_frz_model_se_r.py b/source/tests/test_init_frz_model_se_r.py index 34eca9bd05..84d109bcfd 100644 --- a/source/tests/test_init_frz_model_se_r.py +++ b/source/tests/test_init_frz_model_se_r.py @@ -10,23 +10,23 @@ tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, tf, ) -from deepmd.train.run_options import ( +from deepmd.tf.train.run_options import ( RunOptions, ) -from deepmd.train.trainer import ( +from deepmd.tf.train.trainer import ( DPTrainer, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( normalize, ) -from deepmd.utils.compat import ( +from deepmd.tf.utils.compat import ( update_deepmd_input, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) diff --git a/source/tests/test_init_frz_model_spin.py b/source/tests/test_init_frz_model_spin.py index c6f257dd7b..7aa3d514dc 100644 --- a/source/tests/test_init_frz_model_spin.py +++ b/source/tests/test_init_frz_model_spin.py @@ -10,23 +10,23 @@ tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, tf, ) -from deepmd.train.run_options import ( +from deepmd.tf.train.run_options import ( RunOptions, ) -from deepmd.train.trainer import ( +from deepmd.tf.train.trainer import ( DPTrainer, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( normalize, ) -from deepmd.utils.compat import ( +from deepmd.tf.utils.compat import ( update_deepmd_input, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) diff --git a/source/tests/test_lammps.py b/source/tests/test_lammps.py index 19dbe70ade..d235d6576e 100644 --- a/source/tests/test_lammps.py +++ b/source/tests/test_lammps.py @@ -6,7 +6,7 @@ Path, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_layer_name.py b/source/tests/test_layer_name.py index c6a2f0b09c..71229b5ce7 100644 --- a/source/tests/test_layer_name.py +++ b/source/tests/test_layer_name.py @@ -7,20 +7,20 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( DipoleFittingSeA, EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( MultiModel, ) diff --git a/source/tests/test_linear_model.py b/source/tests/test_linear_model.py index 21f0f6efc8..ef0d324a69 100644 --- a/source/tests/test_linear_model.py +++ b/source/tests/test_linear_model.py @@ -4,18 +4,18 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_ENER_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, tf, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPotential, ) -from deepmd.model.linear import ( +from deepmd.tf.model.linear import ( LinearEnergyModel, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_loss_gf.py b/source/tests/test_loss_gf.py index 04f40d943b..78e5404e03 100644 --- a/source/tests/test_loss_gf.py +++ b/source/tests/test_loss_gf.py @@ -2,7 +2,7 @@ import numpy as np import tensorflow as tf -from deepmd.loss import ( +from deepmd.tf.loss import ( EnerStdLoss, ) diff --git a/source/tests/test_mixed_prec_training.py b/source/tests/test_mixed_prec_training.py index d4c859f958..620a9b9fd0 100644 --- a/source/tests/test_mixed_prec_training.py +++ b/source/tests/test_mixed_prec_training.py @@ -6,7 +6,7 @@ import numpy as np -# from deepmd.entrypoints.compress import compress +# from deepmd.tf.entrypoints.compress import compress from common import ( j_loader, run_dp, @@ -16,7 +16,7 @@ Version, ) -from deepmd.env import ( +from deepmd.tf.env import ( TF_VERSION, ) diff --git a/source/tests/test_model_compression_se_a.py b/source/tests/test_model_compression_se_a.py index 0e6e1361ad..0a6b7c85cb 100644 --- a/source/tests/test_model_compression_se_a.py +++ b/source/tests/test_model_compression_se_a.py @@ -6,17 +6,17 @@ import numpy as np -# from deepmd.entrypoints.compress import compress +# from deepmd.tf.entrypoints.compress import compress from common import ( j_loader, run_dp, tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) @@ -404,7 +404,7 @@ def test_ase(self): Atoms, ) - from deepmd.calculator import ( + from deepmd.tf.calculator import ( DP, ) diff --git a/source/tests/test_model_compression_se_a_ebd.py b/source/tests/test_model_compression_se_a_ebd.py index 2a3163b062..8b64117acd 100644 --- a/source/tests/test_model_compression_se_a_ebd.py +++ b/source/tests/test_model_compression_se_a_ebd.py @@ -6,17 +6,17 @@ import numpy as np -# from deepmd.entrypoints.compress import compress +# from deepmd.tf.entrypoints.compress import compress from common import ( j_loader, run_dp, tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) @@ -416,7 +416,7 @@ def test_ase(self): Atoms, ) - from deepmd.calculator import ( + from deepmd.tf.calculator import ( DP, ) diff --git a/source/tests/test_model_compression_se_a_ebd_type_one_side.py b/source/tests/test_model_compression_se_a_ebd_type_one_side.py index 2f3d16b05f..9ad1970e9b 100644 --- a/source/tests/test_model_compression_se_a_ebd_type_one_side.py +++ b/source/tests/test_model_compression_se_a_ebd_type_one_side.py @@ -6,17 +6,17 @@ import numpy as np -# from deepmd.entrypoints.compress import compress +# from deepmd.tf.entrypoints.compress import compress from common import ( j_loader, run_dp, tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) @@ -416,7 +416,7 @@ def test_ase(self): Atoms, ) - from deepmd.calculator import ( + from deepmd.tf.calculator import ( DP, ) diff --git a/source/tests/test_model_compression_se_a_type_one_side_exclude_types.py b/source/tests/test_model_compression_se_a_type_one_side_exclude_types.py index 10ce352b6c..5b6ac4e13e 100644 --- a/source/tests/test_model_compression_se_a_type_one_side_exclude_types.py +++ b/source/tests/test_model_compression_se_a_type_one_side_exclude_types.py @@ -6,17 +6,17 @@ import numpy as np -# from deepmd.entrypoints.compress import compress +# from deepmd.tf.entrypoints.compress import compress from common import ( j_loader, run_dp, tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) diff --git a/source/tests/test_model_compression_se_atten.py b/source/tests/test_model_compression_se_atten.py index 6bab1a3881..2e250fe80e 100644 --- a/source/tests/test_model_compression_se_atten.py +++ b/source/tests/test_model_compression_se_atten.py @@ -6,7 +6,7 @@ import numpy as np -# from deepmd.entrypoints.compress import compress +# from deepmd.tf.entrypoints.compress import compress from common import ( j_loader, run_dp, @@ -14,10 +14,10 @@ ) from packaging.version import parse as parse_version -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) @@ -552,7 +552,7 @@ def test_ase(self): Atoms, ) - from deepmd.calculator import ( + from deepmd.tf.calculator import ( DP, ) diff --git a/source/tests/test_model_compression_se_r.py b/source/tests/test_model_compression_se_r.py index f79cdbee6c..0c5912164f 100644 --- a/source/tests/test_model_compression_se_r.py +++ b/source/tests/test_model_compression_se_r.py @@ -6,17 +6,17 @@ import numpy as np -# from deepmd.entrypoints.compress import compress +# from deepmd.tf.entrypoints.compress import compress from common import ( j_loader, run_dp, tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) @@ -390,7 +390,7 @@ def test_ase(self): Atoms, ) - from deepmd.calculator import ( + from deepmd.tf.calculator import ( DP, ) diff --git a/source/tests/test_model_compression_se_t.py b/source/tests/test_model_compression_se_t.py index 48fee4ea1d..eb33ce2b93 100644 --- a/source/tests/test_model_compression_se_t.py +++ b/source/tests/test_model_compression_se_t.py @@ -6,17 +6,17 @@ import numpy as np -# from deepmd.entrypoints.compress import compress +# from deepmd.tf.entrypoints.compress import compress from common import ( j_loader, run_dp, tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) @@ -412,7 +412,7 @@ def test_ase(self): Atoms, ) - from deepmd.calculator import ( + from deepmd.tf.calculator import ( DP, ) diff --git a/source/tests/test_model_devi.py b/source/tests/test_model_devi.py index c7d050cd76..21275ee2d1 100644 --- a/source/tests/test_model_devi.py +++ b/source/tests/test_model_devi.py @@ -5,11 +5,11 @@ import numpy as np -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPotential, calc_model_devi, ) -from deepmd.infer.model_devi import ( +from deepmd.tf.infer.model_devi import ( make_model_devi, ) @@ -20,7 +20,7 @@ tests_path, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_model_devi_mix.py b/source/tests/test_model_devi_mix.py index 98caf409eb..5715b49165 100644 --- a/source/tests/test_model_devi_mix.py +++ b/source/tests/test_model_devi_mix.py @@ -5,11 +5,11 @@ import numpy as np -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPotential, calc_model_devi, ) -from deepmd.infer.model_devi import ( +from deepmd.tf.infer.model_devi import ( make_model_devi, ) @@ -21,10 +21,10 @@ ) from packaging.version import parse as parse_version -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_model_dos.py b/source/tests/test_model_dos.py index c7160d4dda..72cd9db524 100644 --- a/source/tests/test_model_dos.py +++ b/source/tests/test_model_dos.py @@ -7,19 +7,19 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( DOSFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( DOSModel, ) diff --git a/source/tests/test_model_loc_frame.py b/source/tests/test_model_loc_frame.py index c493013316..035ffc868e 100644 --- a/source/tests/test_model_loc_frame.py +++ b/source/tests/test_model_loc_frame.py @@ -6,19 +6,19 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptLocFrame, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( EnerModel, ) diff --git a/source/tests/test_model_multi.py b/source/tests/test_model_multi.py index 9017da22e7..fa75951366 100644 --- a/source/tests/test_model_multi.py +++ b/source/tests/test_model_multi.py @@ -9,20 +9,20 @@ strerch_box, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( DipoleFittingSeA, EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( MultiModel, ) diff --git a/source/tests/test_model_pairtab.py b/source/tests/test_model_pairtab.py index fd678894b5..8a7ebd605c 100644 --- a/source/tests/test_model_pairtab.py +++ b/source/tests/test_model_pairtab.py @@ -7,13 +7,13 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.model.model import ( +from deepmd.tf.model.model import ( Model, ) diff --git a/source/tests/test_model_se_a.py b/source/tests/test_model_se_a.py index d3b4323f0d..f537452385 100644 --- a/source/tests/test_model_se_a.py +++ b/source/tests/test_model_se_a.py @@ -8,22 +8,22 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( EnerModel, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/source/tests/test_model_se_a_aparam.py b/source/tests/test_model_se_a_aparam.py index 41111c57ee..aca2d8c63c 100644 --- a/source/tests/test_model_se_a_aparam.py +++ b/source/tests/test_model_se_a_aparam.py @@ -6,19 +6,19 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( EnerModel, ) diff --git a/source/tests/test_model_se_a_ebd.py b/source/tests/test_model_se_a_ebd.py index bf856b7bc5..2e133a9a63 100644 --- a/source/tests/test_model_se_a_ebd.py +++ b/source/tests/test_model_se_a_ebd.py @@ -6,19 +6,19 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor.se_a_ebd import ( +from deepmd.tf.descriptor.se_a_ebd import ( DescrptSeAEbd, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( EnerModel, ) diff --git a/source/tests/test_model_se_a_ebd_v2.py b/source/tests/test_model_se_a_ebd_v2.py index 71860890ce..f302308a73 100644 --- a/source/tests/test_model_se_a_ebd_v2.py +++ b/source/tests/test_model_se_a_ebd_v2.py @@ -6,22 +6,22 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor.se_a_ebd_v2 import ( +from deepmd.tf.descriptor.se_a_ebd_v2 import ( DescrptSeAEbdV2, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( EnerModel, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/source/tests/test_model_se_a_fparam.py b/source/tests/test_model_se_a_fparam.py index cdb85157a4..46aac18fcb 100644 --- a/source/tests/test_model_se_a_fparam.py +++ b/source/tests/test_model_se_a_fparam.py @@ -6,19 +6,19 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( EnerModel, ) diff --git a/source/tests/test_model_se_a_srtab.py b/source/tests/test_model_se_a_srtab.py index 98cab9e073..3fcb55050d 100644 --- a/source/tests/test_model_se_a_srtab.py +++ b/source/tests/test_model_se_a_srtab.py @@ -8,19 +8,19 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( EnerModel, ) diff --git a/source/tests/test_model_se_a_type.py b/source/tests/test_model_se_a_type.py index 85e4a2916d..bc2a2c3045 100644 --- a/source/tests/test_model_se_a_type.py +++ b/source/tests/test_model_se_a_type.py @@ -6,22 +6,22 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( EnerModel, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/source/tests/test_model_se_atten.py b/source/tests/test_model_se_atten.py index 5417201a9f..592858db2a 100644 --- a/source/tests/test_model_se_atten.py +++ b/source/tests/test_model_se_atten.py @@ -12,22 +12,22 @@ ) from packaging.version import parse as parse_version -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeAtten, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( EnerModel, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/source/tests/test_model_se_r.py b/source/tests/test_model_se_r.py index 94812308c6..acfe6e95dd 100644 --- a/source/tests/test_model_se_r.py +++ b/source/tests/test_model_se_r.py @@ -6,19 +6,19 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeR, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( EnerModel, ) diff --git a/source/tests/test_model_se_t.py b/source/tests/test_model_se_t.py index 1d67e852c7..cb8ed97833 100644 --- a/source/tests/test_model_se_t.py +++ b/source/tests/test_model_se_t.py @@ -6,19 +6,19 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeT, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( EnerModel, ) diff --git a/source/tests/test_model_spin.py b/source/tests/test_model_spin.py index 9bdf1d780a..d1a6f59fe1 100644 --- a/source/tests/test_model_spin.py +++ b/source/tests/test_model_spin.py @@ -10,22 +10,22 @@ tests_path, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( EnerFitting, ) -from deepmd.model import ( +from deepmd.tf.model import ( EnerModel, ) -from deepmd.utils.spin import ( +from deepmd.tf.utils.spin import ( Spin, ) diff --git a/source/tests/test_neighbor_stat.py b/source/tests/test_neighbor_stat.py index 49ace29f53..9806e2053a 100644 --- a/source/tests/test_neighbor_stat.py +++ b/source/tests/test_neighbor_stat.py @@ -5,7 +5,7 @@ import dpdata import numpy as np -from deepmd.entrypoints.neighbor_stat import ( +from deepmd.tf.entrypoints.neighbor_stat import ( neighbor_stat, ) diff --git a/source/tests/test_nvnmd_entrypoints.py b/source/tests/test_nvnmd_entrypoints.py index d82c905024..b257f8fffa 100644 --- a/source/tests/test_nvnmd_entrypoints.py +++ b/source/tests/test_nvnmd_entrypoints.py @@ -7,40 +7,40 @@ tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, tf, ) -from deepmd.nvnmd.data.data import ( +from deepmd.tf.nvnmd.data.data import ( jdata_deepmd_input_v0, jdata_deepmd_input_v1, ) -from deepmd.nvnmd.entrypoints.freeze import ( +from deepmd.tf.nvnmd.entrypoints.freeze import ( save_weight, ) -from deepmd.nvnmd.entrypoints.mapt import ( +from deepmd.tf.nvnmd.entrypoints.mapt import ( MapTable, ) -from deepmd.nvnmd.entrypoints.wrap import ( +from deepmd.tf.nvnmd.entrypoints.wrap import ( wrap, ) -from deepmd.nvnmd.utils.config import ( +from deepmd.tf.nvnmd.utils.config import ( nvnmd_cfg, ) -from deepmd.nvnmd.utils.fio import ( +from deepmd.tf.nvnmd.utils.fio import ( FioBin, FioNpyDic, ) -from deepmd.train.run_options import ( +from deepmd.tf.train.run_options import ( RunOptions, ) -from deepmd.train.trainer import ( +from deepmd.tf.train.trainer import ( DPTrainer, ) -from deepmd.utils.argcheck import ( +from deepmd.tf.utils.argcheck import ( normalize, ) -from deepmd.utils.compat import ( +from deepmd.tf.utils.compat import ( update_deepmd_input, ) diff --git a/source/tests/test_nvnmd_op.py b/source/tests/test_nvnmd_op.py index 3419b375e4..beff8375b8 100644 --- a/source/tests/test_nvnmd_op.py +++ b/source/tests/test_nvnmd_op.py @@ -4,7 +4,7 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( op_module, tf, ) diff --git a/source/tests/test_pairwise_dprc.py b/source/tests/test_pairwise_dprc.py index e95b66c7a0..a38c856c26 100644 --- a/source/tests/test_pairwise_dprc.py +++ b/source/tests/test_pairwise_dprc.py @@ -11,30 +11,30 @@ ) from packaging.version import parse as parse_version -from deepmd import ( +from deepmd.tf import ( DeepPotential, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_loader, j_must_have, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_ENER_FLOAT_PRECISION, GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, op_module, tf, ) -from deepmd.model.model import ( +from deepmd.tf.model.model import ( Model, ) -from deepmd.model.pairwise_dprc import ( +from deepmd.tf.model.pairwise_dprc import ( gather_placeholder, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) -from deepmd.utils.sess import ( +from deepmd.tf.utils.sess import ( run_sess, ) diff --git a/source/tests/test_parallel_training.py b/source/tests/test_parallel_training.py index 0a1a63a29b..85311cf558 100644 --- a/source/tests/test_parallel_training.py +++ b/source/tests/test_parallel_training.py @@ -7,7 +7,7 @@ tests_path, ) -from deepmd.cluster.local import ( +from deepmd.tf.cluster.local import ( get_gpus, ) diff --git a/source/tests/test_polar_se_a.py b/source/tests/test_polar_se_a.py index 2564dc0656..39c34f0a01 100644 --- a/source/tests/test_polar_se_a.py +++ b/source/tests/test_polar_se_a.py @@ -8,19 +8,19 @@ strerch_box, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( PolarFittingSeA, ) -from deepmd.model import ( +from deepmd.tf.model import ( PolarModel, ) diff --git a/source/tests/test_polar_se_a_tebd.py b/source/tests/test_polar_se_a_tebd.py index 570c4261d9..1c82488dca 100644 --- a/source/tests/test_polar_se_a_tebd.py +++ b/source/tests/test_polar_se_a_tebd.py @@ -11,22 +11,22 @@ ) from packaging.version import parse as parse_version -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( DescrptSeA, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.fit import ( +from deepmd.tf.fit import ( PolarFittingSeA, ) -from deepmd.model import ( +from deepmd.tf.model import ( PolarModel, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) diff --git a/source/tests/test_prod_env_mat.py b/source/tests/test_prod_env_mat.py index 663b991831..ac1c16bf97 100644 --- a/source/tests/test_prod_env_mat.py +++ b/source/tests/test_prod_env_mat.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, GLOBAL_TF_FLOAT_PRECISION, op_module, diff --git a/source/tests/test_prod_force.py b/source/tests/test_prod_force.py index 83a44c0be9..7d3bcee6ce 100644 --- a/source/tests/test_prod_force.py +++ b/source/tests/test_prod_force.py @@ -4,7 +4,7 @@ import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, op_module, tf, diff --git a/source/tests/test_prod_force_grad.py b/source/tests/test_prod_force_grad.py index 012def217f..49e63d161c 100644 --- a/source/tests/test_prod_force_grad.py +++ b/source/tests/test_prod_force_grad.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, op_grads_module, tf, diff --git a/source/tests/test_prod_virial.py b/source/tests/test_prod_virial.py index 2abcfcb1bf..fa6347382e 100644 --- a/source/tests/test_prod_virial.py +++ b/source/tests/test_prod_virial.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, op_module, tf, diff --git a/source/tests/test_prod_virial_grad.py b/source/tests/test_prod_virial_grad.py index 548b63a54b..470441a939 100644 --- a/source/tests/test_prod_virial_grad.py +++ b/source/tests/test_prod_virial_grad.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, op_grads_module, tf, diff --git a/source/tests/test_sel_idx.py b/source/tests/test_sel_idx.py index d6630e3e83..e340ba55e7 100644 --- a/source/tests/test_sel_idx.py +++ b/source/tests/test_sel_idx.py @@ -3,7 +3,7 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( select_idx_map, ) diff --git a/source/tests/test_tab_nonsmth.py b/source/tests/test_tab_nonsmth.py index 9e3f9ff640..6b09b98428 100644 --- a/source/tests/test_tab_nonsmth.py +++ b/source/tests/test_tab_nonsmth.py @@ -15,12 +15,12 @@ ) # load grad of force module -import deepmd.op # noqa: F401 -from deepmd.env import ( +import deepmd.tf.op # noqa: F401 +from deepmd.tf.env import ( op_module, tf, ) -from deepmd.utils.pair_tab import ( +from deepmd.tf.utils.pair_tab import ( PairTab, ) diff --git a/source/tests/test_tab_smooth.py b/source/tests/test_tab_smooth.py index 49b18e14f3..f823b366c8 100644 --- a/source/tests/test_tab_smooth.py +++ b/source/tests/test_tab_smooth.py @@ -15,11 +15,11 @@ ) # load grad of force module -from deepmd.env import ( +from deepmd.tf.env import ( op_module, tf, ) -from deepmd.utils.pair_tab import ( +from deepmd.tf.utils.pair_tab import ( PairTab, ) diff --git a/source/tests/test_tabulate.py b/source/tests/test_tabulate.py index 12c805fe79..2ffb5e19c6 100644 --- a/source/tests/test_tabulate.py +++ b/source/tests/test_tabulate.py @@ -3,10 +3,10 @@ import numpy as np -from deepmd.common import ( +from deepmd.tf.common import ( gelu, ) -from deepmd.env import ( +from deepmd.tf.env import ( op_module, tf, ) diff --git a/source/tests/test_train.py b/source/tests/test_train.py index 145457260f..3da62475ea 100644 --- a/source/tests/test_train.py +++ b/source/tests/test_train.py @@ -4,7 +4,7 @@ patch, ) -from deepmd.entrypoints.train import ( +from deepmd.tf.entrypoints.train import ( parse_auto_sel, parse_auto_sel_ratio, update_one_sel, @@ -31,7 +31,7 @@ def test_train_parse_auto_sel_ratio(self): with self.assertRaises(RuntimeError): parse_auto_sel_ratio([1, 2, 3]) - @patch("deepmd.entrypoints.train.get_sel") + @patch("deepmd.tf.entrypoints.train.get_sel") def test_update_one_sel(self, sel_mock): sel_mock.return_value = [10, 20] jdata = {} @@ -44,7 +44,7 @@ def test_update_one_sel(self, sel_mock): # self.assertEqual(descriptor['sel'], [15,30]) self.assertEqual(descriptor["sel"], [16, 32]) - @patch("deepmd.entrypoints.train.get_sel") + @patch("deepmd.tf.entrypoints.train.get_sel") def test_update_sel_hybrid(self, sel_mock): sel_mock.return_value = [10, 20] jdata = { @@ -72,7 +72,7 @@ def test_update_sel_hybrid(self, sel_mock): jdata = update_sel(jdata) self.assertEqual(jdata, expected_out) - @patch("deepmd.entrypoints.train.get_sel") + @patch("deepmd.tf.entrypoints.train.get_sel") def test_update_sel(self, sel_mock): sel_mock.return_value = [10, 20] jdata = {"model": {"descriptor": {"type": "se_e2_a", "rcut": 6, "sel": "auto"}}} @@ -82,7 +82,7 @@ def test_update_sel(self, sel_mock): jdata = update_sel(jdata) self.assertEqual(jdata, expected_out) - @patch("deepmd.entrypoints.train.get_sel") + @patch("deepmd.tf.entrypoints.train.get_sel") def test_update_sel_atten_auto(self, sel_mock): sel_mock.return_value = [25] jdata = { @@ -106,7 +106,7 @@ def test_update_sel_atten_auto(self, sel_mock): jdata = update_sel(jdata) self.assertEqual(jdata, expected_out) - @patch("deepmd.entrypoints.train.get_sel") + @patch("deepmd.tf.entrypoints.train.get_sel") def test_update_sel_atten_int(self, sel_mock): sel_mock.return_value = [25] jdata = { @@ -130,7 +130,7 @@ def test_update_sel_atten_int(self, sel_mock): jdata = update_sel(jdata) self.assertEqual(jdata, expected_out) - @patch("deepmd.entrypoints.train.get_sel") + @patch("deepmd.tf.entrypoints.train.get_sel") def test_update_sel_atten_list(self, sel_mock): sel_mock.return_value = [25] jdata = { @@ -200,7 +200,7 @@ def test_skip_linear_frozen(self): jdata = update_sel(jdata) self.assertEqual(jdata, expected_out) - @patch("deepmd.entrypoints.train.get_min_nbor_dist") + @patch("deepmd.tf.entrypoints.train.get_min_nbor_dist") def test_pairwise_dprc(self, sel_mock): sel_mock.return_value = 0.5 jdata = { diff --git a/source/tests/test_transfer.py b/source/tests/test_transfer.py index 27b97571c9..f73c9eef66 100644 --- a/source/tests/test_transfer.py +++ b/source/tests/test_transfer.py @@ -9,13 +9,13 @@ tests_path, ) -from deepmd.env import ( +from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) diff --git a/source/tests/test_type_embed.py b/source/tests/test_type_embed.py index 3e79bad70b..ceaf2cc5ff 100644 --- a/source/tests/test_type_embed.py +++ b/source/tests/test_type_embed.py @@ -1,10 +1,10 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) -from deepmd.utils.type_embed import ( +from deepmd.tf.utils.type_embed import ( TypeEmbedNet, embed_atom_type, ) diff --git a/source/tests/test_type_one_side.py b/source/tests/test_type_one_side.py index 8e7c173912..d1c02981e7 100644 --- a/source/tests/test_type_one_side.py +++ b/source/tests/test_type_one_side.py @@ -6,13 +6,13 @@ j_loader, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.descriptor import ( +from deepmd.tf.descriptor import ( Descriptor, ) -from deepmd.env import ( +from deepmd.tf.env import ( tf, ) diff --git a/source/tests/test_uni_infer.py b/source/tests/test_uni_infer.py index 6b70d17f7e..ea82507069 100644 --- a/source/tests/test_uni_infer.py +++ b/source/tests/test_uni_infer.py @@ -8,8 +8,8 @@ tests_path, ) -from deepmd.infer.deep_pot import DeepPot as DeepPotTF -from deepmd.utils.convert import ( +from deepmd.tf.infer.deep_pot import DeepPot as DeepPotTF +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) from deepmd_utils.infer.deep_pot import DeepPot as DeepPot diff --git a/source/tests/test_virtual_type.py b/source/tests/test_virtual_type.py index f7fc3c0127..0aca54dfd6 100644 --- a/source/tests/test_virtual_type.py +++ b/source/tests/test_virtual_type.py @@ -10,19 +10,19 @@ tests_path, ) -from deepmd.common import ( +from deepmd.tf.common import ( j_must_have, ) -from deepmd.infer import ( +from deepmd.tf.infer import ( DeepPot, ) -from deepmd.utils.convert import ( +from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) -from deepmd.utils.data_system import ( +from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) -from deepmd.utils.neighbor_stat import ( +from deepmd.tf.utils.neighbor_stat import ( NeighborStat, ) From 3618702786873f4c0ee90c4f557b1b9c433ab01e Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 25 Jan 2024 00:42:04 -0500 Subject: [PATCH 012/686] breaking: move deepmd_utils to deepmd (#3178) Signed-off-by: Jinzhe Zeng --- .github/labeler.yml | 1 - .github/workflows/test_cuda.yml | 2 +- .github/workflows/test_python.yml | 2 +- backend/dynamic_metadata.py | 2 +- codecov.yml | 1 - deepmd/__init__.py | 10 +++++++++- deepmd/__main__.py | 9 +++++++++ {deepmd_utils => deepmd}/calculator.py | 2 +- {deepmd_utils => deepmd}/common.py | 4 ++-- {deepmd_utils => deepmd}/driver.py | 2 +- {deepmd_utils => deepmd}/entrypoints/__init__.py | 0 {deepmd_utils => deepmd}/entrypoints/doc.py | 2 +- {deepmd_utils => deepmd}/entrypoints/gui.py | 0 {deepmd_utils => deepmd}/env.py | 0 {deepmd_utils => deepmd}/infer/__init__.py | 0 {deepmd_utils => deepmd}/infer/backend.py | 0 {deepmd_utils => deepmd}/infer/deep_pot.py | 2 +- {deepmd_utils => deepmd}/infer/model_devi.py | 6 +++--- {deepmd_utils => deepmd}/loggers/__init__.py | 0 {deepmd_utils => deepmd}/loggers/loggers.py | 0 {deepmd_utils => deepmd}/main.py | 2 +- .../model_format/__init__.py | 0 {deepmd_utils => deepmd}/model_format/common.py | 0 {deepmd_utils => deepmd}/model_format/env_mat.py | 0 {deepmd_utils => deepmd}/model_format/network.py | 2 +- .../model_format/output_def.py | 0 {deepmd_utils => deepmd}/model_format/se_e2_a.py | 2 +- deepmd/tf/__init__.py | 2 +- deepmd/tf/calculator.py | 2 +- deepmd/tf/common.py | 16 ++++++++-------- deepmd/tf/entrypoints/doc.py | 2 +- deepmd/tf/entrypoints/gui.py | 2 +- deepmd/tf/entrypoints/main.py | 10 +++++----- deepmd/tf/env.py | 2 +- deepmd/tf/infer/deep_pot.py | 2 +- deepmd/tf/infer/model_devi.py | 2 +- deepmd/tf/loggers/__init__.py | 4 ++-- deepmd/tf/loggers/loggers.py | 4 ++-- deepmd/tf/model/model_stat.py | 2 +- deepmd/tf/nvnmd/utils/argcheck.py | 2 +- deepmd/tf/utils/argcheck.py | 2 +- deepmd/tf/utils/batch_size.py | 2 +- deepmd/tf/utils/compat.py | 2 +- deepmd/tf/utils/data.py | 2 +- deepmd/tf/utils/data_system.py | 2 +- deepmd/tf/utils/errors.py | 2 +- deepmd/tf/utils/pair_tab.py | 2 +- deepmd/tf/utils/path.py | 2 +- deepmd/tf/utils/plugin.py | 2 +- deepmd/tf/utils/random.py | 2 +- deepmd/tf/utils/weight_avg.py | 2 +- {deepmd_utils => deepmd}/utils/__init__.py | 0 {deepmd_utils => deepmd}/utils/argcheck.py | 4 ++-- {deepmd_utils => deepmd}/utils/argcheck_nvnmd.py | 0 {deepmd_utils => deepmd}/utils/batch_size.py | 2 +- {deepmd_utils => deepmd}/utils/compat.py | 0 {deepmd_utils => deepmd}/utils/data.py | 6 +++--- {deepmd_utils => deepmd}/utils/data_system.py | 8 ++++---- {deepmd_utils => deepmd}/utils/errors.py | 0 {deepmd_utils => deepmd}/utils/model_stat.py | 0 {deepmd_utils => deepmd}/utils/pair_tab.py | 0 {deepmd_utils => deepmd}/utils/path.py | 0 {deepmd_utils => deepmd}/utils/plugin.py | 0 {deepmd_utils => deepmd}/utils/random.py | 0 {deepmd_utils => deepmd}/utils/weight_avg.py | 0 deepmd_utils/__init__.py | 6 ------ doc/inference/python.md | 10 +++++----- doc/third-party/ase.md | 2 +- pyproject.toml | 11 +++++------ source/install/docker/Dockerfile | 2 +- source/lmp/tests/test_deeptensor.py | 4 ++-- source/lmp/tests/test_dplr.py | 2 +- source/lmp/tests/test_lammps.py | 4 ++-- source/lmp/tests/test_lammps_3types.py | 4 ++-- source/lmp/tests/test_lammps_faparam.py | 2 +- source/tests/test_model_format_utils.py | 2 +- source/tests/test_output_def.py | 4 ++-- source/tests/test_uni_infer.py | 2 +- 78 files changed, 104 insertions(+), 96 deletions(-) create mode 100644 deepmd/__main__.py rename {deepmd_utils => deepmd}/calculator.py (99%) rename {deepmd_utils => deepmd}/common.py (99%) rename {deepmd_utils => deepmd}/driver.py (98%) rename {deepmd_utils => deepmd}/entrypoints/__init__.py (100%) rename {deepmd_utils => deepmd}/entrypoints/doc.py (92%) rename {deepmd_utils => deepmd}/entrypoints/gui.py (100%) rename {deepmd_utils => deepmd}/env.py (100%) rename {deepmd_utils => deepmd}/infer/__init__.py (100%) rename {deepmd_utils => deepmd}/infer/backend.py (100%) rename {deepmd_utils => deepmd}/infer/deep_pot.py (99%) rename {deepmd_utils => deepmd}/infer/model_devi.py (99%) rename {deepmd_utils => deepmd}/loggers/__init__.py (100%) rename {deepmd_utils => deepmd}/loggers/loggers.py (100%) rename {deepmd_utils => deepmd}/main.py (99%) rename {deepmd_utils => deepmd}/model_format/__init__.py (100%) rename {deepmd_utils => deepmd}/model_format/common.py (100%) rename {deepmd_utils => deepmd}/model_format/env_mat.py (100%) rename {deepmd_utils => deepmd}/model_format/network.py (99%) rename {deepmd_utils => deepmd}/model_format/output_def.py (100%) rename {deepmd_utils => deepmd}/model_format/se_e2_a.py (99%) rename {deepmd_utils => deepmd}/utils/__init__.py (100%) rename {deepmd_utils => deepmd}/utils/argcheck.py (99%) rename {deepmd_utils => deepmd}/utils/argcheck_nvnmd.py (100%) rename {deepmd_utils => deepmd}/utils/batch_size.py (99%) rename {deepmd_utils => deepmd}/utils/compat.py (100%) rename {deepmd_utils => deepmd}/utils/data.py (99%) rename {deepmd_utils => deepmd}/utils/data_system.py (99%) rename {deepmd_utils => deepmd}/utils/errors.py (100%) rename {deepmd_utils => deepmd}/utils/model_stat.py (100%) rename {deepmd_utils => deepmd}/utils/pair_tab.py (100%) rename {deepmd_utils => deepmd}/utils/path.py (100%) rename {deepmd_utils => deepmd}/utils/plugin.py (100%) rename {deepmd_utils => deepmd}/utils/random.py (100%) rename {deepmd_utils => deepmd}/utils/weight_avg.py (100%) delete mode 100644 deepmd_utils/__init__.py diff --git a/.github/labeler.yml b/.github/labeler.yml index b0a85679de..bca580cfea 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -2,7 +2,6 @@ Python: - changed-files: - any-glob-to-any-file: - deepmd/**/* - - deepmd_utils/**/* - source/tests/**/* Docs: - changed-files: diff --git a/.github/workflows/test_cuda.yml b/.github/workflows/test_cuda.yml index e74c0abde2..049fb95e3a 100644 --- a/.github/workflows/test_cuda.yml +++ b/.github/workflows/test_cuda.yml @@ -43,7 +43,7 @@ jobs: DP_VARIANT: cuda CUDA_PATH: /usr/local/cuda-12.2 - run: dp --version - - run: python -m pytest -s --cov=deepmd --cov=deepmd_utils source/tests --durations=0 + - run: python -m pytest -s --cov=deepmd source/tests --durations=0 - run: source/install/test_cc_local.sh env: OMP_NUM_THREADS: 1 diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index 1bd78bfae0..55ef041532 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -38,7 +38,7 @@ jobs: HOROVOD_WITH_TENSORFLOW: 1 HOROVOD_WITHOUT_GLOO: 1 - run: dp --version - - run: pytest --cov=deepmd --cov=deepmd_utils source/tests --durations=0 + - run: pytest --cov=deepmd source/tests --durations=0 - uses: codecov/codecov-action@v3 with: gcov: true diff --git a/backend/dynamic_metadata.py b/backend/dynamic_metadata.py index 210c04235e..72dfcaef45 100644 --- a/backend/dynamic_metadata.py +++ b/backend/dynamic_metadata.py @@ -27,7 +27,7 @@ def dynamic_metadata( _, _, find_libpython_requires, extra_scripts, tf_version = get_argument_from_env() if field == "scripts": return { - "dp": "deepmd_utils.main:main", + "dp": "deepmd.main:main", **extra_scripts, } elif field == "optional-dependencies": diff --git a/codecov.yml b/codecov.yml index 3654859423..8f639ec037 100644 --- a/codecov.yml +++ b/codecov.yml @@ -20,7 +20,6 @@ component_management: name: Python paths: - deepmd/** - - deepmd_utils/** - component_id: module_op name: OP paths: diff --git a/deepmd/__init__.py b/deepmd/__init__.py index 5fc690b94f..f95536db50 100644 --- a/deepmd/__init__.py +++ b/deepmd/__init__.py @@ -1,6 +1,14 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +"""DeePMD-kit is a package written in Python/C++, designed to +minimize the effort required to build deep learning-based model +of interatomic potential energy and force field and to perform +molecular dynamics (MD). + +The top module (deepmd.__init__) should not import any third-party +modules for performance. +""" try: - from deepmd_utils._version import version as __version__ + from deepmd._version import version as __version__ except ImportError: from .__about__ import ( __version__, diff --git a/deepmd/__main__.py b/deepmd/__main__.py new file mode 100644 index 0000000000..a31379b5e3 --- /dev/null +++ b/deepmd/__main__.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Package dp entry point.""" + +from deepmd.main import ( + main, +) + +if __name__ == "__main__": + main() diff --git a/deepmd_utils/calculator.py b/deepmd/calculator.py similarity index 99% rename from deepmd_utils/calculator.py rename to deepmd/calculator.py index 65022d3ccc..2d3e7ce831 100644 --- a/deepmd_utils/calculator.py +++ b/deepmd/calculator.py @@ -19,7 +19,7 @@ all_changes, ) -from deepmd_utils.infer import ( +from deepmd.infer import ( DeepPot, ) diff --git a/deepmd_utils/common.py b/deepmd/common.py similarity index 99% rename from deepmd_utils/common.py rename to deepmd/common.py index b594c54030..f950b50919 100644 --- a/deepmd_utils/common.py +++ b/deepmd/common.py @@ -22,10 +22,10 @@ import numpy as np import yaml -from deepmd_utils.env import ( +from deepmd.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd_utils.utils.path import ( +from deepmd.utils.path import ( DPPath, ) diff --git a/deepmd_utils/driver.py b/deepmd/driver.py similarity index 98% rename from deepmd_utils/driver.py rename to deepmd/driver.py index b9e70f15e4..1e5e36c652 100644 --- a/deepmd_utils/driver.py +++ b/deepmd/driver.py @@ -29,7 +29,7 @@ class DPDriver(dpdata.driver.Driver): """ def __init__(self, dp: str) -> None: - from deepmd_utils.infer.deep_pot import ( + from deepmd.infer.deep_pot import ( DeepPot, ) diff --git a/deepmd_utils/entrypoints/__init__.py b/deepmd/entrypoints/__init__.py similarity index 100% rename from deepmd_utils/entrypoints/__init__.py rename to deepmd/entrypoints/__init__.py diff --git a/deepmd_utils/entrypoints/doc.py b/deepmd/entrypoints/doc.py similarity index 92% rename from deepmd_utils/entrypoints/doc.py rename to deepmd/entrypoints/doc.py index 9f1fd39095..087eb10f73 100644 --- a/deepmd_utils/entrypoints/doc.py +++ b/deepmd/entrypoints/doc.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Module that prints train input arguments docstrings.""" -from deepmd_utils.utils.argcheck import ( +from deepmd.utils.argcheck import ( gen_doc, gen_json, ) diff --git a/deepmd_utils/entrypoints/gui.py b/deepmd/entrypoints/gui.py similarity index 100% rename from deepmd_utils/entrypoints/gui.py rename to deepmd/entrypoints/gui.py diff --git a/deepmd_utils/env.py b/deepmd/env.py similarity index 100% rename from deepmd_utils/env.py rename to deepmd/env.py diff --git a/deepmd_utils/infer/__init__.py b/deepmd/infer/__init__.py similarity index 100% rename from deepmd_utils/infer/__init__.py rename to deepmd/infer/__init__.py diff --git a/deepmd_utils/infer/backend.py b/deepmd/infer/backend.py similarity index 100% rename from deepmd_utils/infer/backend.py rename to deepmd/infer/backend.py diff --git a/deepmd_utils/infer/deep_pot.py b/deepmd/infer/deep_pot.py similarity index 99% rename from deepmd_utils/infer/deep_pot.py rename to deepmd/infer/deep_pot.py index cc328356d3..b863a7ddc2 100644 --- a/deepmd_utils/infer/deep_pot.py +++ b/deepmd/infer/deep_pot.py @@ -12,7 +12,7 @@ import numpy as np -from deepmd_utils.utils.batch_size import ( +from deepmd.utils.batch_size import ( AutoBatchSize, ) diff --git a/deepmd_utils/infer/model_devi.py b/deepmd/infer/model_devi.py similarity index 99% rename from deepmd_utils/infer/model_devi.py rename to deepmd/infer/model_devi.py index cf303e2245..1eb639ed68 100644 --- a/deepmd_utils/infer/model_devi.py +++ b/deepmd/infer/model_devi.py @@ -7,13 +7,13 @@ import numpy as np -from deepmd_utils.common import ( +from deepmd.common import ( expand_sys_str, ) -from deepmd_utils.infer.deep_pot import ( +from deepmd.infer.deep_pot import ( DeepPot, ) -from deepmd_utils.utils.data import ( +from deepmd.utils.data import ( DeepmdData, ) diff --git a/deepmd_utils/loggers/__init__.py b/deepmd/loggers/__init__.py similarity index 100% rename from deepmd_utils/loggers/__init__.py rename to deepmd/loggers/__init__.py diff --git a/deepmd_utils/loggers/loggers.py b/deepmd/loggers/loggers.py similarity index 100% rename from deepmd_utils/loggers/loggers.py rename to deepmd/loggers/loggers.py diff --git a/deepmd_utils/main.py b/deepmd/main.py similarity index 99% rename from deepmd_utils/main.py rename to deepmd/main.py index 32c43e17ac..142bf860cb 100644 --- a/deepmd_utils/main.py +++ b/deepmd/main.py @@ -13,7 +13,7 @@ ) try: - from deepmd_utils._version import version as __version__ + from deepmd._version import version as __version__ except ImportError: __version__ = "unknown" diff --git a/deepmd_utils/model_format/__init__.py b/deepmd/model_format/__init__.py similarity index 100% rename from deepmd_utils/model_format/__init__.py rename to deepmd/model_format/__init__.py diff --git a/deepmd_utils/model_format/common.py b/deepmd/model_format/common.py similarity index 100% rename from deepmd_utils/model_format/common.py rename to deepmd/model_format/common.py diff --git a/deepmd_utils/model_format/env_mat.py b/deepmd/model_format/env_mat.py similarity index 100% rename from deepmd_utils/model_format/env_mat.py rename to deepmd/model_format/env_mat.py diff --git a/deepmd_utils/model_format/network.py b/deepmd/model_format/network.py similarity index 99% rename from deepmd_utils/model_format/network.py rename to deepmd/model_format/network.py index 71ed659787..a327d990c9 100644 --- a/deepmd_utils/model_format/network.py +++ b/deepmd/model_format/network.py @@ -18,7 +18,7 @@ import numpy as np try: - from deepmd_utils._version import version as __version__ + from deepmd._version import version as __version__ except ImportError: __version__ = "unknown" diff --git a/deepmd_utils/model_format/output_def.py b/deepmd/model_format/output_def.py similarity index 100% rename from deepmd_utils/model_format/output_def.py rename to deepmd/model_format/output_def.py diff --git a/deepmd_utils/model_format/se_e2_a.py b/deepmd/model_format/se_e2_a.py similarity index 99% rename from deepmd_utils/model_format/se_e2_a.py rename to deepmd/model_format/se_e2_a.py index 37a2deb967..fe516c8620 100644 --- a/deepmd_utils/model_format/se_e2_a.py +++ b/deepmd/model_format/se_e2_a.py @@ -2,7 +2,7 @@ import numpy as np try: - from deepmd_utils._version import version as __version__ + from deepmd._version import version as __version__ except ImportError: __version__ = "unknown" diff --git a/deepmd/tf/__init__.py b/deepmd/tf/__init__.py index faa0b20bab..65aa03b39e 100644 --- a/deepmd/tf/__init__.py +++ b/deepmd/tf/__init__.py @@ -32,7 +32,7 @@ set_mkl() try: - from deepmd_utils._version import version as __version__ + from deepmd._version import version as __version__ except ImportError: from .__about__ import ( __version__, diff --git a/deepmd/tf/calculator.py b/deepmd/tf/calculator.py index 4dbed51fac..5fc4b59f5f 100644 --- a/deepmd/tf/calculator.py +++ b/deepmd/tf/calculator.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from deepmd_utils.calculator import ( +from deepmd.calculator import ( DP, ) diff --git a/deepmd/tf/common.py b/deepmd/tf/common.py index 9860f82017..9d2f4ee1a7 100644 --- a/deepmd/tf/common.py +++ b/deepmd/tf/common.py @@ -17,12 +17,7 @@ tensor_util, ) -from deepmd.tf.env import ( - GLOBAL_TF_FLOAT_PRECISION, - op_module, - tf, -) -from deepmd_utils.common import ( +from deepmd.common import ( add_data_requirement, data_requirement, expand_sys_str, @@ -32,15 +27,20 @@ make_default_mesh, select_idx_map, ) +from deepmd.tf.env import ( + GLOBAL_TF_FLOAT_PRECISION, + op_module, + tf, +) if TYPE_CHECKING: - from deepmd_utils.common import ( + from deepmd.common import ( _ACTIVATION, _PRECISION, ) __all__ = [ - # from deepmd_utils.common + # from deepmd.common "data_requirement", "add_data_requirement", "select_idx_map", diff --git a/deepmd/tf/entrypoints/doc.py b/deepmd/tf/entrypoints/doc.py index cc28e52930..941f989109 100644 --- a/deepmd/tf/entrypoints/doc.py +++ b/deepmd/tf/entrypoints/doc.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from deepmd_utils.entrypoints.doc import ( +from deepmd.entrypoints.doc import ( doc_train_input, ) diff --git a/deepmd/tf/entrypoints/gui.py b/deepmd/tf/entrypoints/gui.py index 72de65f1c2..ffeee29f7d 100644 --- a/deepmd/tf/entrypoints/gui.py +++ b/deepmd/tf/entrypoints/gui.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from deepmd_utils.entrypoints.gui import ( +from deepmd.entrypoints.gui import ( start_dpgui, ) diff --git a/deepmd/tf/entrypoints/main.py b/deepmd/tf/entrypoints/main.py index 21f5e10aa9..d9618e7498 100644 --- a/deepmd/tf/entrypoints/main.py +++ b/deepmd/tf/entrypoints/main.py @@ -11,6 +11,11 @@ Union, ) +from deepmd.main import ( + get_ll, + main_parser, + parse_args, +) from deepmd.tf.common import ( clear_session, ) @@ -32,11 +37,6 @@ from deepmd.tf.nvnmd.entrypoints.train import ( train_nvnmd, ) -from deepmd_utils.main import ( - get_ll, - main_parser, - parse_args, -) __all__ = ["main", "parse_args", "get_ll", "main_parser"] diff --git a/deepmd/tf/env.py b/deepmd/tf/env.py index f290dc0a90..da03631689 100644 --- a/deepmd/tf/env.py +++ b/deepmd/tf/env.py @@ -28,7 +28,7 @@ ) import deepmd.lib -from deepmd_utils.env import ( +from deepmd.env import ( GLOBAL_ENER_FLOAT_PRECISION, GLOBAL_NP_FLOAT_PRECISION, global_float_prec, diff --git a/deepmd/tf/infer/deep_pot.py b/deepmd/tf/infer/deep_pot.py index df1f8a477f..0663d2daee 100644 --- a/deepmd/tf/infer/deep_pot.py +++ b/deepmd/tf/infer/deep_pot.py @@ -11,6 +11,7 @@ import numpy as np +from deepmd.infer.deep_pot import DeepPot as DeepPotBase from deepmd.tf.common import ( make_default_mesh, ) @@ -26,7 +27,6 @@ from deepmd.tf.utils.sess import ( run_sess, ) -from deepmd_utils.infer.deep_pot import DeepPot as DeepPotBase if TYPE_CHECKING: from pathlib import ( diff --git a/deepmd/tf/infer/model_devi.py b/deepmd/tf/infer/model_devi.py index 802e0ae401..4ee979ac67 100644 --- a/deepmd/tf/infer/model_devi.py +++ b/deepmd/tf/infer/model_devi.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from deepmd_utils.infer.model_devi import ( +from deepmd.infer.model_devi import ( calc_model_devi, calc_model_devi_e, calc_model_devi_f, diff --git a/deepmd/tf/loggers/__init__.py b/deepmd/tf/loggers/__init__.py index 71057e3056..d9227d3620 100644 --- a/deepmd/tf/loggers/__init__.py +++ b/deepmd/tf/loggers/__init__.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -"""Alias of deepmd_utils.loggers for backward compatibility.""" +"""Alias of deepmd.loggers for backward compatibility.""" -from deepmd_utils.loggers.loggers import ( +from deepmd.loggers.loggers import ( set_log_handles, ) diff --git a/deepmd/tf/loggers/loggers.py b/deepmd/tf/loggers/loggers.py index 74ca7de63e..eae99f5367 100644 --- a/deepmd/tf/loggers/loggers.py +++ b/deepmd/tf/loggers/loggers.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -"""Alias of deepmd_utils.loggers.loggers for backward compatibility.""" -from deepmd_utils.loggers.loggers import ( +"""Alias of deepmd.loggers.loggers for backward compatibility.""" +from deepmd.loggers.loggers import ( set_log_handles, ) diff --git a/deepmd/tf/model/model_stat.py b/deepmd/tf/model/model_stat.py index 933a634ce8..9149c0b666 100644 --- a/deepmd/tf/model/model_stat.py +++ b/deepmd/tf/model/model_stat.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" -from deepmd_utils.utils.model_stat import ( +from deepmd.utils.model_stat import ( _make_all_stat_ref, make_stat_input, merge_sys_stat, diff --git a/deepmd/tf/nvnmd/utils/argcheck.py b/deepmd/tf/nvnmd/utils/argcheck.py index 2b9362efb0..c22d9e0cd4 100644 --- a/deepmd/tf/nvnmd/utils/argcheck.py +++ b/deepmd/tf/nvnmd/utils/argcheck.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" -from deepmd_utils.utils.argcheck_nvnmd import ( +from deepmd.utils.argcheck_nvnmd import ( nvnmd_args, ) diff --git a/deepmd/tf/utils/argcheck.py b/deepmd/tf/utils/argcheck.py index 05e7c767b8..c3c0ed4f22 100644 --- a/deepmd/tf/utils/argcheck.py +++ b/deepmd/tf/utils/argcheck.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" -from deepmd_utils.utils.argcheck import ( +from deepmd.utils.argcheck import ( gen_args, gen_doc, gen_json, diff --git a/deepmd/tf/utils/batch_size.py b/deepmd/tf/utils/batch_size.py index 50d02a887b..8436934cee 100644 --- a/deepmd/tf/utils/batch_size.py +++ b/deepmd/tf/utils/batch_size.py @@ -10,7 +10,7 @@ from deepmd.tf.utils.errors import ( OutOfMemoryError, ) -from deepmd_utils.utils.batch_size import AutoBatchSize as AutoBatchSizeBase +from deepmd.utils.batch_size import AutoBatchSize as AutoBatchSizeBase class AutoBatchSize(AutoBatchSizeBase): diff --git a/deepmd/tf/utils/compat.py b/deepmd/tf/utils/compat.py index 91bf4021ee..6c95476ac8 100644 --- a/deepmd/tf/utils/compat.py +++ b/deepmd/tf/utils/compat.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" -from deepmd_utils.utils.compat import ( +from deepmd.utils.compat import ( convert_input_v0_v1, convert_input_v1_v2, deprecate_numb_test, diff --git a/deepmd/tf/utils/data.py b/deepmd/tf/utils/data.py index a6f888beac..3c2eb4298d 100644 --- a/deepmd/tf/utils/data.py +++ b/deepmd/tf/utils/data.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" -from deepmd_utils.utils.data import ( +from deepmd.utils.data import ( DeepmdData, ) diff --git a/deepmd/tf/utils/data_system.py b/deepmd/tf/utils/data_system.py index 65e87d8ebc..88c38d3dd4 100644 --- a/deepmd/tf/utils/data_system.py +++ b/deepmd/tf/utils/data_system.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" -from deepmd_utils.utils.data_system import ( +from deepmd.utils.data_system import ( DeepmdDataSystem, prob_sys_size_ext, process_sys_probs, diff --git a/deepmd/tf/utils/errors.py b/deepmd/tf/utils/errors.py index 683131e48a..5f7291c7ce 100644 --- a/deepmd/tf/utils/errors.py +++ b/deepmd/tf/utils/errors.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from deepmd_utils.utils.errors import ( +from deepmd.utils.errors import ( OutOfMemoryError, ) diff --git a/deepmd/tf/utils/pair_tab.py b/deepmd/tf/utils/pair_tab.py index 1a526ac5fc..a5f5e64aae 100644 --- a/deepmd/tf/utils/pair_tab.py +++ b/deepmd/tf/utils/pair_tab.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" -from deepmd_utils.utils.pair_tab import ( +from deepmd.utils.pair_tab import ( PairTab, ) diff --git a/deepmd/tf/utils/path.py b/deepmd/tf/utils/path.py index 780bc8cabf..63c82b9da0 100644 --- a/deepmd/tf/utils/path.py +++ b/deepmd/tf/utils/path.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" -from deepmd_utils.utils.path import ( +from deepmd.utils.path import ( DPH5Path, DPOSPath, DPPath, diff --git a/deepmd/tf/utils/plugin.py b/deepmd/tf/utils/plugin.py index 3b5b297304..436a80a819 100644 --- a/deepmd/tf/utils/plugin.py +++ b/deepmd/tf/utils/plugin.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" -from deepmd_utils.utils.plugin import ( +from deepmd.utils.plugin import ( Plugin, PluginVariant, VariantABCMeta, diff --git a/deepmd/tf/utils/random.py b/deepmd/tf/utils/random.py index 09547eeac9..6d875df224 100644 --- a/deepmd/tf/utils/random.py +++ b/deepmd/tf/utils/random.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" -from deepmd_utils.utils.random import ( +from deepmd.utils.random import ( choice, random, seed, diff --git a/deepmd/tf/utils/weight_avg.py b/deepmd/tf/utils/weight_avg.py index 267f89ed28..fe162aa1ea 100644 --- a/deepmd/tf/utils/weight_avg.py +++ b/deepmd/tf/utils/weight_avg.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" -from deepmd_utils.utils.weight_avg import ( +from deepmd.utils.weight_avg import ( weighted_average, ) diff --git a/deepmd_utils/utils/__init__.py b/deepmd/utils/__init__.py similarity index 100% rename from deepmd_utils/utils/__init__.py rename to deepmd/utils/__init__.py diff --git a/deepmd_utils/utils/argcheck.py b/deepmd/utils/argcheck.py similarity index 99% rename from deepmd_utils/utils/argcheck.py rename to deepmd/utils/argcheck.py index aaaf8973ab..2acf8ed80b 100644 --- a/deepmd_utils/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -18,10 +18,10 @@ ACTIVATION_FN_DICT, PRECISION_DICT, ) -from deepmd_utils.utils.argcheck_nvnmd import ( +from deepmd.utils.argcheck_nvnmd import ( nvnmd_args, ) -from deepmd_utils.utils.plugin import ( +from deepmd.utils.plugin import ( Plugin, ) diff --git a/deepmd_utils/utils/argcheck_nvnmd.py b/deepmd/utils/argcheck_nvnmd.py similarity index 100% rename from deepmd_utils/utils/argcheck_nvnmd.py rename to deepmd/utils/argcheck_nvnmd.py diff --git a/deepmd_utils/utils/batch_size.py b/deepmd/utils/batch_size.py similarity index 99% rename from deepmd_utils/utils/batch_size.py rename to deepmd/utils/batch_size.py index 1b93a51242..c85806458f 100644 --- a/deepmd_utils/utils/batch_size.py +++ b/deepmd/utils/batch_size.py @@ -12,7 +12,7 @@ import numpy as np -from deepmd_utils.utils.errors import ( +from deepmd.utils.errors import ( OutOfMemoryError, ) diff --git a/deepmd_utils/utils/compat.py b/deepmd/utils/compat.py similarity index 100% rename from deepmd_utils/utils/compat.py rename to deepmd/utils/compat.py diff --git a/deepmd_utils/utils/data.py b/deepmd/utils/data.py similarity index 99% rename from deepmd_utils/utils/data.py rename to deepmd/utils/data.py index 2689257e16..423745cddf 100644 --- a/deepmd_utils/utils/data.py +++ b/deepmd/utils/data.py @@ -9,12 +9,12 @@ import numpy as np -from deepmd_utils.env import ( +from deepmd.env import ( GLOBAL_ENER_FLOAT_PRECISION, GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd_utils.utils import random as dp_random -from deepmd_utils.utils.path import ( +from deepmd.utils import random as dp_random +from deepmd.utils.path import ( DPPath, ) diff --git a/deepmd_utils/utils/data_system.py b/deepmd/utils/data_system.py similarity index 99% rename from deepmd_utils/utils/data_system.py rename to deepmd/utils/data_system.py index f83f587590..20111558cf 100644 --- a/deepmd_utils/utils/data_system.py +++ b/deepmd/utils/data_system.py @@ -12,14 +12,14 @@ import numpy as np -import deepmd_utils.utils.random as dp_random -from deepmd_utils.common import ( +import deepmd.utils.random as dp_random +from deepmd.common import ( make_default_mesh, ) -from deepmd_utils.env import ( +from deepmd.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd_utils.utils.data import ( +from deepmd.utils.data import ( DeepmdData, ) diff --git a/deepmd_utils/utils/errors.py b/deepmd/utils/errors.py similarity index 100% rename from deepmd_utils/utils/errors.py rename to deepmd/utils/errors.py diff --git a/deepmd_utils/utils/model_stat.py b/deepmd/utils/model_stat.py similarity index 100% rename from deepmd_utils/utils/model_stat.py rename to deepmd/utils/model_stat.py diff --git a/deepmd_utils/utils/pair_tab.py b/deepmd/utils/pair_tab.py similarity index 100% rename from deepmd_utils/utils/pair_tab.py rename to deepmd/utils/pair_tab.py diff --git a/deepmd_utils/utils/path.py b/deepmd/utils/path.py similarity index 100% rename from deepmd_utils/utils/path.py rename to deepmd/utils/path.py diff --git a/deepmd_utils/utils/plugin.py b/deepmd/utils/plugin.py similarity index 100% rename from deepmd_utils/utils/plugin.py rename to deepmd/utils/plugin.py diff --git a/deepmd_utils/utils/random.py b/deepmd/utils/random.py similarity index 100% rename from deepmd_utils/utils/random.py rename to deepmd/utils/random.py diff --git a/deepmd_utils/utils/weight_avg.py b/deepmd/utils/weight_avg.py similarity index 100% rename from deepmd_utils/utils/weight_avg.py rename to deepmd/utils/weight_avg.py diff --git a/deepmd_utils/__init__.py b/deepmd_utils/__init__.py deleted file mode 100644 index 1c5314bb7e..0000000000 --- a/deepmd_utils/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-License-Identifier: LGPL-3.0-or-later -"""Untilization methods for DeePMD-kit. - -The __init__ module should not import any modules -for performance. -""" diff --git a/doc/inference/python.md b/doc/inference/python.md index b01f371356..b5d3ca1efc 100644 --- a/doc/inference/python.md +++ b/doc/inference/python.md @@ -2,7 +2,7 @@ One may use the python interface of DeePMD-kit for model inference, an example is given as follows ```python -from deepmd_utils.infer import DeepPot +from deepmd.infer import DeepPot import numpy as np dp = DeepPot("graph.pb") @@ -15,8 +15,8 @@ where `e`, `f` and `v` are predicted energy, force and virial of the system, res Furthermore, one can use the python interface to calculate model deviation. ```python -from deepmd_utils.infer import calc_model_devi -from deepmd_utils.infer import DeepPot as DP +from deepmd.infer import calc_model_devi +from deepmd.infer import DeepPot as DP import numpy as np coord = np.array([[1, 0, 0], [0, 0, 1.5], [1, 0, 3]]).reshape([1, -1]) @@ -32,7 +32,7 @@ Note that if the model inference or model deviation is performed cyclically, one The native neighbor list algorithm of the DeePMD-kit is in $O(N^2)$ complexity ($N$ is the number of atoms). While this is not a problem for small systems that quantum methods can afford, the large systems for molecular dynamics have slow performance. -In this case, one may pass an external neighbor list that has lower complexity to {class}`DeepPot `, once it is compatible with {class}`ase.neighborlist.NewPrimitiveNeighborList`. +In this case, one may pass an external neighbor list that has lower complexity to {class}`DeepPot `, once it is compatible with {class}`ase.neighborlist.NewPrimitiveNeighborList`. ```py import ase.neighborlist @@ -43,4 +43,4 @@ neighbor_list = ase.neighborlist.NewPrimitiveNeighborList( dp = DeepPot("graph.pb", neighbor_list=neighbor_list) ``` -The `update` and `build` methods will be called by {class}`DeepPot `, and `first_neigh`, `pair_second`, and `offset_vec` properties will be used. +The `update` and `build` methods will be called by {class}`DeepPot `, and `first_neigh`, `pair_second`, and `offset_vec` properties will be used. diff --git a/doc/third-party/ase.md b/doc/third-party/ase.md index d9ab67ae3d..ac65fc926e 100644 --- a/doc/third-party/ase.md +++ b/doc/third-party/ase.md @@ -3,7 +3,7 @@ Deep potential can be set up as a calculator with ASE to obtain potential energies and forces. ```python from ase import Atoms -from deepmd_utils.calculator import DP +from deepmd.calculator import DP water = Atoms( "H2O", diff --git a/pyproject.toml b/pyproject.toml index 46632c8ca5..8b8da65aaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,10 +53,10 @@ keywords = ["deepmd"] deepmd = "deepmd.tf.lmp:get_op_dir" [project.entry-points."dpgui"] -"DeePMD-kit" = "deepmd_utils.utils.argcheck:gen_args" +"DeePMD-kit" = "deepmd.utils.argcheck:gen_args" [project.entry-points."dpdata.plugins"] -deepmd_driver = "deepmd_utils.driver:DPDriver" +deepmd_driver = "deepmd.driver:DPDriver" [project.urls] Homepage = "https://github.com/deepmodeling/deepmd-kit" @@ -85,7 +85,6 @@ sdist.exclude = [ ] wheel.packages = [ "deepmd", - "deepmd_utils", ] wheel.py-api = "py37" build-dir = "build/{wheel_tag}" @@ -105,7 +104,7 @@ provider-path = "backend" provider = "scikit_build_core.metadata.fancy_pypi_readme" [[tool.scikit-build.generate]] -path = "deepmd_utils/_version.py" +path = "deepmd/_version.py" template = ''' version = "${version}" ''' @@ -128,7 +127,7 @@ replacement = '\1="https://github.com/deepmodeling/deepmd-kit/raw/master/\g<2>"' [tool.cibuildwheel] test-command = [ - "python -m deepmd.tf -h", + "python -m deepmd -h", "dp -h", "dp_ipi", "pytest {project}/source/tests/test_lammps.py" @@ -171,7 +170,7 @@ before-all = [ environment = { PIP_PREFER_BINARY="1" } test-extras = ["cpu"] test-command = [ - "python -m deepmd.tf -h", + "python -m deepmd -h", "dp -h", ] diff --git a/source/install/docker/Dockerfile b/source/install/docker/Dockerfile index 78a53cb895..26b7be9f19 100644 --- a/source/install/docker/Dockerfile +++ b/source/install/docker/Dockerfile @@ -10,7 +10,7 @@ RUN pip install "$(ls /dist/deepmd_kit${VARIANT}-*manylinux*_x86_64.whl)[gpu,cu$ && dp -h \ && lmp -h \ && dp_ipi \ - && python -m deepmd.tf -h + && python -m deepmd -h FROM python:3.11 AS build-image COPY --from=compile-image /opt/deepmd-kit /opt/deepmd-kit diff --git a/source/lmp/tests/test_deeptensor.py b/source/lmp/tests/test_deeptensor.py index 4fcf693482..3e684b386e 100644 --- a/source/lmp/tests/test_deeptensor.py +++ b/source/lmp/tests/test_deeptensor.py @@ -57,7 +57,7 @@ sp.check_output( - "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file.resolve(), pb_file.resolve(), @@ -65,7 +65,7 @@ ) sp.check_output( - "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file2.resolve(), pb_file2.resolve(), diff --git a/source/lmp/tests/test_dplr.py b/source/lmp/tests/test_dplr.py index ac0acad68f..9c8f1c0d4f 100644 --- a/source/lmp/tests/test_dplr.py +++ b/source/lmp/tests/test_dplr.py @@ -264,7 +264,7 @@ sp.check_output( - "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file.resolve(), pb_file.resolve(), diff --git a/source/lmp/tests/test_lammps.py b/source/lmp/tests/test_lammps.py index 7c7172bcb6..028b403abf 100644 --- a/source/lmp/tests/test_lammps.py +++ b/source/lmp/tests/test_lammps.py @@ -219,14 +219,14 @@ sp.check_output( - "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file.resolve(), pb_file.resolve(), ).split() ) sp.check_output( - "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file2.resolve(), pb_file2.resolve(), diff --git a/source/lmp/tests/test_lammps_3types.py b/source/lmp/tests/test_lammps_3types.py index 72d3ca2e2e..46e1a00c8f 100644 --- a/source/lmp/tests/test_lammps_3types.py +++ b/source/lmp/tests/test_lammps_3types.py @@ -245,14 +245,14 @@ nktv2p = 1.6021765e6 sp.check_output( - "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file.resolve(), pb_file.resolve(), ).split() ) sp.check_output( - "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file2.resolve(), pb_file2.resolve(), diff --git a/source/lmp/tests/test_lammps_faparam.py b/source/lmp/tests/test_lammps_faparam.py index 8a6d0e01ef..064928eeb1 100644 --- a/source/lmp/tests/test_lammps_faparam.py +++ b/source/lmp/tests/test_lammps_faparam.py @@ -134,7 +134,7 @@ sp.check_output( - "{} -m deepmd.tf convert-from pbtxt -i {} -o {}".format( + "{} -m deepmd convert-from pbtxt -i {} -o {}".format( sys.executable, pbtxt_file.resolve(), pb_file.resolve(), diff --git a/source/tests/test_model_format_utils.py b/source/tests/test_model_format_utils.py index f588647096..22393515ec 100644 --- a/source/tests/test_model_format_utils.py +++ b/source/tests/test_model_format_utils.py @@ -8,7 +8,7 @@ import numpy as np -from deepmd_utils.model_format import ( +from deepmd.model_format import ( DescrptSeA, EmbeddingNet, EnvMat, diff --git a/source/tests/test_output_def.py b/source/tests/test_output_def.py index 82d1b13a80..4316fa5982 100644 --- a/source/tests/test_output_def.py +++ b/source/tests/test_output_def.py @@ -6,7 +6,7 @@ import numpy as np -from deepmd_utils.model_format import ( +from deepmd.model_format import ( FittingOutputDef, ModelOutputDef, NativeOP, @@ -14,7 +14,7 @@ fitting_check_output, model_check_output, ) -from deepmd_utils.model_format.output_def import ( +from deepmd.model_format.output_def import ( check_var, ) diff --git a/source/tests/test_uni_infer.py b/source/tests/test_uni_infer.py index ea82507069..4cff5e10d5 100644 --- a/source/tests/test_uni_infer.py +++ b/source/tests/test_uni_infer.py @@ -8,11 +8,11 @@ tests_path, ) +from deepmd.infer.deep_pot import DeepPot as DeepPot from deepmd.tf.infer.deep_pot import DeepPot as DeepPotTF from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) -from deepmd_utils.infer.deep_pot import DeepPot as DeepPot class TestUniversalInfer(unittest.TestCase): From 5c545f7520de41c341f8a4587cbb2fba0af4c669 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 25 Jan 2024 21:26:23 -0500 Subject: [PATCH 013/686] docs: rewrite README; deprecate manually written TOC (#3179) Deprecate per discussion. --------- Signed-off-by: Jinzhe Zeng --- CONTRIBUTING.md | 2 +- README.md | 168 +-- doc/conf.py | 94 -- doc/data/index.md | 9 - doc/freeze/index.md | 4 - doc/getting-started/quick_start.ipynb | 2 +- doc/inference/index.md | 7 - doc/install/index.md | 11 - doc/model/index.md | 20 - doc/test/index.md | 4 - doc/third-party/index.md | 10 - doc/train-input-auto.rst | 1502 ------------------------- doc/train/index.md | 10 - doc/troubleshooting/index.md | 15 - 14 files changed, 46 insertions(+), 1812 deletions(-) delete mode 100644 doc/data/index.md delete mode 100644 doc/freeze/index.md delete mode 100644 doc/inference/index.md delete mode 100644 doc/install/index.md delete mode 100644 doc/model/index.md delete mode 100644 doc/test/index.md delete mode 100644 doc/third-party/index.md delete mode 100644 doc/train-input-auto.rst delete mode 100644 doc/train/index.md delete mode 100644 doc/troubleshooting/index.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e43e23beb6..f2c28ae59b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ Currently, we maintain two main branch: - devel : branch for developers ### Developer guide -See [here](doc/development/index.md) for coding conventions, API and other needs-to-know of the code. +See [documentation](https://deepmd.readthedocs.io/) for coding conventions, API and other needs-to-know of the code. ## How to contribute Please perform the following steps to create your Pull Request to this repository. If don't like to use commands, you can also use [GitHub Desktop](https://desktop.github.com/), which is easier to get started. Go to [git documentation](https://git-scm.com/doc) if you want to really master git. diff --git a/README.md b/README.md index 27c8dab4bc..e61c18dbcb 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ -------------------------------------------------------------------------------- -DeePMD-kit Manual -======== +# DeePMD-kit + [![GitHub release](https://img.shields.io/github/release/deepmodeling/deepmd-kit.svg?maxAge=86400)](https://github.com/deepmodeling/deepmd-kit/releases) [![offline packages](https://img.shields.io/github/downloads/deepmodeling/deepmd-kit/total?label=offline%20packages)](https://github.com/deepmodeling/deepmd-kit/releases) [![conda-forge](https://img.shields.io/conda/dn/conda-forge/deepmd-kit?color=red&label=conda-forge&logo=conda-forge)](https://anaconda.org/conda-forge/deepmd-kit) @@ -11,39 +11,19 @@ [![docker pull](https://img.shields.io/docker/pulls/deepmodeling/deepmd-kit)](https://hub.docker.com/r/deepmodeling/deepmd-kit) [![Documentation Status](https://readthedocs.org/projects/deepmd/badge/)](https://deepmd.readthedocs.io/) -# Table of contents -- [About DeePMD-kit](#about-deepmd-kit) - - [Highlights in v2.0](#highlights-in-deepmd-kit-v2.0) - - [Highlighted features](#highlighted-features) - - [License and credits](#license-and-credits) - - [Deep Potential in a nutshell](#deep-potential-in-a-nutshell) -- [Download and install](#download-and-install) -- [Use DeePMD-kit](#use-deepmd-kit) -- [Code structure](#code-structure) -- [Troubleshooting](#troubleshooting) - -# About DeePMD-kit +## About DeePMD-kit DeePMD-kit is a package written in Python/C++, designed to minimize the effort required to build deep learning-based model of interatomic potential energy and force field and to perform molecular dynamics (MD). This brings new hopes to addressing the accuracy-versus-efficiency dilemma in molecular simulations. Applications of DeePMD-kit span from finite molecules to extended systems and from metallic systems to chemically bonded systems. For more information, check the [documentation](https://deepmd.readthedocs.io/). -# Highlights in DeePMD-kit v2.0 -* [Model compression](doc/freeze/compress.md). Accelerate the efficiency of model inference 4-15 times. -* [New descriptors](doc/model/overall.md). Including [`se_e2_r`](doc/model/train-se-e2-r.md) and [`se_e3`](doc/model/train-se-e3.md). -* [Hybridization of descriptors](doc/model/train-hybrid.md). Hybrid descriptor constructed from the concatenation of several descriptors. -* [Atom type embedding](doc/model/train-se-e2-a-tebd.md). Enable atom-type embedding to decline training complexity and refine performance. -* Training and inference of the dipole (vector) and polarizability (matrix). -* Split of training and validation dataset. -* Optimized training on GPUs. - -## Highlighted features -* **interfaced with TensorFlow**, one of the most popular deep learning frameworks, making the training process highly automatic and efficient, in addition, Tensorboard can be used to visualize training procedures. -* **interfaced with high-performance classical MD and quantum (path-integral) MD packages**, i.e., LAMMPS and i-PI, respectively. -* **implements the Deep Potential series models**, which have been successfully applied to finite and extended systems including organic molecules, metals, semiconductors, insulators, etc. +### Highlighted features +* **interfaced with multiple backends**, including TensorFlow and PyTorch, the most popular deep learning frameworks, making the training process highly automatic and efficient. +* **interfaced with high-performance classical MD and quantum (path-integral) MD packages**, including LAMMPS, i-PI, AMBER, CP2K, GROMACS, OpenMM, and ABUCUS. +* **implements the Deep Potential series models**, which have been successfully applied to finite and extended systems, including organic molecules, metals, semiconductors, insulators, etc. * **implements MPI and GPU supports**, making it highly efficient for high-performance parallel and distributed computing. * **highly modularized**, easy to adapt to different descriptors for deep learning-based potential energy models. -## License and credits +### License and credits The project DeePMD-kit is licensed under [GNU LGPLv3.0](./LICENSE). If you use this code in any future publications, please cite the following publications for general purpose: - Han Wang, Linfeng Zhang, Jiequn Han, and Weinan E. "DeePMD-kit: A deep learning package for many-body potential energy representation and molecular dynamics." Computer Physics Communications 228 (2018): 178-184. @@ -55,7 +35,9 @@ If you use this code in any future publications, please cite the following publi In addition, please follow [the bib file](CITATIONS.bib) to cite the methods you used. -## Deep Potential in a nutshell +### Highlights in major versions + +#### Initial version The goal of Deep Potential is to employ deep learning techniques and realize an inter-atomic potential energy model that is general, accurate, computationally efficient and scalable. The key component is to respect the extensive and symmetry-invariant properties of a potential energy model by assigning a local reference frame and a local environment to each atom. Each environment contains a finite number of atoms, whose local coordinates are arranged in a symmetry-preserving way. These local coordinates are then transformed, through a sub-network, to so-called *atomic energy*. Summing up all the atomic energies gives the potential energy of the system. The initial proof of concept is in the [Deep Potential][1] paper, which employed an approach that was devised to train the neural network model with the potential energy only. With typical *ab initio* molecular dynamics (AIMD) datasets this is insufficient to reproduce the trajectories. The Deep Potential Molecular Dynamics ([DeePMD][2]) model overcomes this limitation. In addition, the learning process in DeePMD improves significantly over the Deep Potential method thanks to the introduction of a flexible family of loss functions. The NN potential constructed in this way reproduces accurately the AIMD trajectories, both classical and quantum (path integral), in extended and finite systems, at a cost that scales linearly with system size and is always several orders of magnitude lower than that of equivalent AIMD simulations. @@ -64,110 +46,48 @@ Although highly efficient, the original Deep Potential model satisfies the exten In addition to building up potential energy models, DeePMD-kit can also be used to build up coarse-grained models. In these models, the quantity that we want to parameterize is the free energy, or the coarse-grained potential, of the coarse-grained particles. See the [DeePCG paper][4] for more details. -See [our latest paper](https://doi.org/10.48550/arXiv.2304.09409) for details of all features. - -# Download and install - -Please follow our [GitHub](https://github.com/deepmodeling/deepmd-kit) webpage to download the [latest released version](https://github.com/deepmodeling/deepmd-kit/tree/master) and [development version](https://github.com/deepmodeling/deepmd-kit/tree/devel). - -DeePMD-kit offers multiple installation methods. It is recommended to use easy methods like [offline packages](doc/install/easy-install.md#offline-packages), [conda](doc/install/easy-install.md#with-conda) and [docker](doc/install/easy-install.md#with-docker). - -One may manually install DeePMD-kit by following the instructions on [installing the Python interface](doc/install/install-from-source.md#install-the-python-interface) and [installing the C++ interface](doc/install/install-from-source.md#install-the-c-interface). The C++ interface is necessary when using DeePMD-kit with LAMMPS, i-PI or GROMACS. - - -# Use DeePMD-kit - -A quick start on using DeePMD-kit can be found [here](doc/getting-started/quick_start.ipynb). - -A full [document](doc/train/train-input-auto.rst) on options in the training input script is available. - -# Advanced - -- [Installation](doc/install/index.md) - - [Easy install](doc/install/easy-install.md) - - [Install from source code](doc/install/install-from-source.md) - - [Install from pre-compiled C library](doc/install/install-from-c-library.md) - - [Install LAMMPS](doc/install/install-lammps.md) - - [Install i-PI](doc/install/install-ipi.md) - - [Install GROMACS](doc/install/install-gromacs.md) - - [Building conda packages](doc/install/build-conda.md) - - [Install Node.js interface](doc/install/install-nodejs.md) - - [Easy install the latest development version](doc/install/easy-install-dev.md) -- [Data](doc/data/index.md) - - [System](doc/data/system.md) - - [Formats of a system](doc/data/data-conv.md) - - [Prepare data with dpdata](doc/data/dpdata.md) -- [Model](doc/model/index.md) - - [Overall](doc/model/overall.md) - - [Descriptor `"se_e2_a"`](doc/model/train-se-e2-a.md) - - [Descriptor `"se_e2_r"`](doc/model/train-se-e2-r.md) - - [Descriptor `"se_e3"`](doc/model/train-se-e3.md) - - [Descriptor `"se_atten"`](doc/model/train-se-atten.md) - - [Descriptor `"se_atten_v2"`](doc/model/train-se-atten.md#descriptor-se_atten_v2) - - [Descriptor `"hybrid"`](doc/model/train-hybrid.md) - - [Descriptor `sel`](doc/model/sel.md) - - [Fit energy](doc/model/train-energy.md) - - [Fit spin energy](doc/model/train-energy-spin.md) - - [Fit `tensor` like `Dipole` and `Polarizability`](doc/model/train-fitting-tensor.md) - - [Fit electronic density of states (DOS)](doc/model/train-fitting-dos.md) - - [Train a Deep Potential model using `type embedding` approach](doc/model/train-se-e2-a-tebd.md) - - [Deep potential long-range](doc/model/dplr.md) - - [Deep Potential - Range Correction (DPRc)](doc/model/dprc.md) - - [Linear model](doc/model/linear.md) - - [Interpolation or combination with a pairwise potential](doc/model/pairtab.md) -- [Training](doc/train/index.md) - - [Training a model](doc/train/training.md) - - [Advanced options](doc/train/training-advanced.md) - - [Parallel training](doc/train/parallel-training.md) - - [Multi-task training](doc/train/multi-task-training.md) - - [TensorBoard Usage](doc/train/tensorboard.md) - - [Known limitations of using GPUs](doc/train/gpu-limitations.md) - - [Training Parameters](doc/train-input-auto.rst) -- [Freeze and Compress](doc/freeze/index.rst) - - [Freeze a model](doc/freeze/freeze.md) - - [Compress a model](doc/freeze/compress.md) -- [Test](doc/test/index.rst) - - [Test a model](doc/test/test.md) - - [Calculate Model Deviation](doc/test/model-deviation.md) -- [Inference](doc/inference/index.rst) - - [Python interface](doc/inference/python.md) - - [C++ interface](doc/inference/cxx.md) - - [Node.js interface](doc/inference/nodejs.md) -- [Integrate with third-party packages](doc/third-party/index.rst) - - [Use deep potential with dpdata](doc/third-party/dpdata.md) - - [Use deep potential with ASE](doc/third-party/ase.md) - - [Run MD with LAMMPS](doc/third-party/lammps-command.md) - - [Run path-integral MD with i-PI](doc/third-party/ipi.md) - - [Run MD with GROMACS](doc/third-party/gromacs.md) - - [Interfaces out of DeePMD-kit](doc/third-party/out-of-deepmd-kit.md) -- [Use NVNMD](doc/nvnmd/index.md) - -# Code structure +#### v1 + +* Code refactor to make it highly modularized. +* GPU support for descriptors. + +#### v2 + +* Model compression. Accelerate the efficiency of model inference 4-15 times. +* New descriptors. Including `se_e2_r`, `se_e3`, and `se_atten` (DPA-1). +* Hybridization of descriptors. Hybrid descriptor constructed from the concatenation of several descriptors. +* Atom type embedding. Enable atom-type embedding to decline training complexity and refine performance. +* Training and inference of the dipole (vector) and polarizability (matrix). +* Split of training and validation dataset. +* Optimized training on GPUs, including CUDA and ROCm. +* Non-von-Neumann. +* C API to interface with the third-party packages. + +See [our latest paper](https://doi.org/10.1063/5.0155600) for details of all features until v2.2.3. + +#### v3 + +* Multiple backends supported. Add a PyTorch backend. +* The DPA-2 model. + +## Install and use DeePMD-kit + +Please read the [online documentation](https://deepmd.readthedocs.io/) for how to install and use DeePMD-kit. + +## Code structure The code is organized as follows: -* `data/raw`: tools manipulating the raw data files. * `examples`: examples. * `deepmd`: DeePMD-kit python modules. +* `source/lib`: source code of the core library. +* `source/op`: Operator (OP) implementation. * `source/api_cc`: source code of DeePMD-kit C++ API. +* `source/api_c`: source code of the C API. +* `source/nodejs`: source code of the Node.js API. * `source/ipi`: source code of i-PI client. -* `source/lib`: source code of DeePMD-kit library. * `source/lmp`: source code of Lammps module. * `source/gmx`: source code of Gromacs plugin. -* `source/op`: TensorFlow op implementation. working with the library. - - -# Troubleshooting - -- [Model compatibility](doc/troubleshooting/model_compatability.md) -- [Installation](doc/troubleshooting/installation.md) -- [The temperature undulates violently during the early stages of MD](doc/troubleshooting/md_energy_undulation.md) -- [MD: cannot run LAMMPS after installing a new version of DeePMD-kit](doc/troubleshooting/md_version_compatibility.md) -- [Do we need to set rcut < half boxsize?](doc/troubleshooting/howtoset_rcut.md) -- [How to set sel?](doc/troubleshooting/howtoset_sel.md) -- [How to control the parallelism of a job?](doc/troubleshooting/howtoset_num_nodes.md) -- [How to tune Fitting/embedding-net size?](doc/troubleshooting/howtoset_netsize.md) -- [Why does a model have low precision?](doc/troubleshooting/precision.md) # Contributing diff --git a/doc/conf.py b/doc/conf.py index 8138f82ba4..11803a9e2d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -28,96 +28,6 @@ sys.path.append(os.path.dirname(__file__)) import sphinx_contrib_exhale_multiproject # noqa: F401 - -def mkindex(dirname): - dirname = dirname + "/" - oldfindex = open(dirname + "index.md") - oldlist = oldfindex.readlines() - oldfindex.close() - - oldnames = [] - for entry in oldlist: - _name = entry[entry.find("(") + 1 : entry.find(")")] - oldnames.append(_name) - - newfindex = open(dirname + "index.md", "a") - for root, dirs, files in os.walk(dirname, topdown=False): - newnames = [ - name for name in files if "index.md" not in name and name not in oldnames - ] - for name in newnames: - f = open(dirname + name) - _lines = f.readlines() - for _headline in _lines: - _headline = _headline.strip("#") - headline = _headline.strip() - if len(headline) == 0 or headline[0] == "." or headline[0] == "=": - continue - else: - break - longname = "- [" + headline + "]" + "(" + name + ")\n" - newfindex.write(longname) - - newfindex.close() - - -def classify_index_TS(): - dirname = "troubleshooting/" - oldfindex = open(dirname + "index.md") - oldlist = oldfindex.readlines() - oldfindex.close() - - oldnames = [] - sub_titles = [] - heads = [] - while len(oldlist) > 0: - entry = oldlist.pop(0) - if entry.find("(") >= 0: - _name = entry[entry.find("(") + 1 : entry.find(")")] - oldnames.append(_name) - continue - if entry.find("##") >= 0: - _name = entry[entry.find("##") + 3 : -1] - sub_titles.append(_name) - continue - entry.strip() - if entry != "\n": - heads.append(entry) - - newfindex = open(dirname + "index.md", "w") - for entry in heads: - newfindex.write(entry) - newfindex.write("\n") - sub_lists = [[], []] - for root, dirs, files in os.walk(dirname, topdown=False): - newnames = [name for name in files if "index.md" not in name] - for name in newnames: - f = open(dirname + name) - _lines = f.readlines() - f.close() - for _headline in _lines: - _headline = _headline.strip("#") - headline = _headline.strip() - if len(headline) == 0 or headline[0] == "." or headline[0] == "=": - continue - else: - break - longname = "- [" + headline + "]" + "(" + name + ")\n" - if "howtoset_" in name: - sub_lists[1].append(longname) - else: - sub_lists[0].append(longname) - - newfindex.write("## Trouble shooting\n") - for entry in sub_lists[0]: - newfindex.write(entry) - newfindex.write("\n") - newfindex.write("## Parameters setting\n") - for entry in sub_lists[1]: - newfindex.write(entry) - newfindex.close() - - # -- Project information ----------------------------------------------------- project = "DeePMD-kit" @@ -169,10 +79,6 @@ def setup(app): # 'sphinx.ext.autosummary' # ] -# mkindex("troubleshooting") -# mkindex("development") -# classify_index_TS() - extensions = [ "deepmodeling_sphinx", "dargs.sphinx", diff --git a/doc/data/index.md b/doc/data/index.md deleted file mode 100644 index 838265427b..0000000000 --- a/doc/data/index.md +++ /dev/null @@ -1,9 +0,0 @@ -# Data - -In this section, we will introduce how to convert the DFT-labeled data into the data format used by DeePMD-kit. - -The DeePMD-kit organizes data in `systems`. Each `system` is composed of a number of `frames`. One may roughly view a `frame` as a snapshot of an MD trajectory, but it does not necessarily come from an MD simulation. A `frame` records the coordinates and types of atoms, cell vectors if the periodic boundary condition is assumed, energy, atomic forces and virials. It is noted that the `frames` in one `system` share the same number of atoms with the same type. - -- [System](system.md) -- [Formats of a system](data-conv.md) -- [Prepare data with dpdata](dpdata.md) diff --git a/doc/freeze/index.md b/doc/freeze/index.md deleted file mode 100644 index 0bc3664144..0000000000 --- a/doc/freeze/index.md +++ /dev/null @@ -1,4 +0,0 @@ -# Freeze and Compress - -- [Freeze a model](freeze.md) -- [Compress a model](compress.md) diff --git a/doc/getting-started/quick_start.ipynb b/doc/getting-started/quick_start.ipynb index d0f7d8db0b..1c53665b7d 100644 --- a/doc/getting-started/quick_start.ipynb +++ b/doc/getting-started/quick_start.ipynb @@ -239,7 +239,7 @@ "id": "a999f41b-e343-4dc2-8499-84fee6e52221", "metadata": {}, "source": [ - "The DeePMD-kit adopts a compressed data format. All training data should first be converted into this format and can then be used by DeePMD-kit. The data format is explained in detail in the DeePMD-kit manual that can be found in [the DeePMD-kit Data Introduction](../data/index.md)." + "The DeePMD-kit adopts a compressed data format. All training data should first be converted into this format and can then be used by DeePMD-kit. The data format is explained in detail in the DeePMD-kit manual that can be found in [the DeePMD-kit Data Introduction](../data/system.md)." ] }, { diff --git a/doc/inference/index.md b/doc/inference/index.md deleted file mode 100644 index fa0a747eb4..0000000000 --- a/doc/inference/index.md +++ /dev/null @@ -1,7 +0,0 @@ -# Inference - -Note that the model for inference is required to be compatible with the DeePMD-kit package. See [Model compatibility](../troubleshooting/model-compatability.html) for details. - -- [Python interface](python.md) -- [C++ interface](cxx.md) -- [Node.js interface](nodejs.md) diff --git a/doc/install/index.md b/doc/install/index.md deleted file mode 100644 index 8428255f5a..0000000000 --- a/doc/install/index.md +++ /dev/null @@ -1,11 +0,0 @@ -# Installation - -- [Easy install](easy-install.md) -- [Install from source code](install-from-source.md) -- [Install from pre-compiled C library](doc/install/install-from-c-library.md) -- [Install LAMMPS](install-lammps.md) -- [Install i-PI](install-ipi.md) -- [Install GROMACS](install-gromacs.md) -- [Building conda packages](build-conda.md) -- [Install Node.js interface](install-nodejs.md) -- [Easy install the latest development version](easy-install-dev.md) diff --git a/doc/model/index.md b/doc/model/index.md deleted file mode 100644 index 589b39b2b5..0000000000 --- a/doc/model/index.md +++ /dev/null @@ -1,20 +0,0 @@ -# Model - -- [Overall](overall.md) -- [Descriptor `"se_e2_a"`](train-se-e2-a.md) -- [Descriptor `"se_e2_r"`](train-se-e2-r.md) -- [Descriptor `"se_e3"`](train-se-e3.md) -- [Descriptor `"se_atten"`](train-se-atten.md) -- [Descriptor `"se_atten_v2"`](train-se-atten.md#descriptor-se_atten_v2) -- [Descriptor `"se_a_mask"`](train-se-a-mask.md) -- [Descriptor `"hybrid"`](train-hybrid.md) -- [Descriptor `sel`](sel.md) -- [Fit energy](train-energy.md) -- [Fit spin energy](train-energy-spin.md) -- [Fit `tensor` like `Dipole` and `Polarizability`](train-fitting-tensor.md) -- [Fit electronic density of states (DOS)](train-fitting-dos.md) -- [Train a Deep Potential model using `type embedding` approach](train-se-e2-a-tebd.md) -- [Deep potential long-range](dplr.md) -- [Deep Potential - Range Correction (DPRc)](dprc.md) -- [Linear model](linear.md) -- [Interpolation or combination with a pairwise potential](pairtab.md) diff --git a/doc/test/index.md b/doc/test/index.md deleted file mode 100644 index 4a502123d9..0000000000 --- a/doc/test/index.md +++ /dev/null @@ -1,4 +0,0 @@ -# Test - -- [Test a model](test.md) -- [Calculate Model Deviation](model-deviation.md) diff --git a/doc/third-party/index.md b/doc/third-party/index.md deleted file mode 100644 index 419f1fbb5c..0000000000 --- a/doc/third-party/index.md +++ /dev/null @@ -1,10 +0,0 @@ -# Integrate with third-party packages - -Note that the model for inference is required to be compatible with the DeePMD-kit package. See [Model compatibility](../troubleshooting/model-compatability.html) for details. - -- [Use deep potential with dpdata](dpdata.md) -- [Use deep potential with ASE](ase.md) -- [Run MD with LAMMPS](lammps-command.md) -- [Run path-integral MD with i-PI](ipi.md) -- [Run MD with GROMACS](gromacs.md) -- [Interfaces out of DeePMD-kit](out-of-deepmd-kit.md) diff --git a/doc/train-input-auto.rst b/doc/train-input-auto.rst deleted file mode 100644 index a3b69eade9..0000000000 --- a/doc/train-input-auto.rst +++ /dev/null @@ -1,1502 +0,0 @@ -.. _`model`: - -model: - | type: ``dict`` - | argument path: ``model`` - - .. _`model/type_map`: - - type_map: - | type: ``list``, optional - | argument path: ``model/type_map`` - - A list of strings. Give the name to each type of atoms. It is noted that the number of atom type of training system must be less than 128 in a GPU environment. - - .. _`model/data_stat_nbatch`: - - data_stat_nbatch: - | type: ``int``, optional, default: ``10`` - | argument path: ``model/data_stat_nbatch`` - - The model determines the normalization from the statistics of the data. This key specifies the number of `frames` in each `system` used for statistics. - - .. _`model/data_stat_protect`: - - data_stat_protect: - | type: ``float``, optional, default: ``0.01`` - | argument path: ``model/data_stat_protect`` - - Protect parameter for atomic energy regression. - - .. _`model/use_srtab`: - - use_srtab: - | type: ``str``, optional - | argument path: ``model/use_srtab`` - - The table for the short-range pairwise interaction added on top of DP. The table is a text data file with (N_t + 1) * N_t / 2 + 1 columes. The first colume is the distance between atoms. The second to the last columes are energies for pairs of certain types. For example we have two atom types, 0 and 1. The columes from 2nd to 4th are for 0-0, 0-1 and 1-1 correspondingly. - - .. _`model/smin_alpha`: - - smin_alpha: - | type: ``float``, optional - | argument path: ``model/smin_alpha`` - - The short-range tabulated interaction will be swithed according to the distance of the nearest neighbor. This distance is calculated by softmin. This parameter is the decaying parameter in the softmin. It is only required when `use_srtab` is provided. - - .. _`model/sw_rmin`: - - sw_rmin: - | type: ``float``, optional - | argument path: ``model/sw_rmin`` - - The lower boundary of the interpolation between short-range tabulated interaction and DP. It is only required when `use_srtab` is provided. - - .. _`model/sw_rmax`: - - sw_rmax: - | type: ``float``, optional - | argument path: ``model/sw_rmax`` - - The upper boundary of the interpolation between short-range tabulated interaction and DP. It is only required when `use_srtab` is provided. - - .. _`model/type_embedding`: - - type_embedding: - | type: ``dict``, optional - | argument path: ``model/type_embedding`` - - The type embedding. - - .. _`model/type_embedding/neuron`: - - neuron: - | type: ``list``, optional, default: ``[2, 4, 8]`` - | argument path: ``model/type_embedding/neuron`` - - Number of neurons in each hidden layers of the embedding net. When two layers are of the same size or one layer is twice as large as the previous layer, a skip connection is built. - - .. _`model/type_embedding/activation_function`: - - activation_function: - | type: ``str``, optional, default: ``tanh`` - | argument path: ``model/type_embedding/activation_function`` - - The activation function in the embedding net. Supported activation functions are "relu", "relu6", "softplus", "sigmoid", "tanh", "gelu". - - .. _`model/type_embedding/resnet_dt`: - - resnet_dt: - | type: ``bool``, optional, default: ``False`` - | argument path: ``model/type_embedding/resnet_dt`` - - Whether to use a "Timestep" in the skip connection - - .. _`model/type_embedding/precision`: - - precision: - | type: ``str``, optional, default: ``float64`` - | argument path: ``model/type_embedding/precision`` - - The precision of the embedding net parameters, supported options are "default", "float16", "float32", "float64". - - .. _`model/type_embedding/trainable`: - - trainable: - | type: ``bool``, optional, default: ``True`` - | argument path: ``model/type_embedding/trainable`` - - If the parameters in the embedding net are trainable - - .. _`model/type_embedding/seed`: - - seed: - | type: ``int`` | ``NoneType``, optional - | argument path: ``model/type_embedding/seed`` - - Random seed for parameter initialization - - .. _`model/descriptor`: - - descriptor: - | type: ``dict`` - | argument path: ``model/descriptor`` - - The descriptor of atomic environment. - - - Depending on the value of *type*, different sub args are accepted. - - .. _`model/descriptor/type`: - - type: - | type: ``str`` (flag key) - | argument path: ``model/descriptor/type`` - | possible choices: |code:model/descriptor[loc_frame]|_, |code:model/descriptor[se_e2_a]|_, |code:model/descriptor[se_e2_r]|_, |code:model/descriptor[se_e3]|_, |code:model/descriptor[se_a_tpe]|_, |code:model/descriptor[hybrid]|_ - - The type of the descritpor. See explanation below. - - - `loc_frame`: Defines a local frame at each atom, and the compute the descriptor as local coordinates under this frame. - - - `se_e2_a`: Used by the smooth edition of Deep Potential. The full relative coordinates are used to construct the descriptor. - - - `se_e2_r`: Used by the smooth edition of Deep Potential. Only the distance between atoms is used to construct the descriptor. - - - `se_e3`: Used by the smooth edition of Deep Potential. The full relative coordinates are used to construct the descriptor. Three-body embedding will be used by this descriptor. - - - `se_a_tpe`: Used by the smooth edition of Deep Potential. The full relative coordinates are used to construct the descriptor. Type embedding will be used by this descriptor. - - - `hybrid`: Concatenate of a list of descriptors as a new descriptor. - - .. |code:model/descriptor[loc_frame]| replace:: ``loc_frame`` - .. _`code:model/descriptor[loc_frame]`: `model/descriptor[loc_frame]`_ - .. |code:model/descriptor[se_e2_a]| replace:: ``se_e2_a`` - .. _`code:model/descriptor[se_e2_a]`: `model/descriptor[se_e2_a]`_ - .. |code:model/descriptor[se_e2_r]| replace:: ``se_e2_r`` - .. _`code:model/descriptor[se_e2_r]`: `model/descriptor[se_e2_r]`_ - .. |code:model/descriptor[se_e3]| replace:: ``se_e3`` - .. _`code:model/descriptor[se_e3]`: `model/descriptor[se_e3]`_ - .. |code:model/descriptor[se_a_tpe]| replace:: ``se_a_tpe`` - .. _`code:model/descriptor[se_a_tpe]`: `model/descriptor[se_a_tpe]`_ - .. |code:model/descriptor[hybrid]| replace:: ``hybrid`` - .. _`code:model/descriptor[hybrid]`: `model/descriptor[hybrid]`_ - - .. |flag:model/descriptor/type| replace:: *type* - .. _`flag:model/descriptor/type`: `model/descriptor/type`_ - - - .. _`model/descriptor[loc_frame]`: - - When |flag:model/descriptor/type|_ is set to ``loc_frame``: - - .. _`model/descriptor[loc_frame]/sel_a`: - - sel_a: - | type: ``list`` - | argument path: ``model/descriptor[loc_frame]/sel_a`` - - A list of integers. The length of the list should be the same as the number of atom types in the system. `sel_a[i]` gives the selected number of type-i neighbors. The full relative coordinates of the neighbors are used by the descriptor. - - .. _`model/descriptor[loc_frame]/sel_r`: - - sel_r: - | type: ``list`` - | argument path: ``model/descriptor[loc_frame]/sel_r`` - - A list of integers. The length of the list should be the same as the number of atom types in the system. `sel_r[i]` gives the selected number of type-i neighbors. Only relative distance of the neighbors are used by the descriptor. sel_a[i] + sel_r[i] is recommended to be larger than the maximally possible number of type-i neighbors in the cut-off radius. - - .. _`model/descriptor[loc_frame]/rcut`: - - rcut: - | type: ``float``, optional, default: ``6.0`` - | argument path: ``model/descriptor[loc_frame]/rcut`` - - The cut-off radius. The default value is 6.0 - - .. _`model/descriptor[loc_frame]/axis_rule`: - - axis_rule: - | type: ``list`` - | argument path: ``model/descriptor[loc_frame]/axis_rule`` - - A list of integers. The length should be 6 times of the number of types. - - - axis_rule[i*6+0]: class of the atom defining the first axis of type-i atom. 0 for neighbors with full coordinates and 1 for neighbors only with relative distance. - - - axis_rule[i*6+1]: type of the atom defining the first axis of type-i atom. - - - axis_rule[i*6+2]: index of the axis atom defining the first axis. Note that the neighbors with the same class and type are sorted according to their relative distance. - - - axis_rule[i*6+3]: class of the atom defining the first axis of type-i atom. 0 for neighbors with full coordinates and 1 for neighbors only with relative distance. - - - axis_rule[i*6+4]: type of the atom defining the second axis of type-i atom. - - - axis_rule[i*6+5]: class of the atom defining the second axis of type-i atom. 0 for neighbors with full coordinates and 1 for neighbors only with relative distance. - - - .. _`model/descriptor[se_e2_a]`: - - When |flag:model/descriptor/type|_ is set to ``se_e2_a`` (or its alias ``se_a``): - - .. _`model/descriptor[se_e2_a]/sel`: - - sel: - | type: ``list`` | ``str``, optional, default: ``auto`` - | argument path: ``model/descriptor[se_e2_a]/sel`` - - This parameter set the number of selected neighbors for each type of atom. It can be: - - - `List[int]`. The length of the list should be the same as the number of atom types in the system. `sel[i]` gives the selected number of type-i neighbors. `sel[i]` is recommended to be larger than the maximally possible number of type-i neighbors in the cut-off radius. It is noted that the total sel value must be less than 4096 in a GPU environment. - - - `str`. Can be "auto:factor" or "auto". "factor" is a float number larger than 1. This option will automatically determine the `sel`. In detail it counts the maximal number of neighbors with in the cutoff radius for each type of neighbor, then multiply the maximum by the "factor". Finally the number is wraped up to 4 divisible. The option "auto" is equivalent to "auto:1.1". - - .. _`model/descriptor[se_e2_a]/rcut`: - - rcut: - | type: ``float``, optional, default: ``6.0`` - | argument path: ``model/descriptor[se_e2_a]/rcut`` - - The cut-off radius. - - .. _`model/descriptor[se_e2_a]/rcut_smth`: - - rcut_smth: - | type: ``float``, optional, default: ``0.5`` - | argument path: ``model/descriptor[se_e2_a]/rcut_smth`` - - Where to start smoothing. For example the 1/r term is smoothed from `rcut` to `rcut_smth` - - .. _`model/descriptor[se_e2_a]/neuron`: - - neuron: - | type: ``list``, optional, default: ``[10, 20, 40]`` - | argument path: ``model/descriptor[se_e2_a]/neuron`` - - Number of neurons in each hidden layers of the embedding net. When two layers are of the same size or one layer is twice as large as the previous layer, a skip connection is built. - - .. _`model/descriptor[se_e2_a]/axis_neuron`: - - axis_neuron: - | type: ``int``, optional, default: ``4``, alias: *n_axis_neuron* - | argument path: ``model/descriptor[se_e2_a]/axis_neuron`` - - Size of the submatrix of G (embedding matrix). - - .. _`model/descriptor[se_e2_a]/activation_function`: - - activation_function: - | type: ``str``, optional, default: ``tanh`` - | argument path: ``model/descriptor[se_e2_a]/activation_function`` - - The activation function in the embedding net. Supported activation functions are "relu", "relu6", "softplus", "sigmoid", "tanh", "gelu". - - .. _`model/descriptor[se_e2_a]/resnet_dt`: - - resnet_dt: - | type: ``bool``, optional, default: ``False`` - | argument path: ``model/descriptor[se_e2_a]/resnet_dt`` - - Whether to use a "Timestep" in the skip connection - - .. _`model/descriptor[se_e2_a]/type_one_side`: - - type_one_side: - | type: ``bool``, optional, default: ``False`` - | argument path: ``model/descriptor[se_e2_a]/type_one_side`` - - Try to build N_types embedding nets. Otherwise, building N_types^2 embedding nets - - .. _`model/descriptor[se_e2_a]/precision`: - - precision: - | type: ``str``, optional, default: ``float64`` - | argument path: ``model/descriptor[se_e2_a]/precision`` - - The precision of the embedding net parameters, supported options are "default", "float16", "float32", "float64". - - .. _`model/descriptor[se_e2_a]/trainable`: - - trainable: - | type: ``bool``, optional, default: ``True`` - | argument path: ``model/descriptor[se_e2_a]/trainable`` - - If the parameters in the embedding net is trainable - - .. _`model/descriptor[se_e2_a]/seed`: - - seed: - | type: ``int`` | ``NoneType``, optional - | argument path: ``model/descriptor[se_e2_a]/seed`` - - Random seed for parameter initialization - - .. _`model/descriptor[se_e2_a]/exclude_types`: - - exclude_types: - | type: ``list``, optional, default: ``[]`` - | argument path: ``model/descriptor[se_e2_a]/exclude_types`` - - The excluded pairs of types which have no interaction with each other. For example, `[[0, 1]]` means no interaction between type 0 and type 1. - - .. _`model/descriptor[se_e2_a]/set_davg_zero`: - - set_davg_zero: - | type: ``bool``, optional, default: ``False`` - | argument path: ``model/descriptor[se_e2_a]/set_davg_zero`` - - Set the normalization average to zero. This option should be set when `atom_ener` in the energy fitting is used - - - .. _`model/descriptor[se_e2_r]`: - - When |flag:model/descriptor/type|_ is set to ``se_e2_r`` (or its alias ``se_r``): - - .. _`model/descriptor[se_e2_r]/sel`: - - sel: - | type: ``list`` | ``str``, optional, default: ``auto`` - | argument path: ``model/descriptor[se_e2_r]/sel`` - - This parameter set the number of selected neighbors for each type of atom. It can be: - - - `List[int]`. The length of the list should be the same as the number of atom types in the system. `sel[i]` gives the selected number of type-i neighbors. `sel[i]` is recommended to be larger than the maximally possible number of type-i neighbors in the cut-off radius. It is noted that the total sel value must be less than 4096 in a GPU environment. - - - `str`. Can be "auto:factor" or "auto". "factor" is a float number larger than 1. This option will automatically determine the `sel`. In detail it counts the maximal number of neighbors with in the cutoff radius for each type of neighbor, then multiply the maximum by the "factor". Finally the number is wraped up to 4 divisible. The option "auto" is equivalent to "auto:1.1". - - .. _`model/descriptor[se_e2_r]/rcut`: - - rcut: - | type: ``float``, optional, default: ``6.0`` - | argument path: ``model/descriptor[se_e2_r]/rcut`` - - The cut-off radius. - - .. _`model/descriptor[se_e2_r]/rcut_smth`: - - rcut_smth: - | type: ``float``, optional, default: ``0.5`` - | argument path: ``model/descriptor[se_e2_r]/rcut_smth`` - - Where to start smoothing. For example the 1/r term is smoothed from `rcut` to `rcut_smth` - - .. _`model/descriptor[se_e2_r]/neuron`: - - neuron: - | type: ``list``, optional, default: ``[10, 20, 40]`` - | argument path: ``model/descriptor[se_e2_r]/neuron`` - - Number of neurons in each hidden layers of the embedding net. When two layers are of the same size or one layer is twice as large as the previous layer, a skip connection is built. - - .. _`model/descriptor[se_e2_r]/activation_function`: - - activation_function: - | type: ``str``, optional, default: ``tanh`` - | argument path: ``model/descriptor[se_e2_r]/activation_function`` - - The activation function in the embedding net. Supported activation functions are "relu", "relu6", "softplus", "sigmoid", "tanh", "gelu". - - .. _`model/descriptor[se_e2_r]/resnet_dt`: - - resnet_dt: - | type: ``bool``, optional, default: ``False`` - | argument path: ``model/descriptor[se_e2_r]/resnet_dt`` - - Whether to use a "Timestep" in the skip connection - - .. _`model/descriptor[se_e2_r]/type_one_side`: - - type_one_side: - | type: ``bool``, optional, default: ``False`` - | argument path: ``model/descriptor[se_e2_r]/type_one_side`` - - Try to build N_types embedding nets. Otherwise, building N_types^2 embedding nets - - .. _`model/descriptor[se_e2_r]/precision`: - - precision: - | type: ``str``, optional, default: ``float64`` - | argument path: ``model/descriptor[se_e2_r]/precision`` - - The precision of the embedding net parameters, supported options are "default", "float16", "float32", "float64". - - .. _`model/descriptor[se_e2_r]/trainable`: - - trainable: - | type: ``bool``, optional, default: ``True`` - | argument path: ``model/descriptor[se_e2_r]/trainable`` - - If the parameters in the embedding net are trainable - - .. _`model/descriptor[se_e2_r]/seed`: - - seed: - | type: ``int`` | ``NoneType``, optional - | argument path: ``model/descriptor[se_e2_r]/seed`` - - Random seed for parameter initialization - - .. _`model/descriptor[se_e2_r]/exclude_types`: - - exclude_types: - | type: ``list``, optional, default: ``[]`` - | argument path: ``model/descriptor[se_e2_r]/exclude_types`` - - The excluded pairs of types which have no interaction with each other. For example, `[[0, 1]]` means no interaction between type 0 and type 1. - - .. _`model/descriptor[se_e2_r]/set_davg_zero`: - - set_davg_zero: - | type: ``bool``, optional, default: ``False`` - | argument path: ``model/descriptor[se_e2_r]/set_davg_zero`` - - Set the normalization average to zero. This option should be set when `atom_ener` in the energy fitting is used - - - .. _`model/descriptor[se_e3]`: - - When |flag:model/descriptor/type|_ is set to ``se_e3`` (or its aliases ``se_at``, ``se_a_3be``, ``se_t``): - - .. _`model/descriptor[se_e3]/sel`: - - sel: - | type: ``list`` | ``str``, optional, default: ``auto`` - | argument path: ``model/descriptor[se_e3]/sel`` - - This parameter set the number of selected neighbors for each type of atom. It can be: - - - `List[int]`. The length of the list should be the same as the number of atom types in the system. `sel[i]` gives the selected number of type-i neighbors. `sel[i]` is recommended to be larger than the maximally possible number of type-i neighbors in the cut-off radius. It is noted that the total sel value must be less than 4096 in a GPU environment. - - - `str`. Can be "auto:factor" or "auto". "factor" is a float number larger than 1. This option will automatically determine the `sel`. In detail it counts the maximal number of neighbors with in the cutoff radius for each type of neighbor, then multiply the maximum by the "factor". Finally the number is wraped up to 4 divisible. The option "auto" is equivalent to "auto:1.1". - - .. _`model/descriptor[se_e3]/rcut`: - - rcut: - | type: ``float``, optional, default: ``6.0`` - | argument path: ``model/descriptor[se_e3]/rcut`` - - The cut-off radius. - - .. _`model/descriptor[se_e3]/rcut_smth`: - - rcut_smth: - | type: ``float``, optional, default: ``0.5`` - | argument path: ``model/descriptor[se_e3]/rcut_smth`` - - Where to start smoothing. For example the 1/r term is smoothed from `rcut` to `rcut_smth` - - .. _`model/descriptor[se_e3]/neuron`: - - neuron: - | type: ``list``, optional, default: ``[10, 20, 40]`` - | argument path: ``model/descriptor[se_e3]/neuron`` - - Number of neurons in each hidden layers of the embedding net. When two layers are of the same size or one layer is twice as large as the previous layer, a skip connection is built. - - .. _`model/descriptor[se_e3]/activation_function`: - - activation_function: - | type: ``str``, optional, default: ``tanh`` - | argument path: ``model/descriptor[se_e3]/activation_function`` - - The activation function in the embedding net. Supported activation functions are "relu", "relu6", "softplus", "sigmoid", "tanh", "gelu". - - .. _`model/descriptor[se_e3]/resnet_dt`: - - resnet_dt: - | type: ``bool``, optional, default: ``False`` - | argument path: ``model/descriptor[se_e3]/resnet_dt`` - - Whether to use a "Timestep" in the skip connection - - .. _`model/descriptor[se_e3]/precision`: - - precision: - | type: ``str``, optional, default: ``float64`` - | argument path: ``model/descriptor[se_e3]/precision`` - - The precision of the embedding net parameters, supported options are "default", "float16", "float32", "float64". - - .. _`model/descriptor[se_e3]/trainable`: - - trainable: - | type: ``bool``, optional, default: ``True`` - | argument path: ``model/descriptor[se_e3]/trainable`` - - If the parameters in the embedding net are trainable - - .. _`model/descriptor[se_e3]/seed`: - - seed: - | type: ``int`` | ``NoneType``, optional - | argument path: ``model/descriptor[se_e3]/seed`` - - Random seed for parameter initialization - - .. _`model/descriptor[se_e3]/set_davg_zero`: - - set_davg_zero: - | type: ``bool``, optional, default: ``False`` - | argument path: ``model/descriptor[se_e3]/set_davg_zero`` - - Set the normalization average to zero. This option should be set when `atom_ener` in the energy fitting is used - - - .. _`model/descriptor[se_a_tpe]`: - - When |flag:model/descriptor/type|_ is set to ``se_a_tpe`` (or its alias ``se_a_ebd``): - - .. _`model/descriptor[se_a_tpe]/sel`: - - sel: - | type: ``list`` | ``str``, optional, default: ``auto`` - | argument path: ``model/descriptor[se_a_tpe]/sel`` - - This parameter set the number of selected neighbors for each type of atom. It can be: - - - `List[int]`. The length of the list should be the same as the number of atom types in the system. `sel[i]` gives the selected number of type-i neighbors. `sel[i]` is recommended to be larger than the maximally possible number of type-i neighbors in the cut-off radius. It is noted that the total sel value must be less than 4096 in a GPU environment. - - - `str`. Can be "auto:factor" or "auto". "factor" is a float number larger than 1. This option will automatically determine the `sel`. In detail it counts the maximal number of neighbors with in the cutoff radius for each type of neighbor, then multiply the maximum by the "factor". Finally the number is wraped up to 4 divisible. The option "auto" is equivalent to "auto:1.1". - - .. _`model/descriptor[se_a_tpe]/rcut`: - - rcut: - | type: ``float``, optional, default: ``6.0`` - | argument path: ``model/descriptor[se_a_tpe]/rcut`` - - The cut-off radius. - - .. _`model/descriptor[se_a_tpe]/rcut_smth`: - - rcut_smth: - | type: ``float``, optional, default: ``0.5`` - | argument path: ``model/descriptor[se_a_tpe]/rcut_smth`` - - Where to start smoothing. For example the 1/r term is smoothed from `rcut` to `rcut_smth` - - .. _`model/descriptor[se_a_tpe]/neuron`: - - neuron: - | type: ``list``, optional, default: ``[10, 20, 40]`` - | argument path: ``model/descriptor[se_a_tpe]/neuron`` - - Number of neurons in each hidden layers of the embedding net. When two layers are of the same size or one layer is twice as large as the previous layer, a skip connection is built. - - .. _`model/descriptor[se_a_tpe]/axis_neuron`: - - axis_neuron: - | type: ``int``, optional, default: ``4``, alias: *n_axis_neuron* - | argument path: ``model/descriptor[se_a_tpe]/axis_neuron`` - - Size of the submatrix of G (embedding matrix). - - .. _`model/descriptor[se_a_tpe]/activation_function`: - - activation_function: - | type: ``str``, optional, default: ``tanh`` - | argument path: ``model/descriptor[se_a_tpe]/activation_function`` - - The activation function in the embedding net. Supported activation functions are "relu", "relu6", "softplus", "sigmoid", "tanh", "gelu". - - .. _`model/descriptor[se_a_tpe]/resnet_dt`: - - resnet_dt: - | type: ``bool``, optional, default: ``False`` - | argument path: ``model/descriptor[se_a_tpe]/resnet_dt`` - - Whether to use a "Timestep" in the skip connection - - .. _`model/descriptor[se_a_tpe]/type_one_side`: - - type_one_side: - | type: ``bool``, optional, default: ``False`` - | argument path: ``model/descriptor[se_a_tpe]/type_one_side`` - - Try to build N_types embedding nets. Otherwise, building N_types^2 embedding nets - - .. _`model/descriptor[se_a_tpe]/precision`: - - precision: - | type: ``str``, optional, default: ``float64`` - | argument path: ``model/descriptor[se_a_tpe]/precision`` - - The precision of the embedding net parameters, supported options are "default", "float16", "float32", "float64". - - .. _`model/descriptor[se_a_tpe]/trainable`: - - trainable: - | type: ``bool``, optional, default: ``True`` - | argument path: ``model/descriptor[se_a_tpe]/trainable`` - - If the parameters in the embedding net is trainable - - .. _`model/descriptor[se_a_tpe]/seed`: - - seed: - | type: ``int`` | ``NoneType``, optional - | argument path: ``model/descriptor[se_a_tpe]/seed`` - - Random seed for parameter initialization - - .. _`model/descriptor[se_a_tpe]/exclude_types`: - - exclude_types: - | type: ``list``, optional, default: ``[]`` - | argument path: ``model/descriptor[se_a_tpe]/exclude_types`` - - The excluded pairs of types which have no interaction with each other. For example, `[[0, 1]]` means no interaction between type 0 and type 1. - - .. _`model/descriptor[se_a_tpe]/set_davg_zero`: - - set_davg_zero: - | type: ``bool``, optional, default: ``False`` - | argument path: ``model/descriptor[se_a_tpe]/set_davg_zero`` - - Set the normalization average to zero. This option should be set when `atom_ener` in the energy fitting is used - - .. _`model/descriptor[se_a_tpe]/type_nchanl`: - - type_nchanl: - | type: ``int``, optional, default: ``4`` - | argument path: ``model/descriptor[se_a_tpe]/type_nchanl`` - - number of channels for type embedding - - .. _`model/descriptor[se_a_tpe]/type_nlayer`: - - type_nlayer: - | type: ``int``, optional, default: ``2`` - | argument path: ``model/descriptor[se_a_tpe]/type_nlayer`` - - number of hidden layers of type embedding net - - .. _`model/descriptor[se_a_tpe]/numb_aparam`: - - numb_aparam: - | type: ``int``, optional, default: ``0`` - | argument path: ``model/descriptor[se_a_tpe]/numb_aparam`` - - dimension of atomic parameter. if set to a value > 0, the atomic parameters are embedded. - - - .. _`model/descriptor[hybrid]`: - - When |flag:model/descriptor/type|_ is set to ``hybrid``: - - .. _`model/descriptor[hybrid]/list`: - - list: - | type: ``list`` - | argument path: ``model/descriptor[hybrid]/list`` - - A list of descriptor definitions - - .. _`model/fitting_net`: - - fitting_net: - | type: ``dict`` - | argument path: ``model/fitting_net`` - - The fitting of physical properties. - - - Depending on the value of *type*, different sub args are accepted. - - .. _`model/fitting_net/type`: - - type: - | type: ``str`` (flag key), default: ``ener`` - | argument path: ``model/fitting_net/type`` - | possible choices: |code:model/fitting_net[ener]|_, |code:model/fitting_net[dipole]|_, |code:model/fitting_net[polar]|_ - - The type of the fitting. See explanation below. - - - `ener`: Fit an energy model (potential energy surface). - - - `dipole`: Fit an atomic dipole model. Global dipole labels or atomic dipole labels for all the selected atoms (see `sel_type`) should be provided by `dipole.npy` in each data system. The file either has number of frames lines and 3 times of number of selected atoms columns, or has number of frames lines and 3 columns. See `loss` parameter. - - - `polar`: Fit an atomic polarizability model. Global polarizazbility labels or atomic polarizability labels for all the selected atoms (see `sel_type`) should be provided by `polarizability.npy` in each data system. The file eith has number of frames lines and 9 times of number of selected atoms columns, or has number of frames lines and 9 columns. See `loss` parameter. - - - - .. |code:model/fitting_net[ener]| replace:: ``ener`` - .. _`code:model/fitting_net[ener]`: `model/fitting_net[ener]`_ - .. |code:model/fitting_net[dipole]| replace:: ``dipole`` - .. _`code:model/fitting_net[dipole]`: `model/fitting_net[dipole]`_ - .. |code:model/fitting_net[polar]| replace:: ``polar`` - .. _`code:model/fitting_net[polar]`: `model/fitting_net[polar]`_ - - .. |flag:model/fitting_net/type| replace:: *type* - .. _`flag:model/fitting_net/type`: `model/fitting_net/type`_ - - - .. _`model/fitting_net[ener]`: - - When |flag:model/fitting_net/type|_ is set to ``ener``: - - .. _`model/fitting_net[ener]/numb_fparam`: - - numb_fparam: - | type: ``int``, optional, default: ``0`` - | argument path: ``model/fitting_net[ener]/numb_fparam`` - - The dimension of the frame parameter. If set to >0, file `fparam.npy` should be included to provided the input fparams. - - .. _`model/fitting_net[ener]/numb_aparam`: - - numb_aparam: - | type: ``int``, optional, default: ``0`` - | argument path: ``model/fitting_net[ener]/numb_aparam`` - - The dimension of the atomic parameter. If set to >0, file `aparam.npy` should be included to provided the input aparams. - - .. _`model/fitting_net[ener]/neuron`: - - neuron: - | type: ``list``, optional, default: ``[120, 120, 120]``, alias: *n_neuron* - | argument path: ``model/fitting_net[ener]/neuron`` - - The number of neurons in each hidden layers of the fitting net. When two hidden layers are of the same size, a skip connection is built. - - .. _`model/fitting_net[ener]/activation_function`: - - activation_function: - | type: ``str``, optional, default: ``tanh`` - | argument path: ``model/fitting_net[ener]/activation_function`` - - The activation function in the fitting net. Supported activation functions are "relu", "relu6", "softplus", "sigmoid", "tanh", "gelu". - - .. _`model/fitting_net[ener]/precision`: - - precision: - | type: ``str``, optional, default: ``float64`` - | argument path: ``model/fitting_net[ener]/precision`` - - The precision of the fitting net parameters, supported options are "default", "float16", "float32", "float64". - - .. _`model/fitting_net[ener]/resnet_dt`: - - resnet_dt: - | type: ``bool``, optional, default: ``True`` - | argument path: ``model/fitting_net[ener]/resnet_dt`` - - Whether to use a "Timestep" in the skip connection - - .. _`model/fitting_net[ener]/trainable`: - - trainable: - | type: ``list`` | ``bool``, optional, default: ``True`` - | argument path: ``model/fitting_net[ener]/trainable`` - - Whether the parameters in the fitting net are trainable. This option can be - - - bool: True if all parameters of the fitting net are trainable, False otherwise. - - - list of bool: Specifies if each layer is trainable. Since the fitting net is composed by hidden layers followed by a output layer, the length of tihs list should be equal to len(`neuron`)+1. - - .. _`model/fitting_net[ener]/rcond`: - - rcond: - | type: ``float``, optional, default: ``0.001`` - | argument path: ``model/fitting_net[ener]/rcond`` - - The condition number used to determine the inital energy shift for each type of atoms. - - .. _`model/fitting_net[ener]/seed`: - - seed: - | type: ``int`` | ``NoneType``, optional - | argument path: ``model/fitting_net[ener]/seed`` - - Random seed for parameter initialization of the fitting net - - .. _`model/fitting_net[ener]/atom_ener`: - - atom_ener: - | type: ``list``, optional, default: ``[]`` - | argument path: ``model/fitting_net[ener]/atom_ener`` - - Specify the atomic energy in vacuum for each type - - - .. _`model/fitting_net[dipole]`: - - When |flag:model/fitting_net/type|_ is set to ``dipole``: - - .. _`model/fitting_net[dipole]/neuron`: - - neuron: - | type: ``list``, optional, default: ``[120, 120, 120]``, alias: *n_neuron* - | argument path: ``model/fitting_net[dipole]/neuron`` - - The number of neurons in each hidden layers of the fitting net. When two hidden layers are of the same size, a skip connection is built. - - .. _`model/fitting_net[dipole]/activation_function`: - - activation_function: - | type: ``str``, optional, default: ``tanh`` - | argument path: ``model/fitting_net[dipole]/activation_function`` - - The activation function in the fitting net. Supported activation functions are "relu", "relu6", "softplus", "sigmoid", "tanh", "gelu". - - .. _`model/fitting_net[dipole]/resnet_dt`: - - resnet_dt: - | type: ``bool``, optional, default: ``True`` - | argument path: ``model/fitting_net[dipole]/resnet_dt`` - - Whether to use a "Timestep" in the skip connection - - .. _`model/fitting_net[dipole]/precision`: - - precision: - | type: ``str``, optional, default: ``float64`` - | argument path: ``model/fitting_net[dipole]/precision`` - - The precision of the fitting net parameters, supported options are "default", "float16", "float32", "float64". - - .. _`model/fitting_net[dipole]/sel_type`: - - sel_type: - | type: ``list`` | ``int`` | ``NoneType``, optional, alias: *dipole_type* - | argument path: ``model/fitting_net[dipole]/sel_type`` - - The atom types for which the atomic dipole will be provided. If not set, all types will be selected. - - .. _`model/fitting_net[dipole]/seed`: - - seed: - | type: ``int`` | ``NoneType``, optional - | argument path: ``model/fitting_net[dipole]/seed`` - - Random seed for parameter initialization of the fitting net - - - .. _`model/fitting_net[polar]`: - - When |flag:model/fitting_net/type|_ is set to ``polar``: - - .. _`model/fitting_net[polar]/neuron`: - - neuron: - | type: ``list``, optional, default: ``[120, 120, 120]``, alias: *n_neuron* - | argument path: ``model/fitting_net[polar]/neuron`` - - The number of neurons in each hidden layers of the fitting net. When two hidden layers are of the same size, a skip connection is built. - - .. _`model/fitting_net[polar]/activation_function`: - - activation_function: - | type: ``str``, optional, default: ``tanh`` - | argument path: ``model/fitting_net[polar]/activation_function`` - - The activation function in the fitting net. Supported activation functions are "relu", "relu6", "softplus", "sigmoid", "tanh", "gelu". - - .. _`model/fitting_net[polar]/resnet_dt`: - - resnet_dt: - | type: ``bool``, optional, default: ``True`` - | argument path: ``model/fitting_net[polar]/resnet_dt`` - - Whether to use a "Timestep" in the skip connection - - .. _`model/fitting_net[polar]/precision`: - - precision: - | type: ``str``, optional, default: ``float64`` - | argument path: ``model/fitting_net[polar]/precision`` - - The precision of the fitting net parameters, supported options are "default", "float16", "float32", "float64". - - .. _`model/fitting_net[polar]/fit_diag`: - - fit_diag: - | type: ``bool``, optional, default: ``True`` - | argument path: ``model/fitting_net[polar]/fit_diag`` - - Fit the diagonal part of the rotational invariant polarizability matrix, which will be converted to normal polarizability matrix by contracting with the rotation matrix. - - .. _`model/fitting_net[polar]/scale`: - - scale: - | type: ``float`` | ``list``, optional, default: ``1.0`` - | argument path: ``model/fitting_net[polar]/scale`` - - The output of the fitting net (polarizability matrix) will be scaled by ``scale`` - - .. _`model/fitting_net[polar]/shift_diag`: - - shift_diag: - | type: ``bool``, optional, default: ``True`` - | argument path: ``model/fitting_net[polar]/shift_diag`` - - Whether to shift the diagonal of polar, which is beneficial to training. Default is true. - - .. _`model/fitting_net[polar]/sel_type`: - - sel_type: - | type: ``list`` | ``int`` | ``NoneType``, optional, alias: *pol_type* - | argument path: ``model/fitting_net[polar]/sel_type`` - - The atom types for which the atomic polarizability will be provided. If not set, all types will be selected. - - .. _`model/fitting_net[polar]/seed`: - - seed: - | type: ``int`` | ``NoneType``, optional - | argument path: ``model/fitting_net[polar]/seed`` - - Random seed for parameter initialization of the fitting net - - .. _`model/modifier`: - - modifier: - | type: ``dict``, optional - | argument path: ``model/modifier`` - - The modifier of model output. - - - Depending on the value of *type*, different sub args are accepted. - - .. _`model/modifier/type`: - - type: - | type: ``str`` (flag key) - | argument path: ``model/modifier/type`` - | possible choices: |code:model/modifier[dipole_charge]|_ - - The type of modifier. See explanation below. - - -`dipole_charge`: Use WFCC to model the electronic structure of the system. Correct the long-range interaction - - .. |code:model/modifier[dipole_charge]| replace:: ``dipole_charge`` - .. _`code:model/modifier[dipole_charge]`: `model/modifier[dipole_charge]`_ - - .. |flag:model/modifier/type| replace:: *type* - .. _`flag:model/modifier/type`: `model/modifier/type`_ - - - .. _`model/modifier[dipole_charge]`: - - When |flag:model/modifier/type|_ is set to ``dipole_charge``: - - .. _`model/modifier[dipole_charge]/model_name`: - - model_name: - | type: ``str`` - | argument path: ``model/modifier[dipole_charge]/model_name`` - - The name of the frozen dipole model file. - - .. _`model/modifier[dipole_charge]/model_charge_map`: - - model_charge_map: - | type: ``list`` - | argument path: ``model/modifier[dipole_charge]/model_charge_map`` - - The charge of the WFCC. The list length should be the same as the `sel_type `_. - - .. _`model/modifier[dipole_charge]/sys_charge_map`: - - sys_charge_map: - | type: ``list`` - | argument path: ``model/modifier[dipole_charge]/sys_charge_map`` - - The charge of real atoms. The list length should be the same as the `type_map `_ - - .. _`model/modifier[dipole_charge]/ewald_beta`: - - ewald_beta: - | type: ``float``, optional, default: ``0.4`` - | argument path: ``model/modifier[dipole_charge]/ewald_beta`` - - The splitting parameter of Ewald sum. Unit is A^-1 - - .. _`model/modifier[dipole_charge]/ewald_h`: - - ewald_h: - | type: ``float``, optional, default: ``1.0`` - | argument path: ``model/modifier[dipole_charge]/ewald_h`` - - The grid spacing of the FFT grid. Unit is A - - .. _`model/compress`: - - compress: - | type: ``dict``, optional - | argument path: ``model/compress`` - - Model compression configurations - - - Depending on the value of *type*, different sub args are accepted. - - .. _`model/compress/type`: - - type: - | type: ``str`` (flag key), default: ``se_e2_a`` - | argument path: ``model/compress/type`` - | possible choices: |code:model/compress[se_e2_a]|_ - - The type of model compression, which should be consistent with the descriptor type. - - .. |code:model/compress[se_e2_a]| replace:: ``se_e2_a`` - .. _`code:model/compress[se_e2_a]`: `model/compress[se_e2_a]`_ - - .. |flag:model/compress/type| replace:: *type* - .. _`flag:model/compress/type`: `model/compress/type`_ - - - .. _`model/compress[se_e2_a]`: - - When |flag:model/compress/type|_ is set to ``se_e2_a`` (or its alias ``se_a``): - - .. _`model/compress[se_e2_a]/compress`: - - compress: - | type: ``bool`` - | argument path: ``model/compress[se_e2_a]/compress`` - - The name of the frozen model file. - - .. _`model/compress[se_e2_a]/model_file`: - - model_file: - | type: ``str`` - | argument path: ``model/compress[se_e2_a]/model_file`` - - The input model file, which will be compressed by the DeePMD-kit. - - .. _`model/compress[se_e2_a]/table_config`: - - table_config: - | type: ``list`` - | argument path: ``model/compress[se_e2_a]/table_config`` - - The arguments of model compression, including extrapolate(scale of model extrapolation), stride(uniform stride of tabulation's first and second table), and frequency(frequency of tabulation overflow check). - - .. _`model/compress[se_e2_a]/min_nbor_dist`: - - min_nbor_dist: - | type: ``float`` - | argument path: ``model/compress[se_e2_a]/min_nbor_dist`` - - The nearest distance between neighbor atoms saved in the frozen model. - - -.. _`loss`: - -loss: - | type: ``dict``, optional - | argument path: ``loss`` - - The definition of loss function. The loss type should be set to `tensor`, `ener` or left unset. - \. - - - Depending on the value of *type*, different sub args are accepted. - - .. _`loss/type`: - - type: - | type: ``str`` (flag key), default: ``ener`` - | argument path: ``loss/type`` - | possible choices: |code:loss[ener]|_, |code:loss[tensor]|_ - - The type of the loss. When the fitting type is `ener`, the loss type should be set to `ener` or left unset. When the fitting type is `dipole` or `polar`, the loss type should be set to `tensor`. - \. - - .. |code:loss[ener]| replace:: ``ener`` - .. _`code:loss[ener]`: `loss[ener]`_ - .. |code:loss[tensor]| replace:: ``tensor`` - .. _`code:loss[tensor]`: `loss[tensor]`_ - - .. |flag:loss/type| replace:: *type* - .. _`flag:loss/type`: `loss/type`_ - - - .. _`loss[ener]`: - - When |flag:loss/type|_ is set to ``ener``: - - .. _`loss[ener]/start_pref_e`: - - start_pref_e: - | type: ``float`` | ``int``, optional, default: ``0.02`` - | argument path: ``loss[ener]/start_pref_e`` - - The prefactor of energy loss at the start of the training. Should be larger than or equal to 0. If set to none-zero value, the energy label should be provided by file energy.npy in each data system. If both start_pref_energy and limit_pref_energy are set to 0, then the energy will be ignored. - - .. _`loss[ener]/limit_pref_e`: - - limit_pref_e: - | type: ``float`` | ``int``, optional, default: ``1.0`` - | argument path: ``loss[ener]/limit_pref_e`` - - The prefactor of energy loss at the limit of the training, Should be larger than or equal to 0. i.e. the training step goes to infinity. - - .. _`loss[ener]/start_pref_f`: - - start_pref_f: - | type: ``float`` | ``int``, optional, default: ``1000`` - | argument path: ``loss[ener]/start_pref_f`` - - The prefactor of force loss at the start of the training. Should be larger than or equal to 0. If set to none-zero value, the force label should be provided by file force.npy in each data system. If both start_pref_force and limit_pref_force are set to 0, then the force will be ignored. - - .. _`loss[ener]/limit_pref_f`: - - limit_pref_f: - | type: ``float`` | ``int``, optional, default: ``1.0`` - | argument path: ``loss[ener]/limit_pref_f`` - - The prefactor of force loss at the limit of the training, Should be larger than or equal to 0. i.e. the training step goes to infinity. - - .. _`loss[ener]/start_pref_v`: - - start_pref_v: - | type: ``float`` | ``int``, optional, default: ``0.0`` - | argument path: ``loss[ener]/start_pref_v`` - - The prefactor of virial loss at the start of the training. Should be larger than or equal to 0. If set to none-zero value, the virial label should be provided by file virial.npy in each data system. If both start_pref_virial and limit_pref_virial are set to 0, then the virial will be ignored. - - .. _`loss[ener]/limit_pref_v`: - - limit_pref_v: - | type: ``float`` | ``int``, optional, default: ``0.0`` - | argument path: ``loss[ener]/limit_pref_v`` - - The prefactor of virial loss at the limit of the training, Should be larger than or equal to 0. i.e. the training step goes to infinity. - - .. _`loss[ener]/start_pref_ae`: - - start_pref_ae: - | type: ``float`` | ``int``, optional, default: ``0.0`` - | argument path: ``loss[ener]/start_pref_ae`` - - The prefactor of atom_ener loss at the start of the training. Should be larger than or equal to 0. If set to none-zero value, the atom_ener label should be provided by file atom_ener.npy in each data system. If both start_pref_atom_ener and limit_pref_atom_ener are set to 0, then the atom_ener will be ignored. - - .. _`loss[ener]/limit_pref_ae`: - - limit_pref_ae: - | type: ``float`` | ``int``, optional, default: ``0.0`` - | argument path: ``loss[ener]/limit_pref_ae`` - - The prefactor of atom_ener loss at the limit of the training, Should be larger than or equal to 0. i.e. the training step goes to infinity. - - .. _`loss[ener]/relative_f`: - - relative_f: - | type: ``float`` | ``NoneType``, optional - | argument path: ``loss[ener]/relative_f`` - - If provided, relative force error will be used in the loss. The difference of force will be normalized by the magnitude of the force in the label with a shift given by `relative_f`, i.e. DF_i / ( || F || + relative_f ) with DF denoting the difference between prediction and label and || F || denoting the L2 norm of the label. - - - .. _`loss[tensor]`: - - When |flag:loss/type|_ is set to ``tensor``: - - .. _`loss[tensor]/pref`: - - pref: - | type: ``float`` | ``int`` - | argument path: ``loss[tensor]/pref`` - - The prefactor of the weight of global loss. It should be larger than or equal to 0. If controls the weight of loss corresponding to global label, i.e. 'polarizability.npy` or `dipole.npy`, whose shape should be #frames x [9 or 3]. If it's larger than 0.0, this npy should be included. - - .. _`loss[tensor]/pref_atomic`: - - pref_atomic: - | type: ``float`` | ``int`` - | argument path: ``loss[tensor]/pref_atomic`` - - The prefactor of the weight of atomic loss. It should be larger than or equal to 0. If controls the weight of loss corresponding to atomic label, i.e. `atomic_polarizability.npy` or `atomic_dipole.npy`, whose shape should be #frames x ([9 or 3] x #selected atoms). If it's larger than 0.0, this npy should be included. Both `pref` and `pref_atomic` should be provided, and either can be set to 0.0. - - -.. _`learning_rate`: - -learning_rate: - | type: ``dict`` - | argument path: ``learning_rate`` - - The definitio of learning rate - - - Depending on the value of *type*, different sub args are accepted. - - .. _`learning_rate/type`: - - type: - | type: ``str`` (flag key), default: ``exp`` - | argument path: ``learning_rate/type`` - | possible choices: |code:learning_rate[exp]|_ - - The type of the learning rate. - - .. |code:learning_rate[exp]| replace:: ``exp`` - .. _`code:learning_rate[exp]`: `learning_rate[exp]`_ - - .. |flag:learning_rate/type| replace:: *type* - .. _`flag:learning_rate/type`: `learning_rate/type`_ - - - .. _`learning_rate[exp]`: - - When |flag:learning_rate/type|_ is set to ``exp``: - - .. _`learning_rate[exp]/start_lr`: - - start_lr: - | type: ``float``, optional, default: ``0.001`` - | argument path: ``learning_rate[exp]/start_lr`` - - The learning rate the start of the training. - - .. _`learning_rate[exp]/stop_lr`: - - stop_lr: - | type: ``float``, optional, default: ``1e-08`` - | argument path: ``learning_rate[exp]/stop_lr`` - - The desired learning rate at the end of the training. - - .. _`learning_rate[exp]/decay_steps`: - - decay_steps: - | type: ``int``, optional, default: ``5000`` - | argument path: ``learning_rate[exp]/decay_steps`` - - The learning rate is decaying every this number of training steps. - - -.. _`training`: - -training: - | type: ``dict`` - | argument path: ``training`` - - The training options. - - .. _`training/training_data`: - - training_data: - | type: ``dict`` - | argument path: ``training/training_data`` - - Configurations of training data. - - .. _`training/training_data/systems`: - - systems: - | type: ``list`` | ``str`` - | argument path: ``training/training_data/systems`` - - The data systems for training. This key can be provided with a list that specifies the systems, or be provided with a string by which the prefix of all systems are given and the list of the systems is automatically generated. - - .. _`training/training_data/set_prefix`: - - set_prefix: - | type: ``str``, optional, default: ``set`` - | argument path: ``training/training_data/set_prefix`` - - The prefix of the sets in the `systems `_. - - .. _`training/training_data/batch_size`: - - batch_size: - | type: ``list`` | ``int`` | ``str``, optional, default: ``auto`` - | argument path: ``training/training_data/batch_size`` - - This key can be - - - list: the length of which is the same as the `systems `_. The batch size of each system is given by the elements of the list. - - - int: all `systems `_ use the same batch size. - - - string "auto": automatically determines the batch size so that the batch_size times the number of atoms in the system is no less than 32. - - - string "auto:N": automatically determines the batch size so that the batch_size times the number of atoms in the system is no less than N. - - .. _`training/training_data/auto_prob`: - - auto_prob: - | type: ``str``, optional, default: ``prob_sys_size``, alias: *auto_prob_style* - | argument path: ``training/training_data/auto_prob`` - - Determine the probability of systems automatically. The method is assigned by this key and can be - - - "prob_uniform" : the probability all the systems are equal, namely 1.0/self.get_nsystems() - - - "prob_sys_size" : the probability of a system is proportional to the number of batches in the system - - - "prob_sys_size;stt_idx:end_idx:weight;stt_idx:end_idx:weight;..." : the list of systems is devided into blocks. A block is specified by `stt_idx:end_idx:weight`, where `stt_idx` is the starting index of the system, `end_idx` is then ending (not including) index of the system, the probabilities of the systems in this block sums up to `weight`, and the relatively probabilities within this block is proportional to the number of batches in the system. - - .. _`training/training_data/sys_probs`: - - sys_probs: - | type: ``list`` | ``NoneType``, optional, default: ``None``, alias: *sys_weights* - | argument path: ``training/training_data/sys_probs`` - - A list of float if specified. Should be of the same length as `systems`, specifying the probability of each system. - - .. _`training/validation_data`: - - validation_data: - | type: ``dict`` | ``NoneType``, optional, default: ``None`` - | argument path: ``training/validation_data`` - - Configurations of validation data. Similar to that of training data, except that a `numb_btch` argument may be configured - - .. _`training/validation_data/systems`: - - systems: - | type: ``list`` | ``str`` - | argument path: ``training/validation_data/systems`` - - The data systems for validation. This key can be provided with a list that specifies the systems, or be provided with a string by which the prefix of all systems are given and the list of the systems is automatically generated. - - .. _`training/validation_data/set_prefix`: - - set_prefix: - | type: ``str``, optional, default: ``set`` - | argument path: ``training/validation_data/set_prefix`` - - The prefix of the sets in the `systems `_. - - .. _`training/validation_data/batch_size`: - - batch_size: - | type: ``list`` | ``int`` | ``str``, optional, default: ``auto`` - | argument path: ``training/validation_data/batch_size`` - - This key can be - - - list: the length of which is the same as the `systems `_. The batch size of each system is given by the elements of the list. - - - int: all `systems `_ use the same batch size. - - - string "auto": automatically determines the batch size so that the batch_size times the number of atoms in the system is no less than 32. - - - string "auto:N": automatically determines the batch size so that the batch_size times the number of atoms in the system is no less than N. - - .. _`training/validation_data/auto_prob`: - - auto_prob: - | type: ``str``, optional, default: ``prob_sys_size``, alias: *auto_prob_style* - | argument path: ``training/validation_data/auto_prob`` - - Determine the probability of systems automatically. The method is assigned by this key and can be - - - "prob_uniform" : the probability all the systems are equal, namely 1.0/self.get_nsystems() - - - "prob_sys_size" : the probability of a system is proportional to the number of batches in the system - - - "prob_sys_size;stt_idx:end_idx:weight;stt_idx:end_idx:weight;..." : the list of systems is devided into blocks. A block is specified by `stt_idx:end_idx:weight`, where `stt_idx` is the starting index of the system, `end_idx` is then ending (not including) index of the system, the probabilities of the systems in this block sums up to `weight`, and the relatively probabilities within this block is proportional to the number of batches in the system. - - .. _`training/validation_data/sys_probs`: - - sys_probs: - | type: ``list`` | ``NoneType``, optional, default: ``None``, alias: *sys_weights* - | argument path: ``training/validation_data/sys_probs`` - - A list of float if specified. Should be of the same length as `systems`, specifying the probability of each system. - - .. _`training/validation_data/numb_btch`: - - numb_btch: - | type: ``int``, optional, default: ``1``, alias: *numb_batch* - | argument path: ``training/validation_data/numb_btch`` - - An integer that specifies the number of systems to be sampled for each validation period. - - .. _`training/numb_steps`: - - numb_steps: - | type: ``int``, alias: *stop_batch* - | argument path: ``training/numb_steps`` - - Number of training batch. Each training uses one batch of data. - - .. _`training/seed`: - - seed: - | type: ``int`` | ``NoneType``, optional - | argument path: ``training/seed`` - - The random seed for getting frames from the training data set. - - .. _`training/disp_file`: - - disp_file: - | type: ``str``, optional, default: ``lcurve.out`` - | argument path: ``training/disp_file`` - - The file for printing learning curve. - - .. _`training/disp_freq`: - - disp_freq: - | type: ``int``, optional, default: ``1000`` - | argument path: ``training/disp_freq`` - - The frequency of printing learning curve. - - .. _`training/numb_test`: - - numb_test: - | type: ``list`` | ``int`` | ``str``, optional, default: ``1`` - | argument path: ``training/numb_test`` - - Number of frames used for the test during training. - - .. _`training/save_freq`: - - save_freq: - | type: ``int``, optional, default: ``1000`` - | argument path: ``training/save_freq`` - - The frequency of saving check point. - - .. _`training/save_ckpt`: - - save_ckpt: - | type: ``str``, optional, default: ``model.ckpt`` - | argument path: ``training/save_ckpt`` - - The file name of saving check point. - - .. _`training/disp_training`: - - disp_training: - | type: ``bool``, optional, default: ``True`` - | argument path: ``training/disp_training`` - - Displaying verbose information during training. - - .. _`training/time_training`: - - time_training: - | type: ``bool``, optional, default: ``True`` - | argument path: ``training/time_training`` - - Timing durining training. - - .. _`training/profiling`: - - profiling: - | type: ``bool``, optional, default: ``False`` - | argument path: ``training/profiling`` - - Profiling during training. - - .. _`training/profiling_file`: - - profiling_file: - | type: ``str``, optional, default: ``timeline.json`` - | argument path: ``training/profiling_file`` - - Output file for profiling. - - .. _`training/tensorboard`: - - tensorboard: - | type: ``bool``, optional, default: ``False`` - | argument path: ``training/tensorboard`` - - Enable tensorboard - - .. _`training/tensorboard_log_dir`: - - tensorboard_log_dir: - | type: ``str``, optional, default: ``log`` - | argument path: ``training/tensorboard_log_dir`` - - The log directory of tensorboard outputs - - .. _`training/tensorboard_freq`: - - tensorboard_freq: - | type: ``int``, optional, default: ``1`` - | argument path: ``training/tensorboard_freq`` - - The frequency of writing tensorboard events. diff --git a/doc/train/index.md b/doc/train/index.md deleted file mode 100644 index f37c1a55ce..0000000000 --- a/doc/train/index.md +++ /dev/null @@ -1,10 +0,0 @@ -# Training - -- [Training a model](training.md) -- [Advanced options](training-advanced.md) -- [Parallel training](parallel-training.md) -- [multi-task training](multi-task-training.md) -- [TensorBoard Usage](tensorboard.md) -- [Known limitations of using GPUs](gpu-limitations.md) -- [Training Parameters](../train-input-auto.rst) -- [Finetuning the Pretrained Model](finetuning.md) diff --git a/doc/troubleshooting/index.md b/doc/troubleshooting/index.md deleted file mode 100644 index a77d058811..0000000000 --- a/doc/troubleshooting/index.md +++ /dev/null @@ -1,15 +0,0 @@ -# FAQs - -As a consequence of differences in computers or systems, problems may occur. Some common circumstances are listed as follows. -In addition, some frequently asked questions are listed as follows. -If other unexpected problems occur, you’re welcome to contact us for help. - -- [Model compatibility](model-compatability.md) -- [Installation](installation.md) -- [The temperature undulates violently during the early stages of MD](md-energy-undulation.md) -- [MD: cannot run LAMMPS after installing a new version of DeePMD-kit](md-version-compatibility.md) -- [Do we need to set rcut < half boxsize?](howtoset-rcut.md) -- [How to set sel?](howtoset-sel.md) -- [How to control the parallelism of a job?](howtoset_num_nodes.md) -- [How to tune Fitting/embedding-net size?](howtoset_netsize.md) -- [Why does a model have low precision?](precision.md) From f4d7c7e35110c0063cea269b3d6c2596b42bd999 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Sat, 27 Jan 2024 11:40:50 +0800 Subject: [PATCH 014/686] Merge deepmd-pytorch into main repo (#3180) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge the deepmd-pytorch into main repo🎉 Add the following directories: - deepmd/pt : main implementations of deepmd-pytorch - source/tests/pt: UTs for deepmd-pytorch TODO list: - [x] examples added for water/se_e2_a, water/se_atten, water/dpa2 - [x] README updated (need modified) - [x] Paths in each files have been adapted. - [x] pyproject.toml needed to be merge --------- Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jinzhe Zeng --- .github/workflows/test_cuda.yml | 2 +- .github/workflows/test_python.yml | 13 +- .gitignore | 1 + README.md | 1 - backend/dynamic_metadata.py | 4 + deepmd/pt/__init__.py | 1 + deepmd/pt/entrypoints/__init__.py | 1 + deepmd/pt/entrypoints/main.py | 396 ++++ deepmd/pt/infer/__init__.py | 1 + deepmd/pt/infer/deep_eval.py | 412 ++++ deepmd/pt/infer/inference.py | 417 ++++ deepmd/pt/loss/__init__.py | 16 + deepmd/pt/loss/denoise.py | 109 + deepmd/pt/loss/ener.py | 155 ++ deepmd/pt/loss/loss.py | 12 + deepmd/pt/model/__init__.py | 1 + deepmd/pt/model/backbone/__init__.py | 12 + deepmd/pt/model/backbone/backbone.py | 12 + deepmd/pt/model/backbone/evoformer2b.py | 103 + deepmd/pt/model/descriptor/__init__.py | 46 + deepmd/pt/model/descriptor/descriptor.py | 272 +++ deepmd/pt/model/descriptor/dpa1.py | 152 ++ deepmd/pt/model/descriptor/dpa2.py | 375 ++++ deepmd/pt/model/descriptor/env_mat.py | 57 + deepmd/pt/model/descriptor/gaussian_lcc.py | 315 +++ deepmd/pt/model/descriptor/hybrid.py | 257 +++ deepmd/pt/model/descriptor/repformer_layer.py | 749 +++++++ deepmd/pt/model/descriptor/repformers.py | 348 +++ deepmd/pt/model/descriptor/se_a.py | 478 +++++ deepmd/pt/model/descriptor/se_atten.py | 392 ++++ deepmd/pt/model/model/__init__.py | 27 + deepmd/pt/model/model/atomic_model.py | 77 + deepmd/pt/model/model/dp_atomic_model.py | 214 ++ deepmd/pt/model/model/ener.py | 151 ++ deepmd/pt/model/model/make_model.py | 136 ++ deepmd/pt/model/model/model.py | 150 ++ deepmd/pt/model/model/transform_output.py | 214 ++ deepmd/pt/model/network/__init__.py | 1 + deepmd/pt/model/network/mlp.py | 217 ++ deepmd/pt/model/network/network.py | 1897 +++++++++++++++++ deepmd/pt/model/task/__init__.py | 34 + deepmd/pt/model/task/atten_lcc.py | 55 + deepmd/pt/model/task/denoise.py | 129 ++ deepmd/pt/model/task/dipole.py | 65 + deepmd/pt/model/task/ener.py | 241 +++ deepmd/pt/model/task/fitting.py | 223 ++ deepmd/pt/model/task/task.py | 12 + deepmd/pt/model/task/type_predict.py | 47 + deepmd/pt/optimizer/KFWrapper.py | 145 ++ deepmd/pt/optimizer/LKF.py | 221 ++ deepmd/pt/optimizer/__init__.py | 9 + deepmd/pt/train/__init__.py | 1 + deepmd/pt/train/training.py | 849 ++++++++ deepmd/pt/train/wrapper.py | 192 ++ deepmd/pt/utils/__init__.py | 1 + deepmd/pt/utils/ase_calc.py | 65 + deepmd/pt/utils/auto_batch_size.py | 26 + deepmd/pt/utils/cache.py | 31 + deepmd/pt/utils/dataloader.py | 319 +++ deepmd/pt/utils/dataset.py | 918 ++++++++ deepmd/pt/utils/dp_random.py | 14 + deepmd/pt/utils/env.py | 45 + deepmd/pt/utils/finetune.py | 98 + deepmd/pt/utils/learning_rate.py | 35 + deepmd/pt/utils/multi_task.py | 129 ++ deepmd/pt/utils/nlist.py | 431 ++++ deepmd/pt/utils/plugin.py | 15 + deepmd/pt/utils/preprocess.py | 318 +++ deepmd/pt/utils/region.py | 116 + deepmd/pt/utils/stat.py | 112 + deepmd/pt/utils/utils.py | 43 + examples/water/dpa2/input_torch.json | 102 + examples/water/se_atten/input_torch.json | 91 + examples/water/se_e2_a/input_torch.json | 79 + source/install/docker/Dockerfile | 2 +- source/tests/pt/__init__.py | 5 + source/tests/pt/models/dpa1.json | 39 + source/tests/pt/models/dpa1.pth | Bin 0 -> 15469 bytes source/tests/pt/models/dpa2.json | 48 + source/tests/pt/models/dpa2.pth | Bin 0 -> 179745 bytes source/tests/pt/models/dpa2_hyb.json | 69 + source/tests/pt/models/dpa2_tebd.pth | Bin 0 -> 1085 bytes source/tests/pt/requirements.txt | 6 + source/tests/pt/test_LKF.py | 35 + source/tests/pt/test_autodiff.py | 190 ++ source/tests/pt/test_calculator.py | 95 + source/tests/pt/test_deeppot.py | 81 + source/tests/pt/test_descriptor.py | 166 ++ source/tests/pt/test_descriptor_dpa1.py | 367 ++++ source/tests/pt/test_descriptor_dpa2.py | 264 +++ source/tests/pt/test_dp_test.py | 71 + source/tests/pt/test_embedding_net.py | 176 ++ source/tests/pt/test_env_mat.py | 84 + source/tests/pt/test_fitting_net.py | 139 ++ source/tests/pt/test_force_grad.py | 123 ++ source/tests/pt/test_jit.py | 140 ++ source/tests/pt/test_loss.py | 189 ++ source/tests/pt/test_lr.py | 59 + source/tests/pt/test_mlp.py | 321 +++ source/tests/pt/test_model.py | 415 ++++ source/tests/pt/test_nlist.py | 212 ++ source/tests/pt/test_permutation.py | 322 +++ source/tests/pt/test_permutation_denoise.py | 102 + source/tests/pt/test_region.py | 78 + source/tests/pt/test_rot.py | 181 ++ source/tests/pt/test_rot_denoise.py | 133 ++ source/tests/pt/test_rotation.py | 133 ++ source/tests/pt/test_sampler.py | 115 + source/tests/pt/test_saveload_dpa1.py | 151 ++ source/tests/pt/test_saveload_se_e2_a.py | 145 ++ source/tests/pt/test_se_e2_a.py | 199 ++ source/tests/pt/test_smooth.py | 230 ++ source/tests/pt/test_smooth_denoise.py | 151 ++ source/tests/pt/test_stat.py | 194 ++ source/tests/pt/test_training.py | 116 + source/tests/pt/test_trans.py | 137 ++ source/tests/pt/test_trans_denoise.py | 92 + source/tests/pt/test_unused_params.py | 98 + .../pt/water/data/data_0/set.000/box.npy | Bin 0 -> 3008 bytes .../pt/water/data/data_0/set.000/coord.npy | Bin 0 -> 184448 bytes .../pt/water/data/data_0/set.000/energy.npy | Bin 0 -> 448 bytes .../pt/water/data/data_0/set.000/force.npy | Bin 0 -> 184448 bytes source/tests/pt/water/data/data_0/type.raw | 192 ++ .../tests/pt/water/data/data_0/type_map.raw | 2 + .../pt/water/data/single/set.000/box.npy | Bin 0 -> 164 bytes .../pt/water/data/single/set.000/coord.npy | Bin 0 -> 2432 bytes .../pt/water/data/single/set.000/energy.npy | Bin 0 -> 132 bytes .../pt/water/data/single/set.000/force.npy | Bin 0 -> 2432 bytes source/tests/pt/water/data/single/type.raw | 192 ++ .../tests/pt/water/data/single/type_map.raw | 2 + source/tests/pt/water/lkf.json | 79 + source/tests/pt/water/se_atten.json | 84 + source/tests/pt/water/se_e2_a.json | 77 + source/tests/test_adjust_sel.py | 4 +- source/tests/test_finetune_se_atten.py | 150 +- source/tests/test_init_frz_model_multi.py | 43 +- source/tests/test_init_frz_model_se_a.py | 42 +- source/tests/test_init_frz_model_se_a_tebd.py | 43 +- source/tests/test_init_frz_model_se_a_type.py | 42 +- source/tests/test_init_frz_model_se_atten.py | 88 +- source/tests/test_init_frz_model_se_r.py | 43 +- source/tests/test_init_frz_model_spin.py | 43 +- ...odel_compression_se_a_ebd_type_one_side.py | 16 +- ...ession_se_a_type_one_side_exclude_types.py | 5 +- 144 files changed, 20162 insertions(+), 263 deletions(-) create mode 100644 deepmd/pt/__init__.py create mode 100644 deepmd/pt/entrypoints/__init__.py create mode 100644 deepmd/pt/entrypoints/main.py create mode 100644 deepmd/pt/infer/__init__.py create mode 100644 deepmd/pt/infer/deep_eval.py create mode 100644 deepmd/pt/infer/inference.py create mode 100644 deepmd/pt/loss/__init__.py create mode 100644 deepmd/pt/loss/denoise.py create mode 100644 deepmd/pt/loss/ener.py create mode 100644 deepmd/pt/loss/loss.py create mode 100644 deepmd/pt/model/__init__.py create mode 100644 deepmd/pt/model/backbone/__init__.py create mode 100644 deepmd/pt/model/backbone/backbone.py create mode 100644 deepmd/pt/model/backbone/evoformer2b.py create mode 100644 deepmd/pt/model/descriptor/__init__.py create mode 100644 deepmd/pt/model/descriptor/descriptor.py create mode 100644 deepmd/pt/model/descriptor/dpa1.py create mode 100644 deepmd/pt/model/descriptor/dpa2.py create mode 100644 deepmd/pt/model/descriptor/env_mat.py create mode 100644 deepmd/pt/model/descriptor/gaussian_lcc.py create mode 100644 deepmd/pt/model/descriptor/hybrid.py create mode 100644 deepmd/pt/model/descriptor/repformer_layer.py create mode 100644 deepmd/pt/model/descriptor/repformers.py create mode 100644 deepmd/pt/model/descriptor/se_a.py create mode 100644 deepmd/pt/model/descriptor/se_atten.py create mode 100644 deepmd/pt/model/model/__init__.py create mode 100644 deepmd/pt/model/model/atomic_model.py create mode 100644 deepmd/pt/model/model/dp_atomic_model.py create mode 100644 deepmd/pt/model/model/ener.py create mode 100644 deepmd/pt/model/model/make_model.py create mode 100644 deepmd/pt/model/model/model.py create mode 100644 deepmd/pt/model/model/transform_output.py create mode 100644 deepmd/pt/model/network/__init__.py create mode 100644 deepmd/pt/model/network/mlp.py create mode 100644 deepmd/pt/model/network/network.py create mode 100644 deepmd/pt/model/task/__init__.py create mode 100644 deepmd/pt/model/task/atten_lcc.py create mode 100644 deepmd/pt/model/task/denoise.py create mode 100644 deepmd/pt/model/task/dipole.py create mode 100644 deepmd/pt/model/task/ener.py create mode 100644 deepmd/pt/model/task/fitting.py create mode 100644 deepmd/pt/model/task/task.py create mode 100644 deepmd/pt/model/task/type_predict.py create mode 100644 deepmd/pt/optimizer/KFWrapper.py create mode 100644 deepmd/pt/optimizer/LKF.py create mode 100644 deepmd/pt/optimizer/__init__.py create mode 100644 deepmd/pt/train/__init__.py create mode 100644 deepmd/pt/train/training.py create mode 100644 deepmd/pt/train/wrapper.py create mode 100644 deepmd/pt/utils/__init__.py create mode 100644 deepmd/pt/utils/ase_calc.py create mode 100644 deepmd/pt/utils/auto_batch_size.py create mode 100644 deepmd/pt/utils/cache.py create mode 100644 deepmd/pt/utils/dataloader.py create mode 100644 deepmd/pt/utils/dataset.py create mode 100644 deepmd/pt/utils/dp_random.py create mode 100644 deepmd/pt/utils/env.py create mode 100644 deepmd/pt/utils/finetune.py create mode 100644 deepmd/pt/utils/learning_rate.py create mode 100644 deepmd/pt/utils/multi_task.py create mode 100644 deepmd/pt/utils/nlist.py create mode 100644 deepmd/pt/utils/plugin.py create mode 100644 deepmd/pt/utils/preprocess.py create mode 100644 deepmd/pt/utils/region.py create mode 100644 deepmd/pt/utils/stat.py create mode 100644 deepmd/pt/utils/utils.py create mode 100644 examples/water/dpa2/input_torch.json create mode 100644 examples/water/se_atten/input_torch.json create mode 100644 examples/water/se_e2_a/input_torch.json create mode 100644 source/tests/pt/__init__.py create mode 100644 source/tests/pt/models/dpa1.json create mode 100644 source/tests/pt/models/dpa1.pth create mode 100644 source/tests/pt/models/dpa2.json create mode 100644 source/tests/pt/models/dpa2.pth create mode 100644 source/tests/pt/models/dpa2_hyb.json create mode 100644 source/tests/pt/models/dpa2_tebd.pth create mode 100644 source/tests/pt/requirements.txt create mode 100644 source/tests/pt/test_LKF.py create mode 100644 source/tests/pt/test_autodiff.py create mode 100644 source/tests/pt/test_calculator.py create mode 100644 source/tests/pt/test_deeppot.py create mode 100644 source/tests/pt/test_descriptor.py create mode 100644 source/tests/pt/test_descriptor_dpa1.py create mode 100644 source/tests/pt/test_descriptor_dpa2.py create mode 100644 source/tests/pt/test_dp_test.py create mode 100644 source/tests/pt/test_embedding_net.py create mode 100644 source/tests/pt/test_env_mat.py create mode 100644 source/tests/pt/test_fitting_net.py create mode 100644 source/tests/pt/test_force_grad.py create mode 100644 source/tests/pt/test_jit.py create mode 100644 source/tests/pt/test_loss.py create mode 100644 source/tests/pt/test_lr.py create mode 100644 source/tests/pt/test_mlp.py create mode 100644 source/tests/pt/test_model.py create mode 100644 source/tests/pt/test_nlist.py create mode 100644 source/tests/pt/test_permutation.py create mode 100644 source/tests/pt/test_permutation_denoise.py create mode 100644 source/tests/pt/test_region.py create mode 100644 source/tests/pt/test_rot.py create mode 100644 source/tests/pt/test_rot_denoise.py create mode 100644 source/tests/pt/test_rotation.py create mode 100644 source/tests/pt/test_sampler.py create mode 100644 source/tests/pt/test_saveload_dpa1.py create mode 100644 source/tests/pt/test_saveload_se_e2_a.py create mode 100644 source/tests/pt/test_se_e2_a.py create mode 100644 source/tests/pt/test_smooth.py create mode 100644 source/tests/pt/test_smooth_denoise.py create mode 100644 source/tests/pt/test_stat.py create mode 100644 source/tests/pt/test_training.py create mode 100644 source/tests/pt/test_trans.py create mode 100644 source/tests/pt/test_trans_denoise.py create mode 100644 source/tests/pt/test_unused_params.py create mode 100644 source/tests/pt/water/data/data_0/set.000/box.npy create mode 100644 source/tests/pt/water/data/data_0/set.000/coord.npy create mode 100644 source/tests/pt/water/data/data_0/set.000/energy.npy create mode 100644 source/tests/pt/water/data/data_0/set.000/force.npy create mode 100644 source/tests/pt/water/data/data_0/type.raw create mode 100644 source/tests/pt/water/data/data_0/type_map.raw create mode 100644 source/tests/pt/water/data/single/set.000/box.npy create mode 100644 source/tests/pt/water/data/single/set.000/coord.npy create mode 100644 source/tests/pt/water/data/single/set.000/energy.npy create mode 100644 source/tests/pt/water/data/single/set.000/force.npy create mode 100644 source/tests/pt/water/data/single/type.raw create mode 100644 source/tests/pt/water/data/single/type_map.raw create mode 100644 source/tests/pt/water/lkf.json create mode 100644 source/tests/pt/water/se_atten.json create mode 100644 source/tests/pt/water/se_e2_a.json diff --git a/.github/workflows/test_cuda.yml b/.github/workflows/test_cuda.yml index 049fb95e3a..f164758304 100644 --- a/.github/workflows/test_cuda.yml +++ b/.github/workflows/test_cuda.yml @@ -37,7 +37,7 @@ jobs: run: python -m pip config --user set global.index-url https://mirrors.aliyun.com/pypi/simple/ - run: python -m pip install -U "pip>=21.3.1,!=23.0.0" - run: python -m pip install "tensorflow>=2.15.0rc0" - - run: python -m pip install -v -e .[gpu,test,lmp,cu12] "ase @ https://gitlab.com/ase/ase/-/archive/8c5aa5fd6448c5cfb517a014dccf2b214a9dfa8f/ase-8c5aa5fd6448c5cfb517a014dccf2b214a9dfa8f.tar.gz" + - run: python -m pip install -v -e .[gpu,test,lmp,cu12,torch] "ase @ https://gitlab.com/ase/ase/-/archive/8c5aa5fd6448c5cfb517a014dccf2b214a9dfa8f/ase-8c5aa5fd6448c5cfb517a014dccf2b214a9dfa8f.tar.gz" env: DP_BUILD_TESTING: 1 DP_VARIANT: cuda diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index 55ef041532..091a2a61f8 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -9,12 +9,12 @@ jobs: strategy: matrix: include: - - python: 3.7 - tf: 1.14 - python: 3.8 tf: + torch: - python: "3.11" tf: + torch: steps: - uses: actions/checkout@v4 @@ -23,22 +23,25 @@ jobs: python-version: ${{ matrix.python }} cache: 'pip' - uses: mpi4py/setup-mpi@v1 - if: ${{ matrix.tf == '' }} with: mpi: openmpi # https://github.com/pypa/pip/issues/11770 - run: python -m pip install -U "pip>=21.3.1,!=23.0.0" - - run: pip install -e .[cpu,test] + - run: python -m pip install -U "torch==${{ matrix.torch }}" "numpy<1.20" + if: matrix.torch != '' + - run: pip install -e .[cpu,test,torch] env: TENSORFLOW_VERSION: ${{ matrix.tf }} DP_BUILD_TESTING: 1 - run: pip install horovod mpi4py - if: ${{ matrix.tf == '' }} env: HOROVOD_WITH_TENSORFLOW: 1 + HOROVOD_WITHOUT_PYTORCH: 1 HOROVOD_WITHOUT_GLOO: 1 - run: dp --version - run: pytest --cov=deepmd source/tests --durations=0 + env: + NUM_WORKERS: 0 - uses: codecov/codecov-action@v3 with: gcov: true diff --git a/.gitignore b/.gitignore index 82d3e4a7da..5e30cf3167 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ *.bz2 *.pyc *.pb +*.DS_Store tmp* CMakeCache.txt CMakeFiles diff --git a/README.md b/README.md index e61c18dbcb..2076e11f1b 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,6 @@ The code is organized as follows: See [DeePMD-kit Contributing Guide](CONTRIBUTING.md) to become a contributor! 🤓 - [1]: https://arxiv.org/abs/1707.01478 [2]: https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.120.143001 [3]: https://arxiv.org/abs/1805.09003 diff --git a/backend/dynamic_metadata.py b/backend/dynamic_metadata.py index 72dfcaef45..e30c97bd98 100644 --- a/backend/dynamic_metadata.py +++ b/backend/dynamic_metadata.py @@ -88,4 +88,8 @@ def dynamic_metadata( "nvidia-cudnn-cu12", "nvidia-cuda-nvcc-cu12", ], + "torch": [ + "torch>=2a", + "tqdm", + ], } diff --git a/deepmd/pt/__init__.py b/deepmd/pt/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/deepmd/pt/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/deepmd/pt/entrypoints/__init__.py b/deepmd/pt/entrypoints/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/deepmd/pt/entrypoints/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py new file mode 100644 index 0000000000..f1cd7ae210 --- /dev/null +++ b/deepmd/pt/entrypoints/main.py @@ -0,0 +1,396 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import argparse +import json +import logging +import os + +import torch +import torch.distributed as dist +from torch.distributed.elastic.multiprocessing.errors import ( + record, +) + +from deepmd import ( + __version__, +) +from deepmd.pt.infer import ( + inference, +) +from deepmd.pt.model.descriptor import ( + Descriptor, +) +from deepmd.pt.train import ( + training, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.dataloader import ( + DpLoaderSet, +) +from deepmd.pt.utils.finetune import ( + change_finetune_model_params, +) +from deepmd.pt.utils.multi_task import ( + preprocess_shared_params, +) +from deepmd.pt.utils.stat import ( + make_stat_input, +) + + +def get_trainer( + config, + init_model=None, + restart_model=None, + finetune_model=None, + model_branch="", + force_load=False, +): + # Initialize DDP + local_rank = os.environ.get("LOCAL_RANK") + if local_rank is not None: + local_rank = int(local_rank) + assert dist.is_nccl_available() + dist.init_process_group(backend="nccl") + + multi_task = "model_dict" in config["model"] + ckpt = init_model if init_model is not None else restart_model + config["model"] = change_finetune_model_params( + ckpt, + finetune_model, + config["model"], + multi_task=multi_task, + model_branch=model_branch, + ) + config["model"]["resuming"] = (finetune_model is not None) or (ckpt is not None) + shared_links = None + if multi_task: + config["model"], shared_links = preprocess_shared_params(config["model"]) + + def prepare_trainer_input_single( + model_params_single, data_dict_single, loss_dict_single, suffix="" + ): + training_dataset_params = data_dict_single["training_data"] + type_split = False + if model_params_single["descriptor"]["type"] in ["se_e2_a"]: + type_split = True + validation_dataset_params = data_dict_single["validation_data"] + training_systems = training_dataset_params["systems"] + validation_systems = validation_dataset_params["systems"] + + # noise params + noise_settings = None + if loss_dict_single.get("type", "ener") == "denoise": + noise_settings = { + "noise_type": loss_dict_single.pop("noise_type", "uniform"), + "noise": loss_dict_single.pop("noise", 1.0), + "noise_mode": loss_dict_single.pop("noise_mode", "fix_num"), + "mask_num": loss_dict_single.pop("mask_num", 8), + "mask_prob": loss_dict_single.pop("mask_prob", 0.15), + "same_mask": loss_dict_single.pop("same_mask", False), + "mask_coord": loss_dict_single.pop("mask_coord", False), + "mask_type": loss_dict_single.pop("mask_type", False), + "max_fail_num": loss_dict_single.pop("max_fail_num", 10), + "mask_type_idx": len(model_params_single["type_map"]) - 1, + } + # noise_settings = None + + # stat files + hybrid_descrpt = model_params_single["descriptor"]["type"] == "hybrid" + has_stat_file_path = True + if not hybrid_descrpt: + ### this design requires "rcut", "rcut_smth" and "sel" in the descriptor + ### VERY BAD DESIGN!!!! + ### not all descriptors provides these parameter in their constructor + default_stat_file_name = Descriptor.get_stat_name( + model_params_single["descriptor"] + ) + model_params_single["stat_file_dir"] = data_dict_single.get( + "stat_file_dir", f"stat_files{suffix}" + ) + model_params_single["stat_file"] = data_dict_single.get( + "stat_file", default_stat_file_name + ) + model_params_single["stat_file_path"] = os.path.join( + model_params_single["stat_file_dir"], model_params_single["stat_file"] + ) + if not os.path.exists(model_params_single["stat_file_path"]): + has_stat_file_path = False + else: ### need to remove this + default_stat_file_name = [] + for descrpt in model_params_single["descriptor"]["list"]: + default_stat_file_name.append( + f'stat_file_rcut{descrpt["rcut"]:.2f}_' + f'smth{descrpt["rcut_smth"]:.2f}_' + f'sel{descrpt["sel"]}_{descrpt["type"]}.npz' + ) + model_params_single["stat_file_dir"] = data_dict_single.get( + "stat_file_dir", f"stat_files{suffix}" + ) + model_params_single["stat_file"] = data_dict_single.get( + "stat_file", default_stat_file_name + ) + assert isinstance( + model_params_single["stat_file"], list + ), "Stat file of hybrid descriptor must be a list!" + stat_file_path = [] + for stat_file_path_item in model_params_single["stat_file"]: + single_file_path = os.path.join( + model_params_single["stat_file_dir"], stat_file_path_item + ) + stat_file_path.append(single_file_path) + if not os.path.exists(single_file_path): + has_stat_file_path = False + model_params_single["stat_file_path"] = stat_file_path + + # validation and training data + validation_data_single = DpLoaderSet( + validation_systems, + validation_dataset_params["batch_size"], + model_params_single, + type_split=type_split, + noise_settings=noise_settings, + ) + if ckpt or finetune_model or has_stat_file_path: + train_data_single = DpLoaderSet( + training_systems, + training_dataset_params["batch_size"], + model_params_single, + type_split=type_split, + noise_settings=noise_settings, + ) + sampled_single = None + else: + train_data_single = DpLoaderSet( + training_systems, + training_dataset_params["batch_size"], + model_params_single, + type_split=type_split, + ) + data_stat_nbatch = model_params_single.get("data_stat_nbatch", 10) + sampled_single = make_stat_input( + train_data_single.systems, + train_data_single.dataloaders, + data_stat_nbatch, + ) + if noise_settings is not None: + train_data_single = DpLoaderSet( + training_systems, + training_dataset_params["batch_size"], + model_params_single, + type_split=type_split, + noise_settings=noise_settings, + ) + return train_data_single, validation_data_single, sampled_single + + if not multi_task: + train_data, validation_data, sampled = prepare_trainer_input_single( + config["model"], config["training"], config["loss"] + ) + else: + train_data, validation_data, sampled = {}, {}, {} + for model_key in config["model"]["model_dict"]: + ( + train_data[model_key], + validation_data[model_key], + sampled[model_key], + ) = prepare_trainer_input_single( + config["model"]["model_dict"][model_key], + config["training"]["data_dict"][model_key], + config["loss_dict"][model_key], + suffix=f"_{model_key}", + ) + + trainer = training.Trainer( + config, + train_data, + sampled, + validation_data=validation_data, + init_model=init_model, + restart_model=restart_model, + finetune_model=finetune_model, + force_load=force_load, + shared_links=shared_links, + ) + return trainer + + +def train(FLAGS): + logging.info("Configuration path: %s", FLAGS.INPUT) + with open(FLAGS.INPUT) as fin: + config = json.load(fin) + trainer = get_trainer( + config, + FLAGS.init_model, + FLAGS.restart, + FLAGS.finetune, + FLAGS.model_branch, + FLAGS.force_load, + ) + trainer.run() + + +def test(FLAGS): + trainer = inference.Tester( + FLAGS.model, + input_script=FLAGS.input_script, + system=FLAGS.system, + datafile=FLAGS.datafile, + numb_test=FLAGS.numb_test, + detail_file=FLAGS.detail_file, + shuffle_test=FLAGS.shuffle_test, + head=FLAGS.head, + ) + trainer.run() + + +def freeze(FLAGS): + model = torch.jit.script( + inference.Tester(FLAGS.model, numb_test=1, head=FLAGS.head).model + ) + torch.jit.save( + model, + FLAGS.output, + { + # TODO: _extra_files + }, + ) + + +# avoid logger conflicts of tf version +def clean_loggers(): + logger = logging.getLogger() + while logger.hasHandlers(): + logger.removeHandler(logger.handlers[0]) + + +@record +def main(args=None): + clean_loggers() + logging.basicConfig( + level=logging.WARNING if env.LOCAL_RANK else logging.INFO, + format=f"%(asctime)-15s {os.environ.get('RANK') or ''} [%(filename)s:%(lineno)d] %(levelname)s %(message)s", + ) + logging.info("DeepMD version: %s", __version__) + parser = argparse.ArgumentParser( + description="A tool to manager deep models of potential energy surface." + ) + subparsers = parser.add_subparsers(dest="command") + train_parser = subparsers.add_parser("train", help="Train a model.") + train_parser.add_argument("INPUT", help="A Json-format configuration file.") + parser_train_subgroup = train_parser.add_mutually_exclusive_group() + parser_train_subgroup.add_argument( + "-i", + "--init-model", + type=str, + default=None, + help="Initialize the model by the provided checkpoint.", + ) + parser_train_subgroup.add_argument( + "-r", + "--restart", + type=str, + default=None, + help="Restart the training from the provided checkpoint.", + ) + parser_train_subgroup.add_argument( + "-t", + "--finetune", + type=str, + default=None, + help="Finetune the frozen pretrained model.", + ) + train_parser.add_argument( + "-m", + "--model-branch", + type=str, + default="", + help="Model branch chosen for fine-tuning if multi-task. If not specified, it will re-init the fitting net.", + ) + train_parser.add_argument( + "--force-load", + action="store_true", + help="Force load from ckpt, other missing tensors will init from scratch", + ) + + test_parser = subparsers.add_parser("test", help="Test a model.") + test_parser_subgroup = test_parser.add_mutually_exclusive_group() + test_parser_subgroup.add_argument( + "-s", + "--system", + default=None, + type=str, + help="The system dir. Recursively detect systems in this directory", + ) + test_parser_subgroup.add_argument( + "-f", + "--datafile", + default=None, + type=str, + help="The path to file of test list.", + ) + test_parser_subgroup.add_argument( + "-i", + "--input-script", + default=None, + type=str, + help="The path to the input script, the validation systems will be tested.", + ) + test_parser.add_argument( + "-m", + "--model", + default="model.pt", + type=str, + help="Model checkpoint to import", + ) + test_parser.add_argument( + "--head", + default=None, + type=str, + help="Task head to test if in multi-task mode.", + ) + test_parser.add_argument( + "-n", "--numb-test", default=100, type=int, help="The number of data for test" + ) + test_parser.add_argument( + "-d", + "--detail-file", + type=str, + default=None, + help="The prefix to files where details of energy, force and virial accuracy/accuracy per atom will be written", + ) + test_parser.add_argument( + "--shuffle-test", action="store_true", default=False, help="Shuffle test data" + ) + + freeze_parser = subparsers.add_parser("freeze", help="Freeze a model.") + freeze_parser.add_argument("model", help="Resumes from checkpoint.") + freeze_parser.add_argument( + "-o", + "--output", + type=str, + default="frozen_model.pth", + help="The frozen model path", + ) + freeze_parser.add_argument( + "--head", + default=None, + type=str, + help="Task head to freeze if in multi-task mode.", + ) + + FLAGS = parser.parse_args(args) + if FLAGS.command == "train": + train(FLAGS) + elif FLAGS.command == "test": + test(FLAGS) + elif FLAGS.command == "freeze": + freeze(FLAGS) + else: + logging.error("Invalid command!") + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/deepmd/pt/infer/__init__.py b/deepmd/pt/infer/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/deepmd/pt/infer/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py new file mode 100644 index 0000000000..79772b47ae --- /dev/null +++ b/deepmd/pt/infer/deep_eval.py @@ -0,0 +1,412 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from pathlib import ( + Path, +) +from typing import ( + Callable, + List, + Optional, + Tuple, + Union, +) + +import numpy as np +import torch + +from deepmd.infer.deep_pot import DeepPot as DeepPotBase +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.train.wrapper import ( + ModelWrapper, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.auto_batch_size import ( + AutoBatchSize, +) +from deepmd.pt.utils.env import ( + DEVICE, + GLOBAL_PT_FLOAT_PRECISION, +) + + +class DeepEval: + def __init__( + self, + model_file: "Path", + auto_batch_size: Union[bool, int, AutoBatchSize] = True, + ): + self.model_path = model_file + state_dict = torch.load(model_file, map_location=env.DEVICE) + if "model" in state_dict: + state_dict = state_dict["model"] + self.input_param = state_dict["_extra_state"]["model_params"] + self.input_param["resuming"] = True + self.multi_task = "model_dict" in self.input_param + assert not self.multi_task, "multitask mode currently not supported!" + self.type_split = self.input_param["descriptor"]["type"] in ["se_e2_a"] + self.type_map = self.input_param["type_map"] + self.dp = ModelWrapper(get_model(self.input_param, None).to(DEVICE)) + self.dp.load_state_dict(state_dict) + self.rcut = self.dp.model["Default"].descriptor.get_rcut() + self.sec = np.cumsum(self.dp.model["Default"].descriptor.get_sel()) + if isinstance(auto_batch_size, bool): + if auto_batch_size: + self.auto_batch_size = AutoBatchSize() + else: + self.auto_batch_size = None + elif isinstance(auto_batch_size, int): + self.auto_batch_size = AutoBatchSize(auto_batch_size) + elif isinstance(auto_batch_size, AutoBatchSize): + self.auto_batch_size = auto_batch_size + else: + raise TypeError("auto_batch_size should be bool, int, or AutoBatchSize") + + def eval( + self, + coords: Union[np.ndarray, torch.Tensor], + cells: Optional[Union[np.ndarray, torch.Tensor]], + atom_types: Union[np.ndarray, torch.Tensor, List[int]], + atomic: bool = False, + ): + raise NotImplementedError + + +class DeepPot(DeepEval, DeepPotBase): + def __init__( + self, + model_file: "Path", + auto_batch_size: Union[bool, int, AutoBatchSize] = True, + neighbor_list=None, + ): + if neighbor_list is not None: + raise NotImplementedError + super().__init__( + model_file, + auto_batch_size=auto_batch_size, + ) + + def eval( + self, + coords: np.ndarray, + cells: np.ndarray, + atom_types: List[int], + atomic: bool = False, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + efield: Optional[np.ndarray] = None, + mixed_type: bool = False, + ): + if fparam is not None or aparam is not None or efield is not None: + raise NotImplementedError + # convert all of the input to numpy array + atom_types = np.array(atom_types, dtype=np.int32) + coords = np.array(coords) + if cells is not None: + cells = np.array(cells) + natoms, numb_test = self._get_natoms_and_nframes( + coords, atom_types, len(atom_types.shape) > 1 + ) + return self._eval_func(self._eval_model, numb_test, natoms)( + coords, cells, atom_types, atomic + ) + + def _eval_func(self, inner_func: Callable, numb_test: int, natoms: int) -> Callable: + """Wrapper method with auto batch size. + + Parameters + ---------- + inner_func : Callable + the method to be wrapped + numb_test : int + number of tests + natoms : int + number of atoms + + Returns + ------- + Callable + the wrapper + """ + if self.auto_batch_size is not None: + + def eval_func(*args, **kwargs): + return self.auto_batch_size.execute_all( + inner_func, numb_test, natoms, *args, **kwargs + ) + + else: + eval_func = inner_func + return eval_func + + def _get_natoms_and_nframes( + self, + coords: np.ndarray, + atom_types: Union[List[int], np.ndarray], + mixed_type: bool = False, + ) -> Tuple[int, int]: + if mixed_type: + natoms = len(atom_types[0]) + else: + natoms = len(atom_types) + if natoms == 0: + assert coords.size == 0 + else: + coords = np.reshape(np.array(coords), [-1, natoms * 3]) + nframes = coords.shape[0] + return natoms, nframes + + def _eval_model( + self, + coords: np.ndarray, + cells: Optional[np.ndarray], + atom_types: np.ndarray, + atomic: bool = False, + ): + model = self.dp.to(DEVICE) + energy_out = None + atomic_energy_out = None + force_out = None + virial_out = None + atomic_virial_out = None + + nframes = coords.shape[0] + if len(atom_types.shape) == 1: + natoms = len(atom_types) + atom_types = np.tile(atom_types, nframes).reshape(nframes, -1) + else: + natoms = len(atom_types[0]) + + coord_input = torch.tensor( + coords.reshape([-1, natoms, 3]), dtype=GLOBAL_PT_FLOAT_PRECISION + ).to(DEVICE) + type_input = torch.tensor(atom_types, dtype=torch.long).to(DEVICE) + if cells is not None: + box_input = torch.tensor( + cells.reshape([-1, 3, 3]), dtype=GLOBAL_PT_FLOAT_PRECISION + ).to(DEVICE) + else: + box_input = None + + batch_output = model( + coord_input, type_input, box=box_input, do_atomic_virial=atomic + ) + if isinstance(batch_output, tuple): + batch_output = batch_output[0] + energy_out = batch_output["energy"].detach().cpu().numpy() + if "atom_energy" in batch_output: + atomic_energy_out = batch_output["atom_energy"].detach().cpu().numpy() + force_out = batch_output["force"].detach().cpu().numpy() + virial_out = batch_output["virial"].detach().cpu().numpy() + if "atomic_virial" in batch_output: + atomic_virial_out = batch_output["atomic_virial"].detach().cpu().numpy() + + if not atomic: + return energy_out, force_out, virial_out + else: + return ( + energy_out, + force_out, + virial_out, + atomic_energy_out, + atomic_virial_out, + ) + + def get_ntypes(self) -> int: + """Get the number of atom types of this model.""" + return len(self.type_map) + + def get_type_map(self) -> List[str]: + """Get the type map (element name of the atom types) of this model.""" + return self.type_map + + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this DP.""" + return 0 + + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this DP.""" + return 0 + + +# For tests only +def eval_model( + model, + coords: Union[np.ndarray, torch.Tensor], + cells: Optional[Union[np.ndarray, torch.Tensor]], + atom_types: Union[np.ndarray, torch.Tensor, List[int]], + atomic: bool = False, + infer_batch_size: int = 2, + denoise: bool = False, +): + model = model.to(DEVICE) + energy_out = [] + atomic_energy_out = [] + force_out = [] + virial_out = [] + atomic_virial_out = [] + updated_coord_out = [] + logits_out = [] + err_msg = ( + f"All inputs should be the same format, " + f"but found {type(coords)}, {type(cells)}, {type(atom_types)} instead! " + ) + return_tensor = True + if isinstance(coords, torch.Tensor): + if cells is not None: + assert isinstance(cells, torch.Tensor), err_msg + assert isinstance(atom_types, torch.Tensor) or isinstance(atom_types, list) + atom_types = torch.tensor(atom_types, dtype=torch.long).to(DEVICE) + elif isinstance(coords, np.ndarray): + if cells is not None: + assert isinstance(cells, np.ndarray), err_msg + assert isinstance(atom_types, np.ndarray) or isinstance(atom_types, list) + atom_types = np.array(atom_types, dtype=np.int32) + return_tensor = False + + nframes = coords.shape[0] + if len(atom_types.shape) == 1: + natoms = len(atom_types) + if isinstance(atom_types, torch.Tensor): + atom_types = torch.tile(atom_types.unsqueeze(0), [nframes, 1]).reshape( + nframes, -1 + ) + else: + atom_types = np.tile(atom_types, nframes).reshape(nframes, -1) + else: + natoms = len(atom_types[0]) + + coord_input = torch.tensor( + coords.reshape([-1, natoms, 3]), dtype=GLOBAL_PT_FLOAT_PRECISION + ).to(DEVICE) + type_input = torch.tensor(atom_types, dtype=torch.long).to(DEVICE) + box_input = None + if cells is None: + pbc = False + else: + pbc = True + box_input = torch.tensor( + cells.reshape([-1, 3, 3]), dtype=GLOBAL_PT_FLOAT_PRECISION + ).to(DEVICE) + num_iter = int((nframes + infer_batch_size - 1) / infer_batch_size) + + for ii in range(num_iter): + batch_coord = coord_input[ii * infer_batch_size : (ii + 1) * infer_batch_size] + batch_atype = type_input[ii * infer_batch_size : (ii + 1) * infer_batch_size] + batch_box = None + if pbc: + batch_box = box_input[ii * infer_batch_size : (ii + 1) * infer_batch_size] + batch_output = model(batch_coord, batch_atype, box=batch_box) + if isinstance(batch_output, tuple): + batch_output = batch_output[0] + if not return_tensor: + if "energy" in batch_output: + energy_out.append(batch_output["energy"].detach().cpu().numpy()) + if "atom_energy" in batch_output: + atomic_energy_out.append( + batch_output["atom_energy"].detach().cpu().numpy() + ) + if "force" in batch_output: + force_out.append(batch_output["force"].detach().cpu().numpy()) + if "virial" in batch_output: + virial_out.append(batch_output["virial"].detach().cpu().numpy()) + if "atomic_virial" in batch_output: + atomic_virial_out.append( + batch_output["atomic_virial"].detach().cpu().numpy() + ) + if "updated_coord" in batch_output: + updated_coord_out.append( + batch_output["updated_coord"].detach().cpu().numpy() + ) + if "logits" in batch_output: + logits_out.append(batch_output["logits"].detach().cpu().numpy()) + else: + if "energy" in batch_output: + energy_out.append(batch_output["energy"]) + if "atom_energy" in batch_output: + atomic_energy_out.append(batch_output["atom_energy"]) + if "force" in batch_output: + force_out.append(batch_output["force"]) + if "virial" in batch_output: + virial_out.append(batch_output["virial"]) + if "atomic_virial" in batch_output: + atomic_virial_out.append(batch_output["atomic_virial"]) + if "updated_coord" in batch_output: + updated_coord_out.append(batch_output["updated_coord"]) + if "logits" in batch_output: + logits_out.append(batch_output["logits"]) + if not return_tensor: + energy_out = ( + np.concatenate(energy_out) if energy_out else np.zeros([nframes, 1]) + ) + atomic_energy_out = ( + np.concatenate(atomic_energy_out) + if atomic_energy_out + else np.zeros([nframes, natoms, 1]) + ) + force_out = ( + np.concatenate(force_out) if force_out else np.zeros([nframes, natoms, 3]) + ) + virial_out = ( + np.concatenate(virial_out) if virial_out else np.zeros([nframes, 3, 3]) + ) + atomic_virial_out = ( + np.concatenate(atomic_virial_out) + if atomic_virial_out + else np.zeros([nframes, natoms, 3, 3]) + ) + updated_coord_out = ( + np.concatenate(updated_coord_out) if updated_coord_out else None + ) + logits_out = np.concatenate(logits_out) if logits_out else None + else: + energy_out = ( + torch.cat(energy_out) + if energy_out + else torch.zeros([nframes, 1], dtype=GLOBAL_PT_FLOAT_PRECISION).to(DEVICE) + ) + atomic_energy_out = ( + torch.cat(atomic_energy_out) + if atomic_energy_out + else torch.zeros([nframes, natoms, 1], dtype=GLOBAL_PT_FLOAT_PRECISION).to( + DEVICE + ) + ) + force_out = ( + torch.cat(force_out) + if force_out + else torch.zeros([nframes, natoms, 3], dtype=GLOBAL_PT_FLOAT_PRECISION).to( + DEVICE + ) + ) + virial_out = ( + torch.cat(virial_out) + if virial_out + else torch.zeros([nframes, 3, 3], dtype=GLOBAL_PT_FLOAT_PRECISION).to( + DEVICE + ) + ) + atomic_virial_out = ( + torch.cat(atomic_virial_out) + if atomic_virial_out + else torch.zeros( + [nframes, natoms, 3, 3], dtype=GLOBAL_PT_FLOAT_PRECISION + ).to(DEVICE) + ) + updated_coord_out = torch.cat(updated_coord_out) if updated_coord_out else None + logits_out = torch.cat(logits_out) if logits_out else None + if denoise: + return updated_coord_out, logits_out + else: + if not atomic: + return energy_out, force_out, virial_out + else: + return ( + energy_out, + force_out, + virial_out, + atomic_energy_out, + atomic_virial_out, + ) diff --git a/deepmd/pt/infer/inference.py b/deepmd/pt/infer/inference.py new file mode 100644 index 0000000000..4906bb7a46 --- /dev/null +++ b/deepmd/pt/infer/inference.py @@ -0,0 +1,417 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import logging +import math +from copy import ( + deepcopy, +) +from pathlib import ( + Path, +) + +import numpy as np +import torch +from torch.utils.data import ( + DataLoader, + RandomSampler, +) + +from deepmd.common import ( + expand_sys_str, +) +from deepmd.pt.loss import ( + DenoiseLoss, + EnergyStdLoss, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.train.wrapper import ( + ModelWrapper, +) +from deepmd.pt.utils.dataloader import ( + DpLoaderSet, +) +from deepmd.pt.utils.env import ( + DEVICE, + JIT, + NUM_WORKERS, +) + +if torch.__version__.startswith("2"): + import torch._dynamo + + +class Tester: + def __init__( + self, + model_ckpt, + input_script=None, + system=None, + datafile=None, + numb_test=100, + detail_file=None, + shuffle_test=False, + head=None, + ): + """Construct a DeePMD tester. + + Args: + - config: The Dict-like configuration with training options. + """ + self.numb_test = numb_test + self.detail_file = detail_file + self.shuffle_test = shuffle_test + # Model + state_dict = torch.load(model_ckpt, map_location=DEVICE) + if "model" in state_dict: + state_dict = state_dict["model"] + model_params = state_dict["_extra_state"]["model_params"] + self.multi_task = "model_dict" in model_params + if self.multi_task: + assert head is not None, "Head must be specified in multitask mode!" + self.head = head + assert head in model_params["model_dict"], ( + f"Specified head {head} not found in model {model_ckpt}! " + f"Available ones are {list(model_params['model_dict'].keys())}." + ) + model_params = model_params["model_dict"][head] + state_dict_head = {"_extra_state": state_dict["_extra_state"]} + for item in state_dict: + if f"model.{head}." in item: + state_dict_head[ + item.replace(f"model.{head}.", "model.Default.") + ] = state_dict[item].clone() + state_dict = state_dict_head + + # Data + if input_script is not None: + with open(input_script) as fin: + self.input_script = json.load(fin) + training_params = self.input_script["training"] + if not self.multi_task: + assert ( + "validation_data" in training_params + ), f"Validation systems not found in {input_script}!" + self.systems = training_params["validation_data"]["systems"] + self.batchsize = training_params["validation_data"]["batch_size"] + logging.info( + f"Testing validation systems in input script: {input_script}" + ) + else: + assert ( + "data_dict" in training_params + ), f"Input script {input_script} is not in multi-task mode!" + assert head in training_params["data_dict"], ( + f"Specified head {head} not found in input script {input_script}! " + f"Available ones are {list(training_params['data_dict'].keys())}." + ) + assert ( + "validation_data" in training_params["data_dict"][head] + ), f"Validation systems not found in head {head} of {input_script}!" + self.systems = training_params["data_dict"][head]["validation_data"][ + "systems" + ] + self.batchsize = training_params["data_dict"][head]["validation_data"][ + "batch_size" + ] + logging.info( + f"Testing validation systems in head {head} of input script: {input_script}" + ) + elif system is not None: + self.systems = expand_sys_str(system) + self.batchsize = "auto" + logging.info("Testing systems in path: %s", system) + elif datafile is not None: + with open(datafile) as fin: + self.systems = fin.read().splitlines() + self.batchsize = "auto" + logging.info("Testing systems in file: %s", datafile) + else: + self.systems = None + self.batchsize = None + + self.type_split = False + if model_params["descriptor"]["type"] in ["se_e2_a"]: + self.type_split = True + self.model_params = deepcopy(model_params) + model_params["resuming"] = True + self.model = get_model(model_params).to(DEVICE) + + # Model Wrapper + self.wrapper = ModelWrapper(self.model) # inference only + if JIT: + self.wrapper = torch.jit.script(self.wrapper) + self.wrapper.load_state_dict(state_dict) + + # Loss + if "fitting_net" not in model_params: + assert ( + input_script is not None + ), "Denoise model must use --input-script mode!" + loss_params = self.input_script["loss"] + loss_type = loss_params.pop("type", "ener") + assert ( + loss_type == "denoise" + ), "Models without fitting_net only support denoise test!" + self.noise_settings = { + "noise_type": loss_params.pop("noise_type", "uniform"), + "noise": loss_params.pop("noise", 1.0), + "noise_mode": loss_params.pop("noise_mode", "fix_num"), + "mask_num": loss_params.pop("mask_num", 8), + "same_mask": loss_params.pop("same_mask", False), + "mask_coord": loss_params.pop("mask_coord", False), + "mask_type": loss_params.pop("mask_type", False), + "mask_type_idx": len(model_params["type_map"]) - 1, + } + loss_params["ntypes"] = len(model_params["type_map"]) + self.loss = DenoiseLoss(**loss_params) + else: + self.noise_settings = None + self.loss = EnergyStdLoss(inference=True) + + @staticmethod + def get_data(data): + batch_data = next(iter(data)) + for key in batch_data.keys(): + if key == "sid" or key == "fid": + continue + elif not isinstance(batch_data[key], list): + if batch_data[key] is not None: + batch_data[key] = batch_data[key].to(DEVICE) + else: + batch_data[key] = [item.to(DEVICE) for item in batch_data[key]] + input_dict = {} + for item in [ + "coord", + "atype", + "box", + ]: + if item in batch_data: + input_dict[item] = batch_data[item] + else: + input_dict[item] = None + label_dict = {} + for item in [ + "energy", + "force", + "virial", + "clean_coord", + "clean_type", + "coord_mask", + "type_mask", + ]: + if item in batch_data: + label_dict[item] = batch_data[item] + return input_dict, label_dict + + def run(self): + systems = self.systems + system_results = {} + global_sum_natoms = 0 + for cc, system in enumerate(systems): + logging.info("# ---------------output of dp test--------------- ") + logging.info(f"# testing system : {system}") + system_pred = [] + system_label = [] + dataset = DpLoaderSet( + [system], + self.batchsize, + self.model_params, + type_split=self.type_split, + noise_settings=self.noise_settings, + shuffle=self.shuffle_test, + ) + sampler = RandomSampler( + dataset, replacement=True, num_samples=dataset.total_batch + ) + if sampler is None: + logging.warning( + "Sampler not specified!" + ) # None sampler will lead to a premature stop iteration. Replacement should be True in attribute of the sampler to produce expected number of items in one iteration. + dataloader = DataLoader( + dataset, + sampler=sampler, + batch_size=None, + num_workers=min( + NUM_WORKERS, 1 + ), # setting to 0 diverges the behavior of its iterator; should be >=1 + drop_last=False, + ) + data = iter(dataloader) + + single_results = {} + sum_natoms = 0 + sys_natoms = None + for ii in range(self.numb_test): + try: + input_dict, label_dict = self.get_data(data) + except StopIteration: + if ( + ii < dataset.total_batch + ): # Unexpected stop iteration.(test step < total batch) + raise StopIteration + else: + break + model_pred, _, _ = self.wrapper(**input_dict) + system_pred.append( + { + item: model_pred[item].detach().cpu().numpy() + for item in model_pred + } + ) + system_label.append( + { + item: label_dict[item].detach().cpu().numpy() + for item in label_dict + } + ) + natoms = int(input_dict["atype"].shape[-1]) + _, more_loss = self.loss( + model_pred, label_dict, natoms, 1.0, mae=True + ) # TODO: lr here is useless + if sys_natoms is None: + sys_natoms = natoms + else: + assert ( + sys_natoms == natoms + ), "Frames in one system must be the same!" + sum_natoms += natoms + for k, v in more_loss.items(): + if "mae" in k: + single_results[k] = single_results.get(k, 0.0) + v * natoms + else: + single_results[k] = single_results.get(k, 0.0) + v**2 * natoms + if self.detail_file is not None: + save_detail_file( + Path(self.detail_file), + system_pred, + system_label, + sys_natoms, + system_name=system, + append=(cc != 0), + ) + results = { + k: v / sum_natoms if "mae" in k else math.sqrt(v / sum_natoms) + for k, v in single_results.items() + } + for item in sorted(results.keys()): + logging.info(f"{item}: {results[item]:.4f}") + logging.info("# ----------------------------------------------- ") + for k, v in single_results.items(): + system_results[k] = system_results.get(k, 0.0) + v + global_sum_natoms += sum_natoms + + global_results = { + k: v / global_sum_natoms if "mae" in k else math.sqrt(v / global_sum_natoms) + for k, v in system_results.items() + } + logging.info("# ----------weighted average of errors----------- ") + if not self.multi_task: + logging.info(f"# number of systems : {len(systems)}") + else: + logging.info(f"# number of systems for {self.head}: {len(systems)}") + for item in sorted(global_results.keys()): + logging.info(f"{item}: {global_results[item]:.4f}") + logging.info("# ----------------------------------------------- ") + return global_results + + +def save_txt_file( + fname: Path, data: np.ndarray, header: str = "", append: bool = False +): + """Save numpy array to test file. + + Parameters + ---------- + fname : str + filename + data : np.ndarray + data to save to disk + header : str, optional + header string to use in file, by default "" + append : bool, optional + if true file will be appended insted of overwriting, by default False + """ + flags = "ab" if append else "w" + with fname.open(flags) as fp: + np.savetxt(fp, data, header=header) + + +def save_detail_file( + detail_path, system_pred, system_label, natoms, system_name, append=False +): + ntest = len(system_pred) + data_e = np.concatenate([item["energy"] for item in system_label]).reshape([-1, 1]) + pred_e = np.concatenate([item["energy"] for item in system_pred]).reshape([-1, 1]) + pe = np.concatenate( + ( + data_e, + pred_e, + ), + axis=1, + ) + save_txt_file( + detail_path.with_suffix(".e.out"), + pe, + header="%s: data_e pred_e" % system_name, + append=append, + ) + pe_atom = pe / natoms + save_txt_file( + detail_path.with_suffix(".e_peratom.out"), + pe_atom, + header="%s: data_e pred_e" % system_name, + append=append, + ) + if "force" in system_pred[0]: + data_f = np.concatenate([item["force"] for item in system_label]).reshape( + [-1, 3] + ) + pred_f = np.concatenate([item["force"] for item in system_pred]).reshape( + [-1, 3] + ) + pf = np.concatenate( + ( + data_f, + pred_f, + ), + axis=1, + ) + save_txt_file( + detail_path.with_suffix(".f.out"), + pf, + header="%s: data_fx data_fy data_fz pred_fx pred_fy pred_fz" % system_name, + append=append, + ) + if "virial" in system_pred[0]: + data_v = np.concatenate([item["virial"] for item in system_label]).reshape( + [-1, 9] + ) + pred_v = np.concatenate([item["virial"] for item in system_pred]).reshape( + [-1, 9] + ) + pv = np.concatenate( + ( + data_v, + pred_v, + ), + axis=1, + ) + save_txt_file( + detail_path.with_suffix(".v.out"), + pv, + header=f"{system_name}: data_vxx data_vxy data_vxz data_vyx data_vyy " + "data_vyz data_vzx data_vzy data_vzz pred_vxx pred_vxy pred_vxz pred_vyx " + "pred_vyy pred_vyz pred_vzx pred_vzy pred_vzz", + append=append, + ) + pv_atom = pv / natoms + save_txt_file( + detail_path.with_suffix(".v_peratom.out"), + pv_atom, + header=f"{system_name}: data_vxx data_vxy data_vxz data_vyx data_vyy " + "data_vyz data_vzx data_vzy data_vzz pred_vxx pred_vxy pred_vxz pred_vyx " + "pred_vyy pred_vyz pred_vzx pred_vzy pred_vzz", + append=append, + ) diff --git a/deepmd/pt/loss/__init__.py b/deepmd/pt/loss/__init__.py new file mode 100644 index 0000000000..d3a095ce13 --- /dev/null +++ b/deepmd/pt/loss/__init__.py @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from .denoise import ( + DenoiseLoss, +) +from .ener import ( + EnergyStdLoss, +) +from .loss import ( + TaskLoss, +) + +__all__ = [ + "DenoiseLoss", + "EnergyStdLoss", + "TaskLoss", +] diff --git a/deepmd/pt/loss/denoise.py b/deepmd/pt/loss/denoise.py new file mode 100644 index 0000000000..cd12e70bb1 --- /dev/null +++ b/deepmd/pt/loss/denoise.py @@ -0,0 +1,109 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import torch +import torch.nn.functional as F + +from deepmd.pt.loss.loss import ( + TaskLoss, +) +from deepmd.pt.utils import ( + env, +) + + +class DenoiseLoss(TaskLoss): + def __init__( + self, + ntypes, + masked_token_loss=1.0, + masked_coord_loss=1.0, + norm_loss=0.01, + use_l1=True, + beta=1.00, + mask_loss_coord=True, + mask_loss_token=True, + **kwargs, + ): + """Construct a layer to compute loss on coord, and type reconstruction.""" + super().__init__() + self.ntypes = ntypes + self.masked_token_loss = masked_token_loss + self.masked_coord_loss = masked_coord_loss + self.norm_loss = norm_loss + self.has_coord = self.masked_coord_loss > 0.0 + self.has_token = self.masked_token_loss > 0.0 + self.has_norm = self.norm_loss > 0.0 + self.use_l1 = use_l1 + self.beta = beta + self.frac_beta = 1.00 / self.beta + self.mask_loss_coord = mask_loss_coord + self.mask_loss_token = mask_loss_token + + def forward(self, model_pred, label, natoms, learning_rate, mae=False): + """Return loss on coord and type denoise. + + Returns + ------- + - loss: Loss to minimize. + """ + updated_coord = model_pred["updated_coord"] + logits = model_pred["logits"] + clean_coord = label["clean_coord"] + clean_type = label["clean_type"] + coord_mask = label["coord_mask"] + type_mask = label["type_mask"] + + loss = torch.tensor(0.0, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE) + more_loss = {} + if self.has_coord: + if self.mask_loss_coord: + masked_updated_coord = updated_coord[coord_mask] + masked_clean_coord = clean_coord[coord_mask] + if masked_updated_coord.size(0) > 0: + coord_loss = F.smooth_l1_loss( + masked_updated_coord.view(-1, 3), + masked_clean_coord.view(-1, 3), + reduction="mean", + beta=self.beta, + ) + else: + coord_loss = torch.tensor( + 0.0, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ) + else: + coord_loss = F.smooth_l1_loss( + updated_coord.view(-1, 3), + clean_coord.view(-1, 3), + reduction="mean", + beta=self.beta, + ) + loss += self.masked_coord_loss * coord_loss + more_loss["coord_l1_error"] = coord_loss.detach() + if self.has_token: + if self.mask_loss_token: + masked_logits = logits[type_mask] + masked_target = clean_type[type_mask] + if masked_logits.size(0) > 0: + token_loss = F.nll_loss( + F.log_softmax(masked_logits, dim=-1), + masked_target, + reduction="mean", + ) + else: + token_loss = torch.tensor( + 0.0, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ) + else: + token_loss = F.nll_loss( + F.log_softmax(logits.view(-1, self.ntypes - 1), dim=-1), + clean_type.view(-1), + reduction="mean", + ) + loss += self.masked_token_loss * token_loss + more_loss["token_error"] = token_loss.detach() + if self.has_norm: + norm_x = model_pred["norm_x"] + norm_delta_pair_rep = model_pred["norm_delta_pair_rep"] + loss += self.norm_loss * (norm_x + norm_delta_pair_rep) + more_loss["norm_loss"] = norm_x.detach() + norm_delta_pair_rep.detach() + + return loss, more_loss diff --git a/deepmd/pt/loss/ener.py b/deepmd/pt/loss/ener.py new file mode 100644 index 0000000000..4ed765cf69 --- /dev/null +++ b/deepmd/pt/loss/ener.py @@ -0,0 +1,155 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import torch +import torch.nn.functional as F + +from deepmd.pt.loss.loss import ( + TaskLoss, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.env import ( + GLOBAL_PT_FLOAT_PRECISION, +) + + +class EnergyStdLoss(TaskLoss): + def __init__( + self, + starter_learning_rate=1.0, + start_pref_e=0.0, + limit_pref_e=0.0, + start_pref_f=0.0, + limit_pref_f=0.0, + start_pref_v=0.0, + limit_pref_v=0.0, + use_l1_all: bool = False, + inference=False, + **kwargs, + ): + """Construct a layer to compute loss on energy, force and virial.""" + super().__init__() + self.starter_learning_rate = starter_learning_rate + self.has_e = (start_pref_e != 0.0 and limit_pref_e != 0.0) or inference + self.has_f = (start_pref_f != 0.0 and limit_pref_f != 0.0) or inference + self.has_v = (start_pref_v != 0.0 and limit_pref_v != 0.0) or inference + self.start_pref_e = start_pref_e + self.limit_pref_e = limit_pref_e + self.start_pref_f = start_pref_f + self.limit_pref_f = limit_pref_f + self.start_pref_v = start_pref_v + self.limit_pref_v = limit_pref_v + self.use_l1_all = use_l1_all + self.inference = inference + + def forward(self, model_pred, label, natoms, learning_rate, mae=False): + """Return loss on loss and force. + + Args: + - natoms: Tell atom count. + - p_energy: Predicted energy of all atoms. + - p_force: Predicted force per atom. + - l_energy: Actual energy of all atoms. + - l_force: Actual force per atom. + + Returns + ------- + - loss: Loss to minimize. + """ + coef = learning_rate / self.starter_learning_rate + pref_e = self.limit_pref_e + (self.start_pref_e - self.limit_pref_e) * coef + pref_f = self.limit_pref_f + (self.start_pref_f - self.limit_pref_f) * coef + pref_v = self.limit_pref_v + (self.start_pref_v - self.limit_pref_v) * coef + loss = torch.tensor(0.0, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE) + more_loss = {} + # more_loss['log_keys'] = [] # showed when validation on the fly + # more_loss['test_keys'] = [] # showed when doing dp test + atom_norm = 1.0 / natoms + if self.has_e and "energy" in model_pred and "energy" in label: + if not self.use_l1_all: + l2_ener_loss = torch.mean( + torch.square(model_pred["energy"] - label["energy"]) + ) + if not self.inference: + more_loss["l2_ener_loss"] = l2_ener_loss.detach() + loss += atom_norm * (pref_e * l2_ener_loss) + rmse_e = l2_ener_loss.sqrt() * atom_norm + more_loss["rmse_e"] = rmse_e.detach() + # more_loss['log_keys'].append('rmse_e') + else: # use l1 and for all atoms + l1_ener_loss = F.l1_loss( + model_pred["energy"].reshape(-1), + label["energy"].reshape(-1), + reduction="sum", + ) + loss += pref_e * l1_ener_loss + more_loss["mae_e"] = F.l1_loss( + model_pred["energy"].reshape(-1), + label["energy"].reshape(-1), + reduction="mean", + ).detach() + # more_loss['log_keys'].append('rmse_e') + if mae: + mae_e = ( + torch.mean(torch.abs(model_pred["energy"] - label["energy"])) + * atom_norm + ) + more_loss["mae_e"] = mae_e.detach() + mae_e_all = torch.mean( + torch.abs(model_pred["energy"] - label["energy"]) + ) + more_loss["mae_e_all"] = mae_e_all.detach() + + if self.has_f and "force" in model_pred and "force" in label: + if "force_target_mask" in model_pred: + force_target_mask = model_pred["force_target_mask"] + else: + force_target_mask = None + if not self.use_l1_all: + if force_target_mask is not None: + diff_f = (label["force"] - model_pred["force"]) * force_target_mask + force_cnt = force_target_mask.squeeze(-1).sum(-1) + l2_force_loss = torch.mean( + torch.square(diff_f).mean(-1).sum(-1) / force_cnt + ) + else: + diff_f = label["force"] - model_pred["force"] + l2_force_loss = torch.mean(torch.square(diff_f)) + if not self.inference: + more_loss["l2_force_loss"] = l2_force_loss.detach() + loss += (pref_f * l2_force_loss).to(GLOBAL_PT_FLOAT_PRECISION) + rmse_f = l2_force_loss.sqrt() + more_loss["rmse_f"] = rmse_f.detach() + else: + l1_force_loss = F.l1_loss( + label["force"], model_pred["force"], reduction="none" + ) + if force_target_mask is not None: + l1_force_loss *= force_target_mask + force_cnt = force_target_mask.squeeze(-1).sum(-1) + more_loss["mae_f"] = ( + l1_force_loss.mean(-1).sum(-1) / force_cnt + ).mean() + l1_force_loss = (l1_force_loss.sum(-1).sum(-1) / force_cnt).sum() + else: + more_loss["mae_f"] = l1_force_loss.mean().detach() + l1_force_loss = l1_force_loss.sum(-1).mean(-1).sum() + loss += (pref_f * l1_force_loss).to(GLOBAL_PT_FLOAT_PRECISION) + if mae: + mae_f = torch.mean(torch.abs(diff_f)) + more_loss["mae_f"] = mae_f.detach() + + if self.has_v and "virial" in model_pred and "virial" in label: + diff_v = label["virial"] - model_pred["virial"].reshape(-1, 9) + l2_virial_loss = torch.mean(torch.square(diff_v)) + if not self.inference: + more_loss["l2_virial_loss"] = l2_virial_loss.detach() + loss += atom_norm * (pref_v * l2_virial_loss) + rmse_v = l2_virial_loss.sqrt() * atom_norm + more_loss["rmse_v"] = rmse_v.detach() + if mae: + mae_v = torch.mean(torch.abs(diff_v)) * atom_norm + more_loss["mae_v"] = mae_v.detach() + if not self.inference: + more_loss["rmse"] = torch.sqrt(loss.detach()) + return loss, more_loss diff --git a/deepmd/pt/loss/loss.py b/deepmd/pt/loss/loss.py new file mode 100644 index 0000000000..9f2c3a7ed7 --- /dev/null +++ b/deepmd/pt/loss/loss.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import torch + + +class TaskLoss(torch.nn.Module): + def __init__(self, **kwargs): + """Construct loss.""" + super().__init__() + + def forward(self, model_pred, label, natoms, learning_rate): + """Return loss .""" + raise NotImplementedError diff --git a/deepmd/pt/model/__init__.py b/deepmd/pt/model/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/deepmd/pt/model/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/deepmd/pt/model/backbone/__init__.py b/deepmd/pt/model/backbone/__init__.py new file mode 100644 index 0000000000..a76bdb2a2d --- /dev/null +++ b/deepmd/pt/model/backbone/__init__.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from .backbone import ( + BackBone, +) +from .evoformer2b import ( + Evoformer2bBackBone, +) + +__all__ = [ + "BackBone", + "Evoformer2bBackBone", +] diff --git a/deepmd/pt/model/backbone/backbone.py b/deepmd/pt/model/backbone/backbone.py new file mode 100644 index 0000000000..ddeedfeff5 --- /dev/null +++ b/deepmd/pt/model/backbone/backbone.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import torch + + +class BackBone(torch.nn.Module): + def __init__(self, **kwargs): + """BackBone base method.""" + super().__init__() + + def forward(self, **kwargs): + """Calculate backBone.""" + raise NotImplementedError diff --git a/deepmd/pt/model/backbone/evoformer2b.py b/deepmd/pt/model/backbone/evoformer2b.py new file mode 100644 index 0000000000..1146b3a298 --- /dev/null +++ b/deepmd/pt/model/backbone/evoformer2b.py @@ -0,0 +1,103 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from deepmd.pt.model.backbone import ( + BackBone, +) +from deepmd.pt.model.network.network import ( + Evoformer2bEncoder, +) + + +class Evoformer2bBackBone(BackBone): + def __init__( + self, + nnei, + layer_num=6, + attn_head=8, + atomic_dim=1024, + pair_dim=100, + feature_dim=1024, + ffn_dim=2048, + post_ln=False, + final_layer_norm=True, + final_head_layer_norm=False, + emb_layer_norm=False, + atomic_residual=False, + evo_residual=False, + residual_factor=1.0, + activation_function="gelu", + **kwargs, + ): + """Construct an evoformer backBone.""" + super().__init__() + self.nnei = nnei + self.layer_num = layer_num + self.attn_head = attn_head + self.atomic_dim = atomic_dim + self.pair_dim = pair_dim + self.feature_dim = feature_dim + self.head_dim = feature_dim // attn_head + assert ( + feature_dim % attn_head == 0 + ), f"feature_dim {feature_dim} must be divided by attn_head {attn_head}!" + self.ffn_dim = ffn_dim + self.post_ln = post_ln + self.final_layer_norm = final_layer_norm + self.final_head_layer_norm = final_head_layer_norm + self.emb_layer_norm = emb_layer_norm + self.activation_function = activation_function + self.atomic_residual = atomic_residual + self.evo_residual = evo_residual + self.residual_factor = float(residual_factor) + self.encoder = Evoformer2bEncoder( + nnei=self.nnei, + layer_num=self.layer_num, + attn_head=self.attn_head, + atomic_dim=self.atomic_dim, + pair_dim=self.pair_dim, + feature_dim=self.feature_dim, + ffn_dim=self.ffn_dim, + post_ln=self.post_ln, + final_layer_norm=self.final_layer_norm, + final_head_layer_norm=self.final_head_layer_norm, + emb_layer_norm=self.emb_layer_norm, + atomic_residual=self.atomic_residual, + evo_residual=self.evo_residual, + residual_factor=self.residual_factor, + activation_function=self.activation_function, + ) + + def forward(self, atomic_rep, pair_rep, nlist, nlist_type, nlist_mask): + """Encoder the atomic and pair representations. + + Args: + - atomic_rep: Atomic representation with shape [nframes, nloc, atomic_dim]. + - pair_rep: Pair representation with shape [nframes, nloc, nnei, pair_dim]. + - nlist: Neighbor list with shape [nframes, nloc, nnei]. + - nlist_type: Neighbor types with shape [nframes, nloc, nnei]. + - nlist_mask: Neighbor mask with shape [nframes, nloc, nnei], `False` if blank. + + Returns + ------- + - atomic_rep: Atomic representation after encoder with shape [nframes, nloc, feature_dim]. + - transformed_atomic_rep: Transformed atomic representation after encoder with shape [nframes, nloc, atomic_dim]. + - pair_rep: Pair representation after encoder with shape [nframes, nloc, nnei, attn_head]. + - delta_pair_rep: Delta pair representation after encoder with shape [nframes, nloc, nnei, attn_head]. + - norm_x: Normalization loss of atomic_rep. + - norm_delta_pair_rep: Normalization loss of delta_pair_rep. + """ + ( + atomic_rep, + transformed_atomic_rep, + pair_rep, + delta_pair_rep, + norm_x, + norm_delta_pair_rep, + ) = self.encoder(atomic_rep, pair_rep, nlist, nlist_type, nlist_mask) + return ( + atomic_rep, + transformed_atomic_rep, + pair_rep, + delta_pair_rep, + norm_x, + norm_delta_pair_rep, + ) diff --git a/deepmd/pt/model/descriptor/__init__.py b/deepmd/pt/model/descriptor/__init__.py new file mode 100644 index 0000000000..4252e34905 --- /dev/null +++ b/deepmd/pt/model/descriptor/__init__.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from .descriptor import ( + Descriptor, + DescriptorBlock, + compute_std, + make_default_type_embedding, +) +from .dpa1 import ( + DescrptBlockSeAtten, + DescrptDPA1, +) +from .dpa2 import ( + DescrptDPA2, +) +from .env_mat import ( + prod_env_mat_se_a, +) +from .gaussian_lcc import ( + DescrptGaussianLcc, +) +from .hybrid import ( + DescrptBlockHybrid, +) +from .repformers import ( + DescrptBlockRepformers, +) +from .se_a import ( + DescrptBlockSeA, + DescrptSeA, +) + +__all__ = [ + "Descriptor", + "DescriptorBlock", + "compute_std", + "make_default_type_embedding", + "DescrptBlockSeA", + "DescrptBlockSeAtten", + "DescrptSeA", + "DescrptDPA1", + "DescrptDPA2", + "prod_env_mat_se_a", + "DescrptGaussianLcc", + "DescrptBlockHybrid", + "DescrptBlockRepformers", +] diff --git a/deepmd/pt/model/descriptor/descriptor.py b/deepmd/pt/model/descriptor/descriptor.py new file mode 100644 index 0000000000..bb98e8dc15 --- /dev/null +++ b/deepmd/pt/model/descriptor/descriptor.py @@ -0,0 +1,272 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from abc import ( + ABC, + abstractmethod, +) +from typing import ( + Callable, + List, + Optional, +) + +import numpy as np +import torch + +from deepmd.pt.model.network.network import ( + TypeEmbedNet, +) +from deepmd.pt.utils.plugin import ( + Plugin, +) + + +class Descriptor(torch.nn.Module, ABC): + """The descriptor. + Given the atomic coordinates, atomic types and neighbor list, + calculate the descriptor. + """ + + __plugins = Plugin() + local_cluster = False + + @abstractmethod + def get_rcut(self) -> float: + """Returns the cut-off radius.""" + raise NotImplementedError + + @abstractmethod + def get_nsel(self) -> int: + """Returns the number of selected atoms in the cut-off radius.""" + raise NotImplementedError + + @abstractmethod + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + raise NotImplementedError + + @abstractmethod + def get_ntype(self) -> int: + """Returns the number of element types.""" + raise NotImplementedError + + @abstractmethod + def get_dim_out(self) -> int: + """Returns the output dimension.""" + raise NotImplementedError + + @abstractmethod + def compute_input_stats(self, merged): + """Update mean and stddev for descriptor elements.""" + raise NotImplementedError + + @abstractmethod + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + """Initialize the model bias by the statistics.""" + raise NotImplementedError + + @abstractmethod + def forward( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[torch.Tensor] = None, + ): + """Calculate descriptor.""" + raise NotImplementedError + + @staticmethod + def register(key: str) -> Callable: + """Register a descriptor plugin. + + Parameters + ---------- + key : str + the key of a descriptor + + Returns + ------- + Descriptor + the registered descriptor + + Examples + -------- + >>> @Descriptor.register("some_descrpt") + class SomeDescript(Descriptor): + pass + """ + return Descriptor.__plugins.register(key) + + @classmethod + def get_stat_name(cls, config): + descrpt_type = config["type"] + return Descriptor.__plugins.plugins[descrpt_type].get_stat_name(config) + + @classmethod + def get_data_process_key(cls, config): + descrpt_type = config["type"] + return Descriptor.__plugins.plugins[descrpt_type].get_data_process_key(config) + + def __new__(cls, *args, **kwargs): + if cls is Descriptor: + try: + descrpt_type = kwargs["type"] + except KeyError: + raise KeyError("the type of descriptor should be set by `type`") + if descrpt_type in Descriptor.__plugins.plugins: + cls = Descriptor.__plugins.plugins[descrpt_type] + else: + raise RuntimeError("Unknown descriptor type: " + descrpt_type) + return super().__new__(cls) + + +class DescriptorBlock(torch.nn.Module, ABC): + """The building block of descriptor. + Given the input descriptor, provide with the atomic coordinates, + atomic types and neighbor list, calculate the new descriptor. + """ + + __plugins = Plugin() + local_cluster = False + + @staticmethod + def register(key: str) -> Callable: + """Register a DescriptorBlock plugin. + + Parameters + ---------- + key : str + the key of a DescriptorBlock + + Returns + ------- + DescriptorBlock + the registered DescriptorBlock + + Examples + -------- + >>> @DescriptorBlock.register("some_descrpt") + class SomeDescript(DescriptorBlock): + pass + """ + return DescriptorBlock.__plugins.register(key) + + def __new__(cls, *args, **kwargs): + if cls is DescriptorBlock: + try: + descrpt_type = kwargs["type"] + except KeyError: + raise KeyError("the type of DescriptorBlock should be set by `type`") + if descrpt_type in DescriptorBlock.__plugins.plugins: + cls = DescriptorBlock.__plugins.plugins[descrpt_type] + else: + raise RuntimeError("Unknown DescriptorBlock type: " + descrpt_type) + return super().__new__(cls) + + @abstractmethod + def get_rcut(self) -> float: + """Returns the cut-off radius.""" + raise NotImplementedError + + @abstractmethod + def get_nsel(self) -> int: + """Returns the number of selected atoms in the cut-off radius.""" + raise NotImplementedError + + @abstractmethod + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + raise NotImplementedError + + @abstractmethod + def get_ntype(self) -> int: + """Returns the number of element types.""" + raise NotImplementedError + + @abstractmethod + def get_dim_out(self) -> int: + """Returns the output dimension.""" + raise NotImplementedError + + @abstractmethod + def get_dim_in(self) -> int: + """Returns the output dimension.""" + raise NotImplementedError + + @abstractmethod + def compute_input_stats(self, merged): + """Update mean and stddev for DescriptorBlock elements.""" + raise NotImplementedError + + @abstractmethod + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + """Initialize the model bias by the statistics.""" + raise NotImplementedError + + def share_params(self, base_class, shared_level, resume=False): + assert ( + self.__class__ == base_class.__class__ + ), "Only descriptors of the same type can share params!" + if shared_level == 0: + # link buffers + if hasattr(self, "mean") and not resume: + # in case of change params during resume + sumr_base, suma_base, sumn_base, sumr2_base, suma2_base = ( + base_class.sumr, + base_class.suma, + base_class.sumn, + base_class.sumr2, + base_class.suma2, + ) + sumr, suma, sumn, sumr2, suma2 = ( + self.sumr, + self.suma, + self.sumn, + self.sumr2, + self.suma2, + ) + base_class.init_desc_stat( + sumr_base + sumr, + suma_base + suma, + sumn_base + sumn, + sumr2_base + sumr2, + suma2_base + suma2, + ) + self.mean = base_class.mean + self.stddev = base_class.stddev + # self.load_state_dict(base_class.state_dict()) # this does not work, because it only inits the model + # the following will successfully link all the params except buffers + for item in self._modules: + self._modules[item] = base_class._modules[item] + else: + raise NotImplementedError + + @abstractmethod + def forward( + self, + nlist: torch.Tensor, + extended_coord: torch.Tensor, + extended_atype: torch.Tensor, + extended_atype_embd: Optional[torch.Tensor] = None, + mapping: Optional[torch.Tensor] = None, + ): + """Calculate DescriptorBlock.""" + raise NotImplementedError + + +def compute_std(sumv2, sumv, sumn, rcut_r): + """Compute standard deviation.""" + if sumn == 0: + return 1.0 / rcut_r + val = np.sqrt(sumv2 / sumn - np.multiply(sumv / sumn, sumv / sumn)) + if np.abs(val) < 1e-2: + val = 1e-2 + return val + + +def make_default_type_embedding( + ntypes, +): + aux = {} + aux["tebd_dim"] = 8 + return TypeEmbedNet(ntypes, aux["tebd_dim"]), aux diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py new file mode 100644 index 0000000000..dd34b815c9 --- /dev/null +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -0,0 +1,152 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + List, + Optional, +) + +import torch + +from deepmd.pt.model.descriptor import ( + Descriptor, +) +from deepmd.pt.model.network.network import ( + TypeEmbedNet, +) + +from .se_atten import ( + DescrptBlockSeAtten, +) + + +@Descriptor.register("dpa1") +@Descriptor.register("se_atten") +class DescrptDPA1(Descriptor): + def __init__( + self, + rcut, + rcut_smth, + sel, + ntypes: int, + neuron: list = [25, 50, 100], + axis_neuron: int = 16, + tebd_dim: int = 8, + tebd_input_mode: str = "concat", + # set_davg_zero: bool = False, + set_davg_zero: bool = True, # TODO + attn: int = 128, + attn_layer: int = 2, + attn_dotr: bool = True, + attn_mask: bool = False, + post_ln=True, + ffn=False, + ffn_embed_dim=1024, + activation="tanh", + scaling_factor=1.0, + head_num=1, + normalize=True, + temperature=None, + return_rot=False, + concat_output_tebd: bool = True, + type: Optional[str] = None, + ): + super().__init__() + del type + self.se_atten = DescrptBlockSeAtten( + rcut, + rcut_smth, + sel, + ntypes, + neuron=neuron, + axis_neuron=axis_neuron, + tebd_dim=tebd_dim, + tebd_input_mode=tebd_input_mode, + set_davg_zero=set_davg_zero, + attn=attn, + attn_layer=attn_layer, + attn_dotr=attn_dotr, + attn_mask=attn_mask, + post_ln=post_ln, + ffn=ffn, + ffn_embed_dim=ffn_embed_dim, + activation=activation, + scaling_factor=scaling_factor, + head_num=head_num, + normalize=normalize, + temperature=temperature, + return_rot=return_rot, + ) + self.type_embedding = TypeEmbedNet(ntypes, tebd_dim) + self.tebd_dim = tebd_dim + self.concat_output_tebd = concat_output_tebd + + def get_rcut(self) -> float: + """Returns the cut-off radius.""" + return self.se_atten.get_rcut() + + def get_nsel(self) -> int: + """Returns the number of selected atoms in the cut-off radius.""" + return self.se_atten.get_nsel() + + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + return self.se_atten.get_sel() + + def get_ntype(self) -> int: + """Returns the number of element types.""" + return self.se_atten.get_ntype() + + def get_dim_out(self) -> int: + """Returns the output dimension.""" + ret = self.se_atten.get_dim_out() + if self.concat_output_tebd: + ret += self.tebd_dim + return ret + + @property + def dim_out(self): + return self.get_dim_out() + + @property + def dim_emb(self): + return self.se_atten.dim_emb + + def compute_input_stats(self, merged): + return self.se_atten.compute_input_stats(merged) + + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + self.se_atten.init_desc_stat(sumr, suma, sumn, sumr2, suma2) + + @classmethod + def get_stat_name(cls, config): + descrpt_type = config["type"] + assert descrpt_type in ["dpa1", "se_atten"] + return f'stat_file_dpa1_rcut{config["rcut"]:.2f}_smth{config["rcut_smth"]:.2f}_sel{config["sel"]}.npz' + + @classmethod + def get_data_process_key(cls, config): + descrpt_type = config["type"] + assert descrpt_type in ["dpa1", "se_atten"] + return {"sel": config["sel"], "rcut": config["rcut"]} + + def forward( + self, + extended_coord: torch.Tensor, + extended_atype: torch.Tensor, + nlist: torch.Tensor, + mapping: Optional[torch.Tensor] = None, + ): + del mapping + nframes, nloc, nnei = nlist.shape + nall = extended_coord.view(nframes, -1).shape[1] // 3 + g1_ext = self.type_embedding(extended_atype) + g1_inp = g1_ext[:, :nloc, :] + g1, env_mat, diff, rot_mat, sw = self.se_atten( + nlist, + extended_coord, + extended_atype, + g1_ext, + mapping=None, + ) + if self.concat_output_tebd: + g1 = torch.cat([g1, g1_inp], dim=-1) + return g1, env_mat, diff, rot_mat, sw diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py new file mode 100644 index 0000000000..fbdbc91dd9 --- /dev/null +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -0,0 +1,375 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + List, + Optional, +) + +import torch + +from deepmd.pt.model.descriptor import ( + Descriptor, +) +from deepmd.pt.model.network.network import ( + Identity, + Linear, + TypeEmbedNet, +) +from deepmd.pt.utils.nlist import ( + build_multiple_neighbor_list, + get_multiple_nlist_key, +) + +from .repformers import ( + DescrptBlockRepformers, +) +from .se_atten import ( + DescrptBlockSeAtten, +) + + +@Descriptor.register("dpa2") +class DescrptDPA2(Descriptor): + def __init__( + self, + ntypes: int, + repinit_rcut: float, + repinit_rcut_smth: float, + repinit_nsel: int, + repformer_rcut: float, + repformer_rcut_smth: float, + repformer_nsel: int, + # kwargs + tebd_dim: int = 8, + concat_output_tebd: bool = True, + repinit_neuron: List[int] = [25, 50, 100], + repinit_axis_neuron: int = 16, + repinit_set_davg_zero: bool = True, # TODO + repinit_activation="tanh", + # repinit still unclear: + # ffn, ffn_embed_dim, scaling_factor, normalize, + repformer_nlayers: int = 3, + repformer_g1_dim: int = 128, + repformer_g2_dim: int = 16, + repformer_axis_dim: int = 4, + repformer_do_bn_mode: str = "no", + repformer_bn_momentum: float = 0.1, + repformer_update_g1_has_conv: bool = True, + repformer_update_g1_has_drrd: bool = True, + repformer_update_g1_has_grrg: bool = True, + repformer_update_g1_has_attn: bool = True, + repformer_update_g2_has_g1g1: bool = True, + repformer_update_g2_has_attn: bool = True, + repformer_update_h2: bool = False, + repformer_attn1_hidden: int = 64, + repformer_attn1_nhead: int = 4, + repformer_attn2_hidden: int = 16, + repformer_attn2_nhead: int = 4, + repformer_attn2_has_gate: bool = False, + repformer_activation: str = "tanh", + repformer_update_style: str = "res_avg", + repformer_set_davg_zero: bool = True, # TODO + repformer_add_type_ebd_to_seq: bool = False, + type: Optional[ + str + ] = None, # work around the bad design in get_trainer and DpLoaderSet! + rcut: Optional[ + float + ] = None, # work around the bad design in get_trainer and DpLoaderSet! + rcut_smth: Optional[ + float + ] = None, # work around the bad design in get_trainer and DpLoaderSet! + sel: Optional[ + int + ] = None, # work around the bad design in get_trainer and DpLoaderSet! + ): + r"""The DPA-2 descriptor. see https://arxiv.org/abs/2312.15492. + + Parameters + ---------- + ntypes : int + Number of atom types + repinit_rcut : float + The cut-off radius of the repinit block + repinit_rcut_smth : float + From this position the inverse distance smoothly decays + to 0 at the cut-off. Use in the repinit block. + repinit_nsel : int + Maximally possible number of neighbors for repinit block. + repformer_rcut : float + The cut-off radius of the repformer block + repformer_rcut_smth : float + From this position the inverse distance smoothly decays + to 0 at the cut-off. Use in the repformer block. + repformer_nsel : int + Maximally possible number of neighbors for repformer block. + tebd_dim : int + The dimension of atom type embedding + concat_output_tebd : bool + Whether to concat type embedding at the output of the descriptor. + repinit_neuron : List[int] + repinit block: the number of neurons in the embedding net. + repinit_axis_neuron : int + repinit block: the number of dimension of split in the + symmetrization op. + repinit_activation : str + repinit block: the activation function in the embedding net + repformer_nlayers : int + repformers block: the number of repformer layers + repformer_g1_dim : int + repformers block: the dimension of single-atom rep + repformer_g2_dim : int + repformers block: the dimension of invariant pair-atom rep + repformer_axis_dim : int + repformers block: the number of dimension of split in the + symmetrization ops. + repformer_do_bn_mode : bool + repformers block: do batch norm in the repformer layers + repformer_bn_momentum : float + repformers block: moment in the batch normalization + repformer_update_g1_has_conv : bool + repformers block: update the g1 rep with convolution term + repformer_update_g1_has_drrd : bool + repformers block: update the g1 rep with the drrd term + repformer_update_g1_has_grrg : bool + repformers block: update the g1 rep with the grrg term + repformer_update_g1_has_attn : bool + repformers block: update the g1 rep with the localized + self-attention + repformer_update_g2_has_g1g1 : bool + repformers block: update the g2 rep with the g1xg1 term + repformer_update_g2_has_attn : bool + repformers block: update the g2 rep with the gated self-attention + repformer_update_h2 : bool + repformers block: update the h2 rep + repformer_attn1_hidden : int + repformers block: the hidden dimension of localized self-attention + repformer_attn1_nhead : int + repformers block: the number of heads in localized self-attention + repformer_attn2_hidden : int + repformers block: the hidden dimension of gated self-attention + repformer_attn2_nhead : int + repformers block: the number of heads in gated self-attention + repformer_attn2_has_gate : bool + repformers block: has gate in the gated self-attention + repformer_activation : str + repformers block: the activation function in the MLPs. + repformer_update_style : str + repformers block: style of update a rep. + can be res_avg or res_incr. + res_avg updates a rep `u` with: + u = 1/\sqrt{n+1} (u + u_1 + u_2 + ... + u_n) + res_incr updates a rep `u` with: + u = u + 1/\sqrt{n} (u_1 + u_2 + ... + u_n) + repformer_set_davg_zero : bool + repformers block: set the avg to zero in statistics + repformer_add_type_ebd_to_seq : bool + repformers block: concatenate the type embedding at the output. + + Returns + ------- + descriptor: torch.Tensor + the descriptor of shape nb x nloc x g1_dim. + invariant single-atom representation. + g2: torch.Tensor + invariant pair-atom representation. + h2: torch.Tensor + equivariant pair-atom representation. + rot_mat: torch.Tensor + rotation matrix for equivariant fittings + sw: torch.Tensor + The switch function for decaying inverse distance. + + """ + super().__init__() + del type, rcut, rcut_smth, sel + self.repinit = DescrptBlockSeAtten( + repinit_rcut, + repinit_rcut_smth, + repinit_nsel, + ntypes, + attn_layer=0, + neuron=repinit_neuron, + axis_neuron=repinit_axis_neuron, + tebd_dim=tebd_dim, + tebd_input_mode="concat", + # tebd_input_mode='dot_residual_s', + set_davg_zero=repinit_set_davg_zero, + activation=repinit_activation, + ) + self.repformers = DescrptBlockRepformers( + repformer_rcut, + repformer_rcut_smth, + repformer_nsel, + ntypes, + nlayers=repformer_nlayers, + g1_dim=repformer_g1_dim, + g2_dim=repformer_g2_dim, + axis_dim=repformer_axis_dim, + direct_dist=False, + do_bn_mode=repformer_do_bn_mode, + bn_momentum=repformer_bn_momentum, + update_g1_has_conv=repformer_update_g1_has_conv, + update_g1_has_drrd=repformer_update_g1_has_drrd, + update_g1_has_grrg=repformer_update_g1_has_grrg, + update_g1_has_attn=repformer_update_g1_has_attn, + update_g2_has_g1g1=repformer_update_g2_has_g1g1, + update_g2_has_attn=repformer_update_g2_has_attn, + update_h2=repformer_update_h2, + attn1_hidden=repformer_attn1_hidden, + attn1_nhead=repformer_attn1_nhead, + attn2_hidden=repformer_attn2_hidden, + attn2_nhead=repformer_attn2_nhead, + attn2_has_gate=repformer_attn2_has_gate, + activation=repformer_activation, + update_style=repformer_update_style, + set_davg_zero=repformer_set_davg_zero, + smooth=True, + add_type_ebd_to_seq=repformer_add_type_ebd_to_seq, + ) + self.type_embedding = TypeEmbedNet(ntypes, tebd_dim) + if self.repinit.dim_out == self.repformers.dim_in: + self.g1_shape_tranform = Identity() + else: + self.g1_shape_tranform = Linear( + self.repinit.dim_out, + self.repformers.dim_in, + bias=False, + init="glorot", + ) + assert self.repinit.rcut > self.repformers.rcut + assert self.repinit.sel[0] > self.repformers.sel[0] + self.concat_output_tebd = concat_output_tebd + self.tebd_dim = tebd_dim + self.rcut = self.repinit.get_rcut() + self.ntypes = ntypes + self.sel = self.repinit.sel + + def get_rcut(self) -> float: + """Returns the cut-off radius.""" + return self.rcut + + def get_nsel(self) -> int: + """Returns the number of selected atoms in the cut-off radius.""" + return sum(self.sel) + + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + return self.sel + + def get_ntype(self) -> int: + """Returns the number of element types.""" + return self.ntypes + + def get_dim_out(self) -> int: + """Returns the output dimension of this descriptor.""" + ret = self.repformers.dim_out + if self.concat_output_tebd: + ret += self.tebd_dim + return ret + + @property + def dim_out(self): + return self.get_dim_out() + + @property + def dim_emb(self): + """Returns the embedding dimension g2.""" + return self.repformers.dim_emb + + def compute_input_stats(self, merged): + sumr, suma, sumn, sumr2, suma2 = [], [], [], [], [] + for ii, descrpt in enumerate([self.repinit, self.repformers]): + merged_tmp = [ + { + key: item[key] if not isinstance(item[key], list) else item[key][ii] + for key in item + } + for item in merged + ] + ( + sumr_tmp, + suma_tmp, + sumn_tmp, + sumr2_tmp, + suma2_tmp, + ) = descrpt.compute_input_stats(merged_tmp) + sumr.append(sumr_tmp) + suma.append(suma_tmp) + sumn.append(sumn_tmp) + sumr2.append(sumr2_tmp) + suma2.append(suma2_tmp) + return sumr, suma, sumn, sumr2, suma2 + + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + for ii, descrpt in enumerate([self.repinit, self.repformers]): + descrpt.init_desc_stat(sumr[ii], suma[ii], sumn[ii], sumr2[ii], suma2[ii]) + + @classmethod + def get_stat_name(cls, config): + descrpt_type = config["type"] + assert descrpt_type in ["dpa2"] + return ( + f'stat_file_dpa2_repinit_rcut{config["repinit_rcut"]:.2f}_smth{config["repinit_rcut_smth"]:.2f}_sel{config["repinit_nsel"]}' + f'_repformer_rcut{config["repformer_rcut"]:.2f}_smth{config["repformer_rcut_smth"]:.2f}_sel{config["repformer_nsel"]}.npz' + ) + + @classmethod + def get_data_process_key(cls, config): + descrpt_type = config["type"] + assert descrpt_type in ["dpa2"] + return { + "sel": [config["repinit_nsel"], config["repformer_nsel"]], + "rcut": [config["repinit_rcut"], config["repformer_rcut"]], + } + + def forward( + self, + extended_coord: torch.Tensor, + extended_atype: torch.Tensor, + nlist: torch.Tensor, + mapping: Optional[torch.Tensor] = None, + ): + nframes, nloc, nnei = nlist.shape + nall = extended_coord.view(nframes, -1).shape[1] // 3 + # nlists + nlist_dict = build_multiple_neighbor_list( + extended_coord, + nlist, + [self.repformers.get_rcut(), self.repinit.get_rcut()], + [self.repformers.get_nsel(), self.repinit.get_nsel()], + ) + # repinit + g1_ext = self.type_embedding(extended_atype) + g1_inp = g1_ext[:, :nloc, :] + g1, _, _, _, _ = self.repinit( + nlist_dict[ + get_multiple_nlist_key(self.repinit.get_rcut(), self.repinit.get_nsel()) + ], + extended_coord, + extended_atype, + g1_ext, + mapping, + ) + # linear to change shape + g1 = self.g1_shape_tranform(g1) + # mapping g1 + assert mapping is not None + mapping_ext = ( + mapping.view(nframes, nall).unsqueeze(-1).expand(-1, -1, g1.shape[-1]) + ) + g1_ext = torch.gather(g1, 1, mapping_ext) + # repformer + g1, g2, h2, rot_mat, sw = self.repformers( + nlist_dict[ + get_multiple_nlist_key( + self.repformers.get_rcut(), self.repformers.get_nsel() + ) + ], + extended_coord, + extended_atype, + g1_ext, + mapping, + ) + if self.concat_output_tebd: + g1 = torch.cat([g1, g1_inp], dim=-1) + return g1, g2, h2, rot_mat, sw diff --git a/deepmd/pt/model/descriptor/env_mat.py b/deepmd/pt/model/descriptor/env_mat.py new file mode 100644 index 0000000000..63181388df --- /dev/null +++ b/deepmd/pt/model/descriptor/env_mat.py @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import torch + +from deepmd.pt.utils.preprocess import ( + compute_smooth_weight, +) + + +def _make_env_mat_se_a(nlist, coord, rcut: float, ruct_smth: float): + """Make smooth environment matrix.""" + bsz, natoms, nnei = nlist.shape + coord = coord.view(bsz, -1, 3) + mask = nlist >= 0 + nlist = nlist * mask + coord_l = coord[:, :natoms].view(bsz, -1, 1, 3) + index = nlist.view(bsz, -1).unsqueeze(-1).expand(-1, -1, 3) + coord_r = torch.gather(coord, 1, index) + coord_r = coord_r.view(bsz, natoms, nnei, 3) + diff = coord_r - coord_l + length = torch.linalg.norm(diff, dim=-1, keepdim=True) + # for index 0 nloc atom + length = length + ~mask.unsqueeze(-1) + t0 = 1 / length + t1 = diff / length**2 + weight = compute_smooth_weight(length, ruct_smth, rcut) + env_mat_se_a = torch.cat([t0, t1], dim=-1) * weight * mask.unsqueeze(-1) + return env_mat_se_a, diff * mask.unsqueeze(-1), weight + + +def prod_env_mat_se_a( + extended_coord, nlist, atype, mean, stddev, rcut: float, rcut_smth: float +): + """Generate smooth environment matrix from atom coordinates and other context. + + Args: + - extended_coord: Copied atom coordinates with shape [nframes, nall*3]. + - atype: Atom types with shape [nframes, nloc]. + - natoms: Batched atom statisics with shape [len(sec)+2]. + - box: Batched simulation box with shape [nframes, 9]. + - mean: Average value of descriptor per element type with shape [len(sec), nnei, 4]. + - stddev: Standard deviation of descriptor per element type with shape [len(sec), nnei, 4]. + - deriv_stddev: StdDev of descriptor derivative per element type with shape [len(sec), nnei, 4, 3]. + - rcut: Cut-off radius. + - rcut_smth: Smooth hyper-parameter for pair force & energy. + + Returns + ------- + - env_mat_se_a: Shape is [nframes, natoms[1]*nnei*4]. + """ + nframes = extended_coord.shape[0] + _env_mat_se_a, diff, switch = _make_env_mat_se_a( + nlist, extended_coord, rcut, rcut_smth + ) # shape [n_atom, dim, 4] + t_avg = mean[atype] # [n_atom, dim, 4] + t_std = stddev[atype] # [n_atom, dim, 4] + env_mat_se_a = (_env_mat_se_a - t_avg) / t_std + return env_mat_se_a, diff, switch diff --git a/deepmd/pt/model/descriptor/gaussian_lcc.py b/deepmd/pt/model/descriptor/gaussian_lcc.py new file mode 100644 index 0000000000..26ec1175b8 --- /dev/null +++ b/deepmd/pt/model/descriptor/gaussian_lcc.py @@ -0,0 +1,315 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import torch +import torch.nn as nn + +from deepmd.pt.model.descriptor import ( + Descriptor, +) +from deepmd.pt.model.network.network import ( + Evoformer3bEncoder, + GaussianEmbedding, + TypeEmbedNet, +) +from deepmd.pt.utils import ( + env, +) + + +class DescrptGaussianLcc(Descriptor): + def __init__( + self, + rcut, + rcut_smth, + sel: int, + ntypes: int, + num_pair: int, + embed_dim: int = 768, + kernel_num: int = 128, + pair_embed_dim: int = 64, + num_block: int = 1, + layer_num: int = 12, + attn_head: int = 48, + pair_hidden_dim: int = 16, + ffn_embedding_dim: int = 768, + dropout: float = 0.0, + droppath_prob: float = 0.1, + pair_dropout: float = 0.25, + attention_dropout: float = 0.1, + activation_dropout: float = 0.1, + pre_ln: bool = True, + do_tag_embedding: bool = False, + tag_ener_pref: bool = False, + atomic_sum_gbf: bool = False, + pre_add_seq: bool = True, + tri_update: bool = True, + **kwargs, + ): + """Construct a descriptor of Gaussian Based Local Cluster. + + Args: + - rcut: Cut-off radius. + - rcut_smth: Smooth hyper-parameter for pair force & energy. **Not used in this descriptor**. + - sel: For each element type, how many atoms is selected as neighbors. + - ntypes: Number of atom types. + - num_pair: Number of atom type pairs. Default is 2 * ntypes. + - kernel_num: Number of gaussian kernels. + - embed_dim: Dimension of atomic representation. + - pair_embed_dim: Dimension of pair representation. + - num_block: Number of evoformer blocks. + - layer_num: Number of attention layers. + - attn_head: Number of attention heads. + - pair_hidden_dim: Hidden dimension of pair representation during attention process. + - ffn_embedding_dim: Dimension during feed forward network. + - dropout: Dropout probability of atomic representation. + - droppath_prob: If not zero, it will use drop paths (Stochastic Depth) per sample and ignore `dropout`. + - pair_dropout: Dropout probability of pair representation during triangular update. + - attention_dropout: Dropout probability during attetion process. + - activation_dropout: Dropout probability of pair feed forward network. + - pre_ln: Do previous layer norm or not. + - do_tag_embedding: Add tag embedding to atomic and pair representations. (`tags`, `tags2`, `tags3` must exist) + - atomic_sum_gbf: Add sum of gaussian outputs to atomic representation or not. + - pre_add_seq: Add output of other descriptor (if has) to the atomic representation before attention. + """ + super().__init__() + self.rcut = rcut + self.rcut_smth = rcut_smth + self.embed_dim = embed_dim + self.num_pair = num_pair + self.kernel_num = kernel_num + self.pair_embed_dim = pair_embed_dim + self.num_block = num_block + self.layer_num = layer_num + self.attention_heads = attn_head + self.pair_hidden_dim = pair_hidden_dim + self.ffn_embedding_dim = ffn_embedding_dim + self.dropout = dropout + self.droppath_prob = droppath_prob + self.pair_dropout = pair_dropout + self.attention_dropout = attention_dropout + self.activation_dropout = activation_dropout + self.pre_ln = pre_ln + self.do_tag_embedding = do_tag_embedding + self.tag_ener_pref = tag_ener_pref + self.atomic_sum_gbf = atomic_sum_gbf + self.local_cluster = True + self.pre_add_seq = pre_add_seq + self.tri_update = tri_update + + if isinstance(sel, int): + sel = [sel] + + self.ntypes = ntypes + self.sec = torch.tensor(sel) + self.nnei = sum(sel) + + if self.do_tag_embedding: + self.tag_encoder = nn.Embedding(3, self.embed_dim) + self.tag_encoder2 = nn.Embedding(2, self.embed_dim) + self.tag_type_embedding = TypeEmbedNet(10, pair_embed_dim) + self.edge_type_embedding = nn.Embedding( + (ntypes + 1) * (ntypes + 1), + pair_embed_dim, + padding_idx=(ntypes + 1) * (ntypes + 1) - 1, + dtype=env.GLOBAL_PT_FLOAT_PRECISION, + ) + self.gaussian_encoder = GaussianEmbedding( + rcut, + kernel_num, + num_pair, + embed_dim, + pair_embed_dim, + sel, + ntypes, + atomic_sum_gbf, + ) + self.backbone = Evoformer3bEncoder( + self.nnei, + layer_num=self.layer_num, + attn_head=self.attention_heads, + atomic_dim=self.embed_dim, + pair_dim=self.pair_embed_dim, + pair_hidden_dim=self.pair_hidden_dim, + ffn_embedding_dim=self.ffn_embedding_dim, + dropout=self.dropout, + droppath_prob=self.droppath_prob, + pair_dropout=self.pair_dropout, + attention_dropout=self.attention_dropout, + activation_dropout=self.activation_dropout, + pre_ln=self.pre_ln, + tri_update=self.tri_update, + ) + + @property + def dim_out(self): + """Returns the output dimension of atomic representation.""" + return self.embed_dim + + @property + def dim_in(self): + """Returns the atomic input dimension of this descriptor.""" + return self.embed_dim + + @property + def dim_emb(self): + """Returns the output dimension of pair representation.""" + return self.pair_embed_dim + + def compute_input_stats(self, merged): + """Update mean and stddev for descriptor elements.""" + return [], [], [], [], [] + + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + pass + + def forward( + self, + extended_coord, + nlist, + atype, + nlist_type, + nlist_loc=None, + atype_tebd=None, + nlist_tebd=None, + seq_input=None, + ): + """Calculate the atomic and pair representations of this descriptor. + + Args: + - extended_coord: Copied atom coordinates with shape [nframes, nall, 3]. + - nlist: Neighbor list with shape [nframes, nloc, nnei]. + - atype: Atom type with shape [nframes, nloc]. + - nlist_type: Atom type of neighbors with shape [nframes, nloc, nnei]. + - nlist_loc: Local index of neighbor list with shape [nframes, nloc, nnei]. + - atype_tebd: Atomic type embedding with shape [nframes, nloc, tebd_dim]. + - nlist_tebd: Type embeddings of neighbor with shape [nframes, nloc, nnei, tebd_dim]. + - seq_input: The sequential input from other descriptor with + shape [nframes, nloc, tebd_dim] or [nframes * nloc, 1 + nnei, tebd_dim] + + Returns + ------- + - result: descriptor with shape [nframes, nloc, self.filter_neuron[-1] * self.axis_neuron]. + - ret: environment matrix with shape [nframes, nloc, self.neei, out_size] + """ + nframes, nloc = nlist.shape[:2] + nall = extended_coord.shape[1] + nlist2 = torch.cat( + [ + torch.arange(0, nloc, device=nlist.device) + .reshape(1, nloc, 1) + .expand(nframes, -1, -1), + nlist, + ], + dim=-1, + ) + nlist_loc2 = torch.cat( + [ + torch.arange(0, nloc, device=nlist_loc.device) + .reshape(1, nloc, 1) + .expand(nframes, -1, -1), + nlist_loc, + ], + dim=-1, + ) + nlist_type2 = torch.cat([atype.reshape(nframes, nloc, 1), nlist_type], dim=-1) + nnei2_mask = nlist2 != -1 + padding_mask = nlist2 == -1 + nlist2 = nlist2 * nnei2_mask + nlist_loc2 = nlist_loc2 * nnei2_mask + + # nframes x nloc x (1 + nnei2) x (1 + nnei2) + pair_mask = nnei2_mask.unsqueeze(-1) * nnei2_mask.unsqueeze(-2) + # nframes x nloc x (1 + nnei2) x (1 + nnei2) x head + attn_mask = torch.zeros( + [nframes, nloc, 1 + self.nnei, 1 + self.nnei, self.attention_heads], + device=nlist.device, + dtype=extended_coord.dtype, + ) + attn_mask.masked_fill_(padding_mask.unsqueeze(2).unsqueeze(-1), float("-inf")) + # (nframes x nloc) x head x (1 + nnei2) x (1 + nnei2) + attn_mask = ( + attn_mask.reshape( + nframes * nloc, 1 + self.nnei, 1 + self.nnei, self.attention_heads + ) + .permute(0, 3, 1, 2) + .contiguous() + ) + + # Atomic feature + # [(nframes x nloc) x (1 + nnei2) x tebd_dim] + atom_feature = torch.gather( + atype_tebd, + dim=1, + index=nlist_loc2.reshape(nframes, -1) + .unsqueeze(-1) + .expand(-1, -1, self.embed_dim), + ).reshape(nframes * nloc, 1 + self.nnei, self.embed_dim) + if self.pre_add_seq and seq_input is not None: + first_dim = seq_input.shape[0] + if first_dim == nframes * nloc: + atom_feature += seq_input + elif first_dim == nframes: + atom_feature_seq = torch.gather( + seq_input, + dim=1, + index=nlist_loc2.reshape(nframes, -1) + .unsqueeze(-1) + .expand(-1, -1, self.embed_dim), + ).reshape(nframes * nloc, 1 + self.nnei, self.embed_dim) + atom_feature += atom_feature_seq + else: + raise RuntimeError + atom_feature = atom_feature * nnei2_mask.reshape( + nframes * nloc, 1 + self.nnei, 1 + ) + + # Pair feature + # [(nframes x nloc) x (1 + nnei2)] + nlist_type2_reshape = nlist_type2.reshape(nframes * nloc, 1 + self.nnei) + # [(nframes x nloc) x (1 + nnei2) x (1 + nnei2)] + edge_type = nlist_type2_reshape.unsqueeze(-1) * ( + self.ntypes + 1 + ) + nlist_type2_reshape.unsqueeze(-2) + # [(nframes x nloc) x (1 + nnei2) x (1 + nnei2) x pair_dim] + edge_feature = self.edge_type_embedding(edge_type) + + # [(nframes x nloc) x (1 + nnei2) x (1 + nnei2) x 2] + edge_type_2dim = torch.cat( + [ + nlist_type2_reshape.view(nframes * nloc, 1 + self.nnei, 1, 1).expand( + -1, -1, 1 + self.nnei, -1 + ), + nlist_type2_reshape.view(nframes * nloc, 1, 1 + self.nnei, 1).expand( + -1, 1 + self.nnei, -1, -1 + ) + + self.ntypes, + ], + dim=-1, + ) + # [(nframes x nloc) x (1 + nnei2) x 3] + coord_selected = torch.gather( + extended_coord.unsqueeze(1) + .expand(-1, nloc, -1, -1) + .reshape(nframes * nloc, nall, 3), + dim=1, + index=nlist2.reshape(nframes * nloc, 1 + self.nnei, 1).expand(-1, -1, 3), + ) + + # Update pair features (or and atomic features) with gbf features + # delta_pos: [(nframes x nloc) x (1 + nnei2) x (1 + nnei2) x 3]. + atomic_feature, pair_feature, delta_pos = self.gaussian_encoder( + coord_selected, atom_feature, edge_type_2dim, edge_feature + ) + # [(nframes x nloc) x (1 + nnei2) x (1 + nnei2) x pair_dim] + attn_bias = pair_feature + + # output: [(nframes x nloc) x (1 + nnei2) x tebd_dim] + # pair: [(nframes x nloc) x (1 + nnei2) x (1 + nnei2) x pair_dim] + output, pair = self.backbone( + atomic_feature, + pair=attn_bias, + attn_mask=attn_mask, + pair_mask=pair_mask, + atom_mask=nnei2_mask.reshape(nframes * nloc, 1 + self.nnei), + ) + + return output, pair, delta_pos, None diff --git a/deepmd/pt/model/descriptor/hybrid.py b/deepmd/pt/model/descriptor/hybrid.py new file mode 100644 index 0000000000..11bbc80729 --- /dev/null +++ b/deepmd/pt/model/descriptor/hybrid.py @@ -0,0 +1,257 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + List, + Optional, +) + +import torch + +from deepmd.pt.model.descriptor import ( + DescriptorBlock, +) +from deepmd.pt.model.network.network import ( + Identity, + Linear, +) + + +@DescriptorBlock.register("hybrid") +class DescrptBlockHybrid(DescriptorBlock): + def __init__( + self, + list, + ntypes: int, + tebd_dim: int = 8, + tebd_input_mode: str = "concat", + hybrid_mode: str = "concat", + **kwargs, + ): + """Construct a hybrid descriptor. + + Args: + - descriptor_list: list of descriptors. + - descriptor_param: descriptor configs. + """ + super().__init__() + supported_descrpt = ["se_atten", "se_uni"] + descriptor_list = [] + for descriptor_param_item in list: + descriptor_type_tmp = descriptor_param_item["type"] + assert ( + descriptor_type_tmp in supported_descrpt + ), f"Only descriptors in {supported_descrpt} are supported for `hybrid` descriptor!" + descriptor_param_item["ntypes"] = ntypes + if descriptor_type_tmp == "se_atten": + descriptor_param_item["tebd_dim"] = tebd_dim + descriptor_param_item["tebd_input_mode"] = tebd_input_mode + descriptor_list.append(DescriptorBlock(**descriptor_param_item)) + self.descriptor_list = torch.nn.ModuleList(descriptor_list) + self.descriptor_param = list + self.rcut = [descrpt.rcut for descrpt in self.descriptor_list] + self.sec = [descrpt.sec for descrpt in self.descriptor_list] + self.sel = [descrpt.sel for descrpt in self.descriptor_list] + self.split_sel = [sum(ii) for ii in self.sel] + self.local_cluster_list = [ + descrpt.local_cluster for descrpt in self.descriptor_list + ] + self.local_cluster = True in self.local_cluster_list + self.hybrid_mode = hybrid_mode + self.tebd_dim = tebd_dim + assert self.hybrid_mode in ["concat", "sequential"] + sequential_transform = [] + if self.hybrid_mode == "sequential": + for ii in range(len(descriptor_list) - 1): + if descriptor_list[ii].dim_out == descriptor_list[ii + 1].dim_in: + sequential_transform.append(Identity()) + else: + sequential_transform.append( + Linear( + descriptor_list[ii].dim_out, + descriptor_list[ii + 1].dim_in, + bias=False, + init="glorot", + ) + ) + sequential_transform.append(Identity()) + self.sequential_transform = torch.nn.ModuleList(sequential_transform) + self.ntypes = ntypes + + def get_rcut(self) -> float: + """Returns the cut-off radius.""" + return self.rcut + + def get_nsel(self) -> int: + """Returns the number of selected atoms in the cut-off radius.""" + return [sum(ii) for ii in self.get_sel()] + + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + return self.sel + + def get_ntype(self) -> int: + """Returns the number of element types.""" + return self.ntypes + + def get_dim_out(self) -> int: + """Returns the output dimension.""" + return self.dim_out + + def get_dim_in(self) -> int: + """Returns the input dimension.""" + return self.dim_in + + @property + def dim_out(self): + """Returns the output dimension of this descriptor.""" + if self.hybrid_mode == "concat": + return sum([descrpt.dim_out for descrpt in self.descriptor_list]) + elif self.hybrid_mode == "sequential": + return self.descriptor_list[-1].dim_out + else: + raise RuntimeError + + @property + def dim_emb_list(self) -> List[int]: + """Returns the output dimension list of embeddings.""" + return [descrpt.dim_emb for descrpt in self.descriptor_list] + + @property + def dim_emb(self): + """Returns the output dimension of embedding.""" + if self.hybrid_mode == "concat": + return sum(self.dim_emb_list) + elif self.hybrid_mode == "sequential": + return self.descriptor_list[-1].dim_emb + else: + raise RuntimeError + + def share_params(self, base_class, shared_level, resume=False): + assert ( + self.__class__ == base_class.__class__ + ), "Only descriptors of the same type can share params!" + if shared_level == 0: + for ii, des in enumerate(self.descriptor_list): + self.descriptor_list[ii].share_params( + base_class.descriptor_list[ii], shared_level, resume=resume + ) + if self.hybrid_mode == "sequential": + self.sequential_transform = base_class.sequential_transform + else: + raise NotImplementedError + + def compute_input_stats(self, merged): + """Update mean and stddev for descriptor elements.""" + sumr, suma, sumn, sumr2, suma2 = [], [], [], [], [] + for ii, descrpt in enumerate(self.descriptor_list): + merged_tmp = [ + { + key: item[key] if not isinstance(item[key], list) else item[key][ii] + for key in item + } + for item in merged + ] + ( + sumr_tmp, + suma_tmp, + sumn_tmp, + sumr2_tmp, + suma2_tmp, + ) = descrpt.compute_input_stats(merged_tmp) + sumr.append(sumr_tmp) + suma.append(suma_tmp) + sumn.append(sumn_tmp) + sumr2.append(sumr2_tmp) + suma2.append(suma2_tmp) + return sumr, suma, sumn, sumr2, suma2 + + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + for ii, descrpt in enumerate(self.descriptor_list): + descrpt.init_desc_stat(sumr[ii], suma[ii], sumn[ii], sumr2[ii], suma2[ii]) + + def forward( + self, + nlist: torch.Tensor, + extended_coord: torch.Tensor, + extended_atype: torch.Tensor, + extended_atype_embd: Optional[torch.Tensor] = None, + mapping: Optional[torch.Tensor] = None, + ): + """Calculate decoded embedding for each atom. + + Args: + - extended_coord: Tell atom coordinates with shape [nframes, natoms[1]*3]. + - nlist: Tell atom types with shape [nframes, natoms[1]]. + - atype: Tell atom count and element count. Its shape is [2+self.ntypes]. + - nlist_type: Tell simulation box with shape [nframes, 9]. + - atype_tebd: Tell simulation box with shape [nframes, 9]. + - nlist_tebd: Tell simulation box with shape [nframes, 9]. + + Returns + ------- + - result: descriptor with shape [nframes, nloc, self.filter_neuron[-1] * self.axis_neuron]. + - ret: environment matrix with shape [nframes, nloc, self.neei, out_size] + """ + nlist_list = list(torch.split(nlist, self.split_sel, -1)) + nframes, nloc, nnei = nlist.shape + concat_rot_mat = True + if self.hybrid_mode == "concat": + out_descriptor = [] + # out_env_mat = [] + out_rot_mat_list = [] + # out_diff = [] + for ii, descrpt in enumerate(self.descriptor_list): + descriptor, env_mat, diff, rot_mat, sw = descrpt( + nlist_list[ii], + extended_coord, + extended_atype, + extended_atype_embd, + mapping, + ) + if descriptor.shape[0] == nframes * nloc: + # [nframes * nloc, 1 + nnei, emb_dim] + descriptor = descriptor[:, 0, :].reshape(nframes, nloc, -1) + out_descriptor.append(descriptor) + # out_env_mat.append(env_mat) + # out_diff.append(diff) + out_rot_mat_list.append(rot_mat) + if rot_mat is None: + concat_rot_mat = False + out_descriptor = torch.concat(out_descriptor, dim=-1) + if concat_rot_mat: + out_rot_mat = torch.concat(out_rot_mat_list, dim=-2) + else: + out_rot_mat = None + return out_descriptor, None, None, out_rot_mat, sw + elif self.hybrid_mode == "sequential": + assert extended_atype_embd is not None + assert mapping is not None + nframes, nloc, nnei = nlist.shape + nall = extended_coord.view(nframes, -1).shape[1] // 3 + seq_input_ext = extended_atype_embd + seq_input = ( + seq_input_ext[:, :nloc, :] if len(self.descriptor_list) == 0 else None + ) + env_mat, diff, rot_mat, sw = None, None, None, None + env_mat_list, diff_list = [], [] + for ii, (descrpt, seq_transform) in enumerate( + zip(self.descriptor_list, self.sequential_transform) + ): + seq_output, env_mat, diff, rot_mat, sw = descrpt( + nlist_list[ii], + extended_coord, + extended_atype, + seq_input_ext, + mapping, + ) + seq_input = seq_transform(seq_output) + mapping_ext = ( + mapping.view(nframes, nall) + .unsqueeze(-1) + .expand(-1, -1, seq_input.shape[-1]) + ) + seq_input_ext = torch.gather(seq_input, 1, mapping_ext) + env_mat_list.append(env_mat) + diff_list.append(diff) + return seq_input, env_mat_list, diff_list, rot_mat, sw + else: + raise RuntimeError diff --git a/deepmd/pt/model/descriptor/repformer_layer.py b/deepmd/pt/model/descriptor/repformer_layer.py new file mode 100644 index 0000000000..21ae0ff6f3 --- /dev/null +++ b/deepmd/pt/model/descriptor/repformer_layer.py @@ -0,0 +1,749 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Callable, + List, +) + +import torch + +from deepmd.pt.model.network.network import ( + SimpleLinear, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.utils import ( + get_activation_fn, +) + + +def torch_linear(*args, **kwargs): + return torch.nn.Linear( + *args, **kwargs, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ) + + +def _make_nei_g1( + g1_ext: torch.Tensor, + nlist: torch.Tensor, +) -> torch.Tensor: + # nlist: nb x nloc x nnei + nb, nloc, nnei = nlist.shape + # g1_ext: nb x nall x ng1 + ng1 = g1_ext.shape[-1] + # index: nb x (nloc x nnei) x ng1 + index = nlist.reshape(nb, nloc * nnei).unsqueeze(-1).expand(-1, -1, ng1) + # gg1 : nb x (nloc x nnei) x ng1 + gg1 = torch.gather(g1_ext, dim=1, index=index) + # gg1 : nb x nloc x nnei x ng1 + gg1 = gg1.view(nb, nloc, nnei, ng1) + return gg1 + + +def _apply_nlist_mask( + gg: torch.Tensor, + nlist_mask: torch.Tensor, +) -> torch.Tensor: + # gg: nf x nloc x nnei x ng + # msk: nf x nloc x nnei + return gg.masked_fill(~nlist_mask.unsqueeze(-1), 0.0) + + +def _apply_switch(gg: torch.Tensor, sw: torch.Tensor) -> torch.Tensor: + # gg: nf x nloc x nnei x ng + # sw: nf x nloc x nnei + return gg * sw.unsqueeze(-1) + + +def _apply_h_norm( + hh: torch.Tensor, # nf x nloc x nnei x 3 +) -> torch.Tensor: + """Normalize h by the std of vector length. + do not have an idea if this is a good way. + """ + nf, nl, nnei, _ = hh.shape + # nf x nloc x nnei + normh = torch.linalg.norm(hh, dim=-1) + # nf x nloc + std = torch.std(normh, dim=-1) + # nf x nloc x nnei x 3 + hh = hh[:, :, :, :] / (1.0 + std[:, :, None, None]) + return hh + + +class Atten2Map(torch.nn.Module): + def __init__( + self, + ni: int, + nd: int, + nh: int, + has_gate: bool = False, # apply gate to attn map + smooth: bool = True, + attnw_shift: float = 20.0, + ): + super().__init__() + self.ni = ni + self.nd = nd + self.nh = nh + self.mapqk = SimpleLinear(ni, nd * 2 * nh, bias=False) + self.has_gate = has_gate + self.smooth = smooth + self.attnw_shift = attnw_shift + + def forward( + self, + g2: torch.Tensor, # nb x nloc x nnei x ng2 + h2: torch.Tensor, # nb x nloc x nnei x 3 + nlist_mask: torch.Tensor, # nb x nloc x nnei + sw: torch.Tensor, # nb x nloc x nnei + ) -> torch.Tensor: + ( + nb, + nloc, + nnei, + _, + ) = g2.shape + nd, nh = self.nd, self.nh + # nb x nloc x nnei x nd x (nh x 2) + g2qk = self.mapqk(g2).view(nb, nloc, nnei, nd, nh * 2) + # nb x nloc x (nh x 2) x nnei x nd + g2qk = torch.permute(g2qk, (0, 1, 4, 2, 3)) + # nb x nloc x nh x nnei x nd + g2q, g2k = torch.split(g2qk, nh, dim=2) + # g2q = torch.nn.functional.normalize(g2q, dim=-1) + # g2k = torch.nn.functional.normalize(g2k, dim=-1) + # nb x nloc x nh x nnei x nnei + attnw = torch.matmul(g2q, torch.transpose(g2k, -1, -2)) / nd**0.5 + if self.has_gate: + gate = torch.matmul(h2, torch.transpose(h2, -1, -2)).unsqueeze(-3) + attnw = attnw * gate + # mask the attenmap, nb x nloc x 1 x 1 x nnei + attnw_mask = ~nlist_mask.unsqueeze(2).unsqueeze(2) + # mask the attenmap, nb x nloc x 1 x nnei x 1 + attnw_mask_c = ~nlist_mask.unsqueeze(2).unsqueeze(-1) + if self.smooth: + attnw = (attnw + self.attnw_shift) * sw[:, :, None, :, None] * sw[ + :, :, None, None, : + ] - self.attnw_shift + else: + attnw = attnw.masked_fill( + attnw_mask, + float("-inf"), + ) + attnw = torch.softmax(attnw, dim=-1) + attnw = attnw.masked_fill( + attnw_mask, + 0.0, + ) + # nb x nloc x nh x nnei x nnei + attnw = attnw.masked_fill( + attnw_mask_c, + 0.0, + ) + if self.smooth: + attnw = attnw * sw[:, :, None, :, None] * sw[:, :, None, None, :] + # nb x nloc x nnei x nnei + h2h2t = torch.matmul(h2, torch.transpose(h2, -1, -2)) / 3.0**0.5 + # nb x nloc x nh x nnei x nnei + ret = attnw * h2h2t[:, :, None, :, :] + # ret = torch.softmax(g2qk, dim=-1) + # nb x nloc x nnei x nnei x nh + ret = torch.permute(ret, (0, 1, 3, 4, 2)) + return ret + + +class Atten2MultiHeadApply(torch.nn.Module): + def __init__( + self, + ni: int, + nh: int, + ): + super().__init__() + self.ni = ni + self.nh = nh + self.mapv = SimpleLinear(ni, ni * nh, bias=False) + self.head_map = SimpleLinear(ni * nh, ni) + + def forward( + self, + AA: torch.Tensor, # nf x nloc x nnei x nnei x nh + g2: torch.Tensor, # nf x nloc x nnei x ng2 + ) -> torch.Tensor: + nf, nloc, nnei, ng2 = g2.shape + nh = self.nh + # nf x nloc x nnei x ng2 x nh + g2v = self.mapv(g2).view(nf, nloc, nnei, ng2, nh) + # nf x nloc x nh x nnei x ng2 + g2v = torch.permute(g2v, (0, 1, 4, 2, 3)) + # g2v = torch.nn.functional.normalize(g2v, dim=-1) + # nf x nloc x nh x nnei x nnei + AA = torch.permute(AA, (0, 1, 4, 2, 3)) + # nf x nloc x nh x nnei x ng2 + ret = torch.matmul(AA, g2v) + # nf x nloc x nnei x ng2 x nh + ret = torch.permute(ret, (0, 1, 3, 4, 2)).reshape(nf, nloc, nnei, (ng2 * nh)) + # nf x nloc x nnei x ng2 + return self.head_map(ret) + + +class Atten2EquiVarApply(torch.nn.Module): + def __init__( + self, + ni: int, + nh: int, + ): + super().__init__() + self.ni = ni + self.nh = nh + self.head_map = SimpleLinear(nh, 1, bias=False) + + def forward( + self, + AA: torch.Tensor, # nf x nloc x nnei x nnei x nh + h2: torch.Tensor, # nf x nloc x nnei x 3 + ) -> torch.Tensor: + nf, nloc, nnei, _ = h2.shape + nh = self.nh + # nf x nloc x nh x nnei x nnei + AA = torch.permute(AA, (0, 1, 4, 2, 3)) + h2m = torch.unsqueeze(h2, dim=2) + # nf x nloc x nh x nnei x 3 + h2m = torch.tile(h2m, [1, 1, nh, 1, 1]) + # nf x nloc x nh x nnei x 3 + ret = torch.matmul(AA, h2m) + # nf x nloc x nnei x 3 x nh + ret = torch.permute(ret, (0, 1, 3, 4, 2)).view(nf, nloc, nnei, 3, nh) + # nf x nloc x nnei x 3 + return torch.squeeze(self.head_map(ret), dim=-1) + + +class LocalAtten(torch.nn.Module): + def __init__( + self, + ni: int, + nd: int, + nh: int, + smooth: bool = True, + attnw_shift: float = 20.0, + ): + super().__init__() + self.ni = ni + self.nd = nd + self.nh = nh + self.mapq = SimpleLinear(ni, nd * 1 * nh, bias=False) + self.mapkv = SimpleLinear(ni, (nd + ni) * nh, bias=False) + self.head_map = SimpleLinear(ni * nh, ni) + self.smooth = smooth + self.attnw_shift = attnw_shift + + def forward( + self, + g1: torch.Tensor, # nb x nloc x ng1 + gg1: torch.Tensor, # nb x nloc x nnei x ng1 + nlist_mask: torch.Tensor, # nb x nloc x nnei + sw: torch.Tensor, # nb x nloc x nnei + ) -> torch.Tensor: + nb, nloc, nnei = nlist_mask.shape + ni, nd, nh = self.ni, self.nd, self.nh + assert ni == g1.shape[-1] + assert ni == gg1.shape[-1] + # nb x nloc x nd x nh + g1q = self.mapq(g1).view(nb, nloc, nd, nh) + # nb x nloc x nh x nd + g1q = torch.permute(g1q, (0, 1, 3, 2)) + # nb x nloc x nnei x (nd+ni) x nh + gg1kv = self.mapkv(gg1).view(nb, nloc, nnei, nd + ni, nh) + gg1kv = torch.permute(gg1kv, (0, 1, 4, 2, 3)) + # nb x nloc x nh x nnei x nd, nb x nloc x nh x nnei x ng1 + gg1k, gg1v = torch.split(gg1kv, [nd, ni], dim=-1) + + # nb x nloc x nh x 1 x nnei + attnw = torch.matmul(g1q.unsqueeze(-2), torch.transpose(gg1k, -1, -2)) / nd**0.5 + # nb x nloc x nh x nnei + attnw = attnw.squeeze(-2) + # mask the attenmap, nb x nloc x 1 x nnei + attnw_mask = ~nlist_mask.unsqueeze(-2) + # nb x nloc x nh x nnei + if self.smooth: + attnw = (attnw + self.attnw_shift) * sw.unsqueeze(-2) - self.attnw_shift + else: + attnw = attnw.masked_fill( + attnw_mask, + float("-inf"), + ) + attnw = torch.softmax(attnw, dim=-1) + attnw = attnw.masked_fill( + attnw_mask, + 0.0, + ) + if self.smooth: + attnw = attnw * sw.unsqueeze(-2) + + # nb x nloc x nh x ng1 + ret = ( + torch.matmul(attnw.unsqueeze(-2), gg1v).squeeze(-2).view(nb, nloc, nh * ni) + ) + # nb x nloc x ng1 + ret = self.head_map(ret) + return ret + + +class RepformerLayer(torch.nn.Module): + def __init__( + self, + rcut, + rcut_smth, + sel: int, + ntypes: int, + g1_dim=128, + g2_dim=16, + axis_dim: int = 4, + update_chnnl_2: bool = True, + do_bn_mode: str = "no", + bn_momentum: float = 0.1, + update_g1_has_conv: bool = True, + update_g1_has_drrd: bool = True, + update_g1_has_grrg: bool = True, + update_g1_has_attn: bool = True, + update_g2_has_g1g1: bool = True, + update_g2_has_attn: bool = True, + update_h2: bool = False, + attn1_hidden: int = 64, + attn1_nhead: int = 4, + attn2_hidden: int = 16, + attn2_nhead: int = 4, + attn2_has_gate: bool = False, + activation: str = "tanh", + update_style: str = "res_avg", + set_davg_zero: bool = True, # TODO + smooth: bool = True, + ): + super().__init__() + self.epsilon = 1e-4 # protection of 1./nnei + self.rcut = rcut + self.rcut_smth = rcut_smth + self.ntypes = ntypes + sel = [sel] if isinstance(sel, int) else sel + self.nnei = sum(sel) + assert len(sel) == 1 + self.sel = torch.tensor(sel) + self.sec = self.sel + self.axis_dim = axis_dim + self.set_davg_zero = set_davg_zero + self.do_bn_mode = do_bn_mode + self.bn_momentum = bn_momentum + self.act = get_activation_fn(activation) + self.update_g1_has_grrg = update_g1_has_grrg + self.update_g1_has_drrd = update_g1_has_drrd + self.update_g1_has_conv = update_g1_has_conv + self.update_g1_has_attn = update_g1_has_attn + self.update_chnnl_2 = update_chnnl_2 + self.update_g2_has_g1g1 = update_g2_has_g1g1 if self.update_chnnl_2 else False + self.update_g2_has_attn = update_g2_has_attn if self.update_chnnl_2 else False + self.update_h2 = update_h2 if self.update_chnnl_2 else False + del update_g2_has_g1g1, update_g2_has_attn, update_h2 + self.update_style = update_style + self.smooth = smooth + self.g1_dim = g1_dim + self.g2_dim = g2_dim + + g1_in_dim = self.cal_1_dim(g1_dim, g2_dim, self.axis_dim) + self.linear1 = SimpleLinear(g1_in_dim, g1_dim) + self.linear2 = None + self.proj_g1g2 = None + self.proj_g1g1g2 = None + self.attn2g_map = None + self.attn2_mh_apply = None + self.attn2_lm = None + self.attn2h_map = None + self.attn2_ev_apply = None + self.loc_attn = None + + if self.update_chnnl_2: + self.linear2 = SimpleLinear(g2_dim, g2_dim) + if self.update_g1_has_conv: + self.proj_g1g2 = SimpleLinear(g1_dim, g2_dim, bias=False) + if self.update_g2_has_g1g1: + self.proj_g1g1g2 = SimpleLinear(g1_dim, g2_dim, bias=False) + if self.update_g2_has_attn: + self.attn2g_map = Atten2Map( + g2_dim, attn2_hidden, attn2_nhead, attn2_has_gate, self.smooth + ) + self.attn2_mh_apply = Atten2MultiHeadApply(g2_dim, attn2_nhead) + self.attn2_lm = torch.nn.LayerNorm( + g2_dim, + elementwise_affine=True, + device=env.DEVICE, + dtype=env.GLOBAL_PT_FLOAT_PRECISION, + ) + if self.update_h2: + self.attn2h_map = Atten2Map( + g2_dim, attn2_hidden, attn2_nhead, attn2_has_gate, self.smooth + ) + self.attn2_ev_apply = Atten2EquiVarApply(g2_dim, attn2_nhead) + if self.update_g1_has_attn: + self.loc_attn = LocalAtten(g1_dim, attn1_hidden, attn1_nhead, self.smooth) + + if self.do_bn_mode == "uniform": + self.bn1 = self._bn_layer() + self.bn2 = self._bn_layer() + elif self.do_bn_mode == "component": + self.bn1 = self._bn_layer(nf=g1_dim) + self.bn2 = self._bn_layer(nf=g2_dim) + elif self.do_bn_mode == "no": + self.bn1, self.bn2 = None, None + else: + raise RuntimeError(f"unknown bn_mode {self.do_bn_mode}") + + def cal_1_dim(self, g1d: int, g2d: int, ax: int) -> int: + ret = g1d + if self.update_g1_has_grrg: + ret += g2d * ax + if self.update_g1_has_drrd: + ret += g1d * ax + if self.update_g1_has_conv: + ret += g2d + return ret + + def _update_h2( + self, + g2: torch.Tensor, + h2: torch.Tensor, + nlist_mask: torch.Tensor, + sw: torch.Tensor, + ) -> torch.Tensor: + assert self.attn2h_map is not None + assert self.attn2_ev_apply is not None + nb, nloc, nnei, _ = g2.shape + # # nb x nloc x nnei x nh2 + # h2_1 = self.attn2_ev_apply(AA, h2) + # h2_update.append(h2_1) + # nb x nloc x nnei x nnei x nh + AAh = self.attn2h_map(g2, h2, nlist_mask, sw) + # nb x nloc x nnei x nh2 + h2_1 = self.attn2_ev_apply(AAh, h2) + return h2_1 + + def _update_g1_conv( + self, + gg1: torch.Tensor, + g2: torch.Tensor, + nlist_mask: torch.Tensor, + sw: torch.Tensor, + ) -> torch.Tensor: + assert self.proj_g1g2 is not None + nb, nloc, nnei, _ = g2.shape + ng1 = gg1.shape[-1] + ng2 = g2.shape[-1] + # gg1 : nb x nloc x nnei x ng2 + gg1 = self.proj_g1g2(gg1).view(nb, nloc, nnei, ng2) + # nb x nloc x nnei x ng2 + gg1 = _apply_nlist_mask(gg1, nlist_mask) + if not self.smooth: + # normalized by number of neighbors, not smooth + # nb x nloc x 1 + invnnei = 1.0 / (self.epsilon + torch.sum(nlist_mask, dim=-1)).unsqueeze(-1) + else: + gg1 = _apply_switch(gg1, sw) + invnnei = (1.0 / float(nnei)) * torch.ones( + (nb, nloc, 1), dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ) + # nb x nloc x ng2 + g1_11 = torch.sum(g2 * gg1, dim=2) * invnnei + return g1_11 + + def _cal_h2g2( + self, + g2: torch.Tensor, + h2: torch.Tensor, + nlist_mask: torch.Tensor, + sw: torch.Tensor, + ) -> torch.Tensor: + # g2: nf x nloc x nnei x ng2 + # h2: nf x nloc x nnei x 3 + # msk: nf x nloc x nnei + nb, nloc, nnei, _ = g2.shape + ng2 = g2.shape[-1] + # nb x nloc x nnei x ng2 + g2 = _apply_nlist_mask(g2, nlist_mask) + if not self.smooth: + # nb x nloc + invnnei = 1.0 / (self.epsilon + torch.sum(nlist_mask, dim=-1)) + # nb x nloc x 1 x 1 + invnnei = invnnei.unsqueeze(-1).unsqueeze(-1) + else: + g2 = _apply_switch(g2, sw) + invnnei = (1.0 / float(nnei)) * torch.ones( + (nb, nloc, 1, 1), dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ) + # nb x nloc x 3 x ng2 + h2g2 = torch.matmul(torch.transpose(h2, -1, -2), g2) * invnnei + return h2g2 + + def _cal_grrg(self, h2g2: torch.Tensor) -> torch.Tensor: + # nb x nloc x 3 x ng2 + nb, nloc, _, ng2 = h2g2.shape + # nb x nloc x 3 x axis + h2g2m = torch.split(h2g2, self.axis_dim, dim=-1)[0] + # nb x nloc x axis x ng2 + g1_13 = torch.matmul(torch.transpose(h2g2m, -1, -2), h2g2) / (3.0**1) + # nb x nloc x (axisxng2) + g1_13 = g1_13.view(nb, nloc, self.axis_dim * ng2) + return g1_13 + + def _update_g1_grrg( + self, + g2: torch.Tensor, + h2: torch.Tensor, + nlist_mask: torch.Tensor, + sw: torch.Tensor, + ) -> torch.Tensor: + # g2: nf x nloc x nnei x ng2 + # h2: nf x nloc x nnei x 3 + # msk: nf x nloc x nnei + nb, nloc, nnei, _ = g2.shape + ng2 = g2.shape[-1] + # nb x nloc x 3 x ng2 + h2g2 = self._cal_h2g2(g2, h2, nlist_mask, sw) + # nb x nloc x (axisxng2) + g1_13 = self._cal_grrg(h2g2) + return g1_13 + + def _update_g2_g1g1( + self, + g1: torch.Tensor, # nb x nloc x ng1 + gg1: torch.Tensor, # nb x nloc x nnei x ng1 + nlist_mask: torch.Tensor, # nb x nloc x nnei + sw: torch.Tensor, # nb x nloc x nnei + ) -> torch.Tensor: + ret = g1.unsqueeze(-2) * gg1 + # nb x nloc x nnei x ng1 + ret = _apply_nlist_mask(ret, nlist_mask) + if self.smooth: + ret = _apply_switch(ret, sw) + return ret + + def _apply_bn( + self, + bn_number: int, + gg: torch.Tensor, + ): + if self.do_bn_mode == "uniform": + return self._apply_bn_uni(bn_number, gg) + elif self.do_bn_mode == "component": + return self._apply_bn_comp(bn_number, gg) + else: + return gg + + def _apply_nb_1(self, bn_number: int, gg: torch.Tensor) -> torch.Tensor: + nb, nl, nf = gg.shape + gg = gg.view([nb, 1, nl * nf]) + if bn_number == 1: + assert self.bn1 is not None + gg = self.bn1(gg) + else: + assert self.bn2 is not None + gg = self.bn2(gg) + return gg.view([nb, nl, nf]) + + def _apply_nb_2( + self, + bn_number: int, + gg: torch.Tensor, + ) -> torch.Tensor: + nb, nl, nnei, nf = gg.shape + gg = gg.view([nb, 1, nl * nnei * nf]) + if bn_number == 1: + assert self.bn1 is not None + gg = self.bn1(gg) + else: + assert self.bn2 is not None + gg = self.bn2(gg) + return gg.view([nb, nl, nnei, nf]) + + def _apply_bn_uni( + self, + bn_number: int, + gg: torch.Tensor, + mode: str = "1", + ) -> torch.Tensor: + if len(gg.shape) == 3: + return self._apply_nb_1(bn_number, gg) + elif len(gg.shape) == 4: + return self._apply_nb_2(bn_number, gg) + else: + raise RuntimeError(f"unsupported input shape {gg.shape}") + + def _apply_bn_comp( + self, + bn_number: int, + gg: torch.Tensor, + ) -> torch.Tensor: + ss = gg.shape + nf = ss[-1] + gg = gg.view([-1, nf]) + if bn_number == 1: + assert self.bn1 is not None + gg = self.bn1(gg).view(ss) + else: + assert self.bn2 is not None + gg = self.bn2(gg).view(ss) + return gg + + def forward( + self, + g1_ext: torch.Tensor, # nf x nall x ng1 + g2: torch.Tensor, # nf x nloc x nnei x ng2 + h2: torch.Tensor, # nf x nloc x nnei x 3 + nlist: torch.Tensor, # nf x nloc x nnei + nlist_mask: torch.Tensor, # nf x nloc x nnei + sw: torch.Tensor, # switch func, nf x nloc x nnei + ): + """ + Parameters + ---------- + g1_ext : nf x nall x ng1 extended single-atom chanel + g2 : nf x nloc x nnei x ng2 pair-atom channel, invariant + h2 : nf x nloc x nnei x 3 pair-atom channel, equivariant + nlist : nf x nloc x nnei neighbor list (padded neis are set to 0) + nlist_mask : nf x nloc x nnei masks of the neighbor list. real nei 1 otherwise 0 + sw : nf x nloc x nnei switch function + + Returns + ------- + g1: nf x nloc x ng1 updated single-atom chanel + g2: nf x nloc x nnei x ng2 updated pair-atom channel, invariant + h2: nf x nloc x nnei x 3 updated pair-atom channel, equivariant + """ + cal_gg1 = ( + self.update_g1_has_drrd + or self.update_g1_has_conv + or self.update_g1_has_attn + or self.update_g2_has_g1g1 + ) + + nb, nloc, nnei, _ = g2.shape + nall = g1_ext.shape[1] + g1, _ = torch.split(g1_ext, [nloc, nall - nloc], dim=1) + assert (nb, nloc) == g1.shape[:2] + assert (nb, nloc, nnei) == h2.shape[:3] + ng1 = g1.shape[-1] + ng2 = g2.shape[-1] + nh2 = h2.shape[-1] + + if self.bn1 is not None: + g1 = self._apply_bn(1, g1) + if self.bn2 is not None: + g2 = self._apply_bn(2, g2) + if self.update_h2: + h2 = _apply_h_norm(h2) + + g2_update: List[torch.Tensor] = [g2] + h2_update: List[torch.Tensor] = [h2] + g1_update: List[torch.Tensor] = [g1] + g1_mlp: List[torch.Tensor] = [g1] + + if cal_gg1: + gg1 = _make_nei_g1(g1_ext, nlist) + else: + gg1 = None + + if self.update_chnnl_2: + # nb x nloc x nnei x ng2 + assert self.linear2 is not None + g2_1 = self.act(self.linear2(g2)) + g2_update.append(g2_1) + + if self.update_g2_has_g1g1: + assert gg1 is not None + assert self.proj_g1g1g2 is not None + g2_update.append( + self.proj_g1g1g2(self._update_g2_g1g1(g1, gg1, nlist_mask, sw)) + ) + + if self.update_g2_has_attn: + assert self.attn2g_map is not None + assert self.attn2_mh_apply is not None + assert self.attn2_lm is not None + # nb x nloc x nnei x nnei x nh + AAg = self.attn2g_map(g2, h2, nlist_mask, sw) + # nb x nloc x nnei x ng2 + g2_2 = self.attn2_mh_apply(AAg, g2) + g2_2 = self.attn2_lm(g2_2) + g2_update.append(g2_2) + + if self.update_h2: + h2_update.append(self._update_h2(g2, h2, nlist_mask, sw)) + + if self.update_g1_has_conv: + assert gg1 is not None + g1_mlp.append(self._update_g1_conv(gg1, g2, nlist_mask, sw)) + + if self.update_g1_has_grrg: + g1_mlp.append(self._update_g1_grrg(g2, h2, nlist_mask, sw)) + + if self.update_g1_has_drrd: + assert gg1 is not None + g1_mlp.append(self._update_g1_grrg(gg1, h2, nlist_mask, sw)) + + # nb x nloc x [ng1+ng2+(axisxng2)+(axisxng1)] + # conv grrg drrd + g1_1 = self.act(self.linear1(torch.cat(g1_mlp, dim=-1))) + g1_update.append(g1_1) + + if self.update_g1_has_attn: + assert gg1 is not None + assert self.loc_attn is not None + g1_update.append(self.loc_attn(g1, gg1, nlist_mask, sw)) + + # update + if self.update_chnnl_2: + g2_new = self.list_update(g2_update) + h2_new = self.list_update(h2_update) + else: + g2_new, h2_new = g2, h2 + g1_new = self.list_update(g1_update) + return g1_new, g2_new, h2_new + + @torch.jit.export + def list_update_res_avg( + self, + update_list: List[torch.Tensor], + ) -> torch.Tensor: + nitem = len(update_list) + uu = update_list[0] + for ii in range(1, nitem): + uu = uu + update_list[ii] + return uu / (float(nitem) ** 0.5) + + @torch.jit.export + def list_update_res_incr(self, update_list: List[torch.Tensor]) -> torch.Tensor: + nitem = len(update_list) + uu = update_list[0] + scale = 1.0 / (float(nitem - 1) ** 0.5) if nitem > 1 else 0.0 + for ii in range(1, nitem): + uu = uu + scale * update_list[ii] + return uu + + @torch.jit.export + def list_update(self, update_list: List[torch.Tensor]) -> torch.Tensor: + if self.update_style == "res_avg": + return self.list_update_res_avg(update_list) + elif self.update_style == "res_incr": + return self.list_update_res_incr(update_list) + else: + raise RuntimeError(f"unknown update style {self.update_style}") + + def _bn_layer( + self, + nf: int = 1, + ) -> Callable: + return torch.nn.BatchNorm1d( + nf, + eps=1e-5, + momentum=self.bn_momentum, + affine=False, + track_running_stats=True, + device=env.DEVICE, + dtype=env.GLOBAL_PT_FLOAT_PRECISION, + ) diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py new file mode 100644 index 0000000000..26887b1b75 --- /dev/null +++ b/deepmd/pt/model/descriptor/repformers.py @@ -0,0 +1,348 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + List, + Optional, +) + +import numpy as np +import torch + +from deepmd.pt.model.descriptor.descriptor import ( + DescriptorBlock, + compute_std, +) +from deepmd.pt.model.descriptor.env_mat import ( + prod_env_mat_se_a, +) +from deepmd.pt.model.network.network import ( + SimpleLinear, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.nlist import ( + build_neighbor_list, +) +from deepmd.pt.utils.utils import ( + get_activation_fn, +) + +from .repformer_layer import ( + RepformerLayer, +) +from .se_atten import ( + analyze_descrpt, +) + +mydtype = env.GLOBAL_PT_FLOAT_PRECISION +mydev = env.DEVICE + + +def torch_linear(*args, **kwargs): + return torch.nn.Linear(*args, **kwargs, dtype=mydtype, device=mydev) + + +simple_linear = SimpleLinear +mylinear = simple_linear + + +@DescriptorBlock.register("se_repformer") +@DescriptorBlock.register("se_uni") +class DescrptBlockRepformers(DescriptorBlock): + def __init__( + self, + rcut, + rcut_smth, + sel: int, + ntypes: int, + nlayers: int = 3, + g1_dim=128, + g2_dim=16, + axis_dim: int = 4, + direct_dist: bool = False, + do_bn_mode: str = "no", + bn_momentum: float = 0.1, + update_g1_has_conv: bool = True, + update_g1_has_drrd: bool = True, + update_g1_has_grrg: bool = True, + update_g1_has_attn: bool = True, + update_g2_has_g1g1: bool = True, + update_g2_has_attn: bool = True, + update_h2: bool = False, + attn1_hidden: int = 64, + attn1_nhead: int = 4, + attn2_hidden: int = 16, + attn2_nhead: int = 4, + attn2_has_gate: bool = False, + activation: str = "tanh", + update_style: str = "res_avg", + set_davg_zero: bool = True, # TODO + smooth: bool = True, + add_type_ebd_to_seq: bool = False, + type: Optional[str] = None, + ): + """ + smooth: + If strictly smooth, cannot be used with update_g1_has_attn + add_type_ebd_to_seq: + At the presence of seq_input (optional input to forward), + whether or not add an type embedding to seq_input. + If no seq_input is given, it has no effect. + """ + super().__init__() + del type + self.epsilon = 1e-4 # protection of 1./nnei + self.rcut = rcut + self.rcut_smth = rcut_smth + self.ntypes = ntypes + self.nlayers = nlayers + sel = [sel] if isinstance(sel, int) else sel + self.nnei = sum(sel) + assert len(sel) == 1 + self.sel = sel + self.sec = self.sel + self.split_sel = self.sel + self.axis_dim = axis_dim + self.set_davg_zero = set_davg_zero + self.g1_dim = g1_dim + self.g2_dim = g2_dim + self.act = get_activation_fn(activation) + self.direct_dist = direct_dist + self.add_type_ebd_to_seq = add_type_ebd_to_seq + + self.g2_embd = mylinear(1, self.g2_dim) + layers = [] + for ii in range(nlayers): + layers.append( + RepformerLayer( + rcut, + rcut_smth, + sel, + ntypes, + self.g1_dim, + self.g2_dim, + axis_dim=self.axis_dim, + update_chnnl_2=(ii != nlayers - 1), + do_bn_mode=do_bn_mode, + bn_momentum=bn_momentum, + update_g1_has_conv=update_g1_has_conv, + update_g1_has_drrd=update_g1_has_drrd, + update_g1_has_grrg=update_g1_has_grrg, + update_g1_has_attn=update_g1_has_attn, + update_g2_has_g1g1=update_g2_has_g1g1, + update_g2_has_attn=update_g2_has_attn, + update_h2=update_h2, + attn1_hidden=attn1_hidden, + attn1_nhead=attn1_nhead, + attn2_has_gate=attn2_has_gate, + attn2_hidden=attn2_hidden, + attn2_nhead=attn2_nhead, + activation=activation, + update_style=update_style, + smooth=smooth, + ) + ) + self.layers = torch.nn.ModuleList(layers) + + sshape = (self.ntypes, self.nnei, 4) + mean = torch.zeros(sshape, dtype=mydtype, device=mydev) + stddev = torch.ones(sshape, dtype=mydtype, device=mydev) + self.register_buffer("mean", mean) + self.register_buffer("stddev", stddev) + + def get_rcut(self) -> float: + """Returns the cut-off radius.""" + return self.rcut + + def get_nsel(self) -> int: + """Returns the number of selected atoms in the cut-off radius.""" + return sum(self.sel) + + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + return self.sel + + def get_ntype(self) -> int: + """Returns the number of element types.""" + return self.ntypes + + def get_dim_out(self) -> int: + """Returns the output dimension.""" + return self.dim_out + + def get_dim_in(self) -> int: + """Returns the input dimension.""" + return self.dim_in + + @property + def dim_out(self): + """Returns the output dimension of this descriptor.""" + return self.g1_dim + + @property + def dim_in(self): + """Returns the atomic input dimension of this descriptor.""" + return self.g1_dim + + @property + def dim_emb(self): + """Returns the embedding dimension g2.""" + return self.g2_dim + + def forward( + self, + nlist: torch.Tensor, + extended_coord: torch.Tensor, + extended_atype: torch.Tensor, + extended_atype_embd: Optional[torch.Tensor] = None, + mapping: Optional[torch.Tensor] = None, + ): + assert mapping is not None + assert extended_atype_embd is not None + nframes, nloc, nnei = nlist.shape + nall = extended_coord.view(nframes, -1).shape[1] // 3 + atype = extended_atype[:, :nloc] + # nb x nloc x nnei x 4, nb x nloc x nnei x 3, nb x nloc x nnei x 1 + dmatrix, diff, sw = prod_env_mat_se_a( + extended_coord, + nlist, + atype, + self.mean, + self.stddev, + self.rcut, + self.rcut_smth, + ) + nlist_mask = nlist != -1 + sw = torch.squeeze(sw, -1) + # beyond the cutoff sw should be 0.0 + sw = sw.masked_fill(~nlist_mask, 0.0) + + # [nframes, nloc, tebd_dim] + atype_embd = extended_atype_embd[:, :nloc, :] + assert list(atype_embd.shape) == [nframes, nloc, self.g1_dim] + + g1 = self.act(atype_embd) + # nb x nloc x nnei x 1, nb x nloc x nnei x 3 + if not self.direct_dist: + g2, h2 = torch.split(dmatrix, [1, 3], dim=-1) + else: + g2, h2 = torch.linalg.norm(diff, dim=-1, keepdim=True), diff + g2 = g2 / self.rcut + h2 = h2 / self.rcut + # nb x nloc x nnei x ng2 + g2 = self.act(self.g2_embd(g2)) + + # set all padding positions to index of 0 + # if the a neighbor is real or not is indicated by nlist_mask + nlist[nlist == -1] = 0 + # nb x nall x ng1 + mapping = mapping.view(nframes, nall).unsqueeze(-1).expand(-1, -1, self.g1_dim) + for idx, ll in enumerate(self.layers): + # g1: nb x nloc x ng1 + # g1_ext: nb x nall x ng1 + g1_ext = torch.gather(g1, 1, mapping) + g1, g2, h2 = ll.forward( + g1_ext, + g2, + h2, + nlist, + nlist_mask, + sw, + ) + + # uses the last layer. + # nb x nloc x 3 x ng2 + h2g2 = ll._cal_h2g2(g2, h2, nlist_mask, sw) + # (nb x nloc) x ng2 x 3 + rot_mat = torch.permute(h2g2, (0, 1, 3, 2)) + + return g1, g2, h2, rot_mat.view(-1, self.dim_emb, 3), sw + + def compute_input_stats(self, merged): + """Update mean and stddev for descriptor elements.""" + ndescrpt = self.nnei * 4 + sumr = [] + suma = [] + sumn = [] + sumr2 = [] + suma2 = [] + mixed_type = "real_natoms_vec" in merged[0] + for system in merged: + index = system["mapping"].unsqueeze(-1).expand(-1, -1, 3) + extended_coord = torch.gather(system["coord"], dim=1, index=index) + extended_coord = extended_coord - system["shift"] + index = system["mapping"] + extended_atype = torch.gather(system["atype"], dim=1, index=index) + nloc = system["atype"].shape[-1] + ####################################################### + # dirty hack here! the interface of dataload should be + # redesigned to support descriptors like dpa2 + ####################################################### + nlist = build_neighbor_list( + extended_coord, + extended_atype, + nloc, + self.rcut, + self.get_sel(), + distinguish_types=False, + ) + env_mat, _, _ = prod_env_mat_se_a( + extended_coord, + nlist, + system["atype"], + self.mean, + self.stddev, + self.rcut, + self.rcut_smth, + ) + if not mixed_type: + sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( + env_mat.detach().cpu().numpy(), ndescrpt, system["natoms"] + ) + else: + sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( + env_mat.detach().cpu().numpy(), + ndescrpt, + system["real_natoms_vec"], + mixed_type=mixed_type, + real_atype=system["atype"].detach().cpu().numpy(), + ) + sumr.append(sysr) + suma.append(sysa) + sumn.append(sysn) + sumr2.append(sysr2) + suma2.append(sysa2) + sumr = np.sum(sumr, axis=0) + suma = np.sum(suma, axis=0) + sumn = np.sum(sumn, axis=0) + sumr2 = np.sum(sumr2, axis=0) + suma2 = np.sum(suma2, axis=0) + return sumr, suma, sumn, sumr2, suma2 + + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + all_davg = [] + all_dstd = [] + for type_i in range(self.ntypes): + davgunit = [[sumr[type_i] / (sumn[type_i] + 1e-15), 0, 0, 0]] + dstdunit = [ + [ + compute_std(sumr2[type_i], sumr[type_i], sumn[type_i], self.rcut), + compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), + compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), + compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), + ] + ] + davg = np.tile(davgunit, [self.nnei, 1]) + dstd = np.tile(dstdunit, [self.nnei, 1]) + all_davg.append(davg) + all_dstd.append(dstd) + self.sumr = sumr + self.suma = suma + self.sumn = sumn + self.sumr2 = sumr2 + self.suma2 = suma2 + if not self.set_davg_zero: + mean = np.stack(all_davg) + self.mean.copy_(torch.tensor(mean, device=env.DEVICE)) + stddev = np.stack(all_dstd) + self.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py new file mode 100644 index 0000000000..10aa66311e --- /dev/null +++ b/deepmd/pt/model/descriptor/se_a.py @@ -0,0 +1,478 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + ClassVar, + List, + Optional, +) + +import numpy as np +import torch + +from deepmd.pt.model.descriptor import ( + Descriptor, + DescriptorBlock, + compute_std, + prod_env_mat_se_a, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.env import ( + PRECISION_DICT, +) + +try: + from typing import ( + Final, + ) +except ImportError: + from torch.jit import Final + +from deepmd.model_format import EnvMat as DPEnvMat +from deepmd.pt.model.network.mlp import ( + EmbeddingNet, + NetworkCollection, +) +from deepmd.pt.model.network.network import ( + TypeFilter, +) + + +@Descriptor.register("se_e2_a") +class DescrptSeA(Descriptor): + def __init__( + self, + rcut, + rcut_smth, + sel, + neuron=[25, 50, 100], + axis_neuron=16, + set_davg_zero: bool = False, + activation_function: str = "tanh", + precision: str = "float64", + resnet_dt: bool = False, + old_impl: bool = False, + **kwargs, + ): + super().__init__() + self.sea = DescrptBlockSeA( + rcut, + rcut_smth, + sel, + neuron, + axis_neuron, + set_davg_zero, + activation_function, + precision, + resnet_dt, + old_impl, + **kwargs, + ) + + def get_rcut(self) -> float: + """Returns the cut-off radius.""" + return self.sea.get_rcut() + + def get_nsel(self) -> int: + """Returns the number of selected atoms in the cut-off radius.""" + return self.sea.get_nsel() + + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + return self.sea.get_sel() + + def get_ntype(self) -> int: + """Returns the number of element types.""" + return self.sea.get_ntype() + + def get_dim_out(self) -> int: + """Returns the output dimension.""" + return self.sea.get_dim_out() + + @property + def dim_out(self): + """Returns the output dimension of this descriptor.""" + return self.sea.dim_out + + def compute_input_stats(self, merged): + """Update mean and stddev for descriptor elements.""" + return self.sea.compute_input_stats(merged) + + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + self.sea.init_desc_stat(sumr, suma, sumn, sumr2, suma2) + + @classmethod + def get_stat_name(cls, config): + descrpt_type = config["type"] + assert descrpt_type in ["se_e2_a"] + return f'stat_file_sea_rcut{config["rcut"]:.2f}_smth{config["rcut_smth"]:.2f}_sel{config["sel"]}.npz' + + @classmethod + def get_data_process_key(cls, config): + descrpt_type = config["type"] + assert descrpt_type in ["se_e2_a"] + return {"sel": config["sel"], "rcut": config["rcut"]} + + def forward( + self, + extended_coord: torch.Tensor, + extended_atype: torch.Tensor, + nlist: torch.Tensor, + mapping: Optional[torch.Tensor] = None, + ): + return self.sea.forward(nlist, extended_coord, extended_atype, None, mapping) + + def set_stat_mean_and_stddev( + self, + mean: torch.Tensor, + stddev: torch.Tensor, + ) -> None: + self.sea.mean = mean + self.sea.stddev = stddev + + def serialize(self) -> dict: + obj = self.sea + return { + "rcut": obj.rcut, + "rcut_smth": obj.rcut_smth, + "sel": obj.sel, + "neuron": obj.neuron, + "axis_neuron": obj.axis_neuron, + "resnet_dt": obj.resnet_dt, + "set_davg_zero": obj.set_davg_zero, + "activation_function": obj.activation_function, + "precision": obj.precision, + "embeddings": obj.filter_layers.serialize(), + "env_mat": DPEnvMat(obj.rcut, obj.rcut_smth).serialize(), + "@variables": { + "davg": obj["davg"].detach().cpu().numpy(), + "dstd": obj["dstd"].detach().cpu().numpy(), + }, + ## to be updated when the options are supported. + "trainable": True, + "type_one_side": True, + "exclude_types": [], + "spin": None, + } + + @classmethod + def deserialize(cls, data: dict) -> "DescrptSeA": + variables = data.pop("@variables") + embeddings = data.pop("embeddings") + env_mat = data.pop("env_mat") + obj = cls(**data) + + def t_cvt(xx): + return torch.tensor(xx, dtype=obj.sea.prec, device=env.DEVICE) + + obj.sea["davg"] = t_cvt(variables["davg"]) + obj.sea["dstd"] = t_cvt(variables["dstd"]) + obj.sea.filter_layers = NetworkCollection.deserialize(embeddings) + return obj + + +@DescriptorBlock.register("se_e2_a") +class DescrptBlockSeA(DescriptorBlock): + ndescrpt: Final[int] + __constants__: ClassVar[list] = ["ndescrpt"] + + def __init__( + self, + rcut, + rcut_smth, + sel, + neuron=[25, 50, 100], + axis_neuron=16, + set_davg_zero: bool = False, + activation_function: str = "tanh", + precision: str = "float64", + resnet_dt: bool = False, + old_impl: bool = False, + **kwargs, + ): + """Construct an embedding net of type `se_a`. + + Args: + - rcut: Cut-off radius. + - rcut_smth: Smooth hyper-parameter for pair force & energy. + - sel: For each element type, how many atoms is selected as neighbors. + - filter_neuron: Number of neurons in each hidden layers of the embedding net. + - axis_neuron: Number of columns of the sub-matrix of the embedding matrix. + """ + super().__init__() + self.rcut = rcut + self.rcut_smth = rcut_smth + self.neuron = neuron + self.filter_neuron = self.neuron + self.axis_neuron = axis_neuron + self.set_davg_zero = set_davg_zero + self.activation_function = activation_function + self.precision = precision + self.prec = PRECISION_DICT[self.precision] + self.resnet_dt = resnet_dt + self.old_impl = old_impl + + self.ntypes = len(sel) + self.sel = sel + self.sec = torch.tensor( + np.append([0], np.cumsum(self.sel)), dtype=int, device=env.DEVICE + ) + self.split_sel = self.sel + self.nnei = sum(sel) + self.ndescrpt = self.nnei * 4 + + wanted_shape = (self.ntypes, self.nnei, 4) + mean = torch.zeros(wanted_shape, dtype=self.prec, device=env.DEVICE) + stddev = torch.ones(wanted_shape, dtype=self.prec, device=env.DEVICE) + self.register_buffer("mean", mean) + self.register_buffer("stddev", stddev) + self.filter_layers_old = None + self.filter_layers = None + + if self.old_impl: + filter_layers = [] + # TODO: remove + start_index = 0 + for type_i in range(self.ntypes): + one = TypeFilter(start_index, sel[type_i], self.filter_neuron) + filter_layers.append(one) + start_index += sel[type_i] + self.filter_layers_old = torch.nn.ModuleList(filter_layers) + else: + filter_layers = NetworkCollection( + ndim=1, ntypes=len(sel), network_type="embedding_network" + ) + # TODO: ndim=2 if type_one_side=False + for ii in range(self.ntypes): + filter_layers[(ii,)] = EmbeddingNet( + 1, + self.filter_neuron, + activation_function=self.activation_function, + precision=self.precision, + resnet_dt=self.resnet_dt, + ) + self.filter_layers = filter_layers + + def get_rcut(self) -> float: + """Returns the cut-off radius.""" + return self.rcut + + def get_nsel(self) -> int: + """Returns the number of selected atoms in the cut-off radius.""" + return sum(self.sel) + + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + return self.sel + + def get_ntype(self) -> int: + """Returns the number of element types.""" + return self.ntypes + + def get_dim_out(self) -> int: + """Returns the output dimension.""" + return self.dim_out + + def get_dim_in(self) -> int: + """Returns the input dimension.""" + return self.dim_in + + @property + def dim_out(self): + """Returns the output dimension of this descriptor.""" + return self.filter_neuron[-1] * self.axis_neuron + + @property + def dim_in(self): + """Returns the atomic input dimension of this descriptor.""" + return 0 + + def __setitem__(self, key, value): + if key in ("avg", "data_avg", "davg"): + self.mean = value + elif key in ("std", "data_std", "dstd"): + self.stddev = value + else: + raise KeyError(key) + + def __getitem__(self, key): + if key in ("avg", "data_avg", "davg"): + return self.mean + elif key in ("std", "data_std", "dstd"): + return self.stddev + else: + raise KeyError(key) + + def compute_input_stats(self, merged): + """Update mean and stddev for descriptor elements.""" + sumr = [] + suma = [] + sumn = [] + sumr2 = [] + suma2 = [] + for system in merged: + index = system["mapping"].unsqueeze(-1).expand(-1, -1, 3) + extended_coord = torch.gather(system["coord"], dim=1, index=index) + extended_coord = extended_coord - system["shift"] + env_mat, _, _ = prod_env_mat_se_a( + extended_coord, + system["nlist"], + system["atype"], + self.mean, + self.stddev, + self.rcut, + self.rcut_smth, + ) + sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( + env_mat.detach().cpu().numpy(), self.ndescrpt, system["natoms"] + ) + sumr.append(sysr) + suma.append(sysa) + sumn.append(sysn) + sumr2.append(sysr2) + suma2.append(sysa2) + sumr = np.sum(sumr, axis=0) + suma = np.sum(suma, axis=0) + sumn = np.sum(sumn, axis=0) + sumr2 = np.sum(sumr2, axis=0) + suma2 = np.sum(suma2, axis=0) + return sumr, suma, sumn, sumr2, suma2 + + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + all_davg = [] + all_dstd = [] + for type_i in range(self.ntypes): + davgunit = [[sumr[type_i] / (sumn[type_i] + 1e-15), 0, 0, 0]] + dstdunit = [ + [ + compute_std(sumr2[type_i], sumr[type_i], sumn[type_i], self.rcut), + compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), + compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), + compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), + ] + ] + davg = np.tile(davgunit, [self.nnei, 1]) + dstd = np.tile(dstdunit, [self.nnei, 1]) + all_davg.append(davg) + all_dstd.append(dstd) + self.sumr = sumr + self.suma = suma + self.sumn = sumn + self.sumr2 = sumr2 + self.suma2 = suma2 + if not self.set_davg_zero: + mean = np.stack(all_davg) + self.mean.copy_(torch.tensor(mean, device=env.DEVICE)) + stddev = np.stack(all_dstd) + self.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) + + def forward( + self, + nlist: torch.Tensor, + extended_coord: torch.Tensor, + extended_atype: torch.Tensor, + extended_atype_embd: Optional[torch.Tensor] = None, + mapping: Optional[torch.Tensor] = None, + ): + """Calculate decoded embedding for each atom. + + Args: + - coord: Tell atom coordinates with shape [nframes, natoms[1]*3]. + - atype: Tell atom types with shape [nframes, natoms[1]]. + - natoms: Tell atom count and element count. Its shape is [2+self.ntypes]. + - box: Tell simulation box with shape [nframes, 9]. + + Returns + ------- + - `torch.Tensor`: descriptor matrix with shape [nframes, natoms[0]*self.filter_neuron[-1]*self.axis_neuron]. + """ + del extended_atype_embd, mapping + nloc = nlist.shape[1] + atype = extended_atype[:, :nloc] + dmatrix, diff, _ = prod_env_mat_se_a( + extended_coord, + nlist, + atype, + self.mean, + self.stddev, + self.rcut, + self.rcut_smth, + ) + + if self.old_impl: + assert self.filter_layers_old is not None + dmatrix = dmatrix.view( + -1, self.ndescrpt + ) # shape is [nframes*nall, self.ndescrpt] + xyz_scatter = torch.empty( + 1, + ) + ret = self.filter_layers_old[0](dmatrix) + xyz_scatter = ret + for ii, transform in enumerate(self.filter_layers_old[1:]): + # shape is [nframes*nall, 4, self.filter_neuron[-1]] + ret = transform.forward(dmatrix) + xyz_scatter = xyz_scatter + ret + else: + assert self.filter_layers is not None + dmatrix = dmatrix.view(-1, self.nnei, 4) + nfnl = dmatrix.shape[0] + # pre-allocate a shape to pass jit + xyz_scatter = torch.zeros( + [nfnl, 4, self.filter_neuron[-1]], dtype=self.prec, device=env.DEVICE + ) + for ii, ll in enumerate(self.filter_layers.networks): + # nfnl x nt x 4 + rr = dmatrix[:, self.sec[ii] : self.sec[ii + 1], :] + ss = rr[:, :, :1] + # nfnl x nt x ng + gg = ll.forward(ss) + # nfnl x 4 x ng + gr = torch.matmul(rr.permute(0, 2, 1), gg) + xyz_scatter += gr + + xyz_scatter /= self.nnei + xyz_scatter_1 = xyz_scatter.permute(0, 2, 1) + rot_mat = xyz_scatter_1[:, :, 1:4] + xyz_scatter_2 = xyz_scatter[:, :, 0 : self.axis_neuron] + result = torch.matmul( + xyz_scatter_1, xyz_scatter_2 + ) # shape is [nframes*nall, self.filter_neuron[-1], self.axis_neuron] + return ( + result.view(-1, nloc, self.filter_neuron[-1] * self.axis_neuron), + None, + None, + None, + None, + ) + + +def analyze_descrpt(matrix, ndescrpt, natoms): + """Collect avg, square avg and count of descriptors in a batch.""" + ntypes = natoms.shape[1] - 2 + start_index = 0 + sysr = [] + sysa = [] + sysn = [] + sysr2 = [] + sysa2 = [] + for type_i in range(ntypes): + end_index = start_index + natoms[0, 2 + type_i] + dd = matrix[:, start_index:end_index] # all descriptors for this element + start_index = end_index + dd = np.reshape( + dd, [-1, 4] + ) # Shape is [nframes*natoms[2+type_id]*self.nnei, 4] + ddr = dd[:, :1] + dda = dd[:, 1:] + sumr = np.sum(ddr) + suma = np.sum(dda) / 3.0 + sumn = dd.shape[0] # Value is nframes*natoms[2+type_id]*self.nnei + sumr2 = np.sum(np.multiply(ddr, ddr)) + suma2 = np.sum(np.multiply(dda, dda)) / 3.0 + sysr.append(sumr) + sysa.append(suma) + sysn.append(sumn) + sysr2.append(sumr2) + sysa2.append(suma2) + return sysr, sysr2, sysa, sysa2, sysn diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py new file mode 100644 index 0000000000..0c932f42f2 --- /dev/null +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -0,0 +1,392 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + List, + Optional, +) + +import numpy as np +import torch + +from deepmd.pt.model.descriptor.descriptor import ( + DescriptorBlock, + compute_std, +) +from deepmd.pt.model.descriptor.env_mat import ( + prod_env_mat_se_a, +) +from deepmd.pt.model.network.network import ( + NeighborWiseAttention, + TypeFilter, +) +from deepmd.pt.utils import ( + env, +) + + +@DescriptorBlock.register("se_atten") +class DescrptBlockSeAtten(DescriptorBlock): + def __init__( + self, + rcut, + rcut_smth, + sel, + ntypes: int, + neuron: list = [25, 50, 100], + axis_neuron: int = 16, + tebd_dim: int = 8, + tebd_input_mode: str = "concat", + # set_davg_zero: bool = False, + set_davg_zero: bool = True, # TODO + attn: int = 128, + attn_layer: int = 2, + attn_dotr: bool = True, + attn_mask: bool = False, + post_ln=True, + ffn=False, + ffn_embed_dim=1024, + activation="tanh", + scaling_factor=1.0, + head_num=1, + normalize=True, + temperature=None, + return_rot=False, + type: Optional[str] = None, + ): + """Construct an embedding net of type `se_atten`. + + Args: + - rcut: Cut-off radius. + - rcut_smth: Smooth hyper-parameter for pair force & energy. + - sel: For each element type, how many atoms is selected as neighbors. + - filter_neuron: Number of neurons in each hidden layers of the embedding net. + - axis_neuron: Number of columns of the sub-matrix of the embedding matrix. + """ + super().__init__() + del type + self.rcut = rcut + self.rcut_smth = rcut_smth + self.filter_neuron = neuron + self.axis_neuron = axis_neuron + self.tebd_dim = tebd_dim + self.tebd_input_mode = tebd_input_mode + self.set_davg_zero = set_davg_zero + self.attn_dim = attn + self.attn_layer = attn_layer + self.attn_dotr = attn_dotr + self.attn_mask = attn_mask + self.post_ln = post_ln + self.ffn = ffn + self.ffn_embed_dim = ffn_embed_dim + self.activation = activation + self.scaling_factor = scaling_factor + self.head_num = head_num + self.normalize = normalize + self.temperature = temperature + self.return_rot = return_rot + + if isinstance(sel, int): + sel = [sel] + + self.ntypes = ntypes + self.sel = sel + self.sec = self.sel + self.split_sel = self.sel + self.nnei = sum(sel) + self.ndescrpt = self.nnei * 4 + self.dpa1_attention = NeighborWiseAttention( + self.attn_layer, + self.nnei, + self.filter_neuron[-1], + self.attn_dim, + dotr=self.attn_dotr, + do_mask=self.attn_mask, + post_ln=self.post_ln, + ffn=self.ffn, + ffn_embed_dim=self.ffn_embed_dim, + activation=self.activation, + scaling_factor=self.scaling_factor, + head_num=self.head_num, + normalize=self.normalize, + temperature=self.temperature, + ) + + wanted_shape = (self.ntypes, self.nnei, 4) + mean = torch.zeros( + wanted_shape, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ) + stddev = torch.ones( + wanted_shape, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ) + self.register_buffer("mean", mean) + self.register_buffer("stddev", stddev) + + filter_layers = [] + one = TypeFilter( + 0, + self.nnei, + self.filter_neuron, + return_G=True, + tebd_dim=self.tebd_dim, + use_tebd=True, + tebd_mode=self.tebd_input_mode, + ) + filter_layers.append(one) + self.filter_layers = torch.nn.ModuleList(filter_layers) + + def get_rcut(self) -> float: + """Returns the cut-off radius.""" + return self.rcut + + def get_nsel(self) -> int: + """Returns the number of selected atoms in the cut-off radius.""" + return sum(self.sel) + + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + return self.sel + + def get_ntype(self) -> int: + """Returns the number of element types.""" + return self.ntypes + + def get_dim_in(self) -> int: + """Returns the output dimension.""" + return self.dim_in + + def get_dim_out(self) -> int: + """Returns the output dimension.""" + return self.dim_out + + @property + def dim_out(self): + """Returns the output dimension of this descriptor.""" + return self.filter_neuron[-1] * self.axis_neuron + + @property + def dim_in(self): + """Returns the atomic input dimension of this descriptor.""" + return self.tebd_dim + + @property + def dim_emb(self): + """Returns the output dimension of embedding.""" + return self.filter_neuron[-1] + + def compute_input_stats(self, merged): + """Update mean and stddev for descriptor elements.""" + sumr = [] + suma = [] + sumn = [] + sumr2 = [] + suma2 = [] + mixed_type = "real_natoms_vec" in merged[0] + for system in merged: + index = system["mapping"].unsqueeze(-1).expand(-1, -1, 3) + extended_coord = torch.gather(system["coord"], dim=1, index=index) + extended_coord = extended_coord - system["shift"] + env_mat, _, _ = prod_env_mat_se_a( + extended_coord, + system["nlist"], + system["atype"], + self.mean, + self.stddev, + self.rcut, + self.rcut_smth, + ) + if not mixed_type: + sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( + env_mat.detach().cpu().numpy(), self.ndescrpt, system["natoms"] + ) + else: + sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( + env_mat.detach().cpu().numpy(), + self.ndescrpt, + system["real_natoms_vec"], + mixed_type=mixed_type, + real_atype=system["atype"].detach().cpu().numpy(), + ) + sumr.append(sysr) + suma.append(sysa) + sumn.append(sysn) + sumr2.append(sysr2) + suma2.append(sysa2) + sumr = np.sum(sumr, axis=0) + suma = np.sum(suma, axis=0) + sumn = np.sum(sumn, axis=0) + sumr2 = np.sum(sumr2, axis=0) + suma2 = np.sum(suma2, axis=0) + return sumr, suma, sumn, sumr2, suma2 + + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + all_davg = [] + all_dstd = [] + for type_i in range(self.ntypes): + davgunit = [[sumr[type_i] / (sumn[type_i] + 1e-15), 0, 0, 0]] + dstdunit = [ + [ + compute_std(sumr2[type_i], sumr[type_i], sumn[type_i], self.rcut), + compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), + compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), + compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), + ] + ] + davg = np.tile(davgunit, [self.nnei, 1]) + dstd = np.tile(dstdunit, [self.nnei, 1]) + all_davg.append(davg) + all_dstd.append(dstd) + self.sumr = sumr + self.suma = suma + self.sumn = sumn + self.sumr2 = sumr2 + self.suma2 = suma2 + if not self.set_davg_zero: + mean = np.stack(all_davg) + self.mean.copy_(torch.tensor(mean, device=env.DEVICE)) + stddev = np.stack(all_dstd) + self.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) + + def forward( + self, + nlist: torch.Tensor, + extended_coord: torch.Tensor, + extended_atype: torch.Tensor, + extended_atype_embd: Optional[torch.Tensor] = None, + mapping: Optional[torch.Tensor] = None, + ) -> List[torch.Tensor]: + """Calculate decoded embedding for each atom. + + Args: + - coord: Tell atom coordinates with shape [nframes, natoms[1]*3]. + - atype: Tell atom types with shape [nframes, natoms[1]]. + - natoms: Tell atom count and element count. Its shape is [2+self.ntypes]. + - box: Tell simulation box with shape [nframes, 9]. + + Returns + ------- + - result: descriptor with shape [nframes, nloc, self.filter_neuron[-1] * self.axis_neuron]. + - ret: environment matrix with shape [nframes, nloc, self.neei, out_size] + """ + del mapping + assert extended_atype_embd is not None + nframes, nloc, nnei = nlist.shape + atype = extended_atype[:, :nloc] + nb = nframes + nall = extended_coord.view(nb, -1, 3).shape[1] + dmatrix, diff, sw = prod_env_mat_se_a( + extended_coord, + nlist, + atype, + self.mean, + self.stddev, + self.rcut, + self.rcut_smth, + ) + dmatrix = dmatrix.view( + -1, self.ndescrpt + ) # shape is [nframes*nall, self.ndescrpt] + nlist_mask = nlist != -1 + nlist[nlist == -1] = 0 + sw = torch.squeeze(sw, -1) + # beyond the cutoff sw should be 0.0 + sw = sw.masked_fill(~nlist_mask, 0.0) + # nf x nloc x nt -> nf x nloc x nnei x nt + atype_tebd = extended_atype_embd[:, :nloc, :] + atype_tebd_nnei = atype_tebd.unsqueeze(2).expand(-1, -1, self.nnei, -1) + # nf x nall x nt + nt = extended_atype_embd.shape[-1] + atype_tebd_ext = extended_atype_embd + # nb x (nloc x nnei) x nt + index = nlist.reshape(nb, nloc * nnei).unsqueeze(-1).expand(-1, -1, nt) + # nb x (nloc x nnei) x nt + atype_tebd_nlist = torch.gather(atype_tebd_ext, dim=1, index=index) + # nb x nloc x nnei x nt + atype_tebd_nlist = atype_tebd_nlist.view(nb, nloc, nnei, nt) + ret = self.filter_layers[0]( + dmatrix, + atype_tebd=atype_tebd_nnei, + nlist_tebd=atype_tebd_nlist, + ) # shape is [nframes*nall, self.neei, out_size] + input_r = torch.nn.functional.normalize( + dmatrix.reshape(-1, self.nnei, 4)[:, :, 1:4], dim=-1 + ) + ret = self.dpa1_attention( + ret, nlist_mask, input_r=input_r, sw=sw + ) # shape is [nframes*nloc, self.neei, out_size] + inputs_reshape = dmatrix.view(-1, self.nnei, 4).permute( + 0, 2, 1 + ) # shape is [nframes*natoms[0], 4, self.neei] + xyz_scatter = torch.matmul( + inputs_reshape, ret + ) # shape is [nframes*natoms[0], 4, out_size] + xyz_scatter = xyz_scatter / self.nnei + xyz_scatter_1 = xyz_scatter.permute(0, 2, 1) + rot_mat = xyz_scatter_1[:, :, 1:4] + xyz_scatter_2 = xyz_scatter[:, :, 0 : self.axis_neuron] + result = torch.matmul( + xyz_scatter_1, xyz_scatter_2 + ) # shape is [nframes*nloc, self.filter_neuron[-1], self.axis_neuron] + return ( + result.view(-1, nloc, self.filter_neuron[-1] * self.axis_neuron), + ret.view(-1, nloc, self.nnei, self.filter_neuron[-1]), + diff, + rot_mat.view(-1, self.filter_neuron[-1], 3), + sw, + ) + + +def analyze_descrpt(matrix, ndescrpt, natoms, mixed_type=False, real_atype=None): + """Collect avg, square avg and count of descriptors in a batch.""" + ntypes = natoms.shape[1] - 2 + if not mixed_type: + sysr = [] + sysa = [] + sysn = [] + sysr2 = [] + sysa2 = [] + start_index = 0 + for type_i in range(ntypes): + end_index = start_index + natoms[0, 2 + type_i] + dd = matrix[:, start_index:end_index] + start_index = end_index + dd = np.reshape( + dd, [-1, 4] + ) # Shape is [nframes*natoms[2+type_id]*self.nnei, 4] + ddr = dd[:, :1] + dda = dd[:, 1:] + sumr = np.sum(ddr) + suma = np.sum(dda) / 3.0 + sumn = dd.shape[0] # Value is nframes*natoms[2+type_id]*self.nnei + sumr2 = np.sum(np.multiply(ddr, ddr)) + suma2 = np.sum(np.multiply(dda, dda)) / 3.0 + sysr.append(sumr) + sysa.append(suma) + sysn.append(sumn) + sysr2.append(sumr2) + sysa2.append(suma2) + else: + sysr = [0.0 for i in range(ntypes)] + sysa = [0.0 for i in range(ntypes)] + sysn = [0 for i in range(ntypes)] + sysr2 = [0.0 for i in range(ntypes)] + sysa2 = [0.0 for i in range(ntypes)] + for frame_item in range(matrix.shape[0]): + dd_ff = matrix[frame_item] + atype_frame = real_atype[frame_item] + for type_i in range(ntypes): + type_idx = atype_frame == type_i + dd = dd_ff[type_idx] + dd = np.reshape(dd, [-1, 4]) # typen_atoms * nnei, 4 + ddr = dd[:, :1] + dda = dd[:, 1:] + sumr = np.sum(ddr) + suma = np.sum(dda) / 3.0 + sumn = dd.shape[0] + sumr2 = np.sum(np.multiply(ddr, ddr)) + suma2 = np.sum(np.multiply(dda, dda)) / 3.0 + sysr[type_i] += sumr + sysa[type_i] += suma + sysn[type_i] += sumn + sysr2[type_i] += sumr2 + sysa2[type_i] += suma2 + + return sysr, sysr2, sysa, sysa2, sysn diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py new file mode 100644 index 0000000000..a3db3dbdec --- /dev/null +++ b/deepmd/pt/model/model/__init__.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from .ener import ( + EnergyModel, +) +from .model import ( + BaseModel, +) + + +def get_model(model_params, sampled=None): + return EnergyModel( + descriptor=model_params["descriptor"], + fitting_net=model_params.get("fitting_net", None), + type_map=model_params["type_map"], + type_embedding=model_params.get("type_embedding", None), + resuming=model_params.get("resuming", False), + stat_file_dir=model_params.get("stat_file_dir", None), + stat_file_path=model_params.get("stat_file_path", None), + sampled=sampled, + ) + + +__all__ = [ + "BaseModel", + "EnergyModel", + "get_model", +] diff --git a/deepmd/pt/model/model/atomic_model.py b/deepmd/pt/model/model/atomic_model.py new file mode 100644 index 0000000000..47fd463fc9 --- /dev/null +++ b/deepmd/pt/model/model/atomic_model.py @@ -0,0 +1,77 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from abc import ( + ABC, + abstractmethod, +) +from typing import ( + Dict, + List, + Optional, +) + +import torch + +from deepmd.model_format import ( + FittingOutputDef, +) +from deepmd.pt.model.task import ( + Fitting, +) + + +class AtomicModel(ABC): + @abstractmethod + def get_fitting_net(self) -> Fitting: + raise NotImplementedError + + @abstractmethod + def get_fitting_output_def(self) -> FittingOutputDef: + raise NotImplementedError + + @abstractmethod + def get_rcut(self) -> float: + raise NotImplementedError + + @abstractmethod + def get_sel(self) -> List[int]: + raise NotImplementedError + + @abstractmethod + def distinguish_types(self) -> bool: + raise NotImplementedError + + @abstractmethod + def forward_atomic( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + raise NotImplementedError + + def do_grad( + self, + var_name: Optional[str] = None, + ) -> bool: + """Tell if the output variable `var_name` is differentiable. + if var_name is None, returns if any of the variable is differentiable. + + """ + odef = self.get_fitting_output_def() + if var_name is None: + require: List[bool] = [] + for vv in odef.keys(): + require.append(self.do_grad_(vv)) + return any(require) + else: + return self.do_grad_(var_name) + + def do_grad_( + self, + var_name: str, + ) -> bool: + """Tell if the output variable `var_name` is differentiable.""" + assert var_name is not None + return self.get_fitting_output_def()[var_name].differentiable diff --git a/deepmd/pt/model/model/dp_atomic_model.py b/deepmd/pt/model/model/dp_atomic_model.py new file mode 100644 index 0000000000..ffeeeda660 --- /dev/null +++ b/deepmd/pt/model/model/dp_atomic_model.py @@ -0,0 +1,214 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + List, + Optional, +) + +import torch + +from deepmd.model_format import ( + FittingOutputDef, +) +from deepmd.pt.model.descriptor.descriptor import ( + Descriptor, +) +from deepmd.pt.model.task import ( + DenoiseNet, + Fitting, +) + +from .atomic_model import ( + AtomicModel, +) +from .model import ( + BaseModel, +) + + +class DPAtomicModel(BaseModel, AtomicModel): + """Model give atomic prediction of some physical property. + + Parameters + ---------- + descriptor + Descriptor + fitting_net + Fitting net + type_map + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. + type_embedding + Type embedding net + resuming + Whether to resume/fine-tune from checkpoint or not. + stat_file_dir + The directory to the state files. + stat_file_path + The path to the state files. + sampled + Sampled frames to compute the statistics. + """ + + def __init__( + self, + descriptor: dict, + fitting_net: dict, + type_map: Optional[List[str]], + type_embedding: Optional[dict] = None, + resuming: bool = False, + stat_file_dir=None, + stat_file_path=None, + sampled=None, + **kwargs, + ): + super().__init__() + # Descriptor + Type Embedding Net (Optional) + ntypes = len(type_map) + self.type_map = type_map + self.ntypes = ntypes + descriptor["ntypes"] = ntypes + self.combination = descriptor.get("combination", False) + if self.combination: + self.prefactor = descriptor.get("prefactor", [0.5, 0.5]) + self.descriptor_type = descriptor["type"] + + self.type_split = True + if self.descriptor_type not in ["se_e2_a"]: + self.type_split = False + + self.descriptor = Descriptor(**descriptor) + self.rcut = self.descriptor.get_rcut() + self.sel = self.descriptor.get_sel() + self.split_nlist = False + + # Statistics + self.compute_or_load_stat( + fitting_net, + ntypes, + resuming=resuming, + type_map=type_map, + stat_file_dir=stat_file_dir, + stat_file_path=stat_file_path, + sampled=sampled, + ) + + # Fitting + if fitting_net: + fitting_net["type"] = fitting_net.get("type", "ener") + if self.descriptor_type not in ["se_e2_a"]: + fitting_net["ntypes"] = 1 + else: + fitting_net["ntypes"] = self.descriptor.get_ntype() + fitting_net["use_tebd"] = False + fitting_net["embedding_width"] = self.descriptor.dim_out + + self.grad_force = "direct" not in fitting_net["type"] + if not self.grad_force: + fitting_net["out_dim"] = self.descriptor.dim_emb + if "ener" in fitting_net["type"]: + fitting_net["return_energy"] = True + self.fitting_net = Fitting(**fitting_net) + else: + self.fitting_net = None + self.grad_force = False + if not self.split_nlist: + self.coord_denoise_net = DenoiseNet( + self.descriptor.dim_out, self.ntypes - 1, self.descriptor.dim_emb + ) + elif self.combination: + self.coord_denoise_net = DenoiseNet( + self.descriptor.dim_out, + self.ntypes - 1, + self.descriptor.dim_emb_list, + self.prefactor, + ) + else: + self.coord_denoise_net = DenoiseNet( + self.descriptor.dim_out, self.ntypes - 1, self.descriptor.dim_emb + ) + + def get_fitting_net(self) -> Fitting: + """Get the fitting net.""" + return ( + self.fitting_net if self.fitting_net is not None else self.coord_denoise_net + ) + + def get_fitting_output_def(self) -> FittingOutputDef: + """Get the output def of the fitting net.""" + return ( + self.fitting_net.output_def() + if self.fitting_net is not None + else self.coord_denoise_net.output_def() + ) + + def get_rcut(self) -> float: + """Get the cut-off radius.""" + return self.rcut + + def get_sel(self) -> List[int]: + """Get the neighbor selection.""" + return self.sel + + def distinguish_types(self) -> bool: + """If distinguish different types by sorting.""" + return self.type_split + + def forward_atomic( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[torch.Tensor] = None, + ) -> Dict[str, torch.Tensor]: + """Return atomic prediction. + + Parameters + ---------- + extended_coord + coodinates in extended region + extended_atype + atomic type in extended region + nlist + neighbor list. nf x nloc x nsel + mapping + mapps the extended indices to local indices + + Returns + ------- + result_dict + the result dict, defined by the fitting net output def. + + """ + nframes, nloc, nnei = nlist.shape + atype = extended_atype[:, :nloc] + if self.do_grad(): + extended_coord.requires_grad_(True) + descriptor, env_mat, diff, rot_mat, sw = self.descriptor( + extended_coord, + extended_atype, + nlist, + mapping=mapping, + ) + assert descriptor is not None + # energy, force + if self.fitting_net is not None: + fit_ret = self.fitting_net( + descriptor, atype, atype_tebd=None, rot_mat=rot_mat + ) + # denoise + else: + nlist_list = [nlist] + if not self.split_nlist: + nnei_mask = nlist != -1 + elif self.combination: + nnei_mask = [] + for item in nlist_list: + nnei_mask_item = item != -1 + nnei_mask.append(nnei_mask_item) + else: + env_mat = env_mat[-1] + diff = diff[-1] + nnei_mask = nlist_list[-1] != -1 + fit_ret = self.coord_denoise_net(env_mat, diff, nnei_mask, descriptor, sw) + return fit_ret diff --git a/deepmd/pt/model/model/ener.py b/deepmd/pt/model/model/ener.py new file mode 100644 index 0000000000..c316c99a86 --- /dev/null +++ b/deepmd/pt/model/model/ener.py @@ -0,0 +1,151 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + List, + Optional, +) + +import torch + +from .dp_atomic_model import ( + DPAtomicModel, +) +from .make_model import ( + make_model, +) + +DPModel = make_model(DPAtomicModel) + + +class EnergyModel(DPModel): + model_type = "ener" + + def __init__( + self, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) + + def forward( + self, + coord, + atype, + box: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + model_ret = self.forward_common( + coord, atype, box, do_atomic_virial=do_atomic_virial + ) + if self.fitting_net is not None: + model_predict = {} + model_predict["atom_energy"] = model_ret["energy"] + model_predict["energy"] = model_ret["energy_redu"] + if self.do_grad("energy"): + model_predict["force"] = model_ret["energy_derv_r"].squeeze(-2) + if do_atomic_virial: + model_predict["atomic_virial"] = model_ret["energy_derv_c"].squeeze( + -3 + ) + model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-3) + else: + model_predict["force"] = model_ret["dforce"] + else: + model_predict = model_ret + model_predict["updated_coord"] += coord + return model_predict + + def forward_lower( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ): + model_ret = self.common_forward_lower( + extended_coord, + extended_atype, + nlist, + mapping, + do_atomic_virial=do_atomic_virial, + ) + if self.fitting_net is not None: + model_predict = {} + model_predict["atom_energy"] = model_ret["energy"] + model_predict["energy"] = model_ret["energy_redu"] + if self.do_grad("energy"): + model_predict["extended_force"] = model_ret["energy_derv_r"].squeeze(-2) + if do_atomic_virial: + model_predict["extended_virial"] = model_ret[ + "energy_derv_c" + ].squeeze(-3) + else: + assert model_ret["dforce"] is not None + model_predict["dforce"] = model_ret["dforce"] + else: + model_predict = model_ret + return model_predict + + +# should be a stand-alone function!!!! +def process_nlist( + nlist, + extended_atype, + mapping: Optional[torch.Tensor] = None, +): + # process the nlist_type and nlist_loc + nframes, nloc = nlist.shape[:2] + nmask = nlist == -1 + nlist[nmask] = 0 + if mapping is not None: + nlist_loc = torch.gather( + mapping, + dim=1, + index=nlist.reshape(nframes, -1), + ).reshape(nframes, nloc, -1) + nlist_loc[nmask] = -1 + else: + nlist_loc = None + nlist_type = torch.gather( + extended_atype, + dim=1, + index=nlist.reshape(nframes, -1), + ).reshape(nframes, nloc, -1) + nlist_type[nmask] = -1 + nlist[nmask] = -1 + return nlist_loc, nlist_type, nframes, nloc + + +def process_nlist_gathered( + nlist, + extended_atype, + split_sel: List[int], + mapping: Optional[torch.Tensor] = None, +): + nlist_list = list(torch.split(nlist, split_sel, -1)) + nframes, nloc = nlist_list[0].shape[:2] + nlist_type_list = [] + nlist_loc_list = [] + for nlist_item in nlist_list: + nmask = nlist_item == -1 + nlist_item[nmask] = 0 + if mapping is not None: + nlist_loc_item = torch.gather( + mapping, dim=1, index=nlist_item.reshape(nframes, -1) + ).reshape(nframes, nloc, -1) + nlist_loc_item[nmask] = -1 + nlist_loc_list.append(nlist_loc_item) + nlist_type_item = torch.gather( + extended_atype, dim=1, index=nlist_item.reshape(nframes, -1) + ).reshape(nframes, nloc, -1) + nlist_type_item[nmask] = -1 + nlist_type_list.append(nlist_type_item) + nlist_item[nmask] = -1 + + if mapping is not None: + nlist_loc = torch.cat(nlist_loc_list, -1) + else: + nlist_loc = None + nlist_type = torch.cat(nlist_type_list, -1) + return nlist_loc, nlist_type, nframes, nloc diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py new file mode 100644 index 0000000000..3ddd21fbb8 --- /dev/null +++ b/deepmd/pt/model/model/make_model.py @@ -0,0 +1,136 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + Optional, +) + +import torch + +from deepmd.model_format import ( + ModelOutputDef, +) +from deepmd.pt.model.model.transform_output import ( + communicate_extended_output, + fit_output_to_model_output, +) +from deepmd.pt.utils.nlist import ( + build_neighbor_list, + extend_coord_with_ghosts, +) +from deepmd.pt.utils.region import ( + normalize_coord, +) + + +def make_model(T_AtomicModel): + class CM(T_AtomicModel): + def __init__( + self, + *args, + **kwargs, + ): + super().__init__( + *args, + **kwargs, + ) + + def get_model_output_def(self): + return ModelOutputDef(self.get_fitting_output_def()) + + # cannot use the name forward. torch script does not work + def forward_common( + self, + coord, + atype, + box: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + """Return total energy of the system. + Args: + - coord: Atom coordinates with shape [nframes, natoms[1]*3]. + - atype: Atom types with shape [nframes, natoms[1]]. + - natoms: Atom statisics with shape [self.ntypes+2]. + - box: Simulation box with shape [nframes, 9]. + - atomic_virial: Whether or not compoute the atomic virial. + + Returns + ------- + - energy: Energy per atom. + - force: XYZ force per atom. + """ + nframes, nloc = atype.shape[:2] + if box is not None: + coord_normalized = normalize_coord(coord, box.reshape(-1, 3, 3)) + else: + coord_normalized = coord.clone() + extended_coord, extended_atype, mapping = extend_coord_with_ghosts( + coord_normalized, atype, box, self.get_rcut() + ) + nlist = build_neighbor_list( + extended_coord, + extended_atype, + nloc, + self.get_rcut(), + self.get_sel(), + distinguish_types=self.distinguish_types(), + ) + extended_coord = extended_coord.reshape(nframes, -1, 3) + model_predict_lower = self.forward_common_lower( + extended_coord, + extended_atype, + nlist, + mapping, + do_atomic_virial=do_atomic_virial, + ) + model_predict = communicate_extended_output( + model_predict_lower, + self.get_model_output_def(), + mapping, + do_atomic_virial=do_atomic_virial, + ) + return model_predict + + def forward_common_lower( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ): + """Return model prediction. + + Parameters + ---------- + extended_coord + coodinates in extended region + extended_atype + atomic type in extended region + nlist + neighbor list. nf x nloc x nsel + mapping + mapps the extended indices to local indices + do_atomic_virial + whether do atomic virial + + Returns + ------- + result_dict + the result dict, defined by the fitting net output def. + + """ + atomic_ret = self.forward_atomic( + extended_coord, + extended_atype, + nlist, + mapping=mapping, + ) + model_predict = fit_output_to_model_output( + atomic_ret, + self.get_fitting_output_def(), + extended_coord, + do_atomic_virial=do_atomic_virial, + ) + return model_predict + + return CM diff --git a/deepmd/pt/model/model/model.py b/deepmd/pt/model/model/model.py new file mode 100644 index 0000000000..139744c1e9 --- /dev/null +++ b/deepmd/pt/model/model/model.py @@ -0,0 +1,150 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging +import os + +import numpy as np +import torch + +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.stat import ( + compute_output_stats, +) + + +class BaseModel(torch.nn.Module): + def __init__(self): + """Construct a basic model for different tasks.""" + super().__init__() + + def forward(self, *args, **kwargs): + """Model output.""" + raise NotImplementedError + + def compute_or_load_stat( + self, + fitting_param, + ntypes, + resuming=False, + type_map=None, + stat_file_dir=None, + stat_file_path=None, + sampled=None, + ): + if fitting_param is None: + fitting_param = {} + if not resuming: + if sampled is not None: # compute stat + for sys in sampled: + for key in sys: + if isinstance(sys[key], list): + sys[key] = [item.to(env.DEVICE) for item in sys[key]] + else: + if sys[key] is not None: + sys[key] = sys[key].to(env.DEVICE) + sumr, suma, sumn, sumr2, suma2 = self.descriptor.compute_input_stats( + sampled + ) + + energy = [item["energy"] for item in sampled] + mixed_type = "real_natoms_vec" in sampled[0] + if mixed_type: + input_natoms = [item["real_natoms_vec"] for item in sampled] + else: + input_natoms = [item["natoms"] for item in sampled] + tmp = compute_output_stats(energy, input_natoms) + fitting_param["bias_atom_e"] = tmp[:, 0] + if stat_file_path is not None: + if not os.path.exists(stat_file_dir): + os.mkdir(stat_file_dir) + if not isinstance(stat_file_path, list): + logging.info(f"Saving stat file to {stat_file_path}") + np.savez_compressed( + stat_file_path, + sumr=sumr, + suma=suma, + sumn=sumn, + sumr2=sumr2, + suma2=suma2, + bias_atom_e=fitting_param["bias_atom_e"], + type_map=type_map, + ) + else: + for ii, file_path in enumerate(stat_file_path): + logging.info(f"Saving stat file to {file_path}") + np.savez_compressed( + file_path, + sumr=sumr[ii], + suma=suma[ii], + sumn=sumn[ii], + sumr2=sumr2[ii], + suma2=suma2[ii], + bias_atom_e=fitting_param["bias_atom_e"], + type_map=type_map, + ) + else: # load stat + target_type_map = type_map + if not isinstance(stat_file_path, list): + logging.info(f"Loading stat file from {stat_file_path}") + stats = np.load(stat_file_path) + stat_type_map = list(stats["type_map"]) + missing_type = [ + i for i in target_type_map if i not in stat_type_map + ] + assert not missing_type, f"These type are not in stat file {stat_file_path}: {missing_type}! Please change the stat file path!" + idx_map = [stat_type_map.index(i) for i in target_type_map] + if stats["sumr"].size: + sumr, suma, sumn, sumr2, suma2 = ( + stats["sumr"][idx_map], + stats["suma"][idx_map], + stats["sumn"][idx_map], + stats["sumr2"][idx_map], + stats["suma2"][idx_map], + ) + else: + sumr, suma, sumn, sumr2, suma2 = [], [], [], [], [] + fitting_param["bias_atom_e"] = stats["bias_atom_e"][idx_map] + else: + sumr, suma, sumn, sumr2, suma2 = [], [], [], [], [] + id_bias_atom_e = None + for ii, file_path in enumerate(stat_file_path): + logging.info(f"Loading stat file from {file_path}") + stats = np.load(file_path) + stat_type_map = list(stats["type_map"]) + missing_type = [ + i for i in target_type_map if i not in stat_type_map + ] + assert not missing_type, f"These type are not in stat file {file_path}: {missing_type}! Please change the stat file path!" + idx_map = [stat_type_map.index(i) for i in target_type_map] + if stats["sumr"].size: + sumr_tmp, suma_tmp, sumn_tmp, sumr2_tmp, suma2_tmp = ( + stats["sumr"][idx_map], + stats["suma"][idx_map], + stats["sumn"][idx_map], + stats["sumr2"][idx_map], + stats["suma2"][idx_map], + ) + else: + sumr_tmp, suma_tmp, sumn_tmp, sumr2_tmp, suma2_tmp = ( + [], + [], + [], + [], + [], + ) + sumr.append(sumr_tmp) + suma.append(suma_tmp) + sumn.append(sumn_tmp) + sumr2.append(sumr2_tmp) + suma2.append(suma2_tmp) + fitting_param["bias_atom_e"] = stats["bias_atom_e"][idx_map] + if id_bias_atom_e is None: + id_bias_atom_e = fitting_param["bias_atom_e"] + else: + assert ( + id_bias_atom_e == fitting_param["bias_atom_e"] + ).all(), "bias_atom_e in stat files are not consistent!" + self.descriptor.init_desc_stat(sumr, suma, sumn, sumr2, suma2) + else: # resuming for checkpoint; init model params from scratch + fitting_param["bias_atom_e"] = [0.0] * ntypes diff --git a/deepmd/pt/model/model/transform_output.py b/deepmd/pt/model/model/transform_output.py new file mode 100644 index 0000000000..673491d788 --- /dev/null +++ b/deepmd/pt/model/model/transform_output.py @@ -0,0 +1,214 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + List, + Optional, +) + +import torch + +from deepmd.model_format import ( + FittingOutputDef, + ModelOutputDef, + OutputVariableDef, + get_deriv_name, + get_reduce_name, +) + + +def atomic_virial_corr( + extended_coord: torch.Tensor, + atom_energy: torch.Tensor, +): + nall = extended_coord.shape[1] + nloc = atom_energy.shape[1] + coord, _ = torch.split(extended_coord, [nloc, nall - nloc], dim=1) + # no derivative with respect to the loc coord. + coord = coord.detach() + ce = coord * atom_energy + sumce0, sumce1, sumce2 = torch.split(torch.sum(ce, dim=1), [1, 1, 1], dim=-1) + faked_grad = torch.ones_like(sumce0) + lst = torch.jit.annotate(List[Optional[torch.Tensor]], [faked_grad]) + extended_virial_corr0 = torch.autograd.grad( + [sumce0], [extended_coord], grad_outputs=lst, create_graph=True + )[0] + assert extended_virial_corr0 is not None + extended_virial_corr1 = torch.autograd.grad( + [sumce1], [extended_coord], grad_outputs=lst, create_graph=True + )[0] + assert extended_virial_corr1 is not None + extended_virial_corr2 = torch.autograd.grad( + [sumce2], [extended_coord], grad_outputs=lst, create_graph=True + )[0] + assert extended_virial_corr2 is not None + extended_virial_corr = torch.concat( + [ + extended_virial_corr0.unsqueeze(-1), + extended_virial_corr1.unsqueeze(-1), + extended_virial_corr2.unsqueeze(-1), + ], + dim=-1, + ) + return extended_virial_corr + + +def task_deriv_one( + atom_energy: torch.Tensor, + energy: torch.Tensor, + extended_coord: torch.Tensor, + do_atomic_virial: bool = False, +): + faked_grad = torch.ones_like(energy) + lst = torch.jit.annotate(List[Optional[torch.Tensor]], [faked_grad]) + extended_force = torch.autograd.grad( + [energy], [extended_coord], grad_outputs=lst, create_graph=True + )[0] + assert extended_force is not None + extended_force = -extended_force + extended_virial = extended_force.unsqueeze(-1) @ extended_coord.unsqueeze(-2) + # the correction sums to zero, which does not contribute to global virial + if do_atomic_virial: + extended_virial_corr = atomic_virial_corr(extended_coord, atom_energy) + extended_virial = extended_virial + extended_virial_corr + return extended_force, extended_virial + + +def get_leading_dims( + vv: torch.Tensor, + vdef: OutputVariableDef, +): + """Get the dimensions of nf x nloc.""" + vshape = vv.shape + return list(vshape[: (len(vshape) - len(vdef.shape))]) + + +def get_atom_axis( + vdef: torch.Tensor, +): + """Get the axis of atoms.""" + atom_axis = -(len(vdef.shape) + 1) + return atom_axis + + +def take_deriv( + vv: torch.Tensor, + svv: torch.Tensor, + vdef: OutputVariableDef, + coord_ext: torch.Tensor, + do_atomic_virial: bool = False, +): + size = 1 + for ii in vdef.shape: + size *= ii + vv1 = vv.view(list(get_leading_dims(vv, vdef)) + [size]) # noqa: RUF005 + svv1 = svv.view(list(get_leading_dims(svv, vdef)) + [size]) # noqa: RUF005 + split_vv1 = torch.split(vv1, [1] * size, dim=-1) + split_svv1 = torch.split(svv1, [1] * size, dim=-1) + split_ff, split_avir = [], [] + for vvi, svvi in zip(split_vv1, split_svv1): + # nf x nloc x 3, nf x nloc x 3 x 3 + ffi, aviri = task_deriv_one( + vvi, svvi, coord_ext, do_atomic_virial=do_atomic_virial + ) + # nf x nloc x 1 x 3, nf x nloc x 1 x 3 x 3 + ffi = ffi.unsqueeze(-2) + aviri = aviri.unsqueeze(-3) + split_ff.append(ffi) + split_avir.append(aviri) + # nf x nloc x v_dim x 3, nf x nloc x v_dim x 3 x 3 + ff = torch.concat(split_ff, dim=-2) + avir = torch.concat(split_avir, dim=-3) + return ff, avir + + +def fit_output_to_model_output( + fit_ret: Dict[str, torch.Tensor], + fit_output_def: FittingOutputDef, + coord_ext: torch.Tensor, + do_atomic_virial: bool = False, +) -> Dict[str, torch.Tensor]: + """Transform the output of the fitting network to + the model output. + + """ + model_ret = dict(fit_ret.items()) + for kk, vv in fit_ret.items(): + vdef = fit_output_def[kk] + shap = vdef.shape + atom_axis = -(len(shap) + 1) + if vdef.reduciable: + kk_redu = get_reduce_name(kk) + model_ret[kk_redu] = torch.sum(vv, dim=atom_axis) + if vdef.differentiable: + kk_derv_r, kk_derv_c = get_deriv_name(kk) + dr, dc = take_deriv( + vv, + model_ret[kk_redu], + vdef, + coord_ext, + do_atomic_virial=do_atomic_virial, + ) + model_ret[kk_derv_r] = dr + model_ret[kk_derv_c] = dc + return model_ret + + +def communicate_extended_output( + model_ret: Dict[str, torch.Tensor], + model_output_def: ModelOutputDef, + mapping: torch.Tensor, # nf x nloc + do_atomic_virial: bool = False, +) -> Dict[str, torch.Tensor]: + """Transform the output of the model network defined on + local and ghost (extended) atoms to local atoms. + + """ + new_ret = {} + for kk in model_output_def.keys_outp(): + vv = model_ret[kk] + vdef = model_output_def[kk] + new_ret[kk] = vv + if vdef.reduciable: + kk_redu = get_reduce_name(kk) + new_ret[kk_redu] = model_ret[kk_redu] + if vdef.differentiable: + # nf x nloc + vldims = get_leading_dims(vv, vdef) + # nf x nall + mldims = list(mapping.shape) + kk_derv_r, kk_derv_c = get_deriv_name(kk) + # vdim x 3 + derv_r_ext_dims = list(vdef.shape) + [3] # noqa:RUF005 + mapping = mapping.view(mldims + [1] * len(derv_r_ext_dims)).expand( + [-1] * len(mldims) + derv_r_ext_dims + ) + force = torch.zeros( + vldims + derv_r_ext_dims, dtype=vv.dtype, device=vv.device + ) + # nf x nloc x 1 x 3 + new_ret[kk_derv_r] = torch.scatter_reduce( + force, + 1, + index=mapping, + src=model_ret[kk_derv_r], + reduce="sum", + ) + mapping = mapping.unsqueeze(-1).expand( + [-1] * (len(mldims) + len(derv_r_ext_dims)) + [3] + ) + virial = torch.zeros( + vldims + derv_r_ext_dims + [3], dtype=vv.dtype, device=vv.device + ) + # nf x nloc x 1 x 3 + new_ret[kk_derv_c] = torch.scatter_reduce( + virial, + 1, + index=mapping, + src=model_ret[kk_derv_c], + reduce="sum", + ) + new_ret[kk_derv_c + "_redu"] = torch.sum(new_ret[kk_derv_c], dim=1) + if not do_atomic_virial: + # pop atomic virial, because it is not correctly calculated. + new_ret.pop(kk_derv_c) + return new_ret diff --git a/deepmd/pt/model/network/__init__.py b/deepmd/pt/model/network/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/deepmd/pt/model/network/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/deepmd/pt/model/network/mlp.py b/deepmd/pt/model/network/mlp.py new file mode 100644 index 0000000000..e3ac0e7bc2 --- /dev/null +++ b/deepmd/pt/model/network/mlp.py @@ -0,0 +1,217 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + ClassVar, + Dict, + Optional, +) + +import numpy as np +import torch +import torch.nn as nn + +from deepmd.pt.utils import ( + env, +) + +device = env.DEVICE + +from deepmd.model_format import ( + NativeLayer, +) +from deepmd.model_format import NetworkCollection as DPNetworkCollection +from deepmd.model_format import ( + make_embedding_network, + make_fitting_network, + make_multilayer_network, +) +from deepmd.pt.utils.env import ( + DEFAULT_PRECISION, + PRECISION_DICT, +) +from deepmd.pt.utils.utils import ( + ActivationFn, +) + +try: + from deepmd._version import version as __version__ +except ImportError: + __version__ = "unknown" + + +def empty_t(shape, precision): + return torch.empty(shape, dtype=precision, device=device) + + +class MLPLayer(nn.Module): + def __init__( + self, + num_in, + num_out, + bias: bool = True, + use_timestep: bool = False, + activation_function: Optional[str] = None, + resnet: bool = False, + bavg: float = 0.0, + stddev: float = 1.0, + precision: str = DEFAULT_PRECISION, + ): + super().__init__() + self.use_timestep = use_timestep + self.activate_name = activation_function + self.activate = ActivationFn(self.activate_name) + self.precision = precision + self.prec = PRECISION_DICT[self.precision] + self.matrix = nn.Parameter(data=empty_t((num_in, num_out), self.prec)) + nn.init.normal_(self.matrix.data, std=stddev / np.sqrt(num_out + num_in)) + if bias: + self.bias = nn.Parameter( + data=empty_t([num_out], self.prec), + ) + nn.init.normal_(self.bias.data, mean=bavg, std=stddev) + else: + self.bias = None + if self.use_timestep: + self.idt = nn.Parameter(data=empty_t([num_out], self.prec)) + nn.init.normal_(self.idt.data, mean=0.1, std=0.001) + else: + self.idt = None + self.resnet = resnet + + def check_type_consistency(self): + precision = self.precision + + def check_var(var): + if var is not None: + # assertion "float64" == "double" would fail + assert PRECISION_DICT[var.dtype.name] is PRECISION_DICT[precision] + + check_var(self.w) + check_var(self.b) + check_var(self.idt) + + def dim_in(self) -> int: + return self.matrix.shape[0] + + def dim_out(self) -> int: + return self.matrix.shape[1] + + def forward( + self, + xx: torch.Tensor, + ) -> torch.Tensor: + """One MLP layer used by DP model. + + Parameters + ---------- + xx : torch.Tensor + The input. + + Returns + ------- + yy: torch.Tensor + The output. + """ + yy = ( + torch.matmul(xx, self.matrix) + self.bias + if self.bias is not None + else torch.matmul(xx, self.matrix) + ) + yy = self.activate(yy).clone() + yy = yy * self.idt if self.idt is not None else yy + if self.resnet: + if xx.shape[-1] == yy.shape[-1]: + yy += xx + elif 2 * xx.shape[-1] == yy.shape[-1]: + yy += torch.concat([xx, xx], dim=-1) + else: + yy = yy + return yy + + def serialize(self) -> dict: + """Serialize the layer to a dict. + + Returns + ------- + dict + The serialized layer. + """ + nl = NativeLayer( + self.matrix.shape[0], + self.matrix.shape[1], + bias=self.bias is not None, + use_timestep=self.idt is not None, + activation_function=self.activate_name, + resnet=self.resnet, + precision=self.precision, + ) + nl.w, nl.b, nl.idt = ( + self.matrix.detach().cpu().numpy(), + self.bias.detach().cpu().numpy() if self.bias is not None else None, + self.idt.detach().cpu().numpy() if self.idt is not None else None, + ) + return nl.serialize() + + @classmethod + def deserialize(cls, data: dict) -> "MLPLayer": + """Deserialize the layer from a dict. + + Parameters + ---------- + data : dict + The dict to deserialize from. + """ + nl = NativeLayer.deserialize(data) + obj = cls( + nl["matrix"].shape[0], + nl["matrix"].shape[1], + bias=nl["bias"] is not None, + use_timestep=nl["idt"] is not None, + activation_function=nl["activation_function"], + resnet=nl["resnet"], + precision=nl["precision"], + ) + prec = PRECISION_DICT[obj.precision] + + def check_load_param(ss): + return ( + nn.Parameter(data=torch.tensor(nl[ss], dtype=prec, device=device)) + if nl[ss] is not None + else None + ) + + obj.matrix = check_load_param("matrix") + obj.bias = check_load_param("bias") + obj.idt = check_load_param("idt") + return obj + + +MLP_ = make_multilayer_network(MLPLayer, nn.Module) + + +class MLP(MLP_): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.layers = torch.nn.ModuleList(self.layers) + + forward = MLP_.call + + +EmbeddingNet = make_embedding_network(MLP, MLPLayer) + +FittingNet = make_fitting_network(EmbeddingNet, MLP, MLPLayer) + + +class NetworkCollection(DPNetworkCollection, nn.Module): + """PyTorch implementation of NetworkCollection.""" + + NETWORK_TYPE_MAP: ClassVar[Dict[str, type]] = { + "network": MLP, + "embedding_network": EmbeddingNet, + # "fitting_network": FittingNet, + } + + def __init__(self, *args, **kwargs): + # init both two base classes + DPNetworkCollection.__init__(self, *args, **kwargs) + nn.Module.__init__(self) + self.networks = self._networks = torch.nn.ModuleList(self._networks) diff --git a/deepmd/pt/model/network/network.py b/deepmd/pt/model/network/network.py new file mode 100644 index 0000000000..8b5b3cf998 --- /dev/null +++ b/deepmd/pt/model/network/network.py @@ -0,0 +1,1897 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Optional, +) + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + +from deepmd.pt.utils import ( + env, +) + +try: + from typing import ( + Final, + ) +except ImportError: + from torch.jit import Final + +from functools import ( + partial, +) + +import torch.utils.checkpoint + +from deepmd.pt.utils.utils import ( + ActivationFn, + get_activation_fn, +) + + +def Tensor(*shape): + return torch.empty(shape, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + + +class Dropout(nn.Module): + def __init__(self, p): + super().__init__() + self.p = p + + def forward(self, x, inplace: bool = False): + if self.p > 0 and self.training: + return F.dropout(x, p=self.p, training=True, inplace=inplace) + else: + return x + + +class Identity(nn.Module): + def __init__(self): + super().__init__() + + def forward(self, x): + return x + + +class DropPath(torch.nn.Module): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).""" + + def __init__(self, prob=None): + super().__init__() + self.drop_prob = prob + + def forward(self, x): + if self.drop_prob == 0.0 or not self.training: + return x + keep_prob = 1 - self.drop_prob + shape = (x.shape[0],) + (1,) * ( + x.ndim - 1 + ) # work with diff dim tensors, not just 2D ConvNets + random_tensor = keep_prob + torch.rand(shape, dtype=x.dtype, device=x.device) + random_tensor.floor_() # binarize + output = x.div(keep_prob) * random_tensor + return output + + def extra_repr(self) -> str: + return f"prob={self.drop_prob}" + + +def softmax_dropout( + input_x, dropout_prob, is_training=True, mask=None, bias=None, inplace=True +): + input_x = input_x.contiguous() + if not inplace: + input_x = input_x.clone() + if mask is not None: + input_x += mask + if bias is not None: + input_x += bias + return F.dropout(F.softmax(input_x, dim=-1), p=dropout_prob, training=is_training) + + +def checkpoint_sequential( + functions, + input_x, + enabled=True, +): + def wrap_tuple(a): + return (a,) if type(a) is not tuple else a + + def exec(func, a): + return wrap_tuple(func(*a)) + + def get_wrap_exec(func): + def wrap_exec(*a): + return exec(func, a) + + return wrap_exec + + input_x = wrap_tuple(input_x) + + is_grad_enabled = torch.is_grad_enabled() + + if enabled and is_grad_enabled: + for func in functions: + input_x = torch.utils.checkpoint.checkpoint(get_wrap_exec(func), *input_x) + else: + for func in functions: + input_x = exec(func, input_x) + return input_x + + +class ResidualLinear(nn.Module): + resnet: Final[int] + + def __init__(self, num_in, num_out, bavg=0.0, stddev=1.0, resnet_dt=False): + """Construct a residual linear layer. + + Args: + - num_in: Width of input tensor. + - num_out: Width of output tensor. + - resnet_dt: Using time-step in the ResNet construction. + """ + super().__init__() + self.num_in = num_in + self.num_out = num_out + self.resnet = resnet_dt + + self.matrix = nn.Parameter(data=Tensor(num_in, num_out)) + nn.init.normal_(self.matrix.data, std=stddev / np.sqrt(num_out + num_in)) + self.bias = nn.Parameter(data=Tensor(1, num_out)) + nn.init.normal_(self.bias.data, mean=bavg, std=stddev) + if self.resnet: + self.idt = nn.Parameter(data=Tensor(1, num_out)) + nn.init.normal_(self.idt.data, mean=1.0, std=0.001) + + def forward(self, inputs): + """Return X ?+ X*W+b.""" + xw_plus_b = torch.matmul(inputs, self.matrix) + self.bias + hidden = torch.tanh(xw_plus_b) + if self.resnet: + hidden = hidden * self.idt + if self.num_in == self.num_out: + return inputs + hidden + elif self.num_in * 2 == self.num_out: + return torch.cat([inputs, inputs], dim=1) + hidden + else: + return hidden + + +class TypeFilter(nn.Module): + use_tebd: Final[bool] + tebd_mode: Final[str] + + def __init__( + self, + offset, + length, + neuron, + return_G=False, + tebd_dim=0, + use_tebd=False, + tebd_mode="concat", + ): + """Construct a filter on the given element as neighbor. + + Args: + - offset: Element offset in the descriptor matrix. + - length: Atom count of this element. + - neuron: Number of neurons in each hidden layers of the embedding net. + """ + super().__init__() + self.offset = offset + self.length = length + self.tebd_dim = tebd_dim + self.use_tebd = use_tebd + self.tebd_mode = tebd_mode + supported_tebd_mode = ["concat", "dot", "dot_residual_s", "dot_residual_t"] + assert ( + tebd_mode in supported_tebd_mode + ), f"Unknown tebd_mode {tebd_mode}! Supported are {supported_tebd_mode}." + if use_tebd and tebd_mode == "concat": + self.neuron = [1 + tebd_dim * 2, *neuron] + else: + self.neuron = [1, *neuron] + + deep_layers = [] + for ii in range(1, len(self.neuron)): + one = ResidualLinear(self.neuron[ii - 1], self.neuron[ii]) + deep_layers.append(one) + self.deep_layers = nn.ModuleList(deep_layers) + + deep_layers_t = [] + if use_tebd and tebd_mode in ["dot", "dot_residual_s", "dot_residual_t"]: + self.neuron_t = [tebd_dim * 2, *neuron] + for ii in range(1, len(self.neuron_t)): + one = ResidualLinear(self.neuron_t[ii - 1], self.neuron_t[ii]) + deep_layers_t.append(one) + self.deep_layers_t = nn.ModuleList(deep_layers_t) + + self.return_G = return_G + + def forward( + self, + inputs, + atype_tebd: Optional[torch.Tensor] = None, + nlist_tebd: Optional[torch.Tensor] = None, + ): + """Calculate decoded embedding for each atom. + + Args: + - inputs: Descriptor matrix. Its shape is [nframes*natoms[0], len_descriptor]. + + Returns + ------- + - `torch.Tensor`: Embedding contributed by me. Its shape is [nframes*natoms[0], 4, self.neuron[-1]]. + """ + inputs_i = inputs[:, self.offset * 4 : (self.offset + self.length) * 4] + inputs_reshape = inputs_i.reshape( + -1, 4 + ) # shape is [nframes*natoms[0]*self.length, 4] + xyz_scatter = inputs_reshape[:, 0:1] + + # concat the tebd as input + if self.use_tebd and self.tebd_mode == "concat": + assert nlist_tebd is not None and atype_tebd is not None + nlist_tebd = nlist_tebd.reshape(-1, self.tebd_dim) + atype_tebd = atype_tebd.reshape(-1, self.tebd_dim) + # [nframes * nloc * nnei, 1 + tebd_dim * 2] + xyz_scatter = torch.concat([xyz_scatter, nlist_tebd, atype_tebd], dim=1) + + for linear in self.deep_layers: + xyz_scatter = linear(xyz_scatter) + # [nframes * nloc * nnei, out_size] + + # dot the tebd output + if self.use_tebd and self.tebd_mode in [ + "dot", + "dot_residual_s", + "dot_residual_t", + ]: + assert nlist_tebd is not None and atype_tebd is not None + nlist_tebd = nlist_tebd.reshape(-1, self.tebd_dim) + atype_tebd = atype_tebd.reshape(-1, self.tebd_dim) + # [nframes * nloc * nnei, tebd_dim * 2] + two_side_tebd = torch.concat([nlist_tebd, atype_tebd], dim=1) + for linear in self.deep_layers_t: + two_side_tebd = linear(two_side_tebd) + # [nframes * nloc * nnei, out_size] + if self.tebd_mode == "dot": + xyz_scatter = xyz_scatter * two_side_tebd + elif self.tebd_mode == "dot_residual_s": + xyz_scatter = xyz_scatter * two_side_tebd + xyz_scatter + elif self.tebd_mode == "dot_residual_t": + xyz_scatter = xyz_scatter * two_side_tebd + two_side_tebd + + xyz_scatter = xyz_scatter.view( + -1, self.length, self.neuron[-1] + ) # shape is [nframes*natoms[0], self.length, self.neuron[-1]] + if self.return_G: + return xyz_scatter + else: + # shape is [nframes*natoms[0], 4, self.length] + inputs_reshape = inputs_i.view(-1, self.length, 4).permute(0, 2, 1) + return torch.matmul(inputs_reshape, xyz_scatter) + + +class SimpleLinear(nn.Module): + use_timestep: Final[bool] + + def __init__( + self, + num_in, + num_out, + bavg=0.0, + stddev=1.0, + use_timestep=False, + activate=None, + bias: bool = True, + ): + """Construct a linear layer. + + Args: + - num_in: Width of input tensor. + - num_out: Width of output tensor. + - use_timestep: Apply time-step to weight. + - activate: type of activate func. + """ + super().__init__() + self.num_in = num_in + self.num_out = num_out + self.use_timestep = use_timestep + self.activate = ActivationFn(activate) + + self.matrix = nn.Parameter(data=Tensor(num_in, num_out)) + nn.init.normal_(self.matrix.data, std=stddev / np.sqrt(num_out + num_in)) + if bias: + self.bias = nn.Parameter(data=Tensor(1, num_out)) + nn.init.normal_(self.bias.data, mean=bavg, std=stddev) + else: + self.bias = None + if self.use_timestep: + self.idt = nn.Parameter(data=Tensor(1, num_out)) + nn.init.normal_(self.idt.data, mean=0.1, std=0.001) + + def forward(self, inputs): + """Return X*W+b.""" + xw = torch.matmul(inputs, self.matrix) + hidden = xw + self.bias if self.bias is not None else xw + hidden = self.activate(hidden) + if self.use_timestep: + hidden = hidden * self.idt + return hidden + + +class Linear(nn.Linear): + def __init__( + self, + d_in: int, + d_out: int, + bias: bool = True, + init: str = "default", + ): + super().__init__(d_in, d_out, bias=bias, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + + self.use_bias = bias + + if self.use_bias: + with torch.no_grad(): + self.bias.fill_(0) + + if init == "default": + self._trunc_normal_init(1.0) + elif init == "relu": + self._trunc_normal_init(2.0) + elif init == "glorot": + self._glorot_uniform_init() + elif init == "gating": + self._zero_init(self.use_bias) + elif init == "normal": + self._normal_init() + elif init == "final": + self._zero_init(False) + else: + raise ValueError("Invalid init method.") + + def _trunc_normal_init(self, scale=1.0): + # Constant from scipy.stats.truncnorm.std(a=-2, b=2, loc=0., scale=1.) + TRUNCATED_NORMAL_STDDEV_FACTOR = 0.87962566103423978 + _, fan_in = self.weight.shape + scale = scale / max(1, fan_in) + std = (scale**0.5) / TRUNCATED_NORMAL_STDDEV_FACTOR + nn.init.trunc_normal_(self.weight, mean=0.0, std=std) + + def _glorot_uniform_init(self): + nn.init.xavier_uniform_(self.weight, gain=1) + + def _zero_init(self, use_bias=True): + with torch.no_grad(): + self.weight.fill_(0.0) + if use_bias: + with torch.no_grad(): + self.bias.fill_(1.0) + + def _normal_init(self): + nn.init.kaiming_normal_(self.weight, nonlinearity="linear") + + +class Transition(nn.Module): + def __init__(self, d_in, n, dropout=0.0): + super().__init__() + + self.d_in = d_in + self.n = n + + self.linear_1 = Linear(self.d_in, self.n * self.d_in, init="relu") + self.act = nn.GELU() + self.linear_2 = Linear(self.n * self.d_in, d_in, init="final") + self.dropout = dropout + + def _transition(self, x): + x = self.linear_1(x) + x = self.act(x) + x = F.dropout(x, p=self.dropout, training=self.training) + x = self.linear_2(x) + return x + + def forward( + self, + x: torch.Tensor, + ) -> torch.Tensor: + x = self._transition(x=x) + return x + + +class Embedding(nn.Embedding): + def __init__( + self, + num_embeddings: int, + embedding_dim: int, + padding_idx: Optional[int] = None, + dtype=torch.float64, + ): + super().__init__( + num_embeddings, embedding_dim, padding_idx=padding_idx, dtype=dtype + ) + self._normal_init() + + if padding_idx is not None: + self.weight.data[self.padding_idx].zero_() + + def _normal_init(self, std=0.02): + nn.init.normal_(self.weight, mean=0.0, std=std) + + +class NonLinearHead(nn.Module): + def __init__(self, input_dim, out_dim, activation_fn, hidden=None): + super().__init__() + hidden = input_dim if not hidden else hidden + self.linear1 = SimpleLinear(input_dim, hidden, activate=activation_fn) + self.linear2 = SimpleLinear(hidden, out_dim) + + def forward(self, x): + x = self.linear1(x) + x = self.linear2(x) + return x + + +class NonLinear(nn.Module): + def __init__(self, input, output_size, hidden=None): + super().__init__() + + if hidden is None: + hidden = input + self.layer1 = Linear(input, hidden, init="relu") + self.layer2 = Linear(hidden, output_size, init="final") + + def forward(self, x): + x = F.linear(x, self.layer1.weight) + # x = fused_ops.bias_torch_gelu(x, self.layer1.bias) + x = nn.GELU()(x) + self.layer1.bias + x = self.layer2(x) + return x + + def zero_init(self): + nn.init.zeros_(self.layer2.weight) + nn.init.zeros_(self.layer2.bias) + + +class MaskLMHead(nn.Module): + """Head for masked language modeling.""" + + def __init__(self, embed_dim, output_dim, activation_fn, weight=None): + super().__init__() + self.dense = SimpleLinear(embed_dim, embed_dim) + self.activation_fn = get_activation_fn(activation_fn) + self.layer_norm = nn.LayerNorm(embed_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + + if weight is None: + weight = nn.Linear( + embed_dim, output_dim, bias=False, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ).weight + self.weight = weight + self.bias = nn.Parameter( + torch.zeros(output_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + ) + + def forward(self, features, masked_tokens: Optional[torch.Tensor] = None, **kwargs): + # Only project the masked tokens while training, + # saves both memory and computation + if masked_tokens is not None: + features = features[masked_tokens, :] + + x = self.dense(features) + x = self.activation_fn(x) + x = self.layer_norm(x) + # project back to size of vocabulary with bias + x = F.linear(x, self.weight) + self.bias + return x + + +class ResidualDeep(nn.Module): + def __init__( + self, type_id, embedding_width, neuron, bias_atom_e, out_dim=1, resnet_dt=False + ): + """Construct a filter on the given element as neighbor. + + Args: + - typei: Element ID. + - embedding_width: Embedding width per atom. + - neuron: Number of neurons in each hidden layers of the embedding net. + - resnet_dt: Using time-step in the ResNet construction. + """ + super().__init__() + self.type_id = type_id + self.neuron = [embedding_width, *neuron] + self.out_dim = out_dim + + deep_layers = [] + for ii in range(1, len(self.neuron)): + one = SimpleLinear( + num_in=self.neuron[ii - 1], + num_out=self.neuron[ii], + use_timestep=( + resnet_dt and ii > 1 and self.neuron[ii - 1] == self.neuron[ii] + ), + activate="tanh", + ) + deep_layers.append(one) + self.deep_layers = nn.ModuleList(deep_layers) + if not env.ENERGY_BIAS_TRAINABLE: + bias_atom_e = 0 + self.final_layer = SimpleLinear(self.neuron[-1], self.out_dim, bias_atom_e) + + def forward(self, inputs): + """Calculate decoded embedding for each atom. + + Args: + - inputs: Embedding net output per atom. Its shape is [nframes*nloc, self.embedding_width]. + + Returns + ------- + - `torch.Tensor`: Output layer with shape [nframes*nloc, self.neuron[-1]]. + """ + outputs = inputs + for idx, linear in enumerate(self.deep_layers): + if idx > 0 and linear.num_in == linear.num_out: + outputs = outputs + linear(outputs) + else: + outputs = linear(outputs) + outputs = self.final_layer(outputs) + return outputs + + +class TypeEmbedNet(nn.Module): + def __init__(self, type_nums, embed_dim, bavg=0.0, stddev=1.0): + """Construct a type embedding net.""" + super().__init__() + self.embedding = nn.Embedding( + type_nums + 1, + embed_dim, + padding_idx=type_nums, + dtype=env.GLOBAL_PT_FLOAT_PRECISION, + ) + # nn.init.normal_(self.embedding.weight[:-1], mean=bavg, std=stddev) + + def forward(self, atype): + """ + Args: + atype: Type of each input, [nframes, nloc] or [nframes, nloc, nnei]. + + Returns + ------- + type_embedding: + + """ + return self.embedding(atype) + + def share_params(self, base_class, shared_level, resume=False): + assert ( + self.__class__ == base_class.__class__ + ), "Only TypeEmbedNet of the same type can share params!" + if shared_level == 0: + # the following will successfully link all the params except buffers, which need manually link. + for item in self._modules: + self._modules[item] = base_class._modules[item] + else: + raise NotImplementedError + + +@torch.jit.script +def gaussian(x, mean, std: float): + pi = 3.14159 + a = (2 * pi) ** 0.5 + return torch.exp(-0.5 * (((x - mean) / std) ** 2)) / (a * std) + + +class GaussianKernel(nn.Module): + def __init__(self, K=128, num_pair=512, std_width=1.0, start=0.0, stop=9.0): + super().__init__() + self.K = K + std_width = std_width + start = start + stop = stop + mean = torch.linspace(start, stop, K, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + self.std = (std_width * (mean[1] - mean[0])).item() + self.register_buffer("mean", mean) + self.mul = Embedding( + num_pair + 1, 1, padding_idx=num_pair, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + self.bias = Embedding( + num_pair + 1, 1, padding_idx=num_pair, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + nn.init.constant_(self.bias.weight, 0) + nn.init.constant_(self.mul.weight, 1.0) + + def forward(self, x, atom_pair): + mul = self.mul(atom_pair).abs().sum(dim=-2) + bias = self.bias(atom_pair).sum(dim=-2) + x = mul * x.unsqueeze(-1) + bias + # [nframes, nloc, nnei, K] + x = x.expand(-1, -1, -1, self.K) + mean = self.mean.view(-1) + return gaussian(x, mean, self.std) + + +class GaussianEmbedding(nn.Module): + def __init__( + self, + rcut, + kernel_num, + num_pair, + embed_dim, + pair_embed_dim, + sel, + ntypes, + atomic_sum_gbf, + ): + """Construct a gaussian kernel based embedding of pair representation. + + Args: + rcut: Radial cutoff. + kernel_num: Number of gaussian kernels. + num_pair: Number of different pairs. + embed_dim: Dimension of atomic representation. + pair_embed_dim: Dimension of pair representation. + sel: Number of neighbors. + ntypes: Number of atom types. + """ + super().__init__() + self.gbf = GaussianKernel(K=kernel_num, num_pair=num_pair, stop=rcut) + self.gbf_proj = NonLinear(kernel_num, pair_embed_dim) + self.embed_dim = embed_dim + self.pair_embed_dim = pair_embed_dim + self.atomic_sum_gbf = atomic_sum_gbf + if self.atomic_sum_gbf: + if kernel_num != self.embed_dim: + self.edge_proj = torch.nn.Linear( + kernel_num, self.embed_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + else: + self.edge_proj = None + self.ntypes = ntypes + self.nnei = sel + + def forward(self, coord_selected, atom_feature, edge_type_2dim, edge_feature): + ## local cluster forward + """Calculate decoded embedding for each atom. + Args: + coord_selected: Clustered atom coordinates with shape [nframes*nloc, natoms, 3]. + atom_feature: Previous calculated atomic features with shape [nframes*nloc, natoms, embed_dim]. + edge_type_2dim: Edge index for gbf calculation with shape [nframes*nloc, natoms, natoms, 2]. + edge_feature: Previous calculated edge features with shape [nframes*nloc, natoms, natoms, pair_dim]. + + Returns + ------- + atom_feature: Updated atomic features with shape [nframes*nloc, natoms, embed_dim]. + attn_bias: Updated edge features as attention bias with shape [nframes*nloc, natoms, natoms, pair_dim]. + delta_pos: Delta position for force/vector prediction with shape [nframes*nloc, natoms, natoms, 3]. + """ + ncluster, natoms, _ = coord_selected.shape + # ncluster x natoms x natoms x 3 + delta_pos = coord_selected.unsqueeze(1) - coord_selected.unsqueeze(2) + # (ncluster x natoms x natoms + dist = delta_pos.norm(dim=-1).view(-1, natoms, natoms) + # [ncluster, natoms, natoms, K] + gbf_feature = self.gbf(dist, edge_type_2dim) + if self.atomic_sum_gbf: + edge_features = gbf_feature + # [ncluster, natoms, K] + sum_edge_features = edge_features.sum(dim=-2) + if self.edge_proj is not None: + sum_edge_features = self.edge_proj(sum_edge_features) + # [ncluster, natoms, embed_dim] + atom_feature = atom_feature + sum_edge_features + + # [ncluster, natoms, natoms, pair_dim] + gbf_result = self.gbf_proj(gbf_feature) + + attn_bias = gbf_result + edge_feature + return atom_feature, attn_bias, delta_pos + + +class NeighborWiseAttention(nn.Module): + def __init__( + self, + layer_num, + nnei, + embed_dim, + hidden_dim, + dotr=False, + do_mask=False, + post_ln=True, + ffn=False, + ffn_embed_dim=1024, + activation="tanh", + scaling_factor=1.0, + head_num=1, + normalize=True, + temperature=None, + ): + """Construct a neighbor-wise attention net.""" + super().__init__() + self.layer_num = layer_num + attention_layers = [] + for i in range(self.layer_num): + attention_layers.append( + NeighborWiseAttentionLayer( + nnei, + embed_dim, + hidden_dim, + dotr=dotr, + do_mask=do_mask, + post_ln=post_ln, + ffn=ffn, + ffn_embed_dim=ffn_embed_dim, + activation=activation, + scaling_factor=scaling_factor, + head_num=head_num, + normalize=normalize, + temperature=temperature, + ) + ) + self.attention_layers = nn.ModuleList(attention_layers) + + def forward( + self, + input_G, + nei_mask, + input_r: Optional[torch.Tensor] = None, + sw: Optional[torch.Tensor] = None, + ): + """ + Args: + input_G: Input G, [nframes * nloc, nnei, embed_dim]. + nei_mask: neighbor mask, [nframes * nloc, nnei]. + input_r: normalized radial, [nframes, nloc, nei, 3]. + + Returns + ------- + out: Output G, [nframes * nloc, nnei, embed_dim] + + """ + out = input_G + # https://github.com/pytorch/pytorch/issues/39165#issuecomment-635472592 + for layer in self.attention_layers: + out = layer(out, nei_mask, input_r=input_r, sw=sw) + return out + + +class NeighborWiseAttentionLayer(nn.Module): + ffn: Final[bool] + + def __init__( + self, + nnei, + embed_dim, + hidden_dim, + dotr=False, + do_mask=False, + post_ln=True, + ffn=False, + ffn_embed_dim=1024, + activation="tanh", + scaling_factor=1.0, + head_num=1, + normalize=True, + temperature=None, + ): + """Construct a neighbor-wise attention layer.""" + super().__init__() + self.nnei = nnei + self.embed_dim = embed_dim + self.hidden_dim = hidden_dim + self.dotr = dotr + self.do_mask = do_mask + self.post_ln = post_ln + self.ffn = ffn + self.attention_layer = GatedSelfAttetion( + nnei, + embed_dim, + hidden_dim, + dotr=dotr, + do_mask=do_mask, + scaling_factor=scaling_factor, + head_num=head_num, + normalize=normalize, + temperature=temperature, + ) + self.attn_layer_norm = nn.LayerNorm( + self.embed_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + if self.ffn: + self.ffn_embed_dim = ffn_embed_dim + self.fc1 = nn.Linear( + self.embed_dim, self.ffn_embed_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + self.activation_fn = get_activation_fn(activation) + self.fc2 = nn.Linear( + self.ffn_embed_dim, self.embed_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + self.final_layer_norm = nn.LayerNorm( + self.embed_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + + def forward( + self, + x, + nei_mask, + input_r: Optional[torch.Tensor] = None, + sw: Optional[torch.Tensor] = None, + ): + residual = x + if not self.post_ln: + x = self.attn_layer_norm(x) + x = self.attention_layer(x, nei_mask, input_r=input_r, sw=sw) + x = residual + x + if self.post_ln: + x = self.attn_layer_norm(x) + if self.ffn: + residual = x + if not self.post_ln: + x = self.final_layer_norm(x) + x = self.fc1(x) + x = self.activation_fn(x) + x = self.fc2(x) + x = residual + x + if self.post_ln: + x = self.final_layer_norm(x) + return x + + +class GatedSelfAttetion(nn.Module): + def __init__( + self, + nnei, + embed_dim, + hidden_dim, + dotr=False, + do_mask=False, + scaling_factor=1.0, + head_num=1, + normalize=True, + temperature=None, + bias=True, + smooth=True, + ): + """Construct a neighbor-wise attention net.""" + super().__init__() + self.nnei = nnei + self.embed_dim = embed_dim + self.hidden_dim = hidden_dim + self.head_num = head_num + self.dotr = dotr + self.do_mask = do_mask + if temperature is None: + self.scaling = (self.hidden_dim * scaling_factor) ** -0.5 + else: + self.scaling = temperature + self.normalize = normalize + self.in_proj = SimpleLinear( + embed_dim, + hidden_dim * 3, + bavg=0.0, + stddev=1.0, + use_timestep=False, + bias=bias, + ) + self.out_proj = SimpleLinear( + hidden_dim, embed_dim, bavg=0.0, stddev=1.0, use_timestep=False, bias=bias + ) + self.smooth = smooth + + def forward( + self, + query, + nei_mask, + input_r: Optional[torch.Tensor] = None, + sw: Optional[torch.Tensor] = None, + attnw_shift: float = 20.0, + ): + """ + Args: + query: input G, [nframes * nloc, nnei, embed_dim]. + nei_mask: neighbor mask, [nframes * nloc, nnei]. + input_r: normalized radial, [nframes, nloc, nei, 3]. + + Returns + ------- + type_embedding: + + """ + q, k, v = self.in_proj(query).chunk(3, dim=-1) + # [nframes * nloc, nnei, hidden_dim] + q = q.view(-1, self.nnei, self.hidden_dim) + k = k.view(-1, self.nnei, self.hidden_dim) + v = v.view(-1, self.nnei, self.hidden_dim) + if self.normalize: + q = F.normalize(q, dim=-1) + k = F.normalize(k, dim=-1) + v = F.normalize(v, dim=-1) + q = q * self.scaling + k = k.transpose(1, 2) + # [nframes * nloc, nnei, nnei] + attn_weights = torch.bmm(q, k) + # [nframes * nloc, nnei] + nei_mask = nei_mask.view(-1, self.nnei) + if self.smooth: + # [nframes * nloc, nnei] + assert sw is not None + sw = sw.view([-1, self.nnei]) + attn_weights = (attn_weights + attnw_shift) * sw[:, :, None] * sw[ + :, None, : + ] - attnw_shift + else: + attn_weights = attn_weights.masked_fill( + ~nei_mask.unsqueeze(1), float("-inf") + ) + attn_weights = F.softmax(attn_weights, dim=-1) + attn_weights = attn_weights.masked_fill(~nei_mask.unsqueeze(-1), 0.0) + if self.smooth: + assert sw is not None + attn_weights = attn_weights * sw[:, :, None] * sw[:, None, :] + if self.dotr: + assert input_r is not None, "input_r must be provided when dotr is True!" + angular_weight = torch.bmm(input_r, input_r.transpose(1, 2)) + attn_weights = attn_weights * angular_weight + o = torch.bmm(attn_weights, v) + output = self.out_proj(o) + return output + + +class LocalSelfMultiheadAttention(nn.Module): + def __init__(self, feature_dim, attn_head, scaling_factor=1.0): + super().__init__() + self.feature_dim = feature_dim + self.attn_head = attn_head + self.head_dim = feature_dim // attn_head + assert ( + feature_dim % attn_head == 0 + ), f"feature_dim {feature_dim} must be divided by attn_head {attn_head}!" + self.scaling = (self.head_dim * scaling_factor) ** -0.5 + self.in_proj = SimpleLinear(self.feature_dim, self.feature_dim * 3) + # TODO debug + # self.out_proj = SimpleLinear(self.feature_dim, self.feature_dim) + + def forward( + self, + query, + attn_bias: Optional[torch.Tensor] = None, + nlist_mask: Optional[torch.Tensor] = None, + nlist: Optional[torch.Tensor] = None, + return_attn=True, + ): + nframes, nloc, feature_dim = query.size() + _, _, nnei = nlist.size() + assert feature_dim == self.feature_dim + # [nframes, nloc, feature_dim] + q, k, v = self.in_proj(query).chunk(3, dim=-1) + # [nframes * attn_head * nloc, 1, head_dim] + q = ( + q.view(nframes, nloc, self.attn_head, self.head_dim) + .transpose(1, 2) + .contiguous() + .view(nframes * self.attn_head * nloc, 1, self.head_dim) + * self.scaling + ) + # [nframes, nloc, feature_dim] --> [nframes, nloc + 1, feature_dim] + # with nlist [nframes, nloc, nnei] --> [nframes, nloc, nnei, feature_dim] + # padding = torch.zeros(feature_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION).to(k.device) + # k = torch.concat([k, padding.unsqueeze(0).unsqueeze(1)], dim=1) + # v = torch.concat([v, padding.unsqueeze(0).unsqueeze(1)], dim=1) + + # [nframes, nloc * nnei, feature_dim] + index = nlist.view(nframes, -1).unsqueeze(-1).expand(-1, -1, feature_dim) + k = torch.gather(k, dim=1, index=index) + # [nframes, nloc * nnei, feature_dim] + v = torch.gather(v, dim=1, index=index) + # [nframes * attn_head * nloc, nnei, head_dim] + k = ( + k.view(nframes, nloc, nnei, self.attn_head, self.head_dim) + .permute(0, 3, 1, 2, 4) + .contiguous() + .view(nframes * self.attn_head * nloc, nnei, self.head_dim) + ) + v = ( + v.view(nframes, nloc, nnei, self.attn_head, self.head_dim) + .permute(0, 3, 1, 2, 4) + .contiguous() + .view(nframes * self.attn_head * nloc, nnei, self.head_dim) + ) + # [nframes * attn_head * nloc, 1, nnei] + attn_weights = torch.bmm(q, k.transpose(1, 2)) + # maskfill + # [nframes, attn_head, nloc, nnei] + attn_weights = attn_weights.view( + nframes, self.attn_head, nloc, nnei + ).masked_fill(~nlist_mask.unsqueeze(1), float("-inf")) + # add bias + if return_attn: + attn_weights = attn_weights + attn_bias + # softmax + # [nframes * attn_head * nloc, 1, nnei] + attn = F.softmax(attn_weights, dim=-1).view( + nframes * self.attn_head * nloc, 1, nnei + ) + # bmm + # [nframes * attn_head * nloc, 1, head_dim] + o = torch.bmm(attn, v) + assert list(o.size()) == [nframes * self.attn_head * nloc, 1, self.head_dim] + # [nframes, nloc, feature_dim] + o = ( + o.view(nframes, self.attn_head, nloc, self.head_dim) + .transpose(1, 2) + .contiguous() + .view(nframes, nloc, self.feature_dim) + ) + # out + ## TODO debug: + # o = self.out_proj(o) + if not return_attn: + return o + else: + return o, attn_weights, attn + + +class NodeTaskHead(nn.Module): + def __init__( + self, + embed_dim: int, + pair_dim: int, + num_head: int, + ): + super().__init__() + self.layer_norm = nn.LayerNorm(embed_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + self.pair_norm = nn.LayerNorm(pair_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + self.embed_dim = embed_dim + self.q_proj = Linear(embed_dim, embed_dim, bias=False, init="glorot") + self.k_proj = Linear(embed_dim, embed_dim, bias=False, init="glorot") + self.v_proj = Linear(embed_dim, embed_dim, bias=False, init="glorot") + self.num_heads = num_head + self.head_dim = embed_dim // num_head + self.scaling = self.head_dim**-0.5 + self.force_proj = Linear(embed_dim, 1, init="final", bias=False) + self.linear_bias = Linear(pair_dim, num_head) + self.dropout = 0.1 + + def zero_init(self): + nn.init.zeros_(self.force_proj.weight) + + def forward( + self, + query: Tensor, + pair: Tensor, + delta_pos: Tensor, + attn_mask: Tensor = None, + ) -> Tensor: + ncluster, natoms, _ = query.size() + query = self.layer_norm(query) + # [ncluster, natoms, natoms, pair_dim] + pair = self.pair_norm(pair) + + # [ncluster, attn_head, natoms, head_dim] + q = ( + self.q_proj(query) + .view(ncluster, natoms, self.num_heads, -1) + .transpose(1, 2) + * self.scaling + ) + # [ncluster, attn_head, natoms, head_dim] + k = ( + self.k_proj(query) + .view(ncluster, natoms, self.num_heads, -1) + .transpose(1, 2) + ) + v = ( + self.v_proj(query) + .view(ncluster, natoms, self.num_heads, -1) + .transpose(1, 2) + ) + # [ncluster, attn_head, natoms, natoms] + attn = q @ k.transpose(-1, -2) + del q, k + # [ncluster, attn_head, natoms, natoms] + bias = self.linear_bias(pair).permute(0, 3, 1, 2).contiguous() + + # [ncluster, attn_head, natoms, natoms] + attn_probs = softmax_dropout( + attn, + self.dropout, + self.training, + mask=attn_mask, + bias=bias.contiguous(), + ).view(ncluster, self.num_heads, natoms, natoms) + + # delta_pos: [ncluster, natoms, natoms, 3] + # [ncluster, attn_head, natoms, natoms, 3] + rot_attn_probs = attn_probs.unsqueeze(-1) * delta_pos.unsqueeze(1).type_as( + attn_probs + ) + # [ncluster, attn_head, 3, natoms, natoms] + rot_attn_probs = rot_attn_probs.permute(0, 1, 4, 2, 3) + # [ncluster, attn_head, 3, natoms, head_dim] + x = rot_attn_probs @ v.unsqueeze(2) + # [ncluster, natoms, 3, embed_dim] + x = x.permute(0, 3, 2, 1, 4).contiguous().view(ncluster, natoms, 3, -1) + cur_force = self.force_proj(x).view(ncluster, natoms, 3) + return cur_force + + +class EnergyHead(nn.Module): + def __init__( + self, + input_dim, + output_dim, + ): + super().__init__() + self.layer_norm = nn.LayerNorm(input_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + self.linear_in = Linear(input_dim, input_dim, init="relu") + + self.linear_out = Linear(input_dim, output_dim, bias=True, init="final") + + def forward(self, x): + x = x.type(self.linear_in.weight.dtype) + x = F.gelu(self.layer_norm(self.linear_in(x))) + x = self.linear_out(x) + return x + + +class OuterProduct(nn.Module): + def __init__(self, d_atom, d_pair, d_hid=32): + super().__init__() + + self.d_atom = d_atom + self.d_pair = d_pair + self.d_hid = d_hid + + self.linear_in = nn.Linear( + d_atom, d_hid * 2, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + self.linear_out = nn.Linear( + d_hid**2, d_pair, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + self.act = nn.GELU() + + def _opm(self, a, b): + # [nframes, nloc, d] + nframes, nloc, d = a.shape + a = a.view(nframes, nloc, 1, d, 1) + b = b.view(nframes, 1, nloc, 1, d) + # [nframes, nloc, nloc, d, d] + outer = a * b + outer = outer.view(outer.shape[:-2] + (-1,)) + outer = self.linear_out(outer) + return outer + + def forward( + self, + m: torch.Tensor, + nlist: torch.Tensor, + op_mask: float, + op_norm: float, + ) -> torch.Tensor: + ab = self.linear_in(m) + ab = ab * op_mask + a, b = ab.chunk(2, dim=-1) + # [ncluster, natoms, natoms, d_pair] + z = self._opm(a, b) + z *= op_norm + return z + + +class Attention(nn.Module): + def __init__( + self, + q_dim: int, + k_dim: int, + v_dim: int, + head_dim: int, + num_heads: int, + gating: bool = False, + dropout: float = 0.0, + ): + super().__init__() + + self.num_heads = num_heads + self.head_dim = head_dim + total_dim = head_dim * self.num_heads + self.total_dim = total_dim + self.q_dim = q_dim + self.gating = gating + self.linear_q = Linear(q_dim, total_dim, bias=False, init="glorot") + self.linear_k = Linear(k_dim, total_dim, bias=False, init="glorot") + self.linear_v = Linear(v_dim, total_dim, bias=False, init="glorot") + self.linear_o = Linear(total_dim, q_dim, init="final") + self.linear_g = None + if self.gating: + self.linear_g = Linear(q_dim, total_dim, init="gating") + # precompute the 1/sqrt(head_dim) + self.norm = head_dim**-0.5 + self.dropout = dropout + + def forward( + self, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + bias: torch.Tensor, + mask: torch.Tensor = None, + ) -> torch.Tensor: + nframes, nloc, embed_dim = q.size() + g = None + if self.linear_g is not None: + # gating, use raw query input + # [nframes, nloc, total_dim] + g = self.linear_g(q) + # [nframes, nloc, total_dim] + q = self.linear_q(q) + q *= self.norm + # [nframes, nloc, total_dim] + k = self.linear_k(k) + # [nframes, nloc, total_dim] + v = self.linear_v(v) + # global + # q [nframes, h, nloc, d] + # k [nframes, h, nloc, d] + # v [nframes, h, nloc, d] + # attn [nframes, h, nloc, nloc] + # o [nframes, h, nloc, d] + + # [nframes, h, nloc, d] + q = q.view(q.shape[:-1] + (self.num_heads, -1)).transpose(-2, -3).contiguous() + k = k.view(k.shape[:-1] + (self.num_heads, -1)).transpose(-2, -3).contiguous() + v = v.view(v.shape[:-1] + (self.num_heads, -1)).transpose(-2, -3) + # [nframes, h, nloc, nloc] + attn = torch.matmul(q, k.transpose(-1, -2)) + del q, k + # [nframes, h, nloc, nloc] + attn = softmax_dropout(attn, self.dropout, self.training, mask=mask, bias=bias) + # [nframes, h, nloc, d] + o = torch.matmul(attn, v) + del attn, v + + # local + # q [nframes, h, nloc, 1, d] + # k [nframes, h, nloc, nnei, d] + # v [nframes, h, nloc, nnei, d] + # attn [nframes, h, nloc, nnei] + # o [nframes, h, nloc, d] + + assert list(o.size()) == [nframes, self.num_heads, nloc, self.head_dim] + # [nframes, nloc, total_dim] + o = o.transpose(-2, -3).contiguous() + o = o.view(*o.shape[:-2], -1) + + if g is not None: + o = torch.sigmoid(g) * o + + # merge heads + o = self.linear_o(o) + return o + + +class AtomAttention(nn.Module): + def __init__( + self, + q_dim: int, + k_dim: int, + v_dim: int, + pair_dim: int, + head_dim: int, + num_heads: int, + gating: bool = False, + dropout: float = 0.0, + ): + super().__init__() + + self.mha = Attention( + q_dim, k_dim, v_dim, head_dim, num_heads, gating=gating, dropout=dropout + ) + self.layer_norm = nn.LayerNorm(pair_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + self.linear_bias = Linear(pair_dim, num_heads) + + def forward( + self, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + nlist: torch.Tensor, + pair: torch.Tensor, + mask: torch.Tensor = None, + ) -> torch.Tensor: + pair = self.layer_norm(pair) + bias = self.linear_bias(pair).permute(0, 3, 1, 2).contiguous() + return self.mha(q, k, v, bias=bias, mask=mask) + + +class TriangleMultiplication(nn.Module): + def __init__(self, d_pair, d_hid): + super().__init__() + + self.linear_ab_p = Linear(d_pair, d_hid * 2) + self.linear_ab_g = Linear(d_pair, d_hid * 2, init="gating") + + self.linear_g = Linear(d_pair, d_pair, init="gating") + self.linear_z = Linear(d_hid, d_pair, init="final") + + self.layer_norm_out = nn.LayerNorm(d_hid, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + + def forward( + self, + z: torch.Tensor, + mask: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + # z : [nframes, nloc, nloc, pair_dim] + + # [nframes, nloc, nloc, pair_dim] + g = self.linear_g(z) + if self.training: + ab = self.linear_ab_p(z) * torch.sigmoid(self.linear_ab_g(z)) + else: + ab = self.linear_ab_p(z) + ab *= torch.sigmoid(self.linear_ab_g(z)) + # [nframes, nloc, nloc, d] + a, b = torch.chunk(ab, 2, dim=-1) + del z, ab + + # [nframes, d, nloc_i, nloc_k] row not trans + a1 = a.permute(0, 3, 1, 2) + # [nframes, d, nloc_k, nloc_j(i)] trans + b1 = b.transpose(-1, -3) + # [nframes, d, nloc_i, nloc_j] + x = torch.matmul(a1, b1) + del a1, b1 + + # [nframes, d, nloc_k, nloc_j(i)] not trans + b2 = b.permute(0, 3, 1, 2) + # [nframes, d, nloc_i, nloc_k] col trans # check TODO + a2 = a.transpose(-1, -3) + + # [nframes, d, nloc_i, nloc_j] + x = x + torch.matmul(a2, b2) + del a, b, a2, b2 + + # [nframes, nloc_i, nloc_j, d] + x = x.permute(0, 2, 3, 1) + + x = self.layer_norm_out(x) + x = self.linear_z(x) + return g * x + + +class EvoformerEncoderLayer(nn.Module): + def __init__( + self, + feature_dim: int = 768, + ffn_dim: int = 2048, + attn_head: int = 8, + activation_fn: str = "gelu", + post_ln: bool = False, + ): + super().__init__() + self.feature_dim = feature_dim + self.ffn_dim = ffn_dim + self.attn_head = attn_head + self.activation_fn = ( + get_activation_fn(activation_fn) if activation_fn is not None else None + ) + self.post_ln = post_ln + self.self_attn_layer_norm = nn.LayerNorm( + self.feature_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + + self.self_attn = LocalSelfMultiheadAttention( + self.feature_dim, + self.attn_head, + ) + self.final_layer_norm = nn.LayerNorm( + self.feature_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + self.fc1 = SimpleLinear(self.feature_dim, self.ffn_dim) + self.fc2 = SimpleLinear(self.ffn_dim, self.feature_dim) + + def forward( + self, + x, + attn_bias: Optional[torch.Tensor] = None, + nlist_mask: Optional[torch.Tensor] = None, + nlist: Optional[torch.Tensor] = None, + return_attn=True, + ): + residual = x + if not self.post_ln: + x = self.self_attn_layer_norm(x) + x = self.self_attn( + query=x, + attn_bias=attn_bias, + nlist_mask=nlist_mask, + nlist=nlist, + return_attn=return_attn, + ) + if return_attn: + x, attn_weights, attn_probs = x + x = residual + x + if self.post_ln: + x = self.self_attn_layer_norm(x) + + residual = x + if not self.post_ln: + x = self.final_layer_norm(x) + x = self.fc1(x) + x = self.activation_fn(x) + x = self.fc2(x) + x = residual + x + if self.post_ln: + x = self.final_layer_norm(x) + if not return_attn: + return x + else: + return x, attn_weights, attn_probs + + +# output: atomic_rep, transformed_atomic_rep, pair_rep, delta_pair_rep, norm_x, norm_delta_pair_rep, +class Evoformer2bEncoder(nn.Module): + def __init__( + self, + nnei: int, + layer_num: int = 6, + attn_head: int = 8, + atomic_dim: int = 1024, + pair_dim: int = 100, + feature_dim: int = 1024, + ffn_dim: int = 2048, + post_ln: bool = False, + final_layer_norm: bool = True, + final_head_layer_norm: bool = False, + emb_layer_norm: bool = False, + atomic_residual: bool = False, + evo_residual: bool = False, + residual_factor: float = 1.0, + activation_function: str = "gelu", + ): + super().__init__() + self.nnei = nnei + self.layer_num = layer_num + self.attn_head = attn_head + self.atomic_dim = atomic_dim + self.pair_dim = pair_dim + self.feature_dim = feature_dim + self.ffn_dim = ffn_dim + self.post_ln = post_ln + self._final_layer_norm = final_layer_norm + self._final_head_layer_norm = final_head_layer_norm + self._emb_layer_norm = emb_layer_norm + self.activation_function = activation_function + self.evo_residual = evo_residual + self.residual_factor = residual_factor + if atomic_residual and atomic_dim == feature_dim: + self.atomic_residual = True + else: + self.atomic_residual = False + self.in_proj = SimpleLinear( + self.atomic_dim, + self.feature_dim, + bavg=0.0, + stddev=1.0, + use_timestep=False, + activate="tanh", + ) # TODO + self.out_proj = SimpleLinear( + self.feature_dim, + self.atomic_dim, + bavg=0.0, + stddev=1.0, + use_timestep=False, + activate="tanh", + ) + if self._emb_layer_norm: + self.emb_layer_norm = nn.LayerNorm( + self.feature_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + + ## TODO debug : self.in_proj_pair = NonLinearHead(self.pair_dim, self.attn_head, activation_fn=None) + self.in_proj_pair = SimpleLinear(self.pair_dim, self.attn_head, activate=None) + evoformer_encoder_layers = [] + for i in range(self.layer_num): + evoformer_encoder_layers.append( + EvoformerEncoderLayer( + feature_dim=self.feature_dim, + ffn_dim=self.ffn_dim, + attn_head=self.attn_head, + activation_fn=self.activation_function, + post_ln=self.post_ln, + ) + ) + self.evoformer_encoder_layers = nn.ModuleList(evoformer_encoder_layers) + if self._final_layer_norm: + self.final_layer_norm = nn.LayerNorm( + self.feature_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + if self._final_head_layer_norm: + self.final_head_layer_norm = nn.LayerNorm( + self.attn_head, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + + def forward(self, atomic_rep, pair_rep, nlist, nlist_type, nlist_mask): + """Encoder the atomic and pair representations. + + Args: + - atomic_rep: Atomic representation with shape [nframes, nloc, atomic_dim]. + - pair_rep: Pair representation with shape [nframes, nloc, nnei, pair_dim]. + - nlist: Neighbor list with shape [nframes, nloc, nnei]. + - nlist_type: Neighbor types with shape [nframes, nloc, nnei]. + - nlist_mask: Neighbor mask with shape [nframes, nloc, nnei], `False` if blank. + + Returns + ------- + - atomic_rep: Atomic representation after encoder with shape [nframes, nloc, feature_dim]. + - transformed_atomic_rep: Transformed atomic representation after encoder with shape [nframes, nloc, atomic_dim]. + - pair_rep: Pair representation after encoder with shape [nframes, nloc, nnei, attn_head]. + - delta_pair_rep: Delta pair representation after encoder with shape [nframes, nloc, nnei, attn_head]. + - norm_x: Normalization loss of atomic_rep. + - norm_delta_pair_rep: Normalization loss of delta_pair_rep. + """ + # Global branch + nframes, nloc, _ = atomic_rep.size() + nnei = pair_rep.shape[2] + input_atomic_rep = atomic_rep + # [nframes, nloc, feature_dim] + if self.atomic_residual: + atomic_rep = atomic_rep + self.in_proj(atomic_rep) + else: + atomic_rep = self.in_proj(atomic_rep) + + if self._emb_layer_norm: + atomic_rep = self.emb_layer_norm(atomic_rep) + + # Local branch + # [nframes, nloc, nnei, attn_head] + pair_rep = self.in_proj_pair(pair_rep) + # [nframes, attn_head, nloc, nnei] + pair_rep = pair_rep.permute(0, 3, 1, 2).contiguous() + input_pair_rep = pair_rep + pair_rep = pair_rep.masked_fill(~nlist_mask.unsqueeze(1), float("-inf")) + + for i in range(self.layer_num): + atomic_rep, pair_rep, _ = self.evoformer_encoder_layers[i]( + atomic_rep, + attn_bias=pair_rep, + nlist_mask=nlist_mask, + nlist=nlist, + return_attn=True, + ) + + def norm_loss(x, eps=1e-10, tolerance=1.0): + # x = x.float() + max_norm = x.shape[-1] ** 0.5 + norm = torch.sqrt(torch.sum(x**2, dim=-1) + eps) + error = F.relu((norm - max_norm).abs() - tolerance) + return error + + def masked_mean(mask, value, dim=-1, eps=1e-10): + return ( + torch.sum(mask * value, dim=dim) / (eps + torch.sum(mask, dim=dim)) + ).mean() + + # atomic_rep shape: [nframes, nloc, feature_dim] + # pair_rep shape: [nframes, attn_head, nloc, nnei] + + norm_x = torch.mean(norm_loss(atomic_rep)) + if self._final_layer_norm: + atomic_rep = self.final_layer_norm(atomic_rep) + + delta_pair_rep = pair_rep - input_pair_rep + delta_pair_rep = delta_pair_rep.masked_fill(~nlist_mask.unsqueeze(1), 0) + # [nframes, nloc, nnei, attn_head] + delta_pair_rep = ( + delta_pair_rep.view(nframes, self.attn_head, nloc, nnei) + .permute(0, 2, 3, 1) + .contiguous() + ) + + # [nframes, nloc, nnei] + norm_delta_pair_rep = norm_loss(delta_pair_rep) + norm_delta_pair_rep = masked_mean(mask=nlist_mask, value=norm_delta_pair_rep) + if self._final_head_layer_norm: + delta_pair_rep = self.final_head_layer_norm(delta_pair_rep) + + if self.atomic_residual: + transformed_atomic_rep = atomic_rep + self.out_proj(atomic_rep) + else: + transformed_atomic_rep = self.out_proj(atomic_rep) + + if self.evo_residual: + transformed_atomic_rep = ( + self.residual_factor * transformed_atomic_rep + input_atomic_rep + ) * (1 / np.sqrt(2)) + + return ( + atomic_rep, + transformed_atomic_rep, + pair_rep, + delta_pair_rep, + norm_x, + norm_delta_pair_rep, + ) + + +class Evoformer3bEncoderLayer(nn.Module): + def __init__( + self, + nnei, + embedding_dim: int = 768, + pair_dim: int = 64, + pair_hidden_dim: int = 32, + ffn_embedding_dim: int = 3072, + num_attention_heads: int = 8, + dropout: float = 0.1, + droppath_prob: float = 0.0, + pair_dropout: float = 0.25, + attention_dropout: float = 0.1, + activation_dropout: float = 0.1, + pre_ln: bool = True, + tri_update: bool = True, + ): + super().__init__() + # Initialize parameters + self.nnei = nnei + self.embedding_dim = embedding_dim + self.num_attention_heads = num_attention_heads + self.attention_dropout = attention_dropout + + # self.dropout = dropout + self.activation_dropout = activation_dropout + + if droppath_prob > 0.0: + self.dropout_module = DropPath(droppath_prob) + else: + self.dropout_module = Dropout(dropout) + + # self.self_attn = AtomAttentionLocal(embedding_dim, embedding_dim, embedding_dim, pair_dim, + # embedding_dim // num_attention_heads, num_attention_heads, + # gating=False, dropout=attention_dropout) + self.self_attn = AtomAttention( + embedding_dim, + embedding_dim, + embedding_dim, + pair_dim, + embedding_dim // num_attention_heads, + num_attention_heads, + gating=False, + dropout=attention_dropout, + ) + # layer norm associated with the self attention layer + self.pre_ln = pre_ln + self.self_attn_layer_norm = nn.LayerNorm( + self.embedding_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + self.fc1 = nn.Linear( + self.embedding_dim, ffn_embedding_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + self.fc2 = nn.Linear( + ffn_embedding_dim, self.embedding_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + self.final_layer_norm = nn.LayerNorm( + self.embedding_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + + self.x_layer_norm_opm = nn.LayerNorm( + self.embedding_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + # self.opm = OuterProductLocal(self.embedding_dim, pair_dim, d_hid=pair_hidden_dim) + self.opm = OuterProduct(self.embedding_dim, pair_dim, d_hid=pair_hidden_dim) + # self.pair_layer_norm_opm = nn.LayerNorm(pair_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + self.pair_layer_norm_ffn = nn.LayerNorm( + pair_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + self.pair_ffn = Transition( + pair_dim, + 1, + dropout=activation_dropout, + ) + self.pair_dropout = pair_dropout + self.tri_update = tri_update + if self.tri_update: + self.pair_layer_norm_trimul = nn.LayerNorm( + pair_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + ) + self.pair_tri_mul = TriangleMultiplication(pair_dim, pair_hidden_dim) + + def update_pair( + self, + x, + pair, + nlist, + op_mask, + op_norm, + ): + # local: + # [nframes, nloc, nnei, pair_dim] + # global: + # [nframes, nloc, nloc, pair_dim] + pair = pair + self.dropout_module( + self.opm(self.x_layer_norm_opm(x), nlist, op_mask, op_norm) + ) + if not self.pre_ln: + pair = self.pair_layer_norm_opm(pair) + return x, pair + + def shared_dropout(self, x, shared_dim, dropout): + shape = list(x.shape) + shape[shared_dim] = 1 + with torch.no_grad(): + mask = x.new_ones(shape) + return F.dropout(mask, p=dropout, training=self.training) * x + + def forward( + self, + x: torch.Tensor, + pair: torch.Tensor, + nlist: torch.Tensor = None, + attn_mask: Optional[torch.Tensor] = None, + pair_mask: Optional[torch.Tensor] = None, + op_mask: float = 1.0, + op_norm: float = 1.0, + ): + """Encoder the atomic and pair representations. + + Args: + - x: Atomic representation with shape [ncluster, natoms, embed_dim]. + - pair: Pair representation with shape [ncluster, natoms, natoms, pair_dim]. + - attn_mask: Attention mask with shape [ncluster, head, natoms, natoms]. + - pair_mask: Neighbor mask with shape [ncluster, natoms, natoms]. + + """ + # [ncluster, natoms, embed_dim] + residual = x + if self.pre_ln: + x = self.self_attn_layer_norm(x) + x = self.self_attn( + x, + x, + x, + nlist=nlist, + pair=pair, + mask=attn_mask, + ) + # x = F.dropout(x, p=self.dropout, training=self.training) + x = self.dropout_module(x) + x = residual + x + if not self.pre_ln: + x = self.self_attn_layer_norm(x) + + residual = x + if self.pre_ln: + x = self.final_layer_norm(x) + x = F.linear(x, self.fc1.weight) + # x = fused_ops.bias_torch_gelu(x, self.fc1.bias) + x = nn.GELU()(x) + self.fc1.bias + x = F.dropout(x, p=self.activation_dropout, training=self.training) + x = self.fc2(x) + # x = F.dropout(x, p=self.dropout, training=self.training) + x = self.dropout_module(x) + + x = residual + x + if not self.pre_ln: + x = self.final_layer_norm(x) + + block = [ + partial( + self.update_pair, + nlist=nlist, + op_mask=op_mask, + op_norm=op_norm, + ) + ] + + x, pair = checkpoint_sequential( + block, + input_x=(x, pair), + ) + + if self.tri_update: + residual_pair = pair + if self.pre_ln: + pair = self.pair_layer_norm_trimul(pair) + + pair = self.shared_dropout( + self.pair_tri_mul(pair, pair_mask), -3, self.pair_dropout + ) + pair = residual_pair + pair + if not self.pre_ln: + pair = self.pair_layer_norm_trimul(pair) + + residual_pair = pair + if self.pre_ln: + pair = self.pair_layer_norm_ffn(pair) + pair = self.dropout_module(self.pair_ffn(pair)) + pair = residual_pair + pair + if not self.pre_ln: + pair = self.pair_layer_norm_ffn(pair) + return x, pair + + +class Evoformer3bEncoder(nn.Module): + def __init__( + self, + nnei, + layer_num=6, + attn_head=8, + atomic_dim=768, + pair_dim=64, + pair_hidden_dim=32, + ffn_embedding_dim=3072, + dropout: float = 0.1, + droppath_prob: float = 0.0, + pair_dropout: float = 0.25, + attention_dropout: float = 0.1, + activation_dropout: float = 0.1, + pre_ln: bool = True, + tri_update: bool = True, + **kwargs, + ): + super().__init__() + self.nnei = nnei + if droppath_prob > 0: + droppath_probs = [ + x.item() for x in torch.linspace(0, droppath_prob, layer_num) + ] + else: + droppath_probs = None + + self.layers = nn.ModuleList( + [ + Evoformer3bEncoderLayer( + nnei, + atomic_dim, + pair_dim, + pair_hidden_dim, + ffn_embedding_dim, + num_attention_heads=attn_head, + dropout=dropout, + droppath_prob=droppath_probs[_], + pair_dropout=pair_dropout, + attention_dropout=attention_dropout, + activation_dropout=activation_dropout, + pre_ln=pre_ln, + tri_update=tri_update, + ) + for _ in range(layer_num) + ] + ) + + def forward(self, x, pair, attn_mask=None, pair_mask=None, atom_mask=None): + """Encoder the atomic and pair representations. + + Args: + x: Atomic representation with shape [ncluster, natoms, atomic_dim]. + pair: Pair representation with shape [ncluster, natoms, natoms, pair_dim]. + attn_mask: Attention mask (with -inf for softmax) with shape [ncluster, head, natoms, natoms]. + pair_mask: Pair mask (with 1 for real atom pair and 0 for padding) with shape [ncluster, natoms, natoms]. + atom_mask: Atom mask (with 1 for real atom and 0 for padding) with shape [ncluster, natoms]. + + Returns + ------- + x: Atomic representation with shape [ncluster, natoms, atomic_dim]. + pair: Pair representation with shape [ncluster, natoms, natoms, pair_dim]. + + """ + # [ncluster, natoms, 1] + op_mask = atom_mask.unsqueeze(-1) + op_mask = op_mask * (op_mask.size(-2) ** -0.5) + eps = 1e-3 + # [ncluster, natoms, natoms, 1] + op_norm = 1.0 / (eps + torch.einsum("...bc,...dc->...bdc", op_mask, op_mask)) + for layer in self.layers: + x, pair = layer( + x, + pair, + nlist=None, + attn_mask=attn_mask, + pair_mask=pair_mask, + op_mask=op_mask, + op_norm=op_norm, + ) + return x, pair diff --git a/deepmd/pt/model/task/__init__.py b/deepmd/pt/model/task/__init__.py new file mode 100644 index 0000000000..fcf46632f3 --- /dev/null +++ b/deepmd/pt/model/task/__init__.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from .atten_lcc import ( + FittingNetAttenLcc, +) +from .denoise import ( + DenoiseNet, +) +from .dipole import ( + DipoleFittingNetType, +) +from .ener import ( + EnergyFittingNet, + EnergyFittingNetDirect, +) +from .fitting import ( + Fitting, +) +from .task import ( + TaskBaseMethod, +) +from .type_predict import ( + TypePredictNet, +) + +__all__ = [ + "FittingNetAttenLcc", + "DenoiseNet", + "DipoleFittingNetType", + "EnergyFittingNet", + "EnergyFittingNetDirect", + "Fitting", + "TaskBaseMethod", + "TypePredictNet", +] diff --git a/deepmd/pt/model/task/atten_lcc.py b/deepmd/pt/model/task/atten_lcc.py new file mode 100644 index 0000000000..41ccf99330 --- /dev/null +++ b/deepmd/pt/model/task/atten_lcc.py @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import torch +import torch.nn as nn + +from deepmd.pt.model.network.network import ( + EnergyHead, + NodeTaskHead, +) +from deepmd.pt.model.task.task import ( + TaskBaseMethod, +) +from deepmd.pt.utils import ( + env, +) + + +class FittingNetAttenLcc(TaskBaseMethod): + def __init__( + self, embedding_width, bias_atom_e, pair_embed_dim, attention_heads, **kwargs + ): + super().__init__() + self.embedding_width = embedding_width + self.engergy_proj = EnergyHead(self.embedding_width, 1) + self.energe_agg_factor = nn.Embedding(4, 1, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + nn.init.normal_(self.energe_agg_factor.weight, 0, 0.01) + bias_atom_e = torch.tensor(bias_atom_e) + self.register_buffer("bias_atom_e", bias_atom_e) + self.pair_embed_dim = pair_embed_dim + self.attention_heads = attention_heads + self.node_proc = NodeTaskHead( + self.embedding_width, self.pair_embed_dim, self.attention_heads + ) + self.node_proc.zero_init() + + def forward(self, output, pair, delta_pos, atype, nframes, nloc): + # [nframes x nloc x tebd_dim] + output_nloc = (output[:, 0, :]).reshape(nframes, nloc, self.embedding_width) + # Optional: GRRG or mean of gbf TODO + + # energy outut + # [nframes, nloc] + energy_out = self.engergy_proj(output_nloc).view(nframes, nloc) + # [nframes, nloc] + energy_factor = self.energe_agg_factor(torch.zeros_like(atype)).view( + nframes, nloc + ) + energy_out = (energy_out * energy_factor) + self.bias_atom_e[atype] + energy_out = energy_out.sum(dim=-1) + + # vector output + # predict_force: [(nframes x nloc) x (1 + nnei2) x 3] + predict_force = self.node_proc(output, pair, delta_pos=delta_pos) + # predict_force_nloc: [nframes x nloc x 3] + predict_force_nloc = (predict_force[:, 0, :]).reshape(nframes, nloc, 3) + return energy_out, predict_force_nloc diff --git a/deepmd/pt/model/task/denoise.py b/deepmd/pt/model/task/denoise.py new file mode 100644 index 0000000000..7e6b6dcdb6 --- /dev/null +++ b/deepmd/pt/model/task/denoise.py @@ -0,0 +1,129 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Optional, +) + +import torch + +from deepmd.model_format import ( + FittingOutputDef, + OutputVariableDef, + fitting_check_output, +) +from deepmd.pt.model.network.network import ( + MaskLMHead, + NonLinearHead, +) +from deepmd.pt.model.task.task import ( + TaskBaseMethod, +) +from deepmd.pt.utils import ( + env, +) + + +@fitting_check_output +class DenoiseNet(TaskBaseMethod): + def __init__( + self, + feature_dim, + ntypes, + attn_head=8, + prefactor=[0.5, 0.5], + activation_function="gelu", + **kwargs, + ): + """Construct a denoise net. + + Args: + - ntypes: Element count. + - embedding_width: Embedding width per atom. + - neuron: Number of neurons in each hidden layers of the fitting net. + - bias_atom_e: Average enery per atom for each element. + - resnet_dt: Using time-step in the ResNet construction. + """ + super().__init__() + self.feature_dim = feature_dim + self.ntypes = ntypes + self.attn_head = attn_head + self.prefactor = torch.tensor( + prefactor, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ) + + self.lm_head = MaskLMHead( + embed_dim=self.feature_dim, + output_dim=ntypes, + activation_fn=activation_function, + weight=None, + ) + + if not isinstance(self.attn_head, list): + self.pair2coord_proj = NonLinearHead( + self.attn_head, 1, activation_fn=activation_function + ) + else: + self.pair2coord_proj = [] + self.ndescriptor = len(self.attn_head) + for ii in range(self.ndescriptor): + _pair2coord_proj = NonLinearHead( + self.attn_head[ii], 1, activation_fn=activation_function + ) + self.pair2coord_proj.append(_pair2coord_proj) + self.pair2coord_proj = torch.nn.ModuleList(self.pair2coord_proj) + + def output_def(self): + return FittingOutputDef( + [ + OutputVariableDef( + "updated_coord", [3], reduciable=False, differentiable=False + ), + OutputVariableDef( + "logits", [-1], reduciable=False, differentiable=False + ), + ] + ) + + def forward( + self, + pair_weights, + diff, + nlist_mask, + features, + sw, + masked_tokens: Optional[torch.Tensor] = None, + ): + """Calculate the updated coord. + Args: + - coord: Input noisy coord with shape [nframes, nloc, 3]. + - pair_weights: Input pair weights with shape [nframes, nloc, nnei, head]. + - diff: Input pair relative coord list with shape [nframes, nloc, nnei, 3]. + - nlist_mask: Input nlist mask with shape [nframes, nloc, nnei]. + + Returns + ------- + - denoised_coord: Denoised updated coord with shape [nframes, nloc, 3]. + """ + # [nframes, nloc, nnei, 1] + logits = self.lm_head(features, masked_tokens=masked_tokens) + if not isinstance(self.attn_head, list): + attn_probs = self.pair2coord_proj(pair_weights) + out_coord = (attn_probs * diff).sum(dim=-2) / ( + sw.sum(dim=-1).unsqueeze(-1) + 1e-6 + ) + else: + assert len(self.prefactor) == self.ndescriptor + all_coord_update = [] + assert len(pair_weights) == len(diff) == len(nlist_mask) == self.ndescriptor + for ii in range(self.ndescriptor): + _attn_probs = self.pair2coord_proj[ii](pair_weights[ii]) + _coord_update = (_attn_probs * diff[ii]).sum(dim=-2) / ( + nlist_mask[ii].sum(dim=-1).unsqueeze(-1) + 1e-6 + ) + all_coord_update.append(_coord_update) + out_coord = self.prefactor[0] * all_coord_update[0] + for ii in range(self.ndescriptor - 1): + out_coord += self.prefactor[ii + 1] * all_coord_update[ii + 1] + return { + "updated_coord": out_coord, + "logits": logits, + } diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py new file mode 100644 index 0000000000..8511c7dc29 --- /dev/null +++ b/deepmd/pt/model/task/dipole.py @@ -0,0 +1,65 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging + +import torch + +from deepmd.pt.model.network.network import ( + ResidualDeep, +) +from deepmd.pt.model.task.task import ( + TaskBaseMethod, +) + + +class DipoleFittingNetType(TaskBaseMethod): + def __init__( + self, ntypes, embedding_width, neuron, out_dim, resnet_dt=True, **kwargs + ): + """Construct a fitting net for dipole. + + Args: + - ntypes: Element count. + - embedding_width: Embedding width per atom. + - neuron: Number of neurons in each hidden layers of the fitting net. + - bias_atom_e: Average enery per atom for each element. + - resnet_dt: Using time-step in the ResNet construction. + """ + super().__init__() + self.ntypes = ntypes + self.embedding_width = embedding_width + self.out_dim = out_dim + + filter_layers = [] + one = ResidualDeep( + 0, embedding_width, neuron, 0.0, out_dim=self.out_dim, resnet_dt=resnet_dt + ) + filter_layers.append(one) + self.filter_layers = torch.nn.ModuleList(filter_layers) + + if "seed" in kwargs: + logging.info("Set seed to %d in fitting net.", kwargs["seed"]) + torch.manual_seed(kwargs["seed"]) + + def forward(self, inputs, atype, atype_tebd, rot_mat): + """Based on embedding net output, alculate total energy. + + Args: + - inputs: Descriptor. Its shape is [nframes, nloc, self.embedding_width]. + - atype: Atom type. Its shape is [nframes, nloc]. + - atype_tebd: Atom type embedding. Its shape is [nframes, nloc, tebd_dim] + - rot_mat: GR during descriptor calculation. Its shape is [nframes * nloc, m1, 3]. + + Returns + ------- + - vec_out: output vector. Its shape is [nframes, nloc, 3]. + """ + nframes, nloc, _ = inputs.size() + if atype_tebd is not None: + inputs = torch.concat([inputs, atype_tebd], dim=-1) + vec_out = self.filter_layers[0](inputs) # Shape is [nframes, nloc, m1] + assert list(vec_out.size()) == [nframes, nloc, self.out_dim] + vec_out = vec_out.view(-1, 1, self.out_dim) + vec_out = ( + torch.bmm(vec_out, rot_mat).squeeze(-2).view(nframes, nloc, 3) + ) # Shape is [nframes, nloc, 3] + return vec_out diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py new file mode 100644 index 0000000000..7ddcbd5c54 --- /dev/null +++ b/deepmd/pt/model/task/ener.py @@ -0,0 +1,241 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging +from typing import ( + Optional, + Tuple, +) + +import torch + +from deepmd.model_format import ( + FittingOutputDef, + OutputVariableDef, + fitting_check_output, +) +from deepmd.pt.model.network.network import ( + ResidualDeep, +) +from deepmd.pt.model.task.fitting import ( + Fitting, +) +from deepmd.pt.utils import ( + env, +) + + +@Fitting.register("ener") +@fitting_check_output +class EnergyFittingNet(Fitting): + def __init__( + self, + ntypes, + embedding_width, + neuron, + bias_atom_e, + resnet_dt=True, + use_tebd=True, + **kwargs, + ): + """Construct a fitting net for energy. + + Args: + - ntypes: Element count. + - embedding_width: Embedding width per atom. + - neuron: Number of neurons in each hidden layers of the fitting net. + - bias_atom_e: Average enery per atom for each element. + - resnet_dt: Using time-step in the ResNet construction. + """ + super().__init__() + self.ntypes = ntypes + self.embedding_width = embedding_width + self.use_tebd = use_tebd + if not use_tebd: + assert self.ntypes == len(bias_atom_e), "Element count mismatches!" + bias_atom_e = torch.tensor(bias_atom_e) + self.register_buffer("bias_atom_e", bias_atom_e) + + filter_layers = [] + for type_i in range(self.ntypes): + bias_type = 0.0 + one = ResidualDeep( + type_i, embedding_width, neuron, bias_type, resnet_dt=resnet_dt + ) + filter_layers.append(one) + self.filter_layers = torch.nn.ModuleList(filter_layers) + + if "seed" in kwargs: + logging.info("Set seed to %d in fitting net.", kwargs["seed"]) + torch.manual_seed(kwargs["seed"]) + + def output_def(self): + return FittingOutputDef( + [ + OutputVariableDef("energy", [1], reduciable=True, differentiable=True), + ] + ) + + def forward( + self, + inputs: torch.Tensor, + atype: torch.Tensor, + atype_tebd: Optional[torch.Tensor] = None, + rot_mat: Optional[torch.Tensor] = None, + ): + """Based on embedding net output, alculate total energy. + + Args: + - inputs: Embedding matrix. Its shape is [nframes, natoms[0], self.embedding_width]. + - natoms: Tell atom count and element count. Its shape is [2+self.ntypes]. + + Returns + ------- + - `torch.Tensor`: Total energy with shape [nframes, natoms[0]]. + """ + outs = torch.zeros_like(atype).unsqueeze(-1) # jit assertion + if self.use_tebd: + if atype_tebd is not None: + inputs = torch.concat([inputs, atype_tebd], dim=-1) + atom_energy = self.filter_layers[0](inputs) + self.bias_atom_e[ + atype + ].unsqueeze(-1) + outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] + else: + for type_i, filter_layer in enumerate(self.filter_layers): + mask = atype == type_i + atom_energy = filter_layer(inputs) + atom_energy = atom_energy + self.bias_atom_e[type_i] + atom_energy = atom_energy * mask.unsqueeze(-1) + outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] + return {"energy": outs.to(env.GLOBAL_PT_FLOAT_PRECISION)} + + +@Fitting.register("direct_force") +@Fitting.register("direct_force_ener") +@fitting_check_output +class EnergyFittingNetDirect(Fitting): + def __init__( + self, + ntypes, + embedding_width, + neuron, + bias_atom_e, + out_dim=1, + resnet_dt=True, + use_tebd=True, + return_energy=False, + **kwargs, + ): + """Construct a fitting net for energy. + + Args: + - ntypes: Element count. + - embedding_width: Embedding width per atom. + - neuron: Number of neurons in each hidden layers of the fitting net. + - bias_atom_e: Average enery per atom for each element. + - resnet_dt: Using time-step in the ResNet construction. + """ + super().__init__() + self.ntypes = ntypes + self.embedding_width = embedding_width + self.use_tebd = use_tebd + self.out_dim = out_dim + if not use_tebd: + assert self.ntypes == len(bias_atom_e), "Element count mismatches!" + bias_atom_e = torch.tensor(bias_atom_e) + self.register_buffer("bias_atom_e", bias_atom_e) + + filter_layers_dipole = [] + for type_i in range(self.ntypes): + one = ResidualDeep( + type_i, + embedding_width, + neuron, + 0.0, + out_dim=out_dim, + resnet_dt=resnet_dt, + ) + filter_layers_dipole.append(one) + self.filter_layers_dipole = torch.nn.ModuleList(filter_layers_dipole) + + self.return_energy = return_energy + filter_layers = [] + if self.return_energy: + for type_i in range(self.ntypes): + bias_type = 0.0 if self.use_tebd else bias_atom_e[type_i] + one = ResidualDeep( + type_i, embedding_width, neuron, bias_type, resnet_dt=resnet_dt + ) + filter_layers.append(one) + self.filter_layers = torch.nn.ModuleList(filter_layers) + + if "seed" in kwargs: + logging.info("Set seed to %d in fitting net.", kwargs["seed"]) + torch.manual_seed(kwargs["seed"]) + + def output_def(self): + return FittingOutputDef( + [ + OutputVariableDef("energy", [1], reduciable=True, differentiable=False), + OutputVariableDef( + "dforce", [3], reduciable=False, differentiable=False + ), + ] + ) + + def forward( + self, + inputs: torch.Tensor, + atype: torch.Tensor, + atype_tebd: Optional[torch.Tensor] = None, + rot_mat: Optional[torch.Tensor] = None, + ) -> Tuple[torch.Tensor, None]: + """Based on embedding net output, alculate total energy. + + Args: + - inputs: Embedding matrix. Its shape is [nframes, natoms[0], self.embedding_width]. + - natoms: Tell atom count and element count. Its shape is [2+self.ntypes]. + + Returns + ------- + - `torch.Tensor`: Total energy with shape [nframes, natoms[0]]. + """ + nframes, nloc, _ = inputs.size() + if self.use_tebd: + if atype_tebd is not None: + inputs = torch.concat([inputs, atype_tebd], dim=-1) + vec_out = self.filter_layers_dipole[0]( + inputs + ) # Shape is [nframes, nloc, m1] + assert list(vec_out.size()) == [nframes, nloc, self.out_dim] + vec_out = vec_out.view(-1, 1, self.out_dim) + assert rot_mat is not None + vec_out = ( + torch.bmm(vec_out, rot_mat).squeeze(-2).view(nframes, nloc, 3) + ) # Shape is [nframes, nloc, 3] + else: + vec_out = torch.zeros_like(atype).unsqueeze(-1) # jit assertion + for type_i, filter_layer in enumerate(self.filter_layers_dipole): + mask = atype == type_i + vec_out_type = filter_layer(inputs) # Shape is [nframes, nloc, m1] + vec_out_type = vec_out_type * mask.unsqueeze(-1) + vec_out = vec_out + vec_out_type # Shape is [nframes, natoms[0], 1] + + outs = torch.zeros_like(atype).unsqueeze(-1) # jit assertion + if self.return_energy: + if self.use_tebd: + atom_energy = self.filter_layers[0](inputs) + self.bias_atom_e[ + atype + ].unsqueeze(-1) + outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] + else: + for type_i, filter_layer in enumerate(self.filter_layers): + mask = atype == type_i + atom_energy = filter_layer(inputs) + if not env.ENERGY_BIAS_TRAINABLE: + atom_energy = atom_energy + self.bias_atom_e[type_i] + atom_energy = atom_energy * mask.unsqueeze(-1) + outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] + return { + "energy": outs.to(env.GLOBAL_PT_FLOAT_PRECISION), + "dforce": vec_out, + } diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py new file mode 100644 index 0000000000..16e80f9c20 --- /dev/null +++ b/deepmd/pt/model/task/fitting.py @@ -0,0 +1,223 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging +from typing import ( + Callable, +) + +import numpy as np +import torch + +from deepmd.model_format import ( + FittingOutputDef, +) +from deepmd.pt.model.task.task import ( + TaskBaseMethod, +) +from deepmd.pt.utils.dataloader import ( + DpLoaderSet, +) +from deepmd.pt.utils.env import ( + DEVICE, +) +from deepmd.pt.utils.plugin import ( + Plugin, +) +from deepmd.pt.utils.stat import ( + make_stat_input, +) + + +class Fitting(TaskBaseMethod): + __plugins = Plugin() + + @staticmethod + def register(key: str) -> Callable: + """Register a Fitting plugin. + + Parameters + ---------- + key : str + the key of a Fitting + + Returns + ------- + Fitting + the registered Fitting + + Examples + -------- + >>> @Fitting.register("some_fitting") + class SomeFitting(Fitting): + pass + """ + return Fitting.__plugins.register(key) + + def __new__(cls, *args, **kwargs): + if cls is Fitting: + try: + fitting_type = kwargs["type"] + except KeyError: + raise KeyError("the type of fitting should be set by `type`") + if fitting_type in Fitting.__plugins.plugins: + cls = Fitting.__plugins.plugins[fitting_type] + else: + raise RuntimeError("Unknown descriptor type: " + fitting_type) + return super().__new__(cls) + + def output_def(self) -> FittingOutputDef: + """Definition for the task Output.""" + raise NotImplementedError + + def forward(self, **kwargs): + """Task Output.""" + raise NotImplementedError + + def share_params(self, base_class, shared_level, resume=False): + assert ( + self.__class__ == base_class.__class__ + ), "Only fitting nets of the same type can share params!" + if shared_level == 0: + # link buffers + if hasattr(self, "bias_atom_e"): + self.bias_atom_e = base_class.bias_atom_e + # the following will successfully link all the params except buffers, which need manually link. + for item in self._modules: + self._modules[item] = base_class._modules[item] + elif shared_level == 1: + # only not share the bias_atom_e + # the following will successfully link all the params except buffers, which need manually link. + for item in self._modules: + self._modules[item] = base_class._modules[item] + elif shared_level == 2: + # share all the layers before final layer + # the following will successfully link all the params except buffers, which need manually link. + self._modules["filter_layers"][0].deep_layers = base_class._modules[ + "filter_layers" + ][0].deep_layers + elif shared_level == 3: + # share the first layers + # the following will successfully link all the params except buffers, which need manually link. + self._modules["filter_layers"][0].deep_layers[0] = base_class._modules[ + "filter_layers" + ][0].deep_layers[0] + else: + raise NotImplementedError + + def change_energy_bias( + self, config, model, old_type_map, new_type_map, bias_shift="delta", ntest=10 + ): + """Change the energy bias according to the input data and the pretrained model. + + Parameters + ---------- + config : Dict + The configuration. + model : EnergyModel + Energy model loaded pre-trained model. + new_type_map : list + The original type_map in dataset, they are targets to change the energy bias. + old_type_map : str + The full type_map in pretrained model + bias_shift : str + The mode for changing energy bias : ['delta', 'statistic'] + 'delta' : perform predictions on energies of target dataset, + and do least sqaure on the errors to obtain the target shift as bias. + 'statistic' : directly use the statistic energy bias in the target dataset. + ntest : int + The number of test samples in a system to change the energy bias. + """ + logging.info( + "Changing energy bias in pretrained model for types {}... " + "(this step may take long time)".format(str(new_type_map)) + ) + # data + systems = config["training"]["training_data"]["systems"] + finetune_data = DpLoaderSet( + systems, ntest, config["model"], type_split=False, noise_settings=None + ) + sampled = make_stat_input(finetune_data.systems, finetune_data.dataloaders, 1) + # map + sorter = np.argsort(old_type_map) + idx_type_map = sorter[ + np.searchsorted(old_type_map, new_type_map, sorter=sorter) + ] + mixed_type = np.all([i.mixed_type for i in finetune_data.systems]) + numb_type = len(old_type_map) + type_numbs, energy_ground_truth, energy_predict = [], [], [] + for test_data in sampled: + nframes = test_data["energy"].shape[0] + if mixed_type: + atype = test_data["atype"].detach().cpu().numpy() + else: + atype = test_data["atype"][0].detach().cpu().numpy() + assert np.array( + [i.item() in idx_type_map for i in list(set(atype.reshape(-1)))] + ).all(), "Some types are not in 'type_map'!" + energy_ground_truth.append(test_data["energy"].cpu().numpy()) + if mixed_type: + type_numbs.append( + np.array( + [(atype == i).sum(axis=-1) for i in idx_type_map], + dtype=np.int32, + ).T + ) + else: + type_numbs.append( + np.tile( + np.bincount(atype, minlength=numb_type)[idx_type_map], + (nframes, 1), + ) + ) + if bias_shift == "delta": + coord = test_data["coord"].to(DEVICE) + atype = test_data["atype"].to(DEVICE) + box = ( + test_data["box"].to(DEVICE) + if test_data["box"] is not None + else None + ) + ret = model(coord, atype, box) + energy_predict.append( + ret["energy"].reshape([nframes, 1]).detach().cpu().numpy() + ) + type_numbs = np.concatenate(type_numbs) + energy_ground_truth = np.concatenate(energy_ground_truth) + old_bias = self.bias_atom_e[idx_type_map] + if bias_shift == "delta": + energy_predict = np.concatenate(energy_predict) + bias_diff = energy_ground_truth - energy_predict + delta_bias = np.linalg.lstsq(type_numbs, bias_diff, rcond=None)[0] + unbias_e = energy_predict + type_numbs @ delta_bias + atom_numbs = type_numbs.sum(-1) + rmse_ae = np.sqrt( + np.mean( + np.square( + (unbias_e.ravel() - energy_ground_truth.ravel()) / atom_numbs + ) + ) + ) + self.bias_atom_e[idx_type_map] += torch.from_numpy( + delta_bias.reshape(-1) + ).to(DEVICE) + logging.info( + f"RMSE of atomic energy after linear regression is: {rmse_ae:10.5e} eV/atom." + ) + elif bias_shift == "statistic": + statistic_bias = np.linalg.lstsq( + type_numbs, energy_ground_truth, rcond=None + )[0] + self.bias_atom_e[idx_type_map] = ( + torch.from_numpy(statistic_bias.reshape(-1)) + .type_as(self.bias_atom_e[idx_type_map]) + .to(DEVICE) + ) + else: + raise RuntimeError("Unknown bias_shift mode: " + bias_shift) + logging.info( + "Change energy bias of {} from {} to {}.".format( + str(new_type_map), + str(old_bias.detach().cpu().numpy()), + str(self.bias_atom_e[idx_type_map].detach().cpu().numpy()), + ) + ) + return None diff --git a/deepmd/pt/model/task/task.py b/deepmd/pt/model/task/task.py new file mode 100644 index 0000000000..a9b2efeb9a --- /dev/null +++ b/deepmd/pt/model/task/task.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import torch + + +class TaskBaseMethod(torch.nn.Module): + def __init__(self, **kwargs): + """Construct a basic head for different tasks.""" + super().__init__() + + def forward(self, **kwargs): + """Task Output.""" + raise NotImplementedError diff --git a/deepmd/pt/model/task/type_predict.py b/deepmd/pt/model/task/type_predict.py new file mode 100644 index 0000000000..57227004d0 --- /dev/null +++ b/deepmd/pt/model/task/type_predict.py @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Optional, +) + +import torch + +from deepmd.pt.model.network.network import ( + MaskLMHead, +) +from deepmd.pt.model.task import ( + TaskBaseMethod, +) + + +class TypePredictNet(TaskBaseMethod): + def __init__(self, feature_dim, ntypes, activation_function="gelu", **kwargs): + """Construct a type predict net. + + Args: + - feature_dim: Input dm. + - ntypes: Numer of types to predict. + - activation_function: Activate function. + """ + super().__init__() + self.feature_dim = feature_dim + self.ntypes = ntypes + self.lm_head = MaskLMHead( + embed_dim=self.feature_dim, + output_dim=ntypes, + activation_fn=activation_function, + weight=None, + ) + + def forward(self, features, masked_tokens: Optional[torch.Tensor] = None): + """Calculate the predicted logits. + Args: + - features: Input features with shape [nframes, nloc, feature_dim]. + - masked_tokens: Input masked tokens with shape [nframes, nloc]. + + Returns + ------- + - logits: Predicted probs with shape [nframes, nloc, ntypes]. + """ + # [nframes, nloc, ntypes] + logits = self.lm_head(features, masked_tokens=masked_tokens) + return logits diff --git a/deepmd/pt/optimizer/KFWrapper.py b/deepmd/pt/optimizer/KFWrapper.py new file mode 100644 index 0000000000..3ab7ffe7a9 --- /dev/null +++ b/deepmd/pt/optimizer/KFWrapper.py @@ -0,0 +1,145 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import math + +import numpy as np +import torch +import torch.distributed as dist +import torch.nn as nn +from torch.optim.optimizer import ( + Optimizer, +) + + +class KFOptimizerWrapper: + def __init__( + self, + model: nn.Module, + optimizer: Optimizer, + atoms_selected: int, + atoms_per_group: int, + is_distributed: bool = False, + ) -> None: + self.model = model + self.optimizer = optimizer + self.atoms_selected = atoms_selected # 24 + self.atoms_per_group = atoms_per_group # 6 + self.is_distributed = is_distributed + + def update_energy( + self, inputs: dict, Etot_label: torch.Tensor, update_prefactor: float = 1 + ) -> None: + model_pred, _, _ = self.model(**inputs, inference_only=True) + Etot_predict = model_pred["energy"] + natoms_sum = int(inputs["atype"].shape[-1]) + self.optimizer.set_grad_prefactor(natoms_sum) + + self.optimizer.zero_grad() + bs = Etot_label.shape[0] + error = Etot_label - Etot_predict + error = error / natoms_sum + mask = error < 0 + + error = error * update_prefactor + error[mask] = -1 * error[mask] + error = error.mean() + + if self.is_distributed: + dist.all_reduce(error) + error /= dist.get_world_size() + + Etot_predict = update_prefactor * Etot_predict + Etot_predict[mask] = -Etot_predict[mask] + + Etot_predict.sum().backward() + error = error * math.sqrt(bs) + self.optimizer.step(error) + return Etot_predict + + def update_force( + self, inputs: dict, Force_label: torch.Tensor, update_prefactor: float = 1 + ) -> None: + natoms_sum = int(inputs["atype"].shape[-1]) + bs = Force_label.shape[0] + self.optimizer.set_grad_prefactor(natoms_sum * self.atoms_per_group * 3) + + index = self.__sample(self.atoms_selected, self.atoms_per_group, natoms_sum) + + for i in range(index.shape[0]): + self.optimizer.zero_grad() + model_pred, _, _ = self.model(**inputs, inference_only=True) + Etot_predict = model_pred["energy"] + natoms_sum = int(inputs["atype"].shape[-1]) + force_predict = model_pred["force"] + error_tmp = Force_label[:, index[i]] - force_predict[:, index[i]] + error_tmp = update_prefactor * error_tmp + mask = error_tmp < 0 + error_tmp[mask] = -1 * error_tmp[mask] + error = error_tmp.mean() / natoms_sum + + if self.is_distributed: + dist.all_reduce(error) + error /= dist.get_world_size() + + tmp_force_predict = force_predict[:, index[i]] * update_prefactor + tmp_force_predict[mask] = -tmp_force_predict[mask] + + # In order to solve a pytorch bug, reference: https://github.com/pytorch/pytorch/issues/43259 + (tmp_force_predict.sum() + Etot_predict.sum() * 0).backward() + error = error * math.sqrt(bs) + self.optimizer.step(error) + return Etot_predict, force_predict + + def update_denoise_coord( + self, + inputs: dict, + clean_coord: torch.Tensor, + update_prefactor: float = 1, + mask_loss_coord: bool = True, + coord_mask: torch.Tensor = None, + ) -> None: + natoms_sum = int(inputs["atype"].shape[-1]) + bs = clean_coord.shape[0] + self.optimizer.set_grad_prefactor(natoms_sum * self.atoms_per_group * 3) + + index = self.__sample(self.atoms_selected, self.atoms_per_group, natoms_sum) + + for i in range(index.shape[0]): + self.optimizer.zero_grad() + model_pred, _, _ = self.model(**inputs, inference_only=True) + updated_coord = model_pred["updated_coord"] + natoms_sum = int(inputs["atype"].shape[-1]) + error_tmp = clean_coord[:, index[i]] - updated_coord[:, index[i]] + error_tmp = update_prefactor * error_tmp + if mask_loss_coord: + error_tmp[~coord_mask[:, index[i]]] = 0 + mask = error_tmp < 0 + error_tmp[mask] = -1 * error_tmp[mask] + error = error_tmp.mean() / natoms_sum + + if self.is_distributed: + dist.all_reduce(error) + error /= dist.get_world_size() + + tmp_coord_predict = updated_coord[:, index[i]] * update_prefactor + tmp_coord_predict[mask] = -update_prefactor * tmp_coord_predict[mask] + + # In order to solve a pytorch bug, reference: https://github.com/pytorch/pytorch/issues/43259 + (tmp_coord_predict.sum() + updated_coord.sum() * 0).backward() + error = error * math.sqrt(bs) + self.optimizer.step(error) + return model_pred + + def __sample( + self, atoms_selected: int, atoms_per_group: int, natoms: int + ) -> np.ndarray: + if atoms_selected % atoms_per_group: + raise Exception("divider") + index = range(natoms) + rng = np.random.default_rng() + res = rng.choice(index, atoms_selected).reshape(-1, atoms_per_group) + return res + + +# with torch.autograd.profiler.profile(enabled=True, use_cuda=True, record_shapes=False) as prof: +# the code u wanna profile +# print(prof.key_averages().table(sort_by="self_cpu_time_total")) diff --git a/deepmd/pt/optimizer/LKF.py b/deepmd/pt/optimizer/LKF.py new file mode 100644 index 0000000000..5e18797c7b --- /dev/null +++ b/deepmd/pt/optimizer/LKF.py @@ -0,0 +1,221 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging +import math + +import torch +from torch.optim.optimizer import ( + Optimizer, +) + + +class LKFOptimizer(Optimizer): + def __init__( + self, + params, + kalman_lambda=0.98, + kalman_nue=0.9987, + block_size=5120, + ): + defaults = { + "lr": 0.1, + "kalman_nue": kalman_nue, + "block_size": block_size, + } + super().__init__(params, defaults) + + self._params = self.param_groups[0]["params"] + + if len(self.param_groups) != 1 or len(self._params) == 0: + raise ValueError( + "LKF doesn't support per-parameter options " "(parameter groups)" + ) + + # NOTE: LKF has only global state, but we register it as state for + # the first param, because this helps with casting in load_state_dict + self._state = self.state[self._params[0]] + self._state.setdefault("kalman_lambda", kalman_lambda) + + self.__init_P() + + def __init_P(self): + param_nums = [] + param_sum = 0 + block_size = self.__get_blocksize() + data_type = self._params[0].dtype + device = self._params[0].device + + for param_group in self.param_groups: + params = param_group["params"] + for param in params: + param_num = param.data.nelement() + if param_sum + param_num > block_size: + if param_sum > 0: + param_nums.append(param_sum) + param_sum = param_num + else: + param_sum += param_num + + param_nums.append(param_sum) + + P = [] + params_packed_index = [] + logging.info("LKF parameter nums: %s" % param_nums) + for param_num in param_nums: + if param_num >= block_size: + block_num = math.ceil(param_num / block_size) + for i in range(block_num): + if i != block_num - 1: + P.append( + torch.eye( + block_size, + dtype=data_type, + device=device, + ) + ) + params_packed_index.append(block_size) + else: + P.append( + torch.eye( + param_num - block_size * i, + dtype=data_type, + device=device, + ) + ) + params_packed_index.append(param_num - block_size * i) + else: + P.append(torch.eye(param_num, dtype=data_type, device=device)) + params_packed_index.append(param_num) + + self._state.setdefault("P", P) + self._state.setdefault("weights_num", len(P)) + self._state.setdefault("params_packed_index", params_packed_index) + + def __get_blocksize(self): + return self.param_groups[0]["block_size"] + + def __get_nue(self): + return self.param_groups[0]["kalman_nue"] + + def __split_weights(self, weight): + block_size = self.__get_blocksize() + param_num = weight.nelement() + res = [] + if param_num < block_size: + res.append(weight) + else: + block_num = math.ceil(param_num / block_size) + for i in range(block_num): + if i != block_num - 1: + res.append(weight[i * block_size : (i + 1) * block_size]) + else: + res.append(weight[i * block_size :]) + return res + + def __update(self, H, error, weights): + P = self._state.get("P") + kalman_lambda = self._state.get("kalman_lambda") + weights_num = self._state.get("weights_num") + params_packed_index = self._state.get("params_packed_index") + + block_size = self.__get_blocksize() + kalman_nue = self.__get_nue() + + tmp = 0 + for i in range(weights_num): + tmp = tmp + (kalman_lambda + torch.matmul(torch.matmul(H[i].T, P[i]), H[i])) + + A = 1 / tmp + + for i in range(weights_num): + K = torch.matmul(P[i], H[i]) + + weights[i] = weights[i] + A * error * K + + P[i] = (1 / kalman_lambda) * (P[i] - A * torch.matmul(K, K.T)) + + kalman_lambda = kalman_nue * kalman_lambda + 1 - kalman_nue + self._state.update({"kalman_lambda": kalman_lambda}) + + i = 0 + param_sum = 0 + for param_group in self.param_groups: + params = param_group["params"] + for param in params: + param_num = param.nelement() + weight_tmp = weights[i][param_sum : param_sum + param_num] + if param_num < block_size: + if param.ndim > 1: + param.data = weight_tmp.reshape( + param.data.T.shape + ).T.contiguous() + else: + param.data = weight_tmp.reshape(param.data.shape) + + param_sum += param_num + + if param_sum == params_packed_index[i]: + i += 1 + param_sum = 0 + else: + block_num = math.ceil(param_num / block_size) + for j in range(block_num): + if j == 0: + tmp_weight = weights[i] + else: + tmp_weight = torch.concat([tmp_weight, weights[i]], dim=0) + i += 1 + param.data = tmp_weight.reshape(param.data.T.shape).T.contiguous() + + def set_grad_prefactor(self, grad_prefactor): + self.grad_prefactor = grad_prefactor + + def step(self, error): + params_packed_index = self._state.get("params_packed_index") + + weights = [] + H = [] + param_index = 0 + param_sum = 0 + + for param in self._params: + if param.ndim > 1: + tmp = param.data.T.contiguous().reshape(param.data.nelement(), 1) + if param.grad is None: + tmp_grad = torch.zeros_like(tmp) + else: + tmp_grad = ( + (param.grad / self.grad_prefactor) + .T.contiguous() + .reshape(param.grad.nelement(), 1) + ) + else: + tmp = param.data.reshape(param.data.nelement(), 1) + if param.grad is None: + tmp_grad = torch.zeros_like(tmp) + else: + tmp_grad = (param.grad / self.grad_prefactor).reshape( + param.grad.nelement(), 1 + ) + + tmp = self.__split_weights(tmp) + tmp_grad = self.__split_weights(tmp_grad) + + for split_grad, split_weight in zip(tmp_grad, tmp): + nelement = split_grad.nelement() + + if param_sum == 0: + res_grad = split_grad + res = split_weight + else: + res_grad = torch.concat((res_grad, split_grad), dim=0) + res = torch.concat((res, split_weight), dim=0) + + param_sum += nelement + + if param_sum == params_packed_index[param_index]: + H.append(res_grad) + weights.append(res) + param_sum = 0 + param_index += 1 + + self.__update(H, error, weights) diff --git a/deepmd/pt/optimizer/__init__.py b/deepmd/pt/optimizer/__init__.py new file mode 100644 index 0000000000..db340b3bb9 --- /dev/null +++ b/deepmd/pt/optimizer/__init__.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from .KFWrapper import ( + KFOptimizerWrapper, +) +from .LKF import ( + LKFOptimizer, +) + +__all__ = ["KFOptimizerWrapper", "LKFOptimizer"] diff --git a/deepmd/pt/train/__init__.py b/deepmd/pt/train/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/deepmd/pt/train/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py new file mode 100644 index 0000000000..049685a6e3 --- /dev/null +++ b/deepmd/pt/train/training.py @@ -0,0 +1,849 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging +import os +import time +from copy import ( + deepcopy, +) +from pathlib import ( + Path, +) +from typing import ( + Any, + Dict, +) + +import numpy as np +import torch +from tqdm import ( + tqdm, +) +from tqdm.contrib.logging import ( + logging_redirect_tqdm, +) + +from deepmd.pt.loss import ( + DenoiseLoss, + EnergyStdLoss, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.optimizer import ( + KFOptimizerWrapper, + LKFOptimizer, +) +from deepmd.pt.train.wrapper import ( + ModelWrapper, +) +from deepmd.pt.utils import ( + dp_random, +) +from deepmd.pt.utils.dataloader import ( + BufferedIterator, + get_weighted_sampler, +) +from deepmd.pt.utils.env import ( + DEVICE, + DISABLE_TQDM, + JIT, + LOCAL_RANK, + NUM_WORKERS, + SAMPLER_RECORD, +) +from deepmd.pt.utils.learning_rate import ( + LearningRateExp, +) + +if torch.__version__.startswith("2"): + import torch._dynamo + +import torch.distributed as dist +from torch.nn.parallel import DistributedDataParallel as DDP +from torch.utils.data import ( + DataLoader, +) + + +class Trainer: + def __init__( + self, + config: Dict[str, Any], + training_data, + sampled, + validation_data=None, + init_model=None, + restart_model=None, + finetune_model=None, + force_load=False, + shared_links=None, + ): + """Construct a DeePMD trainer. + + Args: + - config: The Dict-like configuration with training options. + """ + resume_model = init_model if init_model is not None else restart_model + self.restart_training = restart_model is not None + model_params = config["model"] + training_params = config["training"] + self.multi_task = "model_dict" in model_params + self.finetune_multi_task = model_params.pop( + "finetune_multi_task", False + ) # should use pop for next finetune + self.model_keys = ( + list(model_params["model_dict"]) if self.multi_task else ["Default"] + ) + self.rank = dist.get_rank() if dist.is_initialized() else 0 + self.world_size = dist.get_world_size() if dist.is_initialized() else 1 + self.num_model = len(self.model_keys) + + # Iteration config + self.num_steps = training_params["numb_steps"] + self.disp_file = training_params.get("disp_file", "lcurve.out") + self.disp_freq = training_params.get("disp_freq", 1000) + self.save_ckpt = training_params.get("save_ckpt", "model.pt") + self.save_freq = training_params.get("save_freq", 1000) + self.lcurve_should_print_header = True + + def get_opt_param(params): + opt_type = params.get("opt_type", "Adam") + opt_param = { + "kf_blocksize": params.get("kf_blocksize", 5120), + "kf_start_pref_e": params.get("kf_start_pref_e", 1), + "kf_limit_pref_e": params.get("kf_limit_pref_e", 1), + "kf_start_pref_f": params.get("kf_start_pref_f", 1), + "kf_limit_pref_f": params.get("kf_limit_pref_f", 1), + } + return opt_type, opt_param + + def get_data_loader(_training_data, _validation_data, _training_params): + if "auto_prob" in _training_params["training_data"]: + train_sampler = get_weighted_sampler( + _training_data, _training_params["training_data"]["auto_prob"] + ) + elif "sys_probs" in _training_params["training_data"]: + train_sampler = get_weighted_sampler( + _training_data, + _training_params["training_data"]["sys_probs"], + sys_prob=True, + ) + else: + train_sampler = get_weighted_sampler(_training_data, "prob_sys_size") + + if "auto_prob" in _training_params["validation_data"]: + valid_sampler = get_weighted_sampler( + _validation_data, _training_params["validation_data"]["auto_prob"] + ) + elif "sys_probs" in _training_params["validation_data"]: + valid_sampler = get_weighted_sampler( + _validation_data, + _training_params["validation_data"]["sys_probs"], + sys_prob=True, + ) + else: + valid_sampler = get_weighted_sampler(_validation_data, "prob_sys_size") + + if train_sampler is None or valid_sampler is None: + logging.warning( + "Sampler not specified!" + ) # None sampler will lead to a premature stop iteration. Replacement should be True in attribute of the sampler to produce expected number of items in one iteration. + training_dataloader = DataLoader( + _training_data, + sampler=train_sampler, + batch_size=None, + num_workers=NUM_WORKERS, # setting to 0 diverges the behavior of its iterator; should be >=1 + drop_last=False, + pin_memory=True, + ) + training_data_buffered = BufferedIterator(iter(training_dataloader)) + validation_dataloader = DataLoader( + _validation_data, + sampler=valid_sampler, + batch_size=None, + num_workers=min(NUM_WORKERS, 1), + drop_last=False, + pin_memory=True, + ) + + validation_data_buffered = BufferedIterator(iter(validation_dataloader)) + if _training_params.get("validation_data", None) is not None: + valid_numb_batch = _training_params["validation_data"].get( + "numb_btch", 1 + ) + else: + valid_numb_batch = 1 + return ( + training_dataloader, + training_data_buffered, + validation_dataloader, + validation_data_buffered, + valid_numb_batch, + ) + + def get_single_model(_model_params, _sampled): + model = get_model(deepcopy(_model_params), _sampled).to(DEVICE) + return model + + def get_lr(lr_params): + assert ( + lr_params.get("type", "exp") == "exp" + ), "Only learning rate `exp` is supported!" + lr_params["stop_steps"] = self.num_steps - self.warmup_steps + lr_exp = LearningRateExp(**lr_params) + return lr_exp + + def get_loss(loss_params, start_lr, _ntypes): + loss_type = loss_params.get("type", "ener") + if loss_type == "ener": + loss_params["starter_learning_rate"] = start_lr + return EnergyStdLoss(**loss_params) + elif loss_type == "denoise": + loss_params["ntypes"] = _ntypes + return DenoiseLoss(**loss_params) + else: + raise NotImplementedError + + # Optimizer + if self.multi_task and training_params.get("optim_dict", None) is not None: + self.optim_dict = training_params.get("optim_dict") + missing_keys = [ + key for key in self.model_keys if key not in self.optim_dict + ] + assert ( + not missing_keys + ), f"These keys are not in optim_dict: {missing_keys}!" + self.opt_type = {} + self.opt_param = {} + for model_key in self.model_keys: + self.opt_type[model_key], self.opt_param[model_key] = get_opt_param( + self.optim_dict[model_key] + ) + else: + self.opt_type, self.opt_param = get_opt_param(training_params) + + # Data + Model + dp_random.seed(training_params["seed"]) + if not self.multi_task: + ( + self.training_dataloader, + self.training_data, + self.validation_dataloader, + self.validation_data, + self.valid_numb_batch, + ) = get_data_loader(training_data, validation_data, training_params) + self.model = get_single_model(model_params, sampled) + else: + ( + self.training_dataloader, + self.training_data, + self.validation_dataloader, + self.validation_data, + self.valid_numb_batch, + self.model, + ) = {}, {}, {}, {}, {}, {} + for model_key in self.model_keys: + ( + self.training_dataloader[model_key], + self.training_data[model_key], + self.validation_dataloader[model_key], + self.validation_data[model_key], + self.valid_numb_batch[model_key], + ) = get_data_loader( + training_data[model_key], + validation_data[model_key], + training_params["data_dict"][model_key], + ) + self.model[model_key] = get_single_model( + model_params["model_dict"][model_key], sampled[model_key] + ) + + # Learning rate + self.warmup_steps = training_params.get("warmup_steps", 0) + self.gradient_max_norm = training_params.get("gradient_max_norm", 0.0) + assert ( + self.num_steps - self.warmup_steps > 0 + ), "Warm up steps must be less than total training steps!" + if self.multi_task and config.get("learning_rate_dict", None) is not None: + self.lr_exp = {} + for model_key in self.model_keys: + self.lr_exp[model_key] = get_lr(config["learning_rate_dict"][model_key]) + else: + self.lr_exp = get_lr(config["learning_rate"]) + + # Loss + if not self.multi_task: + self.loss = get_loss( + config["loss"], + config["learning_rate"]["start_lr"], + len(model_params["type_map"]), + ) + else: + self.loss = {} + for model_key in self.model_keys: + loss_param = config["loss_dict"][model_key] + if config.get("learning_rate_dict", None) is not None: + lr_param = config["learning_rate_dict"][model_key]["start_lr"] + else: + lr_param = config["learning_rate"]["start_lr"] + ntypes = len(model_params["model_dict"][model_key]["type_map"]) + self.loss[model_key] = get_loss(loss_param, lr_param, ntypes) + + # JIT + if JIT: + self.model = torch.jit.script(self.model) + + # Model Wrapper + self.wrapper = ModelWrapper(self.model, self.loss, model_params=model_params) + self.start_step = 0 + + # resuming and finetune + optimizer_state_dict = None + if model_params["resuming"]: + ntest = model_params.get("data_bias_nsample", 1) + origin_model = ( + finetune_model if finetune_model is not None else resume_model + ) + logging.info(f"Resuming from {origin_model}.") + state_dict = torch.load(origin_model, map_location=DEVICE) + if "model" in state_dict: + optimizer_state_dict = ( + state_dict["optimizer"] if finetune_model is None else None + ) + state_dict = state_dict["model"] + self.start_step = ( + state_dict["_extra_state"]["train_infos"]["step"] + if self.restart_training + else 0 + ) + if self.rank == 0: + if force_load: + input_keys = list(state_dict.keys()) + target_keys = list(self.wrapper.state_dict().keys()) + missing_keys = [ + item for item in target_keys if item not in input_keys + ] + if missing_keys: + target_state_dict = self.wrapper.state_dict() + slim_keys = [] + for item in missing_keys: + state_dict[item] = target_state_dict[item].clone().detach() + new_key = True + for slim_key in slim_keys: + if slim_key in item: + new_key = False + break + if new_key: + tmp_keys = ".".join(item.split(".")[:3]) + slim_keys.append(tmp_keys) + slim_keys = [i + ".*" for i in slim_keys] + logging.warning( + f"Force load mode allowed! These keys are not in ckpt and will re-init: {slim_keys}" + ) + elif self.finetune_multi_task: + new_state_dict = {} + model_branch_chosen = model_params.pop("model_branch_chosen") + new_fitting = model_params.pop("new_fitting", False) + target_state_dict = self.wrapper.state_dict() + target_keys = [ + i for i in target_state_dict.keys() if i != "_extra_state" + ] + for item_key in target_keys: + if new_fitting and ".fitting_net." in item_key: + # print(f'Keep {item_key} in old model!') + new_state_dict[item_key] = ( + target_state_dict[item_key].clone().detach() + ) + else: + new_key = item_key.replace( + ".Default.", f".{model_branch_chosen}." + ) + # print(f'Replace {item_key} with {new_key} in pretrained_model!') + new_state_dict[item_key] = ( + state_dict[new_key].clone().detach() + ) + state_dict = new_state_dict + if finetune_model is not None: + state_dict["_extra_state"] = self.wrapper.state_dict()[ + "_extra_state" + ] + + self.wrapper.load_state_dict(state_dict) + # finetune + if finetune_model is not None and model_params["fitting_net"].get( + "type", "ener" + ) in ["ener", "direct_force_ener", "atten_vec_lcc"]: + old_type_map, new_type_map = ( + model_params["type_map"], + model_params["new_type_map"], + ) + self.model.fitting_net.change_energy_bias( + config, + self.model, + old_type_map, + new_type_map, + ntest=ntest, + bias_shift=model_params.get("bias_shift", "delta"), + ) + + # Set trainable params + self.wrapper.set_trainable_params() + + # Multi-task share params + if shared_links is not None: + self.wrapper.share_params(shared_links, resume=model_params["resuming"]) + + if dist.is_initialized(): + torch.cuda.set_device(LOCAL_RANK) + # DDP will guarantee the model parameters are identical across all processes + self.wrapper = DDP( + self.wrapper, + device_ids=[LOCAL_RANK], + find_unused_parameters=True, + output_device=LOCAL_RANK, + ) + + # TODO ZD add lr warmups for multitask + def warm_up_linear(step, warmup_steps): + if step < warmup_steps: + return step / warmup_steps + else: + return self.lr_exp.value(step - warmup_steps) / self.lr_exp.start_lr + + # TODO ZD add optimizers for multitask + if self.opt_type == "Adam": + self.optimizer = torch.optim.Adam( + self.wrapper.parameters(), lr=self.lr_exp.start_lr + ) + if optimizer_state_dict is not None and self.restart_training: + self.optimizer.load_state_dict(optimizer_state_dict) + self.scheduler = torch.optim.lr_scheduler.LambdaLR( + self.optimizer, + lambda step: warm_up_linear(step + self.start_step, self.warmup_steps), + ) + elif self.opt_type == "LKF": + self.optimizer = LKFOptimizer( + self.wrapper.parameters(), 0.98, 0.99870, self.opt_param["kf_blocksize"] + ) + else: + raise ValueError("Not supported optimizer type '%s'" % self.opt_type) + + # Get model prob for multi-task + if self.multi_task: + self.model_prob = np.array([0.0 for key in self.model_keys]) + if training_params.get("model_prob", None) is not None: + model_prob = training_params["model_prob"] + for ii, model_key in enumerate(self.model_keys): + if model_key in model_prob: + self.model_prob[ii] += float(model_prob[model_key]) + else: + for ii, model_key in enumerate(self.model_keys): + self.model_prob[ii] += float(len(self.training_data[model_key])) + sum_prob = np.sum(self.model_prob) + assert sum_prob > 0.0, "Sum of model prob must be larger than 0!" + self.model_prob = self.model_prob / sum_prob + + def run(self): + fout = ( + open(self.disp_file, mode="w", buffering=1) if self.rank == 0 else None + ) # line buffered + if SAMPLER_RECORD: + record_file = f"Sample_rank_{self.rank}.txt" + fout1 = open(record_file, mode="w", buffering=1) + logging.info("Start to train %d steps.", self.num_steps) + if dist.is_initialized(): + logging.info(f"Rank: {dist.get_rank()}/{dist.get_world_size()}") + + def step(_step_id, task_key="Default"): + self.wrapper.train() + if isinstance(self.lr_exp, dict): + _lr = self.lr_exp[task_key] + else: + _lr = self.lr_exp + cur_lr = _lr.value(_step_id) + pref_lr = cur_lr + self.optimizer.zero_grad(set_to_none=True) + input_dict, label_dict, log_dict = self.get_data( + is_train=True, task_key=task_key + ) + if SAMPLER_RECORD: + print_str = f"Step {_step_id}: sample system{log_dict['sid']} frame{log_dict['fid']}\n" + fout1.write(print_str) + fout1.flush() + if self.opt_type == "Adam": + cur_lr = self.scheduler.get_last_lr()[0] + if _step_id < self.warmup_steps: + pref_lr = _lr.start_lr + else: + pref_lr = cur_lr + model_pred, loss, more_loss = self.wrapper( + **input_dict, cur_lr=pref_lr, label=label_dict, task_key=task_key + ) + loss.backward() + if self.gradient_max_norm > 0.0: + grad_norm = torch.nn.utils.clip_grad_norm_( + self.wrapper.parameters(), self.gradient_max_norm + ) + if not torch.isfinite(grad_norm).all(): + # check local gradnorm single GPU case, trigger NanDetector + raise FloatingPointError("gradients are Nan/Inf") + self.optimizer.step() + self.scheduler.step() + elif self.opt_type == "LKF": + if isinstance(self.loss, EnergyStdLoss): + KFOptWrapper = KFOptimizerWrapper( + self.wrapper, self.optimizer, 24, 6, dist.is_initialized() + ) + pref_e = self.opt_param["kf_start_pref_e"] * ( + self.opt_param["kf_limit_pref_e"] + / self.opt_param["kf_start_pref_e"] + ) ** (_step_id / self.num_steps) + _ = KFOptWrapper.update_energy( + input_dict, label_dict["energy"], pref_e + ) + pref_f = self.opt_param["kf_start_pref_f"] * ( + self.opt_param["kf_limit_pref_f"] + / self.opt_param["kf_start_pref_f"] + ) ** (_step_id / self.num_steps) + p_energy, p_force = KFOptWrapper.update_force( + input_dict, label_dict["force"], pref_f + ) + # [coord, atype, natoms, mapping, shift, nlist, box] + model_pred = {"energy": p_energy, "force": p_force} + module = ( + self.wrapper.module if dist.is_initialized() else self.wrapper + ) + loss, more_loss = module.loss[task_key]( + model_pred, + label_dict, + int(input_dict["atype"].shape[-1]), + learning_rate=pref_lr, + ) + elif isinstance(self.loss, DenoiseLoss): + KFOptWrapper = KFOptimizerWrapper( + self.wrapper, self.optimizer, 24, 6, dist.is_initialized() + ) + module = ( + self.wrapper.module if dist.is_initialized() else self.wrapper + ) + model_pred = KFOptWrapper.update_denoise_coord( + input_dict, + label_dict["clean_coord"], + 1, + module.loss[task_key].mask_loss_coord, + label_dict["coord_mask"], + ) + loss, more_loss = module.loss[task_key]( + model_pred, + label_dict, + input_dict["natoms"], + learning_rate=pref_lr, + ) + else: + raise ValueError("Not supported optimizer type '%s'" % self.opt_type) + + # Log and persist + if _step_id % self.disp_freq == 0: + self.wrapper.eval() + msg = f"step={_step_id}, lr={cur_lr:.2e}" + + def log_loss_train(_loss, _more_loss, _task_key="Default"): + results = {} + if not self.multi_task: + suffix = "" + else: + suffix = f"_{_task_key}" + _msg = f"loss{suffix}={_loss:.4f}" + rmse_val = { + item: _more_loss[item] + for item in _more_loss + if "l2_" not in item + } + for item in sorted(rmse_val.keys()): + _msg += f", {item}_train{suffix}={rmse_val[item]:.4f}" + results[item] = rmse_val[item] + return _msg, results + + def log_loss_valid(_task_key="Default"): + single_results = {} + sum_natoms = 0 + if not self.multi_task: + suffix = "" + valid_numb_batch = self.valid_numb_batch + else: + suffix = f"_{_task_key}" + valid_numb_batch = self.valid_numb_batch[_task_key] + for ii in range(valid_numb_batch): + self.optimizer.zero_grad() + input_dict, label_dict, _ = self.get_data( + is_train=False, task_key=_task_key + ) + _, loss, more_loss = self.wrapper( + **input_dict, + cur_lr=pref_lr, + label=label_dict, + task_key=_task_key, + ) + # more_loss.update({"rmse": math.sqrt(loss)}) + natoms = int(input_dict["atype"].shape[-1]) + sum_natoms += natoms + for k, v in more_loss.items(): + if "l2_" not in k: + single_results[k] = ( + single_results.get(k, 0.0) + v * natoms + ) + results = {k: v / sum_natoms for k, v in single_results.items()} + _msg = "" + for item in sorted(results.keys()): + _msg += f", {item}_valid{suffix}={results[item]:.4f}" + return _msg, results + + if not self.multi_task: + temp_msg, train_results = log_loss_train(loss, more_loss) + msg += "\n" + temp_msg + temp_msg, valid_results = log_loss_valid() + msg += temp_msg + else: + train_results = {_key: {} for _key in self.model_keys} + valid_results = {_key: {} for _key in self.model_keys} + train_msg = {} + valid_msg = {} + train_msg[task_key], train_results[task_key] = log_loss_train( + loss, more_loss, _task_key=task_key + ) + for _key in self.model_keys: + if _key != task_key: + self.optimizer.zero_grad() + input_dict, label_dict, _ = self.get_data( + is_train=True, task_key=_key + ) + _, loss, more_loss = self.wrapper( + **input_dict, + cur_lr=pref_lr, + label=label_dict, + task_key=_key, + ) + train_msg[_key], train_results[_key] = log_loss_train( + loss, more_loss, _task_key=_key + ) + valid_msg[_key], valid_results[_key] = log_loss_valid( + _task_key=_key + ) + msg += "\n" + train_msg[_key] + msg += valid_msg[_key] + + train_time = time.time() - self.t0 + self.t0 = time.time() + msg += f", speed={train_time:.2f} s/{self.disp_freq if _step_id else 1} batches" + logging.info(msg) + + if fout: + if self.lcurve_should_print_header: + self.print_header(fout, train_results, valid_results) + self.lcurve_should_print_header = False + self.print_on_training( + fout, _step_id, cur_lr, train_results, valid_results + ) + + if ( + ((_step_id + 1) % self.save_freq == 0 and _step_id != self.start_step) + or (_step_id + 1) == self.num_steps + ) and (self.rank == 0 or dist.get_rank() == 0): + # Handle the case if rank 0 aborted and re-assigned + self.latest_model = Path(self.save_ckpt) + self.latest_model = self.latest_model.with_name( + f"{self.latest_model.stem}_{_step_id + 1}{self.latest_model.suffix}" + ) + module = self.wrapper.module if dist.is_initialized() else self.wrapper + self.save_model(self.latest_model, lr=cur_lr, step=_step_id) + logging.info(f"Saved model to {self.latest_model}") + + self.t0 = time.time() + with logging_redirect_tqdm(): + for step_id in tqdm( + range(self.num_steps), + disable=(bool(dist.get_rank()) if dist.is_initialized() else False) + or DISABLE_TQDM, + ): # set to None to disable on non-TTY; disable on not rank 0 + if step_id < self.start_step: + continue + if self.multi_task: + chosen_index_list = dp_random.choice( + np.arange(self.num_model), + p=np.array(self.model_prob), + size=self.world_size, + replace=True, + ) + assert chosen_index_list.size == self.world_size + model_index = chosen_index_list[self.rank] + model_key = self.model_keys[model_index] + else: + model_key = "Default" + step(step_id, model_key) + if JIT: + break + + if ( + self.rank == 0 or dist.get_rank() == 0 + ): # Handle the case if rank 0 aborted and re-assigned + if JIT: + pth_model_path = ( + "frozen_model.pth" # We use .pth to denote the frozen model + ) + self.model.save(pth_model_path) + logging.info( + f"Frozen model for inferencing has been saved to {pth_model_path}" + ) + try: + os.symlink(self.latest_model, self.save_ckpt) + except OSError: + self.save_model(self.save_ckpt, lr=0, step=self.num_steps) + logging.info(f"Trained model has been saved to: {self.save_ckpt}") + + if fout: + fout.close() + if SAMPLER_RECORD: + fout1.close() + + def save_model(self, save_path, lr=0.0, step=0): + module = self.wrapper.module if dist.is_initialized() else self.wrapper + module.train_infos["lr"] = lr + module.train_infos["step"] = step + torch.save( + {"model": module.state_dict(), "optimizer": self.optimizer.state_dict()}, + save_path, + ) + + def get_data(self, is_train=True, task_key="Default"): + if not self.multi_task: + if is_train: + try: + batch_data = next(iter(self.training_data)) + except StopIteration: + # Refresh the status of the dataloader to start from a new epoch + self.training_data = BufferedIterator( + iter(self.training_dataloader) + ) + batch_data = next(iter(self.training_data)) + else: + try: + batch_data = next(iter(self.validation_data)) + except StopIteration: + self.validation_data = BufferedIterator( + iter(self.validation_dataloader) + ) + batch_data = next(iter(self.validation_data)) + else: + if is_train: + try: + batch_data = next(iter(self.training_data[task_key])) + except StopIteration: + # Refresh the status of the dataloader to start from a new epoch + self.training_data[task_key] = BufferedIterator( + iter(self.training_dataloader[task_key]) + ) + batch_data = next(iter(self.training_data[task_key])) + else: + try: + batch_data = next(iter(self.validation_data[task_key])) + except StopIteration: + self.validation_data[task_key] = BufferedIterator( + iter(self.validation_dataloader[task_key]) + ) + batch_data = next(iter(self.validation_data[task_key])) + + for key in batch_data.keys(): + if key == "sid" or key == "fid": + continue + elif not isinstance(batch_data[key], list): + if batch_data[key] is not None: + batch_data[key] = batch_data[key].to(DEVICE) + else: + batch_data[key] = [item.to(DEVICE) for item in batch_data[key]] + input_dict = {} + for item in [ + "coord", + "atype", + "box", + ]: + if item in batch_data: + input_dict[item] = batch_data[item] + else: + input_dict[item] = None + label_dict = {} + for item in [ + "energy", + "force", + "virial", + "clean_coord", + "clean_type", + "coord_mask", + "type_mask", + ]: + if item in batch_data: + label_dict[item] = batch_data[item] + log_dict = {} + if "fid" in batch_data: + log_dict["fid"] = batch_data["fid"] + log_dict["sid"] = batch_data["sid"] + return input_dict, label_dict, log_dict + + def print_header(self, fout, train_results, valid_results): + train_keys = sorted(train_results.keys()) + print_str = "" + print_str += "# %5s" % "step" + if not self.multi_task: + if valid_results is not None: + prop_fmt = " %11s %11s" + for k in train_keys: + print_str += prop_fmt % (k + "_val", k + "_trn") + else: + prop_fmt = " %11s" + for k in train_keys: + print_str += prop_fmt % (k + "_trn") + else: + for model_key in self.model_keys: + if valid_results[model_key] is not None: + prop_fmt = " %11s %11s" + for k in sorted(train_results[model_key].keys()): + print_str += prop_fmt % ( + k + f"_val_{model_key}", + k + f"_trn_{model_key}", + ) + else: + prop_fmt = " %11s" + for k in sorted(train_results[model_key].keys()): + print_str += prop_fmt % (k + f"_trn_{model_key}") + print_str += " %8s\n" % "lr" + fout.write(print_str) + fout.flush() + + def print_on_training(self, fout, step_id, cur_lr, train_results, valid_results): + train_keys = sorted(train_results.keys()) + print_str = "" + print_str += "%7d" % step_id + if not self.multi_task: + if valid_results is not None: + prop_fmt = " %11.2e %11.2e" + for k in train_keys: + print_str += prop_fmt % (valid_results[k], train_results[k]) + else: + prop_fmt = " %11.2e" + for k in train_keys: + print_str += prop_fmt % (train_results[k]) + else: + for model_key in self.model_keys: + if valid_results[model_key] is not None: + prop_fmt = " %11.2e %11.2e" + for k in sorted(valid_results[model_key].keys()): + print_str += prop_fmt % ( + valid_results[model_key][k], + train_results[model_key][k], + ) + else: + prop_fmt = " %11.2e" + for k in sorted(train_results[model_key].keys()): + print_str += prop_fmt % (train_results[model_key][k]) + print_str += " %8.1e\n" % cur_lr + fout.write(print_str) + fout.flush() diff --git a/deepmd/pt/train/wrapper.py b/deepmd/pt/train/wrapper.py new file mode 100644 index 0000000000..fe423e6318 --- /dev/null +++ b/deepmd/pt/train/wrapper.py @@ -0,0 +1,192 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + Optional, + Union, +) + +import torch + +if torch.__version__.startswith("2"): + import torch._dynamo + + +class ModelWrapper(torch.nn.Module): + def __init__( + self, + model: Union[torch.nn.Module, Dict], + loss: Union[torch.nn.Module, Dict] = None, + model_params=None, + shared_links=None, + ): + """Construct a DeePMD model wrapper. + + Args: + - config: The Dict-like configuration with training options. + """ + super().__init__() + self.model_params = model_params if model_params is not None else {} + self.train_infos = { + "lr": 0, + "step": 0, + } + self.multi_task = False + self.model = torch.nn.ModuleDict() + # Model + if isinstance(model, torch.nn.Module): + self.model["Default"] = model + elif isinstance(model, dict): + self.multi_task = True + for task_key in model: + assert isinstance( + model[task_key], torch.nn.Module + ), f"{task_key} in model_dict is not a torch.nn.Module!" + self.model[task_key] = model[task_key] + # Loss + self.loss = None + if loss is not None: + self.loss = torch.nn.ModuleDict() + if isinstance(loss, torch.nn.Module): + self.loss["Default"] = loss + elif isinstance(loss, dict): + for task_key in loss: + assert isinstance( + loss[task_key], torch.nn.Module + ), f"{task_key} in loss_dict is not a torch.nn.Module!" + self.loss[task_key] = loss[task_key] + self.inference_only = self.loss is None + + def set_trainable_params(self): + supported_types = ["type_embedding", "descriptor", "fitting_net"] + for model_item in self.model: + for net_type in supported_types: + trainable = True + if not self.multi_task: + if net_type in self.model_params: + trainable = self.model_params[net_type].get("trainable", True) + else: + if net_type in self.model_params["model_dict"][model_item]: + trainable = self.model_params["model_dict"][model_item][ + net_type + ].get("trainable", True) + if ( + hasattr(self.model[model_item], net_type) + and getattr(self.model[model_item], net_type) is not None + ): + for param in ( + self.model[model_item].__getattr__(net_type).parameters() + ): + param.requires_grad = trainable + + def share_params(self, shared_links, resume=False): + supported_types = ["type_embedding", "descriptor", "fitting_net"] + for shared_item in shared_links: + class_name = shared_links[shared_item]["type"] + shared_base = shared_links[shared_item]["links"][0] + class_type_base = shared_base["shared_type"] + model_key_base = shared_base["model_key"] + shared_level_base = shared_base["shared_level"] + if "descriptor" in class_type_base: + if class_type_base == "descriptor": + base_class = self.model[model_key_base].__getattr__("descriptor") + elif "hybrid" in class_type_base: + hybrid_index = int(class_type_base.split("_")[-1]) + base_class = ( + self.model[model_key_base] + .__getattr__("descriptor") + .descriptor_list[hybrid_index] + ) + else: + raise RuntimeError(f"Unknown class_type {class_type_base}!") + for link_item in shared_links[shared_item]["links"][1:]: + class_type_link = link_item["shared_type"] + model_key_link = link_item["model_key"] + shared_level_link = int(link_item["shared_level"]) + assert ( + shared_level_link >= shared_level_base + ), "The shared_links must be sorted by shared_level!" + assert ( + "descriptor" in class_type_link + ), f"Class type mismatched: {class_type_base} vs {class_type_link}!" + if class_type_link == "descriptor": + link_class = self.model[model_key_link].__getattr__( + "descriptor" + ) + elif "hybrid" in class_type_link: + hybrid_index = int(class_type_link.split("_")[-1]) + link_class = ( + self.model[model_key_link] + .__getattr__("descriptor") + .descriptor_list[hybrid_index] + ) + else: + raise RuntimeError(f"Unknown class_type {class_type_link}!") + link_class.share_params( + base_class, shared_level_link, resume=resume + ) + print( + f"Shared params of {model_key_base}.{class_type_base} and {model_key_link}.{class_type_link}!" + ) + else: + if hasattr(self.model[model_key_base], class_type_base): + base_class = self.model[model_key_base].__getattr__(class_type_base) + for link_item in shared_links[shared_item]["links"][1:]: + class_type_link = link_item["shared_type"] + model_key_link = link_item["model_key"] + shared_level_link = int(link_item["shared_level"]) + assert ( + shared_level_link >= shared_level_base + ), "The shared_links must be sorted by shared_level!" + assert ( + class_type_base == class_type_link + ), f"Class type mismatched: {class_type_base} vs {class_type_link}!" + link_class = self.model[model_key_link].__getattr__( + class_type_link + ) + link_class.share_params( + base_class, shared_level_link, resume=resume + ) + print( + f"Shared params of {model_key_base}.{class_type_base} and {model_key_link}.{class_type_link}!" + ) + + def forward( + self, + coord, + atype, + box: Optional[torch.Tensor] = None, + cur_lr: Optional[torch.Tensor] = None, + label: Optional[torch.Tensor] = None, + task_key: Optional[torch.Tensor] = None, + inference_only=False, + do_atomic_virial=False, + ): + if not self.multi_task: + task_key = "Default" + else: + assert ( + task_key is not None + ), f"Multitask model must specify the inference task! Supported tasks are {list(self.model.keys())}." + model_pred = self.model[task_key]( + coord, atype, box=box, do_atomic_virial=do_atomic_virial + ) + natoms = atype.shape[-1] + if not self.inference_only and not inference_only: + loss, more_loss = self.loss[task_key]( + model_pred, label, natoms=natoms, learning_rate=cur_lr + ) + return model_pred, loss, more_loss + else: + return model_pred, None, None + + def set_extra_state(self, state: Dict): + self.model_params = state["model_params"] + self.train_infos = state["train_infos"] + return None + + def get_extra_state(self) -> Dict: + state = { + "model_params": self.model_params, + "train_infos": self.train_infos, + } + return state diff --git a/deepmd/pt/utils/__init__.py b/deepmd/pt/utils/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/deepmd/pt/utils/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/deepmd/pt/utils/ase_calc.py b/deepmd/pt/utils/ase_calc.py new file mode 100644 index 0000000000..8d5fe8bce9 --- /dev/null +++ b/deepmd/pt/utils/ase_calc.py @@ -0,0 +1,65 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + ClassVar, +) + +import dpdata +import numpy as np +from ase import ( + Atoms, +) +from ase.calculators.calculator import ( + Calculator, + PropertyNotImplementedError, +) + +from deepmd.pt.infer.deep_eval import ( + DeepPot, +) + + +class DPCalculator(Calculator): + implemented_properties: ClassVar[list] = [ + "energy", + "free_energy", + "forces", + "virial", + "stress", + ] + + def __init__(self, model): + Calculator.__init__(self) + self.dp = DeepPot(model) + self.type_map = self.dp.type_map + + def calculate(self, atoms: Atoms, properties, system_changes) -> None: + Calculator.calculate(self, atoms, properties, system_changes) + system = dpdata.System(atoms, fmt="ase/structure") + type_trans = np.array( + [self.type_map.index(i) for i in system.data["atom_names"]] + ) + input_coords = system.data["coords"] + input_cells = system.data["cells"] + input_types = list(type_trans[system.data["atom_types"]]) + model_predict = self.dp.eval(input_coords, input_cells, input_types) + self.results = { + "energy": model_predict[0].item(), + "free_energy": model_predict[0].item(), + "forces": model_predict[1].reshape(-1, 3), + "virial": model_predict[2].reshape(3, 3), + } + + # convert virial into stress for lattice relaxation + if "stress" in properties: + if sum(atoms.get_pbc()) > 0 or (atoms.cell is not None): + # the usual convention (tensile stress is positive) + # stress = -virial / volume + stress = ( + -0.5 + * (self.results["virial"].copy() + self.results["virial"].copy().T) + / atoms.get_volume() + ) + # Voigt notation + self.results["stress"] = stress.flat[[0, 4, 8, 5, 2, 1]] + else: + raise PropertyNotImplementedError diff --git a/deepmd/pt/utils/auto_batch_size.py b/deepmd/pt/utils/auto_batch_size.py new file mode 100644 index 0000000000..5af7760e2a --- /dev/null +++ b/deepmd/pt/utils/auto_batch_size.py @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import torch + +from deepmd.utils.batch_size import AutoBatchSize as AutoBatchSizeBase + + +class AutoBatchSize(AutoBatchSizeBase): + def is_gpu_available(self) -> bool: + """Check if GPU is available. + + Returns + ------- + bool + True if GPU is available + """ + return torch.cuda.is_available() + + def is_oom_error(self, e: Exception) -> bool: + """Check if the exception is an OOM error. + + Parameters + ---------- + e : Exception + Exception + """ + return isinstance(e, RuntimeError) and "CUDA out of memory." in e.args[0] diff --git a/deepmd/pt/utils/cache.py b/deepmd/pt/utils/cache.py new file mode 100644 index 0000000000..c40c4050b7 --- /dev/null +++ b/deepmd/pt/utils/cache.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy as copy_lib +import functools + + +def lru_cache(maxsize=16, typed=False, copy=False, deepcopy=False): + if deepcopy: + + def decorator(f): + cached_func = functools.lru_cache(maxsize, typed)(f) + + @functools.wraps(f) + def wrapper(*args, **kwargs): + return copy_lib.deepcopy(cached_func(*args, **kwargs)) + + return wrapper + + elif copy: + + def decorator(f): + cached_func = functools.lru_cache(maxsize, typed)(f) + + @functools.wraps(f) + def wrapper(*args, **kwargs): + return copy_lib.copy(cached_func(*args, **kwargs)) + + return wrapper + + else: + decorator = functools.lru_cache(maxsize, typed) + return decorator diff --git a/deepmd/pt/utils/dataloader.py b/deepmd/pt/utils/dataloader.py new file mode 100644 index 0000000000..7c95f66c9c --- /dev/null +++ b/deepmd/pt/utils/dataloader.py @@ -0,0 +1,319 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging +import os +import queue +import time +from multiprocessing.dummy import ( + Pool, +) +from threading import ( + Thread, +) +from typing import ( + List, +) + +import h5py +import torch +import torch.distributed as dist +import torch.multiprocessing +from torch.utils.data import ( + DataLoader, + Dataset, + WeightedRandomSampler, +) +from torch.utils.data.distributed import ( + DistributedSampler, +) + +from deepmd.pt.model.descriptor import ( + Descriptor, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.dataset import ( + DeepmdDataSetForLoader, +) +from deepmd.utils.data_system import ( + prob_sys_size_ext, + process_sys_probs, +) + +torch.multiprocessing.set_sharing_strategy("file_system") + + +def setup_seed(seed): + torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + torch.backends.cudnn.deterministic = True + + +class DpLoaderSet(Dataset): + """A dataset for storing DataLoaders to multiple Systems.""" + + def __init__( + self, + systems, + batch_size, + model_params, + seed=10, + type_split=True, + noise_settings=None, + shuffle=True, + ): + setup_seed(seed) + if isinstance(systems, str): + with h5py.File(systems) as file: + systems = [os.path.join(systems, item) for item in file.keys()] + + self.systems: List[DeepmdDataSetForLoader] = [] + if len(systems) >= 100: + logging.info(f"Constructing DataLoaders from {len(systems)} systems") + + def construct_dataset(system): + ### this design requires "rcut" and "sel" in the descriptor + ### VERY BAD DESIGN!!!! + ### not all descriptors provides these parameter in their constructor + if model_params["descriptor"].get("type") != "hybrid": + info_dict = Descriptor.get_data_process_key(model_params["descriptor"]) + rcut = info_dict["rcut"] + sel = info_dict["sel"] + else: ### need to remove this + rcut = [] + sel = [] + for ii in model_params["descriptor"]["list"]: + rcut.append(ii["rcut"]) + sel.append(ii["sel"]) + return DeepmdDataSetForLoader( + system=system, + type_map=model_params["type_map"], + rcut=rcut, + sel=sel, + type_split=type_split, + noise_settings=noise_settings, + shuffle=shuffle, + ) + + with Pool( + os.cpu_count() + // (int(os.environ["LOCAL_WORLD_SIZE"]) if dist.is_initialized() else 1) + ) as pool: + self.systems = pool.map(construct_dataset, systems) + + self.sampler_list: List[DistributedSampler] = [] + self.index = [] + self.total_batch = 0 + + self.dataloaders = [] + for system in self.systems: + if dist.is_initialized(): + system_sampler = DistributedSampler(system) + self.sampler_list.append(system_sampler) + else: + system_sampler = None + if isinstance(batch_size, str): + if batch_size == "auto": + rule = 32 + elif batch_size.startswith("auto:"): + rule = int(batch_size.split(":")[1]) + else: + rule = None + logging.error("Unsupported batch size type") + self.batch_size = rule // system._natoms + if self.batch_size * system._natoms < rule: + self.batch_size += 1 + else: + self.batch_size = batch_size + system_dataloader = DataLoader( + dataset=system, + batch_size=self.batch_size, + num_workers=0, # Should be 0 to avoid too many threads forked + sampler=system_sampler, + collate_fn=collate_batch, + shuffle=(not dist.is_initialized()) and shuffle, + ) + self.dataloaders.append(system_dataloader) + self.index.append(len(system_dataloader)) + self.total_batch += len(system_dataloader) + # Initialize iterator instances for DataLoader + self.iters = [] + for item in self.dataloaders: + self.iters.append(iter(item)) + + def set_noise(self, noise_settings): + # noise_settings['noise_type'] # "trunc_normal", "normal", "uniform" + # noise_settings['noise'] # float, default 1.0 + # noise_settings['noise_mode'] # "prob", "fix_num" + # noise_settings['mask_num'] # if "fix_num", int + # noise_settings['mask_prob'] # if "prob", float + # noise_settings['same_mask'] # coord and type same mask? + for system in self.systems: + system.set_noise(noise_settings) + + def __len__(self): + return len(self.dataloaders) + + def __getitem__(self, idx): + # logging.warning(str(torch.distributed.get_rank())+" idx: "+str(idx)+" index: "+str(self.index[idx])) + try: + batch = next(self.iters[idx]) + except StopIteration: + self.iters[idx] = iter(self.dataloaders[idx]) + batch = next(self.iters[idx]) + batch["sid"] = idx + return batch + + +_sentinel = object() +QUEUESIZE = 32 + + +class BackgroundConsumer(Thread): + def __init__(self, queue, source, max_len): + Thread.__init__(self) + self._queue = queue + self._source = source # Main DL iterator + self._max_len = max_len # + + def run(self): + for item in self._source: + self._queue.put(item) # Blocking if the queue is full + + # Signal the consumer we are done. + self._queue.put(_sentinel) + + +class BufferedIterator: + def __init__(self, iterable): + self._queue = queue.Queue(QUEUESIZE) + self._iterable = iterable + self._consumer = None + + self.start_time = time.time() + self.warning_time = None + self.total = len(iterable) + + def _create_consumer(self): + self._consumer = BackgroundConsumer(self._queue, self._iterable, self.total) + self._consumer.daemon = True + self._consumer.start() + + def __iter__(self): + return self + + def __len__(self): + return self.total + + def __next__(self): + # Create consumer if not created yet + if self._consumer is None: + self._create_consumer() + # Notify the user if there is a data loading bottleneck + if self._queue.qsize() < min(2, max(1, self._queue.maxsize // 2)): + if time.time() - self.start_time > 5 * 60: + if ( + self.warning_time is None + or time.time() - self.warning_time > 15 * 60 + ): + logging.warning( + "Data loading buffer is empty or nearly empty. This may " + "indicate a data loading bottleneck, and increasing the " + "number of workers (--num-workers) may help." + ) + self.warning_time = time.time() + + # Get next example + item = self._queue.get() + if isinstance(item, Exception): + raise item + if item is _sentinel: + raise StopIteration + return item + + +def collate_tensor_fn(batch): + elem = batch[0] + if not isinstance(elem, list): + out = None + if torch.utils.data.get_worker_info() is not None: + # If we're in a background process, concatenate directly into a + # shared memory tensor to avoid an extra copy + numel = sum(x.numel() for x in batch) + storage = elem._typed_storage()._new_shared(numel, device=elem.device) + out = elem.new(storage).resize_(len(batch), *list(elem.size())) + return torch.stack(batch, 0, out=out) + else: + out_hybrid = [] + for ii, hybrid_item in enumerate(elem): + out = None + tmp_batch = [x[ii] for x in batch] + if torch.utils.data.get_worker_info() is not None: + # If we're in a background process, concatenate directly into a + # shared memory tensor to avoid an extra copy + numel = sum(x.numel() for x in tmp_batch) + storage = hybrid_item._typed_storage()._new_shared( + numel, device=hybrid_item.device + ) + out = hybrid_item.new(storage).resize_( + len(tmp_batch), *list(hybrid_item.size()) + ) + out_hybrid.append(torch.stack(tmp_batch, 0, out=out)) + return out_hybrid + + +def collate_batch(batch): + example = batch[0] + result = example.copy() + for key in example.keys(): + if key == "shift" or key == "mapping": + natoms_extended = max([d[key].shape[0] for d in batch]) + n_frames = len(batch) + list = [] + for x in range(n_frames): + list.append(batch[x][key]) + if key == "shift": + result[key] = torch.zeros( + (n_frames, natoms_extended, 3), + dtype=env.GLOBAL_PT_FLOAT_PRECISION, + device=env.PREPROCESS_DEVICE, + ) + else: + result[key] = torch.zeros( + (n_frames, natoms_extended), + dtype=torch.long, + device=env.PREPROCESS_DEVICE, + ) + for i in range(len(batch)): + natoms_tmp = list[i].shape[0] + result[key][i, :natoms_tmp] = list[i] + elif "find_" in key: + result[key] = batch[0][key] + else: + if batch[0][key] is None: + result[key] = None + elif key == "fid": + result[key] = [d[key] for d in batch] + else: + result[key] = collate_tensor_fn([d[key] for d in batch]) + return result + + +def get_weighted_sampler(training_data, prob_style, sys_prob=False): + if sys_prob is False: + if prob_style == "prob_uniform": + prob_v = 1.0 / float(training_data.__len__()) + probs = [prob_v for ii in range(training_data.__len__())] + else: # prob_sys_size;A:B:p1;C:D:p2 or prob_sys_size = prob_sys_size;0:nsys:1.0 + if prob_style == "prob_sys_size": + style = f"prob_sys_size;0:{len(training_data)}:1.0" + else: + style = prob_style + probs = prob_sys_size_ext(style, len(training_data), training_data.index) + else: + probs = process_sys_probs(prob_style, training_data.index) + logging.info("Generated weighted sampler with prob array: " + str(probs)) + # training_data.total_batch is the size of one epoch, you can increase it to avoid too many rebuilding of iteraters + len_sampler = training_data.total_batch * max(env.NUM_WORKERS, 1) + sampler = WeightedRandomSampler(probs, len_sampler, replacement=True) + return sampler diff --git a/deepmd/pt/utils/dataset.py b/deepmd/pt/utils/dataset.py new file mode 100644 index 0000000000..24daa6e37e --- /dev/null +++ b/deepmd/pt/utils/dataset.py @@ -0,0 +1,918 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import glob +import os +from typing import ( + List, + Optional, +) + +import h5py +import numpy as np +import torch +import torch.distributed as dist +from torch.utils.data import ( + Dataset, +) +from tqdm import ( + trange, +) + +from deepmd.pt.utils import ( + dp_random, + env, +) +from deepmd.pt.utils.cache import ( + lru_cache, +) +from deepmd.pt.utils.preprocess import ( + Region3D, + make_env_mat, + normalize_coord, +) + + +class DeepmdDataSystem: + def __init__( + self, + sys_path: str, + rcut, + sec, + type_map: Optional[List[str]] = None, + type_split=True, + noise_settings=None, + shuffle=True, + ): + """Construct DeePMD-style frame collection of one system. + + Args: + - sys_path: Paths to the system. + - type_map: Atom types. + """ + sys_path = sys_path.replace("#", "") + if ".hdf5" in sys_path: + tmp = sys_path.split("/") + path = "/".join(tmp[:-1]) + sys = tmp[-1] + self.file = h5py.File(path)[sys] + self._dirs = [] + for item in self.file.keys(): + if "set." in item: + self._dirs.append(item) + self._dirs.sort() + else: + self.file = None + self._dirs = glob.glob(os.path.join(sys_path, "set.*")) + self._dirs.sort() + self.type_split = type_split + self.noise_settings = noise_settings + self._check_pbc(sys_path) + self.shuffle = shuffle + if noise_settings is not None: + self.noise_type = noise_settings.get("noise_type", "uniform") + self.noise = float(noise_settings.get("noise", 1.0)) + self.noise_mode = noise_settings.get("noise_mode", "fix_num") + self.mask_num = int(noise_settings.get("mask_num", 1)) + self.mask_prob = float(noise_settings.get("mask_prob", 0.15)) + self.same_mask = noise_settings.get("same_mask", False) + self.mask_coord = noise_settings.get("mask_coord", False) + self.mask_type = noise_settings.get("mask_type", False) + self.mask_type_idx = int(noise_settings.get("mask_type_idx", 0)) + self.max_fail_num = int(noise_settings.get("max_fail_num", 10)) + + # check mixed type + error_format_msg = ( + "if one of the set is of mixed_type format, " + "then all of the sets in this system should be of mixed_type format!" + ) + if len(self._dirs) == 0: + raise RuntimeError(f"No set found in system {sys_path}.") + + self.mixed_type = self._check_mode(self._dirs[0]) + for set_item in self._dirs[1:]: + assert self._check_mode(set_item) == self.mixed_type, error_format_msg + + self._atom_type = self._load_type(sys_path) + self._natoms = len(self._atom_type) + + self._type_map = self._load_type_map(sys_path) + self.enforce_type_map = False + if type_map is not None and self._type_map is not None: + if not self.mixed_type: + atom_type = [ + type_map.index(self._type_map[ii]) for ii in self._atom_type + ] + self._atom_type = np.array(atom_type, dtype=np.int32) + + else: + self.enforce_type_map = True + sorter = np.argsort(type_map) + self.type_idx_map = np.array( + sorter[np.searchsorted(type_map, self._type_map, sorter=sorter)] + ) + # padding for virtual atom + self.type_idx_map = np.append( + self.type_idx_map, np.array([-1], dtype=np.int32) + ) + self._type_map = type_map + if type_map is None and self.type_map is None and self.mixed_type: + raise RuntimeError("mixed_type format must have type_map!") + self._idx_map = _make_idx_map(self._atom_type) + + self._data_dict = {} + self.add("box", 9, must=self.pbc) + self.add("coord", 3, atomic=True, must=True) + self.add("energy", 1, atomic=False, must=False, high_prec=True) + self.add("force", 3, atomic=True, must=False, high_prec=False) + self.add("virial", 9, atomic=False, must=False, high_prec=False) + + self._sys_path = sys_path + self.rcut = rcut + self.sec = sec + if isinstance(rcut, float): + self.hybrid = False + elif isinstance(rcut, list): + self.hybrid = True + else: + RuntimeError("Unkown rcut type!") + self.sets = [None for i in range(len(self._sys_path))] + + self.nframes = 0 + i = 1 + self.prefix_sum = [0] * (len(self._dirs) + 1) + for item in self._dirs: + frames = self._load_set(item, fast=True) + self.prefix_sum[i] = self.prefix_sum[i - 1] + frames + i += 1 + self.nframes += frames + + def _check_pbc(self, sys_path): + pbc = True + if os.path.isfile(os.path.join(sys_path, "nopbc")): + pbc = False + self.pbc = pbc + + def set_noise(self, noise_settings): + # noise_settings['noise_type'] # "trunc_normal", "normal", "uniform" + # noise_settings['noise'] # float, default 1.0 + # noise_settings['noise_mode'] # "prob", "fix_num" + # noise_settings['mask_num'] # if "fix_num", int + # noise_settings['mask_prob'] # if "prob", float + # noise_settings['same_mask'] # coord and type same mask? + self.noise_settings = noise_settings + self.noise_type = noise_settings.get("noise_type", "uniform") + self.noise = float(noise_settings.get("noise", 1.0)) + self.noise_mode = noise_settings.get("noise_mode", "fix_num") + self.mask_num = int(noise_settings.get("mask_num", 1)) + self.mask_coord = noise_settings.get("mask_coord", False) + self.mask_type = noise_settings.get("mask_type", False) + self.mask_prob = float(noise_settings.get("mask_prob", 0.15)) + self.same_mask = noise_settings.get("noise_type", False) + + def add( + self, + key: str, + ndof: int, + atomic: bool = False, + must: bool = False, + high_prec: bool = False, + ): + """Add a data item that to be loaded. + + Args: + - key: The key of the item. The corresponding data is stored in `sys_path/set.*/key.npy` + - ndof: The number of dof + - atomic: The item is an atomic property. + - must: The data file `sys_path/set.*/key.npy` must exist. Otherwise, value is set to zero. + - high_prec: Load the data and store in float64, otherwise in float32. + """ + self._data_dict[key] = { + "ndof": ndof, + "atomic": atomic, + "must": must, + "high_prec": high_prec, + } + + # deprecated TODO + def get_batch_for_train(self, batch_size: int): + """Get a batch of data with at most `batch_size` frames. The frames are randomly picked from the data system. + + Args: + - batch_size: Frame count. + """ + if not hasattr(self, "_frames"): + self.set_size = 0 + self._set_count = 0 + self._iterator = 0 + if batch_size == "auto": + batch_size = -(-32 // self._natoms) + if self._iterator + batch_size > self.set_size: + set_idx = self._set_count % len(self._dirs) + if self.sets[set_idx] is None: + frames = self._load_set(self._dirs[set_idx]) + frames = self.preprocess(frames) + cnt = 0 + for item in self.sets: + if item is not None: + cnt += 1 + if cnt < env.CACHE_PER_SYS: + self.sets[set_idx] = frames + else: + frames = self.sets[set_idx] + self._frames = frames + self._shuffle_data() + if dist.is_initialized(): + world_size = dist.get_world_size() + rank = dist.get_rank() + ssize = self._frames["coord"].shape[0] + subsize = ssize // world_size + self._iterator = rank * subsize + self.set_size = min((rank + 1) * subsize, ssize) + else: + self.set_size = self._frames["coord"].shape[0] + self._iterator = 0 + self._set_count += 1 + iterator = min(self._iterator + batch_size, self.set_size) + idx = np.arange(self._iterator, iterator) + self._iterator += batch_size + return self._get_subdata(idx) + + # deprecated TODO + def get_batch(self, batch_size: int): + """Get a batch of data with at most `batch_size` frames. The frames are randomly picked from the data system. + Args: + - batch_size: Frame count. + """ + if not hasattr(self, "_frames"): + self.set_size = 0 + self._set_count = 0 + self._iterator = 0 + if batch_size == "auto": + batch_size = -(-32 // self._natoms) + if self._iterator + batch_size > self.set_size: + set_idx = self._set_count % len(self._dirs) + if self.sets[set_idx] is None: + frames = self._load_set(self._dirs[set_idx]) + frames = self.preprocess(frames) + cnt = 0 + for item in self.sets: + if item is not None: + cnt += 1 + if cnt < env.CACHE_PER_SYS: + self.sets[set_idx] = frames + else: + frames = self.sets[set_idx] + self._frames = frames + self._shuffle_data() + self.set_size = self._frames["coord"].shape[0] + self._iterator = 0 + self._set_count += 1 + iterator = min(self._iterator + batch_size, self.set_size) + idx = np.arange(self._iterator, iterator) + self._iterator += batch_size + return self._get_subdata(idx) + + def get_ntypes(self): + """Number of atom types in the system.""" + if self._type_map is not None: + return len(self._type_map) + else: + return max(self._atom_type) + 1 + + def get_natoms_vec(self, ntypes: int): + """Get number of atoms and number of atoms in different types. + + Args: + - ntypes: Number of types (may be larger than the actual number of types in the system). + """ + natoms = len(self._atom_type) + natoms_vec = np.zeros(ntypes).astype(int) + for ii in range(ntypes): + natoms_vec[ii] = np.count_nonzero(self._atom_type == ii) + tmp = [natoms, natoms] + tmp = np.append(tmp, natoms_vec) + return tmp.astype(np.int32) + + def _load_type(self, sys_path): + if self.file is not None: + return self.file["type.raw"][:] + else: + return np.loadtxt( + os.path.join(sys_path, "type.raw"), dtype=np.int32, ndmin=1 + ) + + def _load_type_map(self, sys_path): + if self.file is not None: + tmp = self.file["type_map.raw"][:].tolist() + tmp = [item.decode("ascii") for item in tmp] + return tmp + else: + fname = os.path.join(sys_path, "type_map.raw") + if os.path.isfile(fname): + with open(fname) as fin: + content = fin.read() + return content.split() + else: + return None + + def _check_mode(self, sys_path): + return os.path.isfile(sys_path + "/real_atom_types.npy") + + def _load_type_mix(self, set_name): + type_path = set_name + "/real_atom_types.npy" + real_type = np.load(type_path).astype(np.int32).reshape([-1, self._natoms]) + return real_type + + @lru_cache(maxsize=16, copy=True) + def _load_set(self, set_name, fast=False): + if self.file is None: + path = os.path.join(set_name, "coord.npy") + if self._data_dict["coord"]["high_prec"]: + coord = np.load(path).astype(env.GLOBAL_ENER_FLOAT_PRECISION) + else: + coord = np.load(path).astype(env.GLOBAL_NP_FLOAT_PRECISION) + if coord.ndim == 1: + coord = coord.reshape([1, -1]) + assert coord.shape[1] == self._data_dict["coord"]["ndof"] * self._natoms + nframes = coord.shape[0] + if fast: + return nframes + data = {"type": np.tile(self._atom_type[self._idx_map], (nframes, 1))} + for kk in self._data_dict.keys(): + data["find_" + kk], data[kk] = self._load_data( + set_name, + kk, + nframes, + self._data_dict[kk]["ndof"], + atomic=self._data_dict[kk]["atomic"], + high_prec=self._data_dict[kk]["high_prec"], + must=self._data_dict[kk]["must"], + ) + if self.mixed_type: + # nframes x natoms + atom_type_mix = self._load_type_mix(set_name) + if self.enforce_type_map: + try: + atom_type_mix_ = self.type_idx_map[atom_type_mix].astype( + np.int32 + ) + except IndexError as e: + raise IndexError( + "some types in 'real_atom_types.npy' of set {} are not contained in {} types!".format( + set_name, self.get_ntypes() + ) + ) from e + atom_type_mix = atom_type_mix_ + real_type = atom_type_mix.reshape([nframes, self._natoms]) + data["type"] = real_type + natoms = data["type"].shape[1] + # nframes x ntypes + atom_type_nums = np.array( + [(real_type == i).sum(axis=-1) for i in range(self.get_ntypes())], + dtype=np.int32, + ).T + ghost_nums = np.array( + [(real_type == -1).sum(axis=-1)], + dtype=np.int32, + ).T + assert ( + atom_type_nums.sum(axis=-1) + ghost_nums.sum(axis=-1) == natoms + ).all(), "some types in 'real_atom_types.npy' of set {} are not contained in {} types!".format( + set_name, self.get_ntypes() + ) + data["real_natoms_vec"] = np.concatenate( + ( + np.tile( + np.array([natoms, natoms], dtype=np.int32), (nframes, 1) + ), + atom_type_nums, + ), + axis=-1, + ) + + return data + else: + data = {} + nframes = self.file[set_name]["coord.npy"].shape[0] + if fast: + return nframes + for key in ["coord", "energy", "force", "box"]: + data[key] = self.file[set_name][f"{key}.npy"][:] + if self._data_dict[key]["atomic"]: + data[key] = data[key].reshape(nframes, self._natoms, -1)[ + :, self._idx_map, : + ] + if self.mixed_type: + # nframes x natoms + atom_type_mix = self._load_type_mix(set_name) + if self.enforce_type_map: + try: + atom_type_mix_ = self.type_idx_map[atom_type_mix].astype( + np.int32 + ) + except IndexError as e: + raise IndexError( + "some types in 'real_atom_types.npy' of set {} are not contained in {} types!".format( + set_name, self.get_ntypes() + ) + ) from e + atom_type_mix = atom_type_mix_ + real_type = atom_type_mix.reshape([nframes, self._natoms]) + data["type"] = real_type + natoms = data["type"].shape[1] + # nframes x ntypes + atom_type_nums = np.array( + [(real_type == i).sum(axis=-1) for i in range(self.get_ntypes())], + dtype=np.int32, + ).T + ghost_nums = np.array( + [(real_type == -1).sum(axis=-1)], + dtype=np.int32, + ).T + assert ( + atom_type_nums.sum(axis=-1) + ghost_nums.sum(axis=-1) == natoms + ).all(), "some types in 'real_atom_types.npy' of set {} are not contained in {} types!".format( + set_name, self.get_ntypes() + ) + data["real_natoms_vec"] = np.concatenate( + ( + np.tile( + np.array([natoms, natoms], dtype=np.int32), (nframes, 1) + ), + atom_type_nums, + ), + axis=-1, + ) + else: + data["type"] = np.tile(self._atom_type[self._idx_map], (nframes, 1)) + return data + + def _load_data( + self, set_name, key, nframes, ndof, atomic=False, must=True, high_prec=False + ): + if atomic: + ndof *= self._natoms + path = os.path.join(set_name, key + ".npy") + # logging.info('Loading data from: %s', path) + if os.path.isfile(path): + if high_prec: + data = np.load(path).astype(env.GLOBAL_ENER_FLOAT_PRECISION) + else: + data = np.load(path).astype(env.GLOBAL_NP_FLOAT_PRECISION) + if atomic: + data = data.reshape([nframes, self._natoms, -1]) + data = data[:, self._idx_map, :] + data = data.reshape([nframes, -1]) + data = np.reshape(data, [nframes, ndof]) + return np.float32(1.0), data + elif must: + raise RuntimeError("%s not found!" % path) + else: + if high_prec: + data = np.zeros([nframes, ndof]).astype(env.GLOBAL_ENER_FLOAT_PRECISION) + else: + data = np.zeros([nframes, ndof]).astype(env.GLOBAL_NP_FLOAT_PRECISION) + return np.float32(0.0), data + + # deprecated TODO + def preprocess(self, batch): + n_frames = batch["coord"].shape[0] + for kk in self._data_dict.keys(): + if "find_" in kk: + pass + else: + batch[kk] = torch.tensor( + batch[kk], + dtype=env.GLOBAL_PT_FLOAT_PRECISION, + device=env.PREPROCESS_DEVICE, + ) + if self._data_dict[kk]["atomic"]: + batch[kk] = batch[kk].view( + n_frames, -1, self._data_dict[kk]["ndof"] + ) + + for kk in ["type", "real_natoms_vec"]: + if kk in batch.keys(): + batch[kk] = torch.tensor( + batch[kk], dtype=torch.long, device=env.PREPROCESS_DEVICE + ) + batch["atype"] = batch.pop("type") + + keys = ["nlist", "nlist_loc", "nlist_type", "shift", "mapping"] + coord = batch["coord"] + atype = batch["atype"] + box = batch["box"] + rcut = self.rcut + sec = self.sec + assert batch["atype"].max() < len(self._type_map) + nlist, nlist_loc, nlist_type, shift, mapping = [], [], [], [], [] + + for sid in trange(n_frames, disable=env.DISABLE_TQDM): + region = Region3D(box[sid]) + nloc = atype[sid].shape[0] + _coord = normalize_coord(coord[sid], region, nloc) + coord[sid] = _coord + a, b, c, d, e = make_env_mat( + _coord, atype[sid], region, rcut, sec, type_split=self.type_split + ) + nlist.append(a) + nlist_loc.append(b) + nlist_type.append(c) + shift.append(d) + mapping.append(e) + nlist = torch.stack(nlist) + nlist_loc = torch.stack(nlist_loc) + nlist_type = torch.stack(nlist_type) + batch["nlist"] = nlist + batch["nlist_loc"] = nlist_loc + batch["nlist_type"] = nlist_type + natoms_extended = max([item.shape[0] for item in shift]) + batch["shift"] = torch.zeros( + (n_frames, natoms_extended, 3), + dtype=env.GLOBAL_PT_FLOAT_PRECISION, + device=env.PREPROCESS_DEVICE, + ) + batch["mapping"] = torch.zeros( + (n_frames, natoms_extended), dtype=torch.long, device=env.PREPROCESS_DEVICE + ) + for i in range(len(shift)): + natoms_tmp = shift[i].shape[0] + batch["shift"][i, :natoms_tmp] = shift[i] + batch["mapping"][i, :natoms_tmp] = mapping[i] + return batch + + def _shuffle_data(self): + nframes = self._frames["coord"].shape[0] + idx = np.arange(nframes) + if self.shuffle: + dp_random.shuffle(idx) + self.idx_mapping = idx + + def _get_subdata(self, idx=None): + data = self._frames + idx = self.idx_mapping[idx] + new_data = {} + for ii in data: + dd = data[ii] + if "find_" in ii: + new_data[ii] = dd + else: + if idx is not None: + new_data[ii] = dd[idx] + else: + new_data[ii] = dd + return new_data + + # note: this function needs to be optimized for single frame process + def single_preprocess(self, batch, sid): + for kk in self._data_dict.keys(): + if "find_" in kk: + pass + else: + batch[kk] = torch.tensor( + batch[kk][sid], + dtype=env.GLOBAL_PT_FLOAT_PRECISION, + device=env.PREPROCESS_DEVICE, + ) + if self._data_dict[kk]["atomic"]: + batch[kk] = batch[kk].view(-1, self._data_dict[kk]["ndof"]) + for kk in ["type", "real_natoms_vec"]: + if kk in batch.keys(): + batch[kk] = torch.tensor( + batch[kk][sid], dtype=torch.long, device=env.PREPROCESS_DEVICE + ) + clean_coord = batch.pop("coord") + clean_type = batch.pop("type") + nloc = clean_type.shape[0] + rcut = self.rcut + sec = self.sec + nlist, nlist_loc, nlist_type, shift, mapping = [], [], [], [], [] + if self.pbc: + box = batch["box"] + region = Region3D(box) + else: + box = None + batch["box"] = None + region = None + if self.noise_settings is None: + batch["atype"] = clean_type + batch["coord"] = clean_coord + coord = clean_coord + atype = batch["atype"] + if self.pbc: + _coord = normalize_coord(coord, region, nloc) + + else: + _coord = coord.clone() + batch["coord"] = _coord + nlist, nlist_loc, nlist_type, shift, mapping = make_env_mat( + _coord, + atype, + region, + rcut, + sec, + pbc=self.pbc, + type_split=self.type_split, + ) + batch["nlist"] = nlist + batch["nlist_loc"] = nlist_loc + batch["nlist_type"] = nlist_type + batch["shift"] = shift + batch["mapping"] = mapping + return batch + else: + batch["clean_type"] = clean_type + if self.pbc: + _clean_coord = normalize_coord(clean_coord, region, nloc) + else: + _clean_coord = clean_coord.clone() + batch["clean_coord"] = _clean_coord + # add noise + for i in range(self.max_fail_num): + mask_num = 0 + if self.noise_mode == "fix_num": + mask_num = self.mask_num + if len(batch["clean_type"]) < mask_num: + mask_num = len(batch["clean_type"]) + elif self.noise_mode == "prob": + mask_num = int(self.mask_prob * nloc) + if mask_num == 0: + mask_num = 1 + else: + NotImplementedError(f"Unknown noise mode {self.noise_mode}!") + rng = np.random.default_rng() + coord_mask_res = rng.choice( + range(nloc), mask_num, replace=False + ).tolist() + coord_mask = np.isin(range(nloc), coord_mask_res) + if self.same_mask: + type_mask = coord_mask.copy() + else: + rng = np.random.default_rng() + type_mask_res = rng.choice( + range(nloc), mask_num, replace=False + ).tolist() + type_mask = np.isin(range(nloc), type_mask_res) + + # add noise for coord + if self.mask_coord: + noise_on_coord = 0.0 + rng = np.random.default_rng() + if self.noise_type == "trunc_normal": + noise_on_coord = np.clip( + rng.standard_normal((mask_num, 3)) * self.noise, + a_min=-self.noise * 2.0, + a_max=self.noise * 2.0, + ) + elif self.noise_type == "normal": + noise_on_coord = rng.standard_normal((mask_num, 3)) * self.noise + elif self.noise_type == "uniform": + noise_on_coord = rng.uniform( + low=-self.noise, high=self.noise, size=(mask_num, 3) + ) + else: + NotImplementedError(f"Unknown noise type {self.noise_type}!") + noised_coord = _clean_coord.clone().detach() + noised_coord[coord_mask] += noise_on_coord + batch["coord_mask"] = torch.tensor( + coord_mask, dtype=torch.bool, device=env.PREPROCESS_DEVICE + ) + else: + noised_coord = _clean_coord + batch["coord_mask"] = torch.tensor( + np.zeros_like(coord_mask, dtype=bool), + dtype=torch.bool, + device=env.PREPROCESS_DEVICE, + ) + + # add mask for type + if self.mask_type: + masked_type = clean_type.clone().detach() + masked_type[type_mask] = self.mask_type_idx + batch["type_mask"] = torch.tensor( + type_mask, dtype=torch.bool, device=env.PREPROCESS_DEVICE + ) + else: + masked_type = clean_type + batch["type_mask"] = torch.tensor( + np.zeros_like(type_mask, dtype=bool), + dtype=torch.bool, + device=env.PREPROCESS_DEVICE, + ) + if self.pbc: + _coord = normalize_coord(noised_coord, region, nloc) + else: + _coord = noised_coord.clone() + try: + nlist, nlist_loc, nlist_type, shift, mapping = make_env_mat( + _coord, + masked_type, + region, + rcut, + sec, + pbc=self.pbc, + type_split=self.type_split, + min_check=True, + ) + except RuntimeError as e: + if i == self.max_fail_num - 1: + RuntimeError( + f"Add noise times beyond max tries {self.max_fail_num}!" + ) + continue + batch["atype"] = masked_type + batch["coord"] = noised_coord + batch["nlist"] = nlist + batch["nlist_loc"] = nlist_loc + batch["nlist_type"] = nlist_type + batch["shift"] = shift + batch["mapping"] = mapping + return batch + + def _get_item(self, index): + for i in range( + 0, len(self._dirs) + 1 + ): # note: if different sets can be merged, prefix sum is unused to calculate + if index < self.prefix_sum[i]: + break + frames = self._load_set(self._dirs[i - 1]) + frame = self.single_preprocess(frames, index - self.prefix_sum[i - 1]) + frame["fid"] = index + return frame + + +def _make_idx_map(atom_type): + natoms = atom_type.shape[0] + idx = np.arange(natoms) + idx_map = np.lexsort((idx, atom_type)) + return idx_map + + +class DeepmdDataSetForLoader(Dataset): + def __init__( + self, + system: str, + type_map: str, + rcut, + sel, + weight=None, + type_split=True, + noise_settings=None, + shuffle=True, + ): + """Construct DeePMD-style dataset containing frames cross different systems. + + Args: + - systems: Paths to systems. + - batch_size: Max frame count in a batch. + - type_map: Atom types. + """ + self._type_map = type_map + if not isinstance(rcut, list): + if isinstance(sel, int): + sel = [sel] + sec = torch.cumsum(torch.tensor(sel), dim=0) + else: + sec = [] + for sel_item in sel: + if isinstance(sel_item, int): + sel_item = [sel_item] + sec.append(torch.cumsum(torch.tensor(sel_item), dim=0)) + self._data_system = DeepmdDataSystem( + system, + rcut, + sec, + type_map=self._type_map, + type_split=type_split, + noise_settings=noise_settings, + shuffle=shuffle, + ) + self.mixed_type = self._data_system.mixed_type + self._ntypes = self._data_system.get_ntypes() + self._natoms = self._data_system._natoms + self._natoms_vec = self._data_system.get_natoms_vec(self._ntypes) + + def set_noise(self, noise_settings): + # noise_settings['noise_type'] # "trunc_normal", "normal", "uniform" + # noise_settings['noise'] # float, default 1.0 + # noise_settings['noise_mode'] # "prob", "fix_num" + # noise_settings['mask_num'] # if "fix_num", int + # noise_settings['mask_prob'] # if "prob", float + # noise_settings['same_mask'] # coord and type same mask? + self._data_system.set_noise(noise_settings) + + def __len__(self): + return self._data_system.nframes + + def __getitem__(self, index): + """Get a frame from the selected system.""" + b_data = self._data_system._get_item(index) + b_data["natoms"] = torch.tensor(self._natoms_vec, device=env.PREPROCESS_DEVICE) + return b_data + + +# deprecated TODO +class DeepmdDataSet(Dataset): + def __init__( + self, + systems: List[str], + batch_size: int, + type_map: List[str], + rcut=None, + sel=None, + weight=None, + type_split=True, + ): + """Construct DeePMD-style dataset containing frames cross different systems. + + Args: + - systems: Paths to systems. + - batch_size: Max frame count in a batch. + - type_map: Atom types. + """ + self._batch_size = batch_size + self._type_map = type_map + if sel is not None: + if isinstance(sel, int): + sel = [sel] + sec = torch.cumsum(torch.tensor(sel), dim=0) + if isinstance(systems, str): + with h5py.File(systems) as file: + systems = [os.path.join(systems, item) for item in file.keys()] + self._data_systems = [ + DeepmdDataSystem( + ii, rcut, sec, type_map=self._type_map, type_split=type_split + ) + for ii in systems + ] + # check mix_type format + error_format_msg = ( + "if one of the system is of mixed_type format, " + "then all of the systems in this dataset should be of mixed_type format!" + ) + self.mixed_type = self._data_systems[0].mixed_type + for sys_item in self._data_systems[1:]: + assert sys_item.mixed_type == self.mixed_type, error_format_msg + + if weight is None: + + def weight(name, sys): + return sys.nframes + + self.probs = [ + weight(item, self._data_systems[i]) for i, item in enumerate(systems) + ] + self.probs = np.array(self.probs, dtype=float) + self.probs /= self.probs.sum() + self._ntypes = max([ii.get_ntypes() for ii in self._data_systems]) + self._natoms_vec = [ + ii.get_natoms_vec(self._ntypes) for ii in self._data_systems + ] + self.cache = [{} for _ in self._data_systems] + + @property + def nsystems(self): + return len(self._data_systems) + + def __len__(self): + return self.nsystems + + def __getitem__(self, index=None): + """Get a batch of frames from the selected system.""" + if index is None: + index = dp_random.choice(np.arange(self.nsystems), self.probs) + b_data = self._data_systems[index].get_batch(self._batch_size) + b_data["natoms"] = torch.tensor( + self._natoms_vec[index], device=env.PREPROCESS_DEVICE + ) + batch_size = b_data["coord"].shape[0] + b_data["natoms"] = b_data["natoms"].unsqueeze(0).expand(batch_size, -1) + return b_data + + # deprecated TODO + def get_training_batch(self, index=None): + """Get a batch of frames from the selected system.""" + if index is None: + index = dp_random.choice(np.arange(self.nsystems), self.probs) + b_data = self._data_systems[index].get_batch_for_train(self._batch_size) + b_data["natoms"] = torch.tensor( + self._natoms_vec[index], device=env.PREPROCESS_DEVICE + ) + batch_size = b_data["coord"].shape[0] + b_data["natoms"] = b_data["natoms"].unsqueeze(0).expand(batch_size, -1) + return b_data + + def get_batch(self, sys_idx=None): + """TF-compatible batch for testing.""" + pt_batch = self[sys_idx] + np_batch = {} + for key in ["coord", "box", "force", "energy", "virial"]: + if key in pt_batch.keys(): + np_batch[key] = pt_batch[key].cpu().numpy() + for key in ["atype", "natoms"]: + if key in pt_batch.keys(): + np_batch[key] = pt_batch[key].cpu().numpy() + batch_size = pt_batch["coord"].shape[0] + np_batch["coord"] = np_batch["coord"].reshape(batch_size, -1) + np_batch["natoms"] = np_batch["natoms"][0] + np_batch["force"] = np_batch["force"].reshape(batch_size, -1) + return np_batch, pt_batch diff --git a/deepmd/pt/utils/dp_random.py b/deepmd/pt/utils/dp_random.py new file mode 100644 index 0000000000..e81488c506 --- /dev/null +++ b/deepmd/pt/utils/dp_random.py @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from deepmd.utils.random import ( + choice, + random, + seed, + shuffle, +) + +__all__ = [ + "choice", + "random", + "seed", + "shuffle", +] diff --git a/deepmd/pt/utils/env.py b/deepmd/pt/utils/env.py new file mode 100644 index 0000000000..5b6eaf7c14 --- /dev/null +++ b/deepmd/pt/utils/env.py @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import os + +import numpy as np +import torch + +PRECISION = os.environ.get("PRECISION", "float64") +GLOBAL_NP_FLOAT_PRECISION = getattr(np, PRECISION) +GLOBAL_PT_FLOAT_PRECISION = getattr(torch, PRECISION) +GLOBAL_ENER_FLOAT_PRECISION = getattr(np, PRECISION) +DISABLE_TQDM = os.environ.get("DISABLE_TQDM", False) +SAMPLER_RECORD = os.environ.get("SAMPLER_RECORD", False) +try: + # only linux + ncpus = len(os.sched_getaffinity(0)) +except AttributeError: + ncpus = os.cpu_count() +NUM_WORKERS = int(os.environ.get("NUM_WORKERS", min(8, ncpus))) +# Make sure DDP uses correct device if applicable +LOCAL_RANK = os.environ.get("LOCAL_RANK") +LOCAL_RANK = int(0 if LOCAL_RANK is None else LOCAL_RANK) + +if os.environ.get("DEVICE") == "cpu" or torch.cuda.is_available() is False: + DEVICE = torch.device("cpu") +else: + DEVICE = torch.device(f"cuda:{LOCAL_RANK}") + +if os.environ.get("PREPROCESS_DEVICE") == "gpu": + PREPROCESS_DEVICE = torch.device(f"cuda:{LOCAL_RANK}") +else: + PREPROCESS_DEVICE = torch.device("cpu") + +JIT = False +CACHE_PER_SYS = 5 # keep at most so many sets per sys in memory +ENERGY_BIAS_TRAINABLE = True + +PRECISION_DICT = { + "float16": torch.float16, + "float32": torch.float32, + "float64": torch.float64, + "half": torch.float16, + "single": torch.float32, + "double": torch.float64, +} +DEFAULT_PRECISION = "float64" diff --git a/deepmd/pt/utils/finetune.py b/deepmd/pt/utils/finetune.py new file mode 100644 index 0000000000..9d82783cc0 --- /dev/null +++ b/deepmd/pt/utils/finetune.py @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging + +import torch + +from deepmd.pt.utils import ( + env, +) + + +def change_finetune_model_params( + ckpt, finetune_model, model_config, multi_task=False, model_branch="" +): + """Load model_params according to the pretrained one. + + Args: + - ckpt & finetune_model: origin model. + - config: Read from json file. + """ + if multi_task: + # TODO + print("finetune mode need modification for multitask mode!") + if finetune_model is not None: + state_dict = torch.load(finetune_model, map_location=env.DEVICE) + if "model" in state_dict: + state_dict = state_dict["model"] + last_model_params = state_dict["_extra_state"]["model_params"] + finetune_multi_task = "model_dict" in last_model_params + trainable_param = { + "type_embedding": True, + "descriptor": True, + "fitting_net": True, + } + for net_type in trainable_param: + if net_type in model_config: + trainable_param[net_type] = model_config[net_type].get( + "trainable", True + ) + if not finetune_multi_task: + old_type_map, new_type_map = ( + last_model_params["type_map"], + model_config["type_map"], + ) + assert set(new_type_map).issubset( + old_type_map + ), "Only support for smaller type map when finetuning or resuming." + model_config = last_model_params + logging.info( + "Change the model configurations according to the pretrained one..." + ) + model_config["new_type_map"] = new_type_map + else: + model_config["finetune_multi_task"] = finetune_multi_task + model_dict_params = last_model_params["model_dict"] + new_fitting = False + if model_branch == "": + model_branch_chosen = next(iter(model_dict_params.keys())) + new_fitting = True + model_config["bias_shift"] = "statistic" # fitting net re-init + print( + "The fitting net will be re-init instead of using that in the pretrained model! " + "The bias_shift will be statistic!" + ) + else: + model_branch_chosen = model_branch + assert model_branch_chosen in model_dict_params, ( + f"No model branch named '{model_branch_chosen}'! " + f"Available ones are {list(model_dict_params.keys())}." + ) + old_type_map, new_type_map = ( + model_dict_params[model_branch_chosen]["type_map"], + model_config["type_map"], + ) + assert set(new_type_map).issubset( + old_type_map + ), "Only support for smaller type map when finetuning or resuming." + for key_item in ["type_map", "type_embedding", "descriptor"]: + if key_item in model_dict_params[model_branch_chosen]: + model_config[key_item] = model_dict_params[model_branch_chosen][ + key_item + ] + if not new_fitting: + model_config["fitting_net"] = model_dict_params[model_branch_chosen][ + "fitting_net" + ] + logging.info( + f"Change the model configurations according to the model branch " + f"{model_branch_chosen} in the pretrained one..." + ) + model_config["new_type_map"] = new_type_map + model_config["model_branch_chosen"] = model_branch_chosen + model_config["new_fitting"] = new_fitting + for net_type in trainable_param: + if net_type in model_config: + model_config[net_type]["trainable"] = trainable_param[net_type] + else: + model_config[net_type] = {"trainable": trainable_param[net_type]} + return model_config diff --git a/deepmd/pt/utils/learning_rate.py b/deepmd/pt/utils/learning_rate.py new file mode 100644 index 0000000000..eca3c6ad87 --- /dev/null +++ b/deepmd/pt/utils/learning_rate.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import numpy as np + + +class LearningRateExp: + def __init__(self, start_lr, stop_lr, decay_steps, stop_steps, **kwargs): + """Construct an exponential-decayed learning rate. + + Args: + - start_lr: Initial learning rate. + - stop_lr: Learning rate at the last step. + - decay_steps: Decay learning rate every N steps. + - stop_steps: When is the last step. + """ + self.start_lr = start_lr + default_ds = 100 if stop_steps // 10 > 100 else stop_steps // 100 + 1 + self.decay_steps = decay_steps + if self.decay_steps >= stop_steps: + self.decay_steps = default_ds + self.decay_rate = np.exp( + np.log(stop_lr / self.start_lr) / (stop_steps / self.decay_steps) + ) + if "decay_rate" in kwargs: + self.decay_rate = kwargs["decay_rate"] + if "min_lr" in kwargs: + self.min_lr = kwargs["min_lr"] + else: + self.min_lr = 3e-10 + + def value(self, step): + """Get the learning rate at the given step.""" + step_lr = self.start_lr * np.power(self.decay_rate, step // self.decay_steps) + if step_lr < self.min_lr: + step_lr = self.min_lr + return step_lr diff --git a/deepmd/pt/utils/multi_task.py b/deepmd/pt/utils/multi_task.py new file mode 100644 index 0000000000..f97a826b03 --- /dev/null +++ b/deepmd/pt/utils/multi_task.py @@ -0,0 +1,129 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from copy import ( + deepcopy, +) + +from deepmd.pt.model.descriptor import ( + DescrptDPA1, + DescrptDPA2, + DescrptSeA, +) +from deepmd.pt.model.network.network import ( + TypeEmbedNet, +) +from deepmd.pt.model.task import ( + EnergyFittingNet, + EnergyFittingNetDirect, + FittingNetAttenLcc, +) + + +def preprocess_shared_params(model_config): + """Preprocess the model params for multitask model, and generate the links dict for further sharing. + + Args: + model_config: Model params of multitask model. + + Returns + ------- + model_config: Preprocessed model params of multitask model. + Those string names are replaced with real params in `shared_dict` of model params. + shared_links: Dict of link infos for further sharing. + Each item, whose key must be in `shared_dict`, is a dict with following keys: + - "type": The real class type of this item. + - "links": List of shared settings, each sub-item is a dict with following keys: + - "model_key": Model key in the `model_dict` to share this item. + - "shared_type": Type of this shard item. + - "shared_level": Shared level (int) of this item in this model. + Lower for more params to share, 0 means to share all params in this item. + This list are sorted by "shared_level". + """ + assert "model_dict" in model_config, "only multi-task model can use this method!" + supported_types = ["type_map", "type_embedding", "descriptor", "fitting_net"] + shared_dict = model_config.get("shared_dict", {}) + shared_links = {} + type_map_keys = [] + + def replace_one_item(params_dict, key_type, key_in_dict, suffix="", index=None): + shared_type = key_type + shared_key = key_in_dict + shared_level = 0 + if ":" in key_in_dict: + shared_key = key_in_dict.split(":")[0] + shared_level = int(key_in_dict.split(":")[1]) + assert ( + shared_key in shared_dict + ), f"Appointed {shared_type} {shared_key} are not in the shared_dict! Please check the input params." + if index is None: + params_dict[shared_type] = deepcopy(shared_dict[shared_key]) + else: + params_dict[index] = deepcopy(shared_dict[shared_key]) + if shared_type == "type_map": + if key_in_dict not in type_map_keys: + type_map_keys.append(key_in_dict) + else: + if shared_key not in shared_links: + class_name = get_class_name(shared_type, shared_dict[key_in_dict]) + shared_links[shared_key] = {"type": class_name, "links": []} + link_item = { + "model_key": model_key, + "shared_type": shared_type + suffix, + "shared_level": shared_level, + } + shared_links[shared_key]["links"].append(link_item) + + for model_key in model_config["model_dict"]: + model_params_item = model_config["model_dict"][model_key] + for item_key in model_params_item: + if item_key in supported_types: + item_params = model_params_item[item_key] + if isinstance(item_params, str): + replace_one_item(model_params_item, item_key, item_params) + elif item_params.get("type", "") == "hybrid": + for ii, hybrid_item in enumerate(item_params["list"]): + if isinstance(hybrid_item, str): + replace_one_item( + model_params_item[item_key]["list"], + item_key, + hybrid_item, + suffix=f"_hybrid_{ii}", + index=ii, + ) + for shared_key in shared_links: + shared_links[shared_key]["links"] = sorted( + shared_links[shared_key]["links"], key=lambda x: x["shared_level"] + ) + assert len(type_map_keys) == 1, "Multitask model must have only one type_map!" + return model_config, shared_links + + +def get_class_name(item_key, item_params): + if item_key == "type_embedding": + return TypeEmbedNet.__name__ + elif item_key == "descriptor": + item_type = item_params.get("type", "se_e2_a") + if item_type == "se_e2_a": + return DescrptSeA.__name__ + elif item_type in ["se_atten", "dpa1"]: + return DescrptDPA1.__name__ + elif item_type in ["dpa2"]: + return DescrptDPA2.__name__ + # todo add support for other combination + # elif item_type == "gaussian_lcc": + # return DescrptGaussianLcc.__name__ + # elif item_type == "hybrid": + # return DescrptHybrid.__name__ + else: + raise RuntimeError(f"Unknown descriptor type {item_type}") + elif item_key == "fitting_net": + item_type = item_params.get("type", "ener") + if item_type == "ener": + return EnergyFittingNet.__name__ + elif item_type in ["direct_force", "direct_force_ener"]: + return EnergyFittingNetDirect.__name__ + elif item_type == "atten_vec_lcc": + return FittingNetAttenLcc.__name__ + else: + raise RuntimeError(f"Unknown fitting_net type {item_type}") + else: + raise RuntimeError(f"Unknown class_name type {item_key}") diff --git a/deepmd/pt/utils/nlist.py b/deepmd/pt/utils/nlist.py new file mode 100644 index 0000000000..23a11684a5 --- /dev/null +++ b/deepmd/pt/utils/nlist.py @@ -0,0 +1,431 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + List, + Optional, + Union, +) + +import torch + +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.region import ( + to_face_distance, +) + + +def _build_neighbor_list( + coord1: torch.Tensor, + nloc: int, + rcut: float, + nsel: int, + rmin: float = 1e-10, + cut_nearest: bool = True, +) -> torch.Tensor: + """Build neightbor list for a single frame. keeps nsel neighbors. + coord1 : [nall x 3]. + + ret: [nloc x nsel] stores indexes of coord1. + """ + nall = coord1.shape[-1] // 3 + coord0 = torch.split(coord1, [nloc * 3, (nall - nloc) * 3])[0] + # nloc x nall x 3 + diff = coord1.view([-1, 3])[None, :, :] - coord0.view([-1, 3])[:, None, :] + assert list(diff.shape) == [nloc, nall, 3] + # nloc x nall + rr = torch.linalg.norm(diff, dim=-1) + rr, nlist = torch.sort(rr, dim=-1) + if cut_nearest: + # nloc x (nall-1) + rr = torch.split(rr, [1, nall - 1], dim=-1)[-1] + nlist = torch.split(nlist, [1, nall - 1], dim=-1)[-1] + # nloc x nsel + nnei = rr.shape[1] + rr = torch.split(rr, [nsel, nnei - nsel], dim=-1)[0] + nlist = torch.split(nlist, [nsel, nnei - nsel], dim=-1)[0] + nlist = nlist.masked_fill((rr > rcut), -1) + return nlist + + +def build_neighbor_list_lower( + coord1: torch.Tensor, + atype: torch.Tensor, + nloc: int, + rcut: float, + sel: Union[int, List[int]], + distinguish_types: bool = True, +) -> torch.Tensor: + """Build neightbor list for a single frame. keeps nsel neighbors. + + Parameters + ---------- + coord1 : torch.Tensor + exptended coordinates of shape [nall x 3] + atype : torch.Tensor + extended atomic types of shape [nall] + nloc : int + number of local atoms. + rcut : float + cut-off radius + sel : int or List[int] + maximal number of neighbors (of each type). + if distinguish_types==True, nsel should be list and + the length of nsel should be equal to number of + types. + distinguish_types : bool + distinguish different types. + + Returns + ------- + neighbor_list : torch.Tensor + Neighbor list of shape [nloc, nsel], the neighbors + are stored in an ascending order. If the number of + neighbors is less than nsel, the positions are masked + with -1. The neighbor list of an atom looks like + |------ nsel ------| + xx xx xx xx -1 -1 -1 + if distinguish_types==True and we have two types + |---- nsel[0] -----| |---- nsel[1] -----| + xx xx xx xx -1 -1 -1 xx xx xx -1 -1 -1 -1 + + """ + nall = coord1.shape[0] // 3 + if isinstance(sel, int): + sel = [sel] + nsel = sum(sel) + # nloc x 3 + coord0 = coord1[: nloc * 3] + # nloc x nall x 3 + diff = coord1.view([-1, 3]).unsqueeze(0) - coord0.view([-1, 3]).unsqueeze(1) + assert list(diff.shape) == [nloc, nall, 3] + # nloc x nall + rr = torch.linalg.norm(diff, dim=-1) + rr, nlist = torch.sort(rr, dim=-1) + # nloc x (nall-1) + rr = rr[:, 1:] + nlist = nlist[:, 1:] + # nloc x nsel + nnei = rr.shape[1] + if nsel <= nnei: + rr = rr[:, :nsel] + nlist = nlist[:, :nsel] + else: + rr = torch.cat( + [rr, torch.ones([nloc, nsel - nnei]).to(rr.device) + rcut], dim=-1 + ) + nlist = torch.cat( + [nlist, torch.ones([nloc, nsel - nnei], dtype=torch.long).to(rr.device)], + dim=-1, + ) + assert list(nlist.shape) == [nloc, nsel] + nlist = nlist.masked_fill((rr > rcut), -1) + + if not distinguish_types: + return nlist + else: + ret_nlist = [] + # nloc x nall + tmp_atype = torch.tile(atype.unsqueeze(0), [nloc, 1]) + mask = nlist == -1 + # nloc x s(nsel) + tnlist = torch.gather( + tmp_atype, + 1, + nlist.masked_fill(mask, 0), + ) + tnlist = tnlist.masked_fill(mask, -1) + snsel = tnlist.shape[1] + for ii, ss in enumerate(sel): + # nloc x s(nsel) + # to int because bool cannot be sort on GPU + pick_mask = (tnlist == ii).to(torch.int32) + # nloc x s(nsel), stable sort, nearer neighbors first + pick_mask, imap = torch.sort( + pick_mask, dim=-1, descending=True, stable=True + ) + # nloc x s(nsel) + inlist = torch.gather(nlist, 1, imap) + inlist = inlist.masked_fill(~(pick_mask.to(torch.bool)), -1) + # nloc x nsel[ii] + ret_nlist.append(torch.split(inlist, [ss, snsel - ss], dim=-1)[0]) + return torch.concat(ret_nlist, dim=-1) + + +def build_neighbor_list( + coord1: torch.Tensor, + atype: torch.Tensor, + nloc: int, + rcut: float, + sel: Union[int, List[int]], + distinguish_types: bool = True, +) -> torch.Tensor: + """Build neightbor list for a single frame. keeps nsel neighbors. + + Parameters + ---------- + coord1 : torch.Tensor + exptended coordinates of shape [batch_size, nall x 3] + atype : torch.Tensor + extended atomic types of shape [batch_size, nall] + nloc : int + number of local atoms. + rcut : float + cut-off radius + sel : int or List[int] + maximal number of neighbors (of each type). + if distinguish_types==True, nsel should be list and + the length of nsel should be equal to number of + types. + distinguish_types : bool + distinguish different types. + + Returns + ------- + neighbor_list : torch.Tensor + Neighbor list of shape [batch_size, nloc, nsel], the neighbors + are stored in an ascending order. If the number of + neighbors is less than nsel, the positions are masked + with -1. The neighbor list of an atom looks like + |------ nsel ------| + xx xx xx xx -1 -1 -1 + if distinguish_types==True and we have two types + |---- nsel[0] -----| |---- nsel[1] -----| + xx xx xx xx -1 -1 -1 xx xx xx -1 -1 -1 -1 + + """ + batch_size = coord1.shape[0] + coord1 = coord1.view(batch_size, -1) + nall = coord1.shape[1] // 3 + if isinstance(sel, int): + sel = [sel] + nsel = sum(sel) + # nloc x 3 + coord0 = coord1[:, : nloc * 3] + # nloc x nall x 3 + diff = coord1.view([batch_size, -1, 3]).unsqueeze(1) - coord0.view( + [batch_size, -1, 3] + ).unsqueeze(2) + assert list(diff.shape) == [batch_size, nloc, nall, 3] + # nloc x nall + rr = torch.linalg.norm(diff, dim=-1) + rr, nlist = torch.sort(rr, dim=-1) + # nloc x (nall-1) + rr = rr[:, :, 1:] + nlist = nlist[:, :, 1:] + # nloc x nsel + nnei = rr.shape[2] + if nsel <= nnei: + rr = rr[:, :, :nsel] + nlist = nlist[:, :, :nsel] + else: + rr = torch.cat( + [rr, torch.ones([batch_size, nloc, nsel - nnei]).to(rr.device) + rcut], + dim=-1, + ) + nlist = torch.cat( + [ + nlist, + torch.ones([batch_size, nloc, nsel - nnei], dtype=torch.long).to( + rr.device + ), + ], + dim=-1, + ) + assert list(nlist.shape) == [batch_size, nloc, nsel] + nlist = nlist.masked_fill((rr > rcut), -1) + + if not distinguish_types: + return nlist + else: + ret_nlist = [] + # nloc x nall + tmp_atype = torch.tile(atype.unsqueeze(1), [1, nloc, 1]) + mask = nlist == -1 + # nloc x s(nsel) + tnlist = torch.gather( + tmp_atype, + 2, + nlist.masked_fill(mask, 0), + ) + tnlist = tnlist.masked_fill(mask, -1) + snsel = tnlist.shape[2] + for ii, ss in enumerate(sel): + # nloc x s(nsel) + # to int because bool cannot be sort on GPU + pick_mask = (tnlist == ii).to(torch.int32) + # nloc x s(nsel), stable sort, nearer neighbors first + pick_mask, imap = torch.sort( + pick_mask, dim=-1, descending=True, stable=True + ) + # nloc x s(nsel) + inlist = torch.gather(nlist, 2, imap) + inlist = inlist.masked_fill(~(pick_mask.to(torch.bool)), -1) + # nloc x nsel[ii] + ret_nlist.append(torch.split(inlist, [ss, snsel - ss], dim=-1)[0]) + return torch.concat(ret_nlist, dim=-1) + + +# build_neighbor_list = torch.vmap( +# build_neighbor_list_lower, +# in_dims=(0,0,None,None,None), +# out_dims=(0), +# ) + + +def get_multiple_nlist_key( + rcut: float, + nsel: int, +) -> str: + return str(rcut) + "_" + str(nsel) + + +def build_multiple_neighbor_list( + coord: torch.Tensor, + nlist: torch.Tensor, + rcuts: List[float], + nsels: List[int], +) -> Dict[str, torch.Tensor]: + """Input one neighbor list, and produce multiple neighbor lists with + different cutoff radius and numbers of selection out of it. The + required rcuts and nsels should be smaller or equal to the input nlist. + + Parameters + ---------- + coord : torch.Tensor + exptended coordinates of shape [batch_size, nall x 3] + nlist : torch.Tensor + Neighbor list of shape [batch_size, nloc, nsel], the neighbors + should be stored in an ascending order. + rcuts : List[float] + list of cut-off radius in ascending order. + nsels : List[int] + maximal number of neighbors in ascending order. + + Returns + ------- + nlist_dict : Dict[str, torch.Tensor] + A dict of nlists, key given by get_multiple_nlist_key(rc, nsel) + value being the corresponding nlist. + + """ + assert len(rcuts) == len(nsels) + if len(rcuts) == 0: + return {} + nb, nloc, nsel = nlist.shape + if nsel < nsels[-1]: + pad = -1 * torch.ones( + [nb, nloc, nsels[-1] - nsel], + dtype=nlist.dtype, + device=nlist.device, + ) + # nb x nloc x nsel + nlist = torch.cat([nlist, pad], dim=-1) + nsel = nsels[-1] + # nb x nall x 3 + coord1 = coord.view(nb, -1, 3) + nall = coord1.shape[1] + # nb x nloc x 3 + coord0 = coord1[:, :nloc, :] + nlist_mask = nlist == -1 + # nb x (nloc x nsel) x 3 + index = ( + nlist.masked_fill(nlist_mask, 0) + .view(nb, nloc * nsel) + .unsqueeze(-1) + .expand(-1, -1, 3) + ) + # nb x nloc x nsel x 3 + coord2 = torch.gather(coord1, dim=1, index=index).view(nb, nloc, nsel, 3) + # nb x nloc x nsel x 3 + diff = coord2 - coord0[:, :, None, :] + # nb x nloc x nsel + rr = torch.linalg.norm(diff, dim=-1) + rr.masked_fill(nlist_mask, float("inf")) + nlist0 = nlist + ret = {} + for rc, ns in zip(rcuts[::-1], nsels[::-1]): + nlist0 = nlist0[:, :, :ns].masked_fill(rr[:, :, :ns] > rc, int(-1)) + ret[get_multiple_nlist_key(rc, ns)] = nlist0 + return ret + + +def extend_coord_with_ghosts( + coord: torch.Tensor, + atype: torch.Tensor, + cell: Optional[torch.Tensor], + rcut: float, +): + """Extend the coordinates of the atoms by appending peridoc images. + The number of images is large enough to ensure all the neighbors + within rcut are appended. + + Parameters + ---------- + coord : torch.Tensor + original coordinates of shape [-1, nloc*3]. + atype : torch.Tensor + atom type of shape [-1, nloc]. + cell : torch.Tensor + simulation cell tensor of shape [-1, 9]. + + Returns + ------- + extended_coord: torch.Tensor + extended coordinates of shape [-1, nall*3]. + extended_atype: torch.Tensor + extended atom type of shape [-1, nall]. + index_mapping: torch.Tensor + maping extended index to the local index + + """ + nf, nloc = atype.shape + aidx = torch.tile(torch.arange(nloc).unsqueeze(0), [nf, 1]) + if cell is None: + nall = nloc + extend_coord = coord.clone() + extend_atype = atype.clone() + extend_aidx = aidx.clone() + else: + coord = coord.view([nf, nloc, 3]) + cell = cell.view([nf, 3, 3]) + # nf x 3 + to_face = to_face_distance(cell) + # nf x 3 + # *2: ghost copies on + and - directions + # +1: central cell + nbuff = torch.ceil(rcut / to_face).to(torch.long) + # 3 + nbuff = torch.max(nbuff, dim=0, keepdim=False).values + xi = torch.arange(-nbuff[0], nbuff[0] + 1, 1, device=env.DEVICE) + yi = torch.arange(-nbuff[1], nbuff[1] + 1, 1, device=env.DEVICE) + zi = torch.arange(-nbuff[2], nbuff[2] + 1, 1, device=env.DEVICE) + xyz = xi.view(-1, 1, 1, 1) * torch.tensor( + [1, 0, 0], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ) + xyz = xyz + yi.view(1, -1, 1, 1) * torch.tensor( + [0, 1, 0], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ) + xyz = xyz + zi.view(1, 1, -1, 1) * torch.tensor( + [0, 0, 1], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ) + xyz = xyz.view(-1, 3) + # ns x 3 + shift_idx = xyz[torch.argsort(torch.norm(xyz, dim=1))] + ns, _ = shift_idx.shape + nall = ns * nloc + # nf x ns x 3 + shift_vec = torch.einsum("sd,fdk->fsk", shift_idx, cell) + # nf x ns x nloc x 3 + extend_coord = coord[:, None, :, :] + shift_vec[:, :, None, :] + # nf x ns x nloc + extend_atype = torch.tile(atype.unsqueeze(-2), [1, ns, 1]) + # nf x ns x nloc + extend_aidx = torch.tile(aidx.unsqueeze(-2), [1, ns, 1]) + + return ( + extend_coord.reshape([nf, nall * 3]).to(env.DEVICE), + extend_atype.view([nf, nall]).to(env.DEVICE), + extend_aidx.view([nf, nall]).to(env.DEVICE), + ) diff --git a/deepmd/pt/utils/plugin.py b/deepmd/pt/utils/plugin.py new file mode 100644 index 0000000000..c24f36f574 --- /dev/null +++ b/deepmd/pt/utils/plugin.py @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Base of plugin systems.""" +from deepmd.utils.plugin import ( + Plugin, + PluginVariant, + VariantABCMeta, + VariantMeta, +) + +__all__ = [ + "Plugin", + "VariantMeta", + "VariantABCMeta", + "PluginVariant", +] diff --git a/deepmd/pt/utils/preprocess.py b/deepmd/pt/utils/preprocess.py new file mode 100644 index 0000000000..463ac112ad --- /dev/null +++ b/deepmd/pt/utils/preprocess.py @@ -0,0 +1,318 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging +from typing import ( + Union, +) + +import torch + +from deepmd.pt.utils import ( + env, +) + + +class Region3D: + def __init__(self, boxt): + """Construct a simulation box.""" + boxt = boxt.reshape([3, 3]) + self.boxt = boxt # convert physical coordinates to internal ones + self.rec_boxt = torch.linalg.inv( + self.boxt + ) # convert internal coordinates to physical ones + + self.volume = torch.linalg.det(self.boxt) # compute the volume + + # boxt = boxt.permute(1, 0) + c_yz = torch.cross(boxt[1], boxt[2]) + self._h2yz = self.volume / torch.linalg.norm(c_yz) + c_zx = torch.cross(boxt[2], boxt[0]) + self._h2zx = self.volume / torch.linalg.norm(c_zx) + c_xy = torch.cross(boxt[0], boxt[1]) + self._h2xy = self.volume / torch.linalg.norm(c_xy) + + def phys2inter(self, coord): + """Convert physical coordinates to internal ones.""" + return coord @ self.rec_boxt + + def inter2phys(self, coord): + """Convert internal coordinates to physical ones.""" + return coord @ self.boxt + + def get_face_distance(self): + """Return face distinces to each surface of YZ, ZX, XY.""" + return torch.stack([self._h2yz, self._h2zx, self._h2xy]) + + +def normalize_coord(coord, region: Region3D, nloc: int): + """Move outer atoms into region by mirror. + + Args: + - coord: shape is [nloc*3] + """ + tmp_coord = coord.clone() + inter_cood = torch.remainder(region.phys2inter(tmp_coord), 1.0) + tmp_coord = region.inter2phys(inter_cood) + return tmp_coord + + +def compute_serial_cid(cell_offset, ncell): + """Tell the sequential cell ID in its 3D space. + + Args: + - cell_offset: shape is [3] + - ncell: shape is [3] + """ + cell_offset[:, 0] *= ncell[1] * ncell[2] + cell_offset[:, 1] *= ncell[2] + return cell_offset.sum(-1) + + +def compute_pbc_shift(cell_offset, ncell): + """Tell shift count to move the atom into region.""" + shift = torch.zeros_like(cell_offset) + shift = shift + (cell_offset < 0) * -( + torch.div(cell_offset, ncell, rounding_mode="floor") + ) + shift = shift + (cell_offset >= ncell) * -( + torch.div((cell_offset - ncell), ncell, rounding_mode="floor") + 1 + ) + assert torch.all(cell_offset + shift * ncell >= 0) + assert torch.all(cell_offset + shift * ncell < ncell) + return shift + + +def build_inside_clist(coord, region: Region3D, ncell): + """Build cell list on atoms inside region. + + Args: + - coord: shape is [nloc*3] + - ncell: shape is [3] + """ + loc_ncell = int(torch.prod(ncell)) # num of local cells + nloc = coord.numel() // 3 # num of local atoms + inter_cell_size = 1.0 / ncell + + inter_cood = region.phys2inter(coord.view(-1, 3)) + cell_offset = torch.floor(inter_cood / inter_cell_size).to(torch.long) + # numerical error brought by conversion from phys to inter back and force + # may lead to negative value + cell_offset[cell_offset < 0] = 0 + delta = cell_offset - ncell + a2c = compute_serial_cid(cell_offset, ncell) # cell id of atoms + arange = torch.arange(0, loc_ncell, 1, device=env.PREPROCESS_DEVICE) + cellid = a2c == arange.unsqueeze(-1) # one hot cellid + c2a = cellid.nonzero() + lst = [] + cnt = 0 + bincount = torch.bincount(a2c, minlength=loc_ncell) + for i in range(loc_ncell): + n = bincount[i] + lst.append(c2a[cnt : cnt + n, 1]) + cnt += n + return a2c, lst + + +def append_neighbors(coord, region: Region3D, atype, rcut: float): + """Make ghost atoms who are valid neighbors. + + Args: + - coord: shape is [nloc*3] + - atype: shape is [nloc] + """ + to_face = region.get_face_distance() + + # compute num and size of local cells + ncell = torch.floor(to_face / rcut).to(torch.long) + ncell[ncell == 0] = 1 + cell_size = to_face / ncell + ngcell = ( + torch.floor(rcut / cell_size).to(torch.long) + 1 + ) # num of cells out of local, which contain ghost atoms + + # add ghost atoms + a2c, c2a = build_inside_clist(coord, region, ncell) + xi = torch.arange(-ngcell[0], ncell[0] + ngcell[0], 1, device=env.PREPROCESS_DEVICE) + yi = torch.arange(-ngcell[1], ncell[1] + ngcell[1], 1, device=env.PREPROCESS_DEVICE) + zi = torch.arange(-ngcell[2], ncell[2] + ngcell[2], 1, device=env.PREPROCESS_DEVICE) + xyz = xi.view(-1, 1, 1, 1) * torch.tensor( + [1, 0, 0], dtype=torch.long, device=env.PREPROCESS_DEVICE + ) + xyz = xyz + yi.view(1, -1, 1, 1) * torch.tensor( + [0, 1, 0], dtype=torch.long, device=env.PREPROCESS_DEVICE + ) + xyz = xyz + zi.view(1, 1, -1, 1) * torch.tensor( + [0, 0, 1], dtype=torch.long, device=env.PREPROCESS_DEVICE + ) + xyz = xyz.view(-1, 3) + mask_a = (xyz >= 0).all(dim=-1) + mask_b = (xyz < ncell).all(dim=-1) + mask = ~torch.logical_and(mask_a, mask_b) + xyz = xyz[mask] # cell coord + shift = compute_pbc_shift(xyz, ncell) + coord_shift = region.inter2phys(shift.to(env.GLOBAL_PT_FLOAT_PRECISION)) + mirrored = shift * ncell + xyz + cid = compute_serial_cid(mirrored, ncell) + + n_atoms = coord.shape[0] + aid = [c2a[ci] + i * n_atoms for i, ci in enumerate(cid)] + aid = torch.cat(aid) + tmp = torch.div(aid, n_atoms, rounding_mode="trunc") + aid = aid % n_atoms + tmp_coord = coord[aid] - coord_shift[tmp] + tmp_atype = atype[aid] + + # merge local and ghost atoms + merged_coord = torch.cat([coord, tmp_coord]) + merged_coord_shift = torch.cat([torch.zeros_like(coord), coord_shift[tmp]]) + merged_atype = torch.cat([atype, tmp_atype]) + merged_mapping = torch.cat( + [torch.arange(atype.numel(), device=env.PREPROCESS_DEVICE), aid] + ) + return merged_coord_shift, merged_atype, merged_mapping + + +def build_neighbor_list( + nloc: int, coord, atype, rcut: float, sec, mapping, type_split=True, min_check=False +): + """For each atom inside region, build its neighbor list. + + Args: + - coord: shape is [nall*3] + - atype: shape is [nall] + """ + nall = coord.numel() // 3 + coord = coord.float() + nlist = [[] for _ in range(nloc)] + coord_l = coord.view(-1, 1, 3)[:nloc] + coord_r = coord.view(1, -1, 3) + distance = coord_l - coord_r + distance = torch.linalg.norm(distance, dim=-1) + DISTANCE_INF = distance.max().detach() + rcut + distance[:nloc, :nloc] += ( + torch.eye(nloc, dtype=torch.bool, device=env.PREPROCESS_DEVICE) * DISTANCE_INF + ) + if min_check: + if distance.min().abs() < 1e-6: + RuntimeError("Atom dist too close!") + if not type_split: + sec = sec[-1:] + lst = [] + nlist = torch.zeros((nloc, sec[-1].item()), device=env.PREPROCESS_DEVICE).long() - 1 + nlist_loc = ( + torch.zeros((nloc, sec[-1].item()), device=env.PREPROCESS_DEVICE).long() - 1 + ) + nlist_type = ( + torch.zeros((nloc, sec[-1].item()), device=env.PREPROCESS_DEVICE).long() - 1 + ) + for i, nnei in enumerate(sec): + if i > 0: + nnei = nnei - sec[i - 1] + if not type_split: + tmp = distance + else: + mask = atype.unsqueeze(0) == i + tmp = distance + (~mask) * DISTANCE_INF + if tmp.shape[1] >= nnei: + _sorted, indices = torch.topk(tmp, nnei, dim=1, largest=False) + else: + # when nnei > nall + indices = torch.zeros((nloc, nnei), device=env.PREPROCESS_DEVICE).long() - 1 + _sorted = ( + torch.ones((nloc, nnei), device=env.PREPROCESS_DEVICE).long() + * DISTANCE_INF + ) + _sorted_nnei, indices_nnei = torch.topk( + tmp, tmp.shape[1], dim=1, largest=False + ) + _sorted[:, : tmp.shape[1]] = _sorted_nnei + indices[:, : tmp.shape[1]] = indices_nnei + mask = (_sorted < rcut).to(torch.long) + indices_loc = mapping[indices] + indices = indices * mask + -1 * (1 - mask) # -1 for padding + indices_loc = indices_loc * mask + -1 * (1 - mask) # -1 for padding + if i == 0: + start = 0 + else: + start = sec[i - 1] + end = min(sec[i], start + indices.shape[1]) + nlist[:, start:end] = indices[:, :nnei] + nlist_loc[:, start:end] = indices_loc[:, :nnei] + nlist_type[:, start:end] = atype[indices[:, :nnei]] * mask + -1 * (1 - mask) + return nlist, nlist_loc, nlist_type + + +def compute_smooth_weight(distance, rmin: float, rmax: float): + """Compute smooth weight for descriptor elements.""" + min_mask = distance <= rmin + max_mask = distance >= rmax + mid_mask = torch.logical_not(torch.logical_or(min_mask, max_mask)) + uu = (distance - rmin) / (rmax - rmin) + vv = uu * uu * uu * (-6 * uu * uu + 15 * uu - 10) + 1 + return vv * mid_mask + min_mask + + +def make_env_mat( + coord, + atype, + region, + rcut: Union[float, list], + sec, + pbc=True, + type_split=True, + min_check=False, +): + """Based on atom coordinates, return environment matrix. + + Returns + ------- + nlist: nlist, [nloc, nnei] + merged_coord_shift: shift on nall atoms, [nall, 3] + merged_mapping: mapping from nall index to nloc index, [nall] + """ + # move outer atoms into cell + hybrid = isinstance(rcut, list) + _rcut = rcut + if hybrid: + _rcut = max(rcut) + if pbc: + merged_coord_shift, merged_atype, merged_mapping = append_neighbors( + coord, region, atype, _rcut + ) + merged_coord = coord[merged_mapping] - merged_coord_shift + if merged_coord.shape[0] <= coord.shape[0]: + logging.warning("No ghost atom is added for system ") + else: + merged_coord_shift = torch.zeros_like(coord) + merged_atype = atype.clone() + merged_mapping = torch.arange(atype.numel(), device=env.PREPROCESS_DEVICE) + merged_coord = coord.clone() + + # build nlist + if not hybrid: + nlist, nlist_loc, nlist_type = build_neighbor_list( + coord.shape[0], + merged_coord, + merged_atype, + rcut, + sec, + merged_mapping, + type_split=type_split, + min_check=min_check, + ) + else: + nlist, nlist_loc, nlist_type = [], [], [] + for ii, single_rcut in enumerate(rcut): + nlist_tmp, nlist_loc_tmp, nlist_type_tmp = build_neighbor_list( + coord.shape[0], + merged_coord, + merged_atype, + single_rcut, + sec[ii], + merged_mapping, + type_split=type_split, + min_check=min_check, + ) + nlist.append(nlist_tmp) + nlist_loc.append(nlist_loc_tmp) + nlist_type.append(nlist_type_tmp) + return nlist, nlist_loc, nlist_type, merged_coord_shift, merged_mapping diff --git a/deepmd/pt/utils/region.py b/deepmd/pt/utils/region.py new file mode 100644 index 0000000000..b07d2f73bf --- /dev/null +++ b/deepmd/pt/utils/region.py @@ -0,0 +1,116 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import torch + + +def phys2inter( + coord: torch.Tensor, + cell: torch.Tensor, +) -> torch.Tensor: + """Convert physical coordinates to internal(direct) coordinates. + + Parameters + ---------- + coord : torch.Tensor + physical coordinates of shape [*, na, 3]. + cell : torch.Tensor + simulation cell tensor of shape [*, 3, 3]. + + Returns + ------- + inter_coord: torch.Tensor + the internal coordinates + + """ + rec_cell = torch.linalg.inv(cell) + return torch.matmul(coord, rec_cell) + + +def inter2phys( + coord: torch.Tensor, + cell: torch.Tensor, +) -> torch.Tensor: + """Convert internal(direct) coordinates to physical coordinates. + + Parameters + ---------- + coord : torch.Tensor + internal coordinates of shape [*, na, 3]. + cell : torch.Tensor + simulation cell tensor of shape [*, 3, 3]. + + Returns + ------- + phys_coord: torch.Tensor + the physical coordinates + + """ + return torch.matmul(coord, cell) + + +def to_face_distance( + cell: torch.Tensor, +) -> torch.Tensor: + """Compute the to-face-distance of the simulation cell. + + Parameters + ---------- + cell : torch.Tensor + simulation cell tensor of shape [*, 3, 3]. + + Returns + ------- + dist: torch.Tensor + the to face distances of shape [*, 3] + + """ + cshape = cell.shape + dist = b_to_face_distance(cell.view([-1, 3, 3])) + return dist.view(list(cshape[:-2]) + [3]) # noqa:RUF005 + + +def _to_face_distance(cell): + volume = torch.linalg.det(cell) + c_yz = torch.cross(cell[1], cell[2]) + _h2yz = volume / torch.linalg.norm(c_yz) + c_zx = torch.cross(cell[2], cell[0]) + _h2zx = volume / torch.linalg.norm(c_zx) + c_xy = torch.cross(cell[0], cell[1]) + _h2xy = volume / torch.linalg.norm(c_xy) + return torch.stack([_h2yz, _h2zx, _h2xy]) + + +def b_to_face_distance(cell): + volume = torch.linalg.det(cell) + c_yz = torch.cross(cell[:, 1], cell[:, 2], dim=-1) + _h2yz = volume / torch.linalg.norm(c_yz, dim=-1) + c_zx = torch.cross(cell[:, 2], cell[:, 0], dim=-1) + _h2zx = volume / torch.linalg.norm(c_zx, dim=-1) + c_xy = torch.cross(cell[:, 0], cell[:, 1], dim=-1) + _h2xy = volume / torch.linalg.norm(c_xy, dim=-1) + return torch.stack([_h2yz, _h2zx, _h2xy], dim=1) + + +# b_to_face_distance = torch.vmap( +# _to_face_distance, in_dims=(0), out_dims=(0)) + + +def normalize_coord( + coord: torch.Tensor, + cell: torch.Tensor, +) -> torch.Tensor: + """Apply PBC according to the atomic coordinates. + + Parameters + ---------- + coord : torch.Tensor + orignal coordinates of shape [*, na, 3]. + + Returns + ------- + wrapped_coord: torch.Tensor + wrapped coordinates of shape [*, na, 3]. + + """ + icoord = phys2inter(coord, cell) + icoord = torch.remainder(icoord, 1.0) + return inter2phys(icoord, cell) diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py new file mode 100644 index 0000000000..837a0104f9 --- /dev/null +++ b/deepmd/pt/utils/stat.py @@ -0,0 +1,112 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging + +import numpy as np +import torch +from tqdm import ( + trange, +) + +from deepmd.pt.utils import ( + env, +) + + +def make_stat_input(datasets, dataloaders, nbatches): + """Pack data for statistics. + + Args: + - dataset: A list of dataset to analyze. + - nbatches: Batch count for collecting stats. + + Returns + ------- + - a list of dicts, each of which contains data from a system + """ + lst = [] + keys = [ + "coord", + "force", + "energy", + "atype", + "box", + "natoms", + "mapping", + "nlist", + "nlist_loc", + "nlist_type", + "shift", + ] + if datasets[0].mixed_type: + keys.append("real_natoms_vec") + logging.info(f"Packing data for statistics from {len(datasets)} systems") + for i in trange(len(datasets), disable=env.DISABLE_TQDM): + sys_stat = {key: [] for key in keys} + iterator = iter(dataloaders[i]) + for _ in range(nbatches): + try: + stat_data = next(iterator) + except StopIteration: + iterator = iter(dataloaders[i]) + stat_data = next(iterator) + for dd in stat_data: + if dd in keys: + sys_stat[dd].append(stat_data[dd]) + for key in keys: + if key == "mapping" or key == "shift": + extend = max(d.shape[1] for d in sys_stat[key]) + for jj in range(len(sys_stat[key])): + l = [] + item = sys_stat[key][jj] + for ii in range(item.shape[0]): + l.append(item[ii]) + n_frames = len(item) + if key == "shift": + shape = torch.zeros( + (n_frames, extend, 3), + dtype=env.GLOBAL_PT_FLOAT_PRECISION, + device=env.PREPROCESS_DEVICE, + ) + else: + shape = torch.zeros( + (n_frames, extend), + dtype=torch.long, + device=env.PREPROCESS_DEVICE, + ) + for i in range(len(item)): + natoms_tmp = l[i].shape[0] + shape[i, :natoms_tmp] = l[i] + sys_stat[key][jj] = shape + if not isinstance(sys_stat[key][0], list): + if sys_stat[key][0] is None: + sys_stat[key] = None + else: + sys_stat[key] = torch.cat(sys_stat[key], dim=0) + else: + sys_stat_list = [] + for ii, _ in enumerate(sys_stat[key][0]): + tmp_stat = [x[ii] for x in sys_stat[key]] + sys_stat_list.append(torch.cat(tmp_stat, dim=0)) + sys_stat[key] = sys_stat_list + lst.append(sys_stat) + return lst + + +def compute_output_stats(energy, natoms, rcond=None): + """Update mean and stddev for descriptor elements. + + Args: + - energy: Batched energy with shape [nframes, 1]. + - natoms: Batched atom statisics with shape [self.ntypes+2]. + + Returns + ------- + - energy_coef: Average enery per atom for each element. + """ + for i in range(len(energy)): + energy[i] = energy[i].mean(dim=0, keepdim=True) + natoms[i] = natoms[i].double().mean(dim=0, keepdim=True) + sys_ener = torch.cat(energy).cpu() + sys_tynatom = torch.cat(natoms)[:, 2:].cpu() + energy_coef, _, _, _ = np.linalg.lstsq(sys_tynatom, sys_ener, rcond) + return energy_coef diff --git a/deepmd/pt/utils/utils.py b/deepmd/pt/utils/utils.py new file mode 100644 index 0000000000..780dbf7e62 --- /dev/null +++ b/deepmd/pt/utils/utils.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Callable, + Optional, +) + +import torch +import torch.nn.functional as F + + +def get_activation_fn(activation: str) -> Callable: + """Returns the activation function corresponding to `activation`.""" + if activation.lower() == "relu": + return F.relu + elif activation.lower() == "gelu": + return F.gelu + elif activation.lower() == "tanh": + return torch.tanh + elif activation.lower() == "linear" or activation.lower() == "none": + return lambda x: x + else: + raise RuntimeError(f"activation function {activation} not supported") + + +class ActivationFn(torch.nn.Module): + def __init__(self, activation: Optional[str]): + super().__init__() + self.activation: str = activation if activation is not None else "linear" + + def forward(self, x: torch.Tensor) -> torch.Tensor: + """Returns the tensor after applying activation function corresponding to `activation`.""" + # See jit supported types: https://pytorch.org/docs/stable/jit_language_reference.html#supported-type + + if self.activation.lower() == "relu": + return F.relu(x) + elif self.activation.lower() == "gelu": + return F.gelu(x) + elif self.activation.lower() == "tanh": + return torch.tanh(x) + elif self.activation.lower() == "linear" or self.activation.lower() == "none": + return x + else: + raise RuntimeError(f"activation function {self.activation} not supported") diff --git a/examples/water/dpa2/input_torch.json b/examples/water/dpa2/input_torch.json new file mode 100644 index 0000000000..9d783b35d5 --- /dev/null +++ b/examples/water/dpa2/input_torch.json @@ -0,0 +1,102 @@ +{ + "_comment": "that's all", + "model": { + "type_embedding": { + "neuron": [ + 8 + ], + "tebd_input_mode": "concat" + }, + "type_map": [ + "O", + "H" + ], + "descriptor": { + "type": "dpa2", + "repinit_rcut": 9.0, + "repinit_rcut_smth": 8.0, + "repinit_nsel": 120, + "repformer_rcut": 4.0, + "repformer_rcut_smth": 3.5, + "repformer_nsel": 40, + "repinit_neuron": [ + 25, + 50, + 100 + ], + "repinit_axis_neuron": 12, + "repinit_activation": "tanh", + "repformer_nlayers": 12, + "repformer_g1_dim": 128, + "repformer_g2_dim": 32, + "repformer_attn2_hidden": 32, + "repformer_attn2_nhead": 4, + "repformer_attn1_hidden": 128, + "repformer_attn1_nhead": 4, + "repformer_axis_dim": 4, + "repformer_update_h2": false, + "repformer_update_g1_has_conv": true, + "repformer_update_g1_has_grrg": true, + "repformer_update_g1_has_drrd": true, + "repformer_update_g1_has_attn": true, + "repformer_update_g2_has_g1g1": true, + "repformer_update_g2_has_attn": true, + "repformer_attn2_has_gate": true, + "repformer_add_type_ebd_to_seq": false + }, + "fitting_net": { + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "seed": 1, + "_comment": " that's all" + }, + "_comment": " that's all" + }, + "learning_rate": { + "type": "exp", + "decay_steps": 5000, + "start_lr": 0.0002, + "stop_lr": 3.51e-08, + "_comment": "that's all" + }, + "loss": { + "type": "ener", + "start_pref_e": 0.02, + "limit_pref_e": 1, + "start_pref_f": 1000, + "limit_pref_f": 1, + "start_pref_v": 0, + "limit_pref_v": 0, + "_comment": " that's all" + }, + "training": { + "training_data": { + "systems": [ + "../data/data_0", + "../data/data_1", + "../data/data_2" + ], + "batch_size": 1, + "_comment": "that's all" + }, + "validation_data": { + "systems": [ + "../data/data_3" + ], + "batch_size": 1, + "_comment": "that's all" + }, + "numb_steps": 1000000, + "warmup_steps": 0, + "gradient_max_norm": 5.0, + "seed": 10, + "disp_file": "lcurve.out", + "disp_freq": 100, + "save_freq": 2000, + "_comment": "that's all" + } +} diff --git a/examples/water/se_atten/input_torch.json b/examples/water/se_atten/input_torch.json new file mode 100644 index 0000000000..7da3d64164 --- /dev/null +++ b/examples/water/se_atten/input_torch.json @@ -0,0 +1,91 @@ +{ + "_comment": "that's all", + "model": { + "type_map": [ + "O", + "H" + ], + "descriptor": { + "type": "dpa1", + "sel": 120, + "rcut_smth": 0.5, + "rcut": 6.0, + "neuron": [ + 25, + 50, + 100 + ], + "axis_neuron": 16, + "attn": 128, + "attn_layer": 2, + "attn_dotr": true, + "attn_mask": false, + "post_ln": true, + "ffn": false, + "ffn_embed_dim": 1024, + "activation": "tanh", + "scaling_factor": 1.0, + "head_num": 1, + "normalize": true, + "temperature": 1.0 + }, + "fitting_net": { + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "seed": 1, + "_comment": " that's all" + }, + "_comment": " that's all" + }, + "learning_rate": { + "type": "exp", + "decay_steps": 5000, + "start_lr": 0.001, + "stop_lr": 3.51e-08, + "_comment": "that's all" + }, + "loss": { + "type": "ener", + "start_pref_e": 0.02, + "limit_pref_e": 1, + "start_pref_f": 1000, + "limit_pref_f": 1, + "start_pref_v": 0, + "limit_pref_v": 0, + "_comment": " that's all" + }, + "training": { + "training_data": { + "systems": [ + "../data/data_0", + "../data/data_1", + "../data/data_2" + ], + "batch_size": 1, + "_comment": "that's all" + }, + "validation_data": { + "systems": [ + "../data/data_3" + ], + "batch_size": 1, + "numb_btch": 3, + "_comment": "that's all" + }, + "wandb_config": { + "wandb_enabled": false, + "entity": "dp_model_engineering", + "project": "DPA" + }, + "numb_steps": 1000000, + "seed": 10, + "disp_file": "lcurve.out", + "disp_freq": 100, + "save_freq": 1000, + "_comment": "that's all" + } +} diff --git a/examples/water/se_e2_a/input_torch.json b/examples/water/se_e2_a/input_torch.json new file mode 100644 index 0000000000..053a721a44 --- /dev/null +++ b/examples/water/se_e2_a/input_torch.json @@ -0,0 +1,79 @@ +{ + "model": { + "type_map": [ + "O", + "H" + ], + "descriptor": { + "type": "se_e2_a", + "sel": [ + 46, + 92 + ], + "rcut_smth": 0.50, + "rcut": 6.00, + "neuron": [ + 25, + 50, + 100 + ], + "resnet_dt": false, + "axis_neuron": 16, + "seed": 1, + "_comment": " that's all" + }, + "fitting_net": { + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "seed": 1, + "_comment": " that's all" + }, + "data_stat_nbatch": 20, + "_comment": " that's all" + }, + "learning_rate": { + "type": "exp", + "decay_steps": 5000, + "start_lr": 0.001, + "stop_lr": 3.51e-8, + "_comment": "that's all" + }, + "loss": { + "type": "ener", + "start_pref_e": 0.02, + "limit_pref_e": 1, + "start_pref_f": 1000, + "limit_pref_f": 1, + "_comment": " that's all" + }, + "training": { + "training_data": { + "systems": [ + "../data/data_0", + "../data/data_1", + "../data/data_2" + ], + "batch_size": 1, + "_comment": "that's all" + }, + "validation_data": { + "systems": [ + "../data/data_3" + ], + "batch_size": 1, + "numb_btch": 3, + "_comment": "that's all" + }, + "numb_steps": 100000, + "seed": 10, + "disp_file": "lcurve.out", + "disp_freq": 100, + "save_freq": 10000, + "_comment": "that's all" + }, + "_comment": "that's all" +} diff --git a/source/install/docker/Dockerfile b/source/install/docker/Dockerfile index 26b7be9f19..793272ae6a 100644 --- a/source/install/docker/Dockerfile +++ b/source/install/docker/Dockerfile @@ -6,7 +6,7 @@ RUN python -m venv /opt/deepmd-kit ENV PATH="/opt/deepmd-kit/bin:$PATH" # Install package COPY dist /dist -RUN pip install "$(ls /dist/deepmd_kit${VARIANT}-*manylinux*_x86_64.whl)[gpu,cu${CUDA_VERSION},lmp,ipi]" \ +RUN pip install "$(ls /dist/deepmd_kit${VARIANT}-*manylinux*_x86_64.whl)[gpu,cu${CUDA_VERSION},lmp,ipi,torch]" \ && dp -h \ && lmp -h \ && dp_ipi \ diff --git a/source/tests/pt/__init__.py b/source/tests/pt/__init__.py new file mode 100644 index 0000000000..fdbdd73f79 --- /dev/null +++ b/source/tests/pt/__init__.py @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import torch + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) diff --git a/source/tests/pt/models/dpa1.json b/source/tests/pt/models/dpa1.json new file mode 100644 index 0000000000..dd838ac692 --- /dev/null +++ b/source/tests/pt/models/dpa1.json @@ -0,0 +1,39 @@ +{ + "type_map": [ + "O", + "H" + ], + "descriptor": { + "type": "se_atten", + "sel": 30, + "rcut_smth": 2.0, + "rcut": 6.0, + "neuron": [ + 2, + 4, + 8 + ], + "axis_neuron": 4, + "attn": 5, + "attn_layer": 2, + "attn_dotr": true, + "attn_mask": false, + "post_ln": true, + "ffn": false, + "ffn_embed_dim": 10, + "activation": "tanh", + "scaling_factor": 1.0, + "head_num": 1, + "normalize": true, + "temperature": 1.0 + }, + "fitting_net": { + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "seed": 1 + } +} diff --git a/source/tests/pt/models/dpa1.pth b/source/tests/pt/models/dpa1.pth new file mode 100644 index 0000000000000000000000000000000000000000..75acf2fa15d874dc7c63a0fe1bbc3653267b2c4b GIT binary patch literal 15469 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 LE<<3fzt6KkgY_j6)ZvHA`NS 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 literal 0 HcmV?d00001 diff --git a/source/tests/pt/models/dpa2_hyb.json b/source/tests/pt/models/dpa2_hyb.json new file mode 100644 index 0000000000..b5d53b0246 --- /dev/null +++ b/source/tests/pt/models/dpa2_hyb.json @@ -0,0 +1,69 @@ +{ + "type_map": [ + "O", + "H" + ], + "descriptor": { + "type": "hybrid", + "hybrid_mode": "sequential", + "list": [ + { + "type": "se_atten", + "sel": 30, + "rcut_smth": 2.0, + "rcut": 6.0, + "neuron": [ + 2, + 4, + 8 + ], + "axis_neuron": 4, + "attn": 5, + "attn_layer": 0, + "attn_dotr": true, + "attn_mask": false, + "post_ln": true, + "ffn": false, + "ffn_embed_dim": 10, + "activation": "tanh", + "scaling_factor": 1.0, + "head_num": 1, + "normalize": true, + "temperature": 1.0 + }, + { + "type": "se_uni", + "sel": 10, + "rcut_smth": 0.5, + "rcut": 4.0, + "nlayers": 12, + "g1_dim": 8, + "g2_dim": 5, + "attn2_hidden": 3, + "attn2_nhead": 1, + "attn1_hidden": 5, + "attn1_nhead": 1, + "axis_dim": 4, + "update_h2": false, + "update_g1_has_conv": true, + "update_g1_has_grrg": true, + "update_g1_has_drrd": true, + "update_g1_has_attn": true, + "update_g2_has_g1g1": true, + "update_g2_has_attn": true, + "attn2_has_gate": true, + "add_type_ebd_to_seq": false, + "smooth": true + } + ] + }, + "fitting_net": { + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "seed": 1 + } +} diff --git a/source/tests/pt/models/dpa2_tebd.pth b/source/tests/pt/models/dpa2_tebd.pth new file mode 100644 index 0000000000000000000000000000000000000000..3d4fc5511c93036a18be1b290fcdecc482215bf6 GIT binary patch literal 1085 zcmWIWW@cev;NW1u0AdV047vF!sX6iGshQ~+CB^zFi6x181=%@nP8Rmh+jRLB@105mx@Hz_qGB{MHw4`Nm!Q*uduQF4Y} zd}&E$PBB+}QEF0YW==|cNornkeo=gx5mzCzhDHQCP;W6%Wny}2AqzwcmrH(WQch|x zM3k$LHG&bS$e@rdf*D9B7nByVdrOoQas+xacr$x*v=wrCb9ghh6>@cEfZW4fQpgij z$Xi@n$QQv0)DWMWT9OFzSRp@HUO*!PD9TVMSX(Frw!SR2s2J$ALSb*lVz3UlUkXKP z3q>73%-(Y`P z?Y_N0-0}TyYI%RGUVnUlUGB=6m&4xKzj|P0d}!wz`|9Kr2MOK>`?tipFM7!OWB;uA z(`2Tk*4yct0hJWn7;su)ND1z^u1=w=}MPy|JPBrvI9nxSui zZUV9kMNv$c0CX*?3D9r}@MdGvfhv__)`e>V=2.14.0 +deepmd-kit>=2.2.7 +dpdata +ase +coverage +pytest diff --git a/source/tests/pt/test_LKF.py b/source/tests/pt/test_LKF.py new file mode 100644 index 0000000000..33aeac7f4f --- /dev/null +++ b/source/tests/pt/test_LKF.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import os +import unittest +from pathlib import ( + Path, +) + +from deepmd.pt.entrypoints.main import ( + main, +) + + +class TestLKF(unittest.TestCase): + def test_lkf(self): + with open(str(Path(__file__).parent / "water/lkf.json")) as fin: + content = fin.read() + self.config = json.loads(content) + self.config["training"]["training_data"]["systems"] = [ + str(Path(__file__).parent / "water/data/data_0") + ] + self.config["training"]["validation_data"]["systems"] = [ + str(Path(__file__).parent / "water/data/data_0") + ] + self.input_json = "test_lkf.json" + with open(self.input_json, "w") as fp: + json.dump(self.config, fp, indent=4) + main(["train", self.input_json]) + + def tearDown(self): + os.remove(self.input_json) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_autodiff.py b/source/tests/pt/test_autodiff.py new file mode 100644 index 0000000000..4f303a8bb3 --- /dev/null +++ b/source/tests/pt/test_autodiff.py @@ -0,0 +1,190 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import numpy as np +import torch + +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) + +dtype = torch.float64 + +from .test_permutation import ( + eval_model, + make_sample, + model_dpa1, + model_dpa2, + model_se_e2_a, +) + + +# from deepmd-kit repo +def finite_difference(f, x, delta=1e-6): + in_shape = x.shape + y0 = f(x) + out_shape = y0.shape + res = np.empty(out_shape + in_shape) + for idx in np.ndindex(*in_shape): + diff = np.zeros(in_shape) + diff[idx] += delta + y1p = f(x + diff) + y1n = f(x - diff) + res[(Ellipsis, *idx)] = (y1p - y1n) / (2 * delta) + return res + + +def stretch_box(old_coord, old_box, new_box): + ocoord = old_coord.reshape(-1, 3) + obox = old_box.reshape(3, 3) + nbox = new_box.reshape(3, 3) + ncoord = ocoord @ np.linalg.inv(obox) @ nbox + return ncoord.reshape(old_coord.shape) + + +class ForceTest: + def test( + self, + ): + places = 8 + delta = 1e-5 + natoms = 5 + cell = torch.rand([3, 3], dtype=dtype) + cell = (cell + cell.T) + 5.0 * torch.eye(3) + coord = torch.rand([natoms, 3], dtype=dtype) + coord = torch.matmul(coord, cell) + atype = torch.IntTensor([0, 0, 0, 1, 1]) + # assumes input to be numpy tensor + coord = coord.numpy() + + def np_infer( + coord, + ): + e0, f0, v0 = eval_model( + self.model, torch.tensor(coord).unsqueeze(0), cell.unsqueeze(0), atype + ) + ret = { + "energy": e0.squeeze(0), + "force": f0.squeeze(0), + "virial": v0.squeeze(0), + } + # detach + ret = {kk: ret[kk].detach().cpu().numpy() for kk in ret} + return ret + + def ff(_coord): + return np_infer(_coord)["energy"] + + fdf = -finite_difference(ff, coord, delta=delta).squeeze() + rff = np_infer(coord)["force"] + np.testing.assert_almost_equal(fdf, rff, decimal=places) + + +class VirialTest: + def test( + self, + ): + places = 8 + delta = 1e-4 + natoms = 5 + cell = torch.rand([3, 3], dtype=dtype) + cell = (cell) + 5.0 * torch.eye(3) + coord = torch.rand([natoms, 3], dtype=dtype) + coord = torch.matmul(coord, cell) + atype = torch.IntTensor([0, 0, 0, 1, 1]) + # assumes input to be numpy tensor + coord = coord.numpy() + cell = cell.numpy() + + def np_infer( + new_cell, + ): + e0, f0, v0 = eval_model( + self.model, + torch.tensor(stretch_box(coord, cell, new_cell)).unsqueeze(0), + torch.tensor(new_cell).unsqueeze(0), + atype, + ) + ret = { + "energy": e0.squeeze(0), + "force": f0.squeeze(0), + "virial": v0.squeeze(0), + } + # detach + ret = {kk: ret[kk].detach().cpu().numpy() for kk in ret} + return ret + + def ff(bb): + return np_infer(bb)["energy"] + + fdv = -( + finite_difference(ff, cell, delta=delta).transpose(0, 2, 1) @ cell + ).squeeze() + rfv = np_infer(cell)["virial"] + np.testing.assert_almost_equal(fdv, rfv, decimal=places) + + +class TestEnergyModelSeAForce(unittest.TestCase, ForceTest): + def setUp(self): + model_params = copy.deepcopy(model_se_e2_a) + sampled = make_sample(model_params) + self.type_split = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestEnergyModelSeAVirial(unittest.TestCase, VirialTest): + def setUp(self): + model_params = copy.deepcopy(model_se_e2_a) + sampled = make_sample(model_params) + self.type_split = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestEnergyModelDPA1Force(unittest.TestCase, ForceTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa1) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestEnergyModelDPA1Virial(unittest.TestCase, VirialTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa1) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestEnergyModelDPA2Force(unittest.TestCase, ForceTest): + def setUp(self): + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + model_params = copy.deepcopy(model_dpa2) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestEnergyModelDPAUniVirial(unittest.TestCase, VirialTest): + def setUp(self): + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + model_params = copy.deepcopy(model_dpa2) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) diff --git a/source/tests/pt/test_calculator.py b/source/tests/pt/test_calculator.py new file mode 100644 index 0000000000..e8382b22b8 --- /dev/null +++ b/source/tests/pt/test_calculator.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import unittest +from copy import ( + deepcopy, +) +from pathlib import ( + Path, +) + +import torch + +from deepmd.pt.entrypoints.main import ( + get_trainer, +) +from deepmd.pt.utils.ase_calc import ( + DPCalculator, +) + +dtype = torch.float64 + + +class TestCalculator(unittest.TestCase): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = [ + str(Path(__file__).parent / "water/data/single") + ] + self.input_json = "test_dp_test.json" + with open(self.input_json, "w") as fp: + json.dump(self.config, fp, indent=4) + + trainer = get_trainer(deepcopy(self.config)) + trainer.run() + + input_dict, label_dict, _ = trainer.get_data(is_train=False) + _, _, more_loss = trainer.wrapper(**input_dict, label=label_dict, cur_lr=1.0) + + self.calculator = DPCalculator("model.pt") + + def test_calculator(self): + from ase import ( + Atoms, + ) + + natoms = 5 + cell = torch.eye(3, dtype=dtype) * 10 + coord = torch.rand([natoms, 3], dtype=dtype) + coord = torch.matmul(coord, cell) + atype = torch.IntTensor([0, 0, 0, 1, 1]) + atomic_numbers = [1, 1, 1, 8, 8] + idx_perm = [1, 0, 4, 3, 2] + + prec = 1e-10 + low_prec = 1e-4 + + ase_atoms0 = Atoms( + numbers=atomic_numbers, + positions=coord, + # positions=[tuple(item) for item in coordinate], + cell=cell, + calculator=self.calculator, + ) + e0, f0 = ase_atoms0.get_potential_energy(), ase_atoms0.get_forces() + s0, v0 = ( + ase_atoms0.get_stress(voigt=True), + -ase_atoms0.get_stress(voigt=False) * ase_atoms0.get_volume(), + ) + + ase_atoms1 = Atoms( + numbers=[atomic_numbers[i] for i in idx_perm], + positions=coord[idx_perm, :], + # positions=[tuple(item) for item in coordinate], + cell=cell, + calculator=self.calculator, + ) + e1, f1 = ase_atoms1.get_potential_energy(), ase_atoms1.get_forces() + s1, v1 = ( + ase_atoms1.get_stress(voigt=True), + -ase_atoms1.get_stress(voigt=False) * ase_atoms1.get_volume(), + ) + + assert isinstance(e0, float) + assert f0.shape == (natoms, 3) + assert v0.shape == (3, 3) + torch.testing.assert_close(e0, e1, rtol=low_prec, atol=prec) + torch.testing.assert_close(f0[idx_perm, :], f1, rtol=low_prec, atol=prec) + torch.testing.assert_close(s0, s1, rtol=low_prec, atol=prec) + torch.testing.assert_close(v0, v1, rtol=low_prec, atol=prec) diff --git a/source/tests/pt/test_deeppot.py b/source/tests/pt/test_deeppot.py new file mode 100644 index 0000000000..7f3ecf7d1b --- /dev/null +++ b/source/tests/pt/test_deeppot.py @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import unittest +from copy import ( + deepcopy, +) +from pathlib import ( + Path, +) + +import numpy as np + +from deepmd.pt.entrypoints.main import ( + get_trainer, +) +from deepmd.pt.infer.deep_eval import ( + DeepPot, +) + + +class TestDeepPot(unittest.TestCase): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + self.config["training"]["training_data"]["systems"] = [ + str(Path(__file__).parent / "water/data/single") + ] + self.config["training"]["validation_data"]["systems"] = [ + str(Path(__file__).parent / "water/data/single") + ] + self.input_json = "test_dp_test.json" + with open(self.input_json, "w") as fp: + json.dump(self.config, fp, indent=4) + + trainer = get_trainer(deepcopy(self.config)) + trainer.run() + + input_dict, label_dict, _ = trainer.get_data(is_train=False) + trainer.wrapper(**input_dict, label=label_dict, cur_lr=1.0) + self.model = "model.pt" + + def test_dp_test(self): + dp = DeepPot(str(self.model)) + cell = np.array( + [ + 5.122106549439247480e00, + 4.016537340154059388e-01, + 6.951654033828678081e-01, + 4.016537340154059388e-01, + 6.112136112297989143e00, + 8.178091365465004481e-01, + 6.951654033828678081e-01, + 8.178091365465004481e-01, + 6.159552512682983760e00, + ] + ).reshape(1, 3, 3) + coord = np.array( + [ + 2.978060152121375648e00, + 3.588469695887098077e00, + 2.792459820604495491e00, + 3.895592322591093115e00, + 2.712091020667753760e00, + 1.366836847133650501e00, + 9.955616170888935690e-01, + 4.121324820711413039e00, + 1.817239061889086571e00, + 3.553661462345699906e00, + 5.313046969500791583e00, + 6.635182659098815883e00, + 6.088601018589653080e00, + 6.575011420004332585e00, + 6.825240650611076099e00, + ] + ).reshape(1, -1, 3) + atype = np.array([0, 0, 0, 1, 1]).reshape(1, -1) + + e, f, v, ae, av = dp.eval(coord, cell, atype, atomic=True) diff --git a/source/tests/pt/test_descriptor.py b/source/tests/pt/test_descriptor.py new file mode 100644 index 0000000000..da38cf007f --- /dev/null +++ b/source/tests/pt/test_descriptor.py @@ -0,0 +1,166 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import os +import unittest + +import numpy as np +import tensorflow.compat.v1 as tf +import torch + +tf.disable_eager_execution() + +import json +from pathlib import ( + Path, +) + +from deepmd.pt.model.descriptor import ( + prod_env_mat_se_a, +) +from deepmd.pt.utils import ( + dp_random, +) +from deepmd.pt.utils.dataset import ( + DeepmdDataSet, +) +from deepmd.pt.utils.env import ( + DEVICE, + GLOBAL_NP_FLOAT_PRECISION, + GLOBAL_PT_FLOAT_PRECISION, +) +from deepmd.tf.common import ( + expand_sys_str, +) +from deepmd.tf.env import ( + op_module, +) + +CUR_DIR = os.path.dirname(__file__) + + +def base_se_a(rcut, rcut_smth, sel, batch, mean, stddev): + g = tf.Graph() + with g.as_default(): + coord = tf.placeholder(GLOBAL_NP_FLOAT_PRECISION, [None, None]) + box = tf.placeholder(GLOBAL_NP_FLOAT_PRECISION, [None, None]) + atype = tf.placeholder(tf.int32, [None, None]) + natoms_vec = tf.placeholder(tf.int32, [None]) + default_mesh = tf.placeholder(tf.int32, [None]) + stat_descrpt, descrpt_deriv, rij, nlist = op_module.prod_env_mat_a( + coord, + atype, + natoms_vec, + box, + default_mesh, + tf.constant(mean), + tf.constant(stddev), + rcut_a=-1.0, + rcut_r=rcut, + rcut_r_smth=rcut_smth, + sel_a=sel, + sel_r=[0 for i in sel], + ) + + net_deriv_reshape = tf.ones_like(stat_descrpt) + force = op_module.prod_force_se_a( + net_deriv_reshape, + descrpt_deriv, + nlist, + natoms_vec, + n_a_sel=sum(sel), + n_r_sel=0, + ) + + with tf.Session(graph=g) as sess: + y = sess.run( + [stat_descrpt, force, nlist], + feed_dict={ + coord: batch["coord"], + box: batch["box"], + natoms_vec: batch["natoms"], + atype: batch["atype"], + default_mesh: np.array([0, 0, 0, 2, 2, 2]), + }, + ) + tf.reset_default_graph() + return y + + +class TestSeA(unittest.TestCase): + def setUp(self): + dp_random.seed(20) + with open(str(Path(__file__).parent / "water/se_e2_a.json")) as fin: + content = fin.read() + config = json.loads(content) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + config["training"]["training_data"]["systems"] = data_file + config["training"]["validation_data"]["systems"] = data_file + model_config = config["model"] + self.rcut = model_config["descriptor"]["rcut"] + self.rcut_smth = model_config["descriptor"]["rcut_smth"] + self.sel = model_config["descriptor"]["sel"] + self.bsz = config["training"]["training_data"]["batch_size"] + self.systems = config["training"]["validation_data"]["systems"] + if isinstance(self.systems, str): + self.systems = expand_sys_str(self.systems) + ds = DeepmdDataSet( + self.systems, self.bsz, model_config["type_map"], self.rcut, self.sel + ) + self.np_batch, self.pt_batch = ds.get_batch() + self.sec = np.cumsum(self.sel) + self.ntypes = len(self.sel) + self.nnei = sum(self.sel) + + def test_consistency(self): + avg_zero = torch.zeros( + [self.ntypes, self.nnei * 4], dtype=GLOBAL_PT_FLOAT_PRECISION + ) + std_ones = torch.ones( + [self.ntypes, self.nnei * 4], dtype=GLOBAL_PT_FLOAT_PRECISION + ) + base_d, base_force, nlist = base_se_a( + rcut=self.rcut, + rcut_smth=self.rcut_smth, + sel=self.sel, + batch=self.np_batch, + mean=avg_zero, + stddev=std_ones, + ) + + pt_coord = self.pt_batch["coord"] + pt_coord.requires_grad_(True) + index = self.pt_batch["mapping"].unsqueeze(-1).expand(-1, -1, 3) + extended_coord = torch.gather(pt_coord, dim=1, index=index) + extended_coord = extended_coord - self.pt_batch["shift"] + my_d, _, _ = prod_env_mat_se_a( + extended_coord.to(DEVICE), + self.pt_batch["nlist"], + self.pt_batch["atype"], + avg_zero.reshape([-1, self.nnei, 4]).to(DEVICE), + std_ones.reshape([-1, self.nnei, 4]).to(DEVICE), + self.rcut, + self.rcut_smth, + ) + my_d.sum().backward() + bsz = pt_coord.shape[0] + my_force = pt_coord.grad.view(bsz, -1, 3).cpu().detach().numpy() + base_force = base_force.reshape(bsz, -1, 3) + base_d = base_d.reshape(bsz, -1, self.nnei, 4) + my_d = my_d.view(bsz, -1, self.nnei, 4).cpu().detach().numpy() + nlist = nlist.reshape(bsz, -1, self.nnei) + + mapping = self.pt_batch["mapping"].cpu() + my_nlist = self.pt_batch["nlist"].view(bsz, -1).cpu() + mask = my_nlist == -1 + my_nlist = my_nlist * ~mask + my_nlist = torch.gather(mapping, dim=-1, index=my_nlist) + my_nlist = my_nlist * ~mask - mask.long() + my_nlist = my_nlist.cpu().view(bsz, -1, self.nnei).numpy() + self.assertTrue(np.allclose(nlist, my_nlist)) + self.assertTrue(np.allclose(np.mean(base_d, axis=2), np.mean(my_d, axis=2))) + self.assertTrue(np.allclose(np.std(base_d, axis=2), np.std(my_d, axis=2))) + # descriptors may be different when there are multiple neighbors in the same distance + self.assertTrue(np.allclose(base_force, -my_force)) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_descriptor_dpa1.py b/source/tests/pt/test_descriptor_dpa1.py new file mode 100644 index 0000000000..689fa7e49c --- /dev/null +++ b/source/tests/pt/test_descriptor_dpa1.py @@ -0,0 +1,367 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import os +import unittest +from pathlib import ( + Path, +) + +import torch + +from deepmd.pt.model.descriptor import ( + DescrptBlockSeAtten, + DescrptDPA1, +) +from deepmd.pt.model.network.network import ( + TypeEmbedNet, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.nlist import ( + build_neighbor_list, + extend_coord_with_ghosts, +) +from deepmd.pt.utils.region import ( + normalize_coord, +) + +dtype = torch.float64 +torch.set_default_dtype(dtype) + +CUR_DIR = os.path.dirname(__file__) + + +class TestDPA1(unittest.TestCase): + def setUp(self): + cell = [ + 5.122106549439247480e00, + 4.016537340154059388e-01, + 6.951654033828678081e-01, + 4.016537340154059388e-01, + 6.112136112297989143e00, + 8.178091365465004481e-01, + 6.951654033828678081e-01, + 8.178091365465004481e-01, + 6.159552512682983760e00, + ] + self.cell = torch.Tensor(cell).view(1, 3, 3).to(env.DEVICE) + coord = [ + 2.978060152121375648e00, + 3.588469695887098077e00, + 2.792459820604495491e00, + 3.895592322591093115e00, + 2.712091020667753760e00, + 1.366836847133650501e00, + 9.955616170888935690e-01, + 4.121324820711413039e00, + 1.817239061889086571e00, + 3.553661462345699906e00, + 5.313046969500791583e00, + 6.635182659098815883e00, + 6.088601018589653080e00, + 6.575011420004332585e00, + 6.825240650611076099e00, + ] + self.coord = torch.Tensor(coord).view(1, -1, 3).to(env.DEVICE) + self.atype = torch.IntTensor([0, 0, 0, 1, 1]).view(1, -1).to(env.DEVICE) + self.ref_d = torch.Tensor( + [ + 8.382518544113587780e-03, + -3.390120566088597812e-03, + 6.145981571114964362e-03, + -4.880300873973819273e-03, + -3.390120566088597812e-03, + 1.372540996564941464e-03, + -2.484163690574096341e-03, + 1.972313058658722688e-03, + 6.145981571114964362e-03, + -2.484163690574096341e-03, + 4.507748738021747671e-03, + -3.579717194906019764e-03, + -4.880300873973819273e-03, + 1.972313058658722688e-03, + -3.579717194906019764e-03, + 2.842794615687799838e-03, + 6.733043802494966066e-04, + -2.721540313345096771e-04, + 4.936158526085561134e-04, + -3.919743287822345223e-04, + -1.311123004527576900e-02, + 5.301179352601203924e-03, + -9.614612349318877454e-03, + 7.634884975521277241e-03, + 8.877088452901006621e-03, + -3.590945566653638409e-03, + 6.508042782015627942e-03, + -5.167671664327699171e-03, + -2.697241463040870365e-03, + 1.091350446825975137e-03, + -1.976895708961905022e-03, + 1.569671412121975348e-03, + 8.645131636261189911e-03, + -3.557395265621639355e-03, + 6.298048561552698106e-03, + -4.999272007935521948e-03, + -3.557395265621639355e-03, + 1.467866637220284964e-03, + -2.587004431651147504e-03, + 2.052752235601402672e-03, + 6.298048561552698106e-03, + -2.587004431651147504e-03, + 4.594085551315935101e-03, + -3.647656549789176847e-03, + -4.999272007935521948e-03, + 2.052752235601402672e-03, + -3.647656549789176847e-03, + 2.896359275520481256e-03, + 6.689620176492027878e-04, + -2.753606422414641049e-04, + 4.864958810186969444e-04, + -3.860599754167503119e-04, + -1.349238259226558101e-02, + 5.547478630961994242e-03, + -9.835472300819447095e-03, + 7.808197926069362048e-03, + 9.220744348752592245e-03, + -3.795799103392961601e-03, + 6.716516319358462918e-03, + -5.331265718473574867e-03, + -2.783836698392940304e-03, + 1.147461939123531121e-03, + -2.025013030986024063e-03, + 1.606944814423778541e-03, + 9.280385723343491378e-03, + -3.515852178447095942e-03, + 7.085282215778941628e-03, + -5.675852414643783178e-03, + -3.515852178447095942e-03, + 1.337760635271160884e-03, + -2.679428786337713451e-03, + 2.145400621815936413e-03, + 7.085282215778941628e-03, + -2.679428786337713451e-03, + 5.414439648102228192e-03, + -4.338426468139268931e-03, + -5.675852414643783178e-03, + 2.145400621815936413e-03, + -4.338426468139268931e-03, + 3.476467482674507146e-03, + 7.166961981167455130e-04, + -2.697932188839837972e-04, + 5.474643906631899504e-04, + -4.386556623669893621e-04, + -1.480434821331240956e-02, + 5.604647062899507579e-03, + -1.130745349141585449e-02, + 9.059113563516829268e-03, + 9.758791063112262978e-03, + -3.701477720487638626e-03, + 7.448215522796466058e-03, + -5.966057584545172120e-03, + -2.845102393948158344e-03, + 1.078743584169829543e-03, + -2.170093031447992756e-03, + 1.738010461687942770e-03, + 9.867599071916231118e-03, + -3.811041717688905522e-03, + 7.121877634386481262e-03, + -5.703120290113914553e-03, + -3.811041717688905522e-03, + 1.474046183772771213e-03, + -2.747386907428428938e-03, + 2.199711055637492037e-03, + 7.121877634386481262e-03, + -2.747386907428428938e-03, + 5.145050639440944609e-03, + -4.120642824501622239e-03, + -5.703120290113914553e-03, + 2.199711055637492037e-03, + -4.120642824501622239e-03, + 3.300262321758350853e-03, + 1.370499995344566383e-03, + -5.313041843655797901e-04, + 9.860110343046961986e-04, + -7.892505817954784597e-04, + -1.507686316307561489e-02, + 5.818961290579217904e-03, + -1.088774506142304276e-02, + 8.719460408506790952e-03, + 9.764630842803939323e-03, + -3.770134041110058572e-03, + 7.049438389985595785e-03, + -5.645302934019884485e-03, + -3.533582373572779437e-03, + 1.367148320603491559e-03, + -2.546602904764623705e-03, + 2.038882844528267305e-03, + 7.448297038731285964e-03, + -2.924276815200288742e-03, + 5.355960540523636154e-03, + -4.280386435083473329e-03, + -2.924276815200288742e-03, + 1.150311064893848757e-03, + -2.100635980860638373e-03, + 1.678427895009850001e-03, + 5.355960540523636154e-03, + -2.100635980860638373e-03, + 3.853607053247790071e-03, + -3.080076301871465493e-03, + -4.280386435083473329e-03, + 1.678427895009850001e-03, + -3.080076301871465493e-03, + 2.461876613756722523e-03, + 9.730712866459405395e-04, + -3.821759579990726546e-04, + 6.994242056622360787e-04, + -5.589662297882965055e-04, + -1.138916742131982317e-02, + 4.469391132927387489e-03, + -8.192016282448397885e-03, + 6.547234460517113892e-03, + 7.460070829043288082e-03, + -2.929867802018087421e-03, + 5.363646855497249989e-03, + -4.286347242903034739e-03, + -2.643569023340565718e-03, + 1.038826463247002245e-03, + -1.899910089750410976e-03, + 1.518237240362583541e-03, + ] + ).to(env.DEVICE) + with open(Path(CUR_DIR) / "models" / "dpa1.json") as fp: + self.model_json = json.load(fp) + self.file_model_param = Path(CUR_DIR) / "models" / "dpa1.pth" + self.file_type_embed = Path(CUR_DIR) / "models" / "dpa2_tebd.pth" + + def test_descriptor_block(self): + # torch.manual_seed(0) + model_dpa1 = self.model_json + dparams = model_dpa1["descriptor"] + ntypes = len(model_dpa1["type_map"]) + assert "se_atten" == dparams.pop("type") + dparams["ntypes"] = ntypes + des = DescrptBlockSeAtten( + **dparams, + ) + des.load_state_dict(torch.load(self.file_model_param)) + rcut = dparams["rcut"] + nsel = dparams["sel"] + coord = self.coord + atype = self.atype + box = self.cell + nf, nloc = coord.shape[:2] + coord_normalized = normalize_coord(coord, box.reshape(-1, 3, 3)) + extended_coord, extended_atype, mapping = extend_coord_with_ghosts( + coord_normalized, atype, box, rcut + ) + # single nlist + nlist = build_neighbor_list( + extended_coord, extended_atype, nloc, rcut, nsel, distinguish_types=False + ) + # handel type_embedding + type_embedding = TypeEmbedNet(ntypes, 8) + type_embedding.load_state_dict(torch.load(self.file_type_embed)) + + ## to save model parameters + # torch.save(des.state_dict(), 'model_weights.pth') + # torch.save(type_embedding.state_dict(), 'model_weights.pth') + descriptor, env_mat, diff, rot_mat, sw = des( + nlist, + extended_coord, + extended_atype, + type_embedding(extended_atype), + mapping=None, + ) + # np.savetxt('tmp.out', descriptor.detach().numpy().reshape(1,-1), delimiter=",") + self.assertEqual(descriptor.shape[-1], des.get_dim_out()) + self.assertAlmostEqual(6.0, des.get_rcut()) + self.assertEqual(30, des.get_nsel()) + self.assertEqual(2, des.get_ntype()) + torch.testing.assert_close( + descriptor.view(-1), self.ref_d, atol=1e-10, rtol=1e-10 + ) + + def test_descriptor(self): + with open(Path(CUR_DIR) / "models" / "dpa1.json") as fp: + self.model_json = json.load(fp) + model_dpa2 = self.model_json + ntypes = len(model_dpa2["type_map"]) + dparams = model_dpa2["descriptor"] + dparams["ntypes"] = ntypes + assert dparams.pop("type") == "se_atten" + dparams["concat_output_tebd"] = False + des = DescrptDPA1( + **dparams, + ) + target_dict = des.state_dict() + source_dict = torch.load(self.file_model_param) + type_embd_dict = torch.load(self.file_type_embed) + target_dict = translate_se_atten_and_type_embd_dicts_to_dpa1( + target_dict, + source_dict, + type_embd_dict, + ) + des.load_state_dict(target_dict) + + coord = self.coord + atype = self.atype + box = self.cell + nf, nloc = coord.shape[:2] + coord_normalized = normalize_coord(coord, box.reshape(-1, 3, 3)) + extended_coord, extended_atype, mapping = extend_coord_with_ghosts( + coord_normalized, atype, box, des.get_rcut() + ) + nlist = build_neighbor_list( + extended_coord, + extended_atype, + nloc, + des.get_rcut(), + des.get_nsel(), + distinguish_types=False, + ) + descriptor, env_mat, diff, rot_mat, sw = des( + extended_coord, + extended_atype, + nlist, + mapping=mapping, + ) + self.assertEqual(descriptor.shape[-1], des.get_dim_out()) + self.assertAlmostEqual(6.0, des.get_rcut()) + self.assertEqual(30, des.get_nsel()) + self.assertEqual(2, des.get_ntype()) + torch.testing.assert_close( + descriptor.view(-1), self.ref_d, atol=1e-10, rtol=1e-10 + ) + + dparams["concat_output_tebd"] = True + des = DescrptDPA1( + **dparams, + ) + descriptor, env_mat, diff, rot_mat, sw = des( + extended_coord, + extended_atype, + nlist, + mapping=mapping, + ) + self.assertEqual(descriptor.shape[-1], des.get_dim_out()) + + +def translate_se_atten_and_type_embd_dicts_to_dpa1( + target_dict, + source_dict, + type_embd_dict, +): + all_keys = list(target_dict.keys()) + record = [False for ii in all_keys] + for kk, vv in source_dict.items(): + tk = "se_atten." + kk + record[all_keys.index(tk)] = True + target_dict[tk] = vv + assert len(type_embd_dict.keys()) == 1 + kk = next(iter(type_embd_dict.keys())) + tk = "type_embedding." + kk + record[all_keys.index(tk)] = True + target_dict[tk] = type_embd_dict[kk] + assert all(record) + return target_dict diff --git a/source/tests/pt/test_descriptor_dpa2.py b/source/tests/pt/test_descriptor_dpa2.py new file mode 100644 index 0000000000..45c95961fe --- /dev/null +++ b/source/tests/pt/test_descriptor_dpa2.py @@ -0,0 +1,264 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import os +import unittest +from pathlib import ( + Path, +) + +import torch + +from deepmd.pt.model.descriptor import ( + DescrptBlockHybrid, + DescrptDPA2, +) +from deepmd.pt.model.network.network import ( + TypeEmbedNet, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.nlist import ( + build_neighbor_list, + extend_coord_with_ghosts, +) +from deepmd.pt.utils.region import ( + normalize_coord, +) + +dtype = torch.float64 +torch.set_default_dtype(dtype) + +CUR_DIR = os.path.dirname(__file__) + + +class TestDPA2(unittest.TestCase): + def setUp(self): + cell = [ + 5.122106549439247480e00, + 4.016537340154059388e-01, + 6.951654033828678081e-01, + 4.016537340154059388e-01, + 6.112136112297989143e00, + 8.178091365465004481e-01, + 6.951654033828678081e-01, + 8.178091365465004481e-01, + 6.159552512682983760e00, + ] + self.cell = torch.Tensor(cell).view(1, 3, 3).to(env.DEVICE) + coord = [ + 2.978060152121375648e00, + 3.588469695887098077e00, + 2.792459820604495491e00, + 3.895592322591093115e00, + 2.712091020667753760e00, + 1.366836847133650501e00, + 9.955616170888935690e-01, + 4.121324820711413039e00, + 1.817239061889086571e00, + 3.553661462345699906e00, + 5.313046969500791583e00, + 6.635182659098815883e00, + 6.088601018589653080e00, + 6.575011420004332585e00, + 6.825240650611076099e00, + ] + self.coord = torch.Tensor(coord).view(1, -1, 3).to(env.DEVICE) + self.atype = torch.IntTensor([0, 0, 0, 1, 1]).view(1, -1).to(env.DEVICE) + self.ref_d = torch.Tensor( + [ + 8.435412613327306630e-01, + -4.717109614540972440e-01, + -1.812643456954206256e00, + -2.315248767961955167e-01, + -7.112973006771171613e-01, + -4.162041919507591392e-01, + -1.505159810095323181e00, + -1.191652416985768403e-01, + 8.439214937875325617e-01, + -4.712976890460106594e-01, + -1.812605149396642856e00, + -2.307222236291133766e-01, + -7.115427800870099961e-01, + -4.164729253167227530e-01, + -1.505483119125936797e00, + -1.191288524278367872e-01, + 8.286420823261241297e-01, + -4.535033763979030574e-01, + -1.787877160970498425e00, + -1.961763875645104460e-01, + -7.475459187804838201e-01, + -5.231446874663764346e-01, + -1.488399984491664219e00, + -3.974117581747104583e-02, + 8.283793431613817315e-01, + -4.551551577556525729e-01, + -1.789253136645859943e00, + -1.977673627726055372e-01, + -7.448826048241211639e-01, + -5.161350182531234676e-01, + -1.487589463573479209e00, + -4.377376017839779143e-02, + 8.295404560710329944e-01, + -4.492219258475603216e-01, + -1.784484611185287450e00, + -1.901182059718481143e-01, + -7.537407667483000395e-01, + -5.384371277650709109e-01, + -1.490368056268364549e00, + -3.073744832541754762e-02, + ] + ).to(env.DEVICE) + with open(Path(CUR_DIR) / "models" / "dpa2_hyb.json") as fp: + self.model_json = json.load(fp) + self.file_model_param = Path(CUR_DIR) / "models" / "dpa2.pth" + self.file_type_embed = Path(CUR_DIR) / "models" / "dpa2_tebd.pth" + + def test_descriptor_hyb(self): + # torch.manual_seed(0) + model_hybrid_dpa2 = self.model_json + dparams = model_hybrid_dpa2["descriptor"] + ntypes = len(model_hybrid_dpa2["type_map"]) + dlist = dparams.pop("list") + des = DescrptBlockHybrid( + dlist, + ntypes, + hybrid_mode=dparams["hybrid_mode"], + ) + model_dict = torch.load(self.file_model_param) + # type_embd of repformer is removed + model_dict.pop("descriptor_list.1.type_embd.embedding.weight") + des.load_state_dict(model_dict) + all_rcut = [ii["rcut"] for ii in dlist] + all_nsel = [ii["sel"] for ii in dlist] + rcut_max = max(all_rcut) + coord = self.coord + atype = self.atype + box = self.cell + nf, nloc = coord.shape[:2] + coord_normalized = normalize_coord(coord, box.reshape(-1, 3, 3)) + extended_coord, extended_atype, mapping = extend_coord_with_ghosts( + coord_normalized, atype, box, rcut_max + ) + ## single nlist + # nlist = build_neighbor_list( + # extended_coord, extended_atype, nloc, + # rcut_max, nsel, distinguish_types=False) + nlist_list = [] + for rcut, sel in zip(all_rcut, all_nsel): + nlist_list.append( + build_neighbor_list( + extended_coord, + extended_atype, + nloc, + rcut, + sel, + distinguish_types=False, + ) + ) + nlist = torch.cat(nlist_list, -1) + # handel type_embedding + type_embedding = TypeEmbedNet(ntypes, 8) + type_embedding.load_state_dict(torch.load(self.file_type_embed)) + + ## to save model parameters + # torch.save(des.state_dict(), 'model_weights.pth') + # torch.save(type_embedding.state_dict(), 'model_weights.pth') + descriptor, env_mat, diff, rot_mat, sw = des( + nlist, + extended_coord, + extended_atype, + type_embedding(extended_atype), + mapping=mapping, + ) + torch.testing.assert_close( + descriptor.view(-1), self.ref_d, atol=1e-10, rtol=1e-10 + ) + + def test_descriptor(self): + with open(Path(CUR_DIR) / "models" / "dpa2.json") as fp: + self.model_json = json.load(fp) + model_dpa2 = self.model_json + ntypes = len(model_dpa2["type_map"]) + dparams = model_dpa2["descriptor"] + dparams["ntypes"] = ntypes + assert dparams.pop("type") == "dpa2" + dparams["concat_output_tebd"] = False + des = DescrptDPA2( + **dparams, + ) + target_dict = des.state_dict() + source_dict = torch.load(self.file_model_param) + # type_embd of repformer is removed + source_dict.pop("descriptor_list.1.type_embd.embedding.weight") + type_embd_dict = torch.load(self.file_type_embed) + target_dict = translate_hybrid_and_type_embd_dicts_to_dpa2( + target_dict, + source_dict, + type_embd_dict, + ) + des.load_state_dict(target_dict) + + coord = self.coord + atype = self.atype + box = self.cell + nf, nloc = coord.shape[:2] + coord_normalized = normalize_coord(coord, box.reshape(-1, 3, 3)) + extended_coord, extended_atype, mapping = extend_coord_with_ghosts( + coord_normalized, atype, box, des.repinit.rcut + ) + nlist = build_neighbor_list( + extended_coord, + extended_atype, + nloc, + des.repinit.rcut, + des.repinit.sel, + distinguish_types=False, + ) + descriptor, env_mat, diff, rot_mat, sw = des( + extended_coord, + extended_atype, + nlist, + mapping=mapping, + ) + self.assertEqual(descriptor.shape[-1], des.get_dim_out()) + self.assertAlmostEqual(6.0, des.get_rcut()) + self.assertEqual(30, des.get_nsel()) + self.assertEqual(2, des.get_ntype()) + torch.testing.assert_close( + descriptor.view(-1), self.ref_d, atol=1e-10, rtol=1e-10 + ) + + dparams["concat_output_tebd"] = True + des = DescrptDPA2( + **dparams, + ) + descriptor, env_mat, diff, rot_mat, sw = des( + extended_coord, + extended_atype, + nlist, + mapping=mapping, + ) + self.assertEqual(descriptor.shape[-1], des.get_dim_out()) + + +def translate_hybrid_and_type_embd_dicts_to_dpa2( + target_dict, + source_dict, + type_embd_dict, +): + all_keys = list(target_dict.keys()) + record = [False for ii in all_keys] + for kk, vv in source_dict.items(): + tk = kk.replace("descriptor_list.1", "repformers") + tk = tk.replace("descriptor_list.0", "repinit") + tk = tk.replace("sequential_transform.0", "g1_shape_tranform") + record[all_keys.index(tk)] = True + target_dict[tk] = vv + assert len(type_embd_dict.keys()) == 1 + kk = next(iter(type_embd_dict.keys())) + tk = "type_embedding." + kk + record[all_keys.index(tk)] = True + target_dict[tk] = type_embd_dict[kk] + assert all(record) + return target_dict diff --git a/source/tests/pt/test_dp_test.py b/source/tests/pt/test_dp_test.py new file mode 100644 index 0000000000..3db66f073f --- /dev/null +++ b/source/tests/pt/test_dp_test.py @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import os +import shutil +import unittest +from copy import ( + deepcopy, +) +from pathlib import ( + Path, +) + +import numpy as np + +from deepmd.pt.entrypoints.main import ( + get_trainer, +) +from deepmd.pt.infer import ( + inference, +) + + +class TestDPTest(unittest.TestCase): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = [ + str(Path(__file__).parent / "water/data/single") + ] + self.input_json = "test_dp_test.json" + with open(self.input_json, "w") as fp: + json.dump(self.config, fp, indent=4) + + def test_dp_test(self): + trainer = get_trainer(deepcopy(self.config)) + trainer.run() + + input_dict, label_dict, _ = trainer.get_data(is_train=False) + _, _, more_loss = trainer.wrapper(**input_dict, label=label_dict, cur_lr=1.0) + + tester = inference.Tester("model.pt", input_script=self.input_json) + try: + res = tester.run() + except StopIteration: + print("Unexpected stop iteration.(test step < total batch)") + raise StopIteration + for k, v in res.items(): + if k == "rmse" or "mae" in k or k not in more_loss: + continue + np.testing.assert_allclose( + v, more_loss[k].cpu().detach().numpy(), rtol=1e-04, atol=1e-07 + ) + + def tearDown(self): + for f in os.listdir("."): + if f.startswith("model") and f.endswith(".pt"): + os.remove(f) + if f in ["lcurve.out"]: + os.remove(f) + if f in ["stat_files"]: + shutil.rmtree(f) + os.remove(self.input_json) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_embedding_net.py b/source/tests/pt/test_embedding_net.py new file mode 100644 index 0000000000..fc98ddc9f9 --- /dev/null +++ b/source/tests/pt/test_embedding_net.py @@ -0,0 +1,176 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import os +import re +import unittest + +import numpy as np +import tensorflow.compat.v1 as tf +import torch + +tf.disable_eager_execution() + +from pathlib import ( + Path, +) + +from deepmd.pt.model.descriptor import ( + DescrptSeA, +) +from deepmd.pt.utils import ( + dp_random, +) +from deepmd.pt.utils.dataset import ( + DeepmdDataSet, +) +from deepmd.pt.utils.env import ( + DEVICE, + GLOBAL_NP_FLOAT_PRECISION, +) +from deepmd.tf.common import ( + expand_sys_str, +) +from deepmd.tf.descriptor import DescrptSeA as DescrptSeA_tf + +CUR_DIR = os.path.dirname(__file__) + + +def gen_key(worb, depth, elemid): + return (worb, depth, elemid) + + +def base_se_a(descriptor, coord, atype, natoms, box): + g = tf.Graph() + with g.as_default(): + name_pfx = "d_sea_" + t_coord = tf.placeholder( + GLOBAL_NP_FLOAT_PRECISION, [None, None], name=name_pfx + "t_coord" + ) + t_atype = tf.placeholder(tf.int32, [None, None], name=name_pfx + "t_type") + t_natoms = tf.placeholder( + tf.int32, [descriptor.ntypes + 2], name=name_pfx + "t_natoms" + ) + t_box = tf.placeholder( + GLOBAL_NP_FLOAT_PRECISION, [None, None], name=name_pfx + "t_box" + ) + t_default_mesh = tf.placeholder(tf.int32, [None], name=name_pfx + "t_mesh") + t_embedding = descriptor.build( + t_coord, t_atype, t_natoms, t_box, t_default_mesh, input_dict={} + ) + fake_energy = tf.reduce_sum(t_embedding) + t_force = descriptor.prod_force_virial(fake_energy, t_natoms)[0] + t_vars = {} + for var in tf.global_variables(): + ms = re.findall(r"([a-z]+)_(\d)_(\d)", var.name) + if len(ms) == 1: + m = ms[0] + key = gen_key(worb=m[0], depth=int(m[1]), elemid=int(m[2])) + t_vars[key] = var + init_op = tf.global_variables_initializer() + + with tf.Session(graph=g) as sess: + sess.run(init_op) + embedding, force, values = sess.run( + [t_embedding, t_force, t_vars], + feed_dict={ + t_coord: coord, + t_atype: atype, + t_natoms: natoms, + t_box: box, + t_default_mesh: np.array([0, 0, 0, 2, 2, 2]), + }, + ) + tf.reset_default_graph() + return embedding, force, values + + +class TestSeA(unittest.TestCase): + def setUp(self): + dp_random.seed(0) + with open(str(Path(__file__).parent / "water/se_e2_a.json")) as fin: + content = fin.read() + config = json.loads(content) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + config["training"]["training_data"]["systems"] = data_file + config["training"]["validation_data"]["systems"] = data_file + model_config = config["model"] + self.rcut = model_config["descriptor"]["rcut"] + self.rcut_smth = model_config["descriptor"]["rcut_smth"] + self.sel = model_config["descriptor"]["sel"] + self.bsz = config["training"]["training_data"]["batch_size"] + self.systems = config["training"]["validation_data"]["systems"] + if isinstance(self.systems, str): + self.systems = expand_sys_str(self.systems) + ds = DeepmdDataSet( + self.systems, self.bsz, model_config["type_map"], self.rcut, self.sel + ) + self.filter_neuron = model_config["descriptor"]["neuron"] + self.axis_neuron = model_config["descriptor"]["axis_neuron"] + self.np_batch, self.torch_batch = ds.get_batch() + + def test_consistency(self): + dp_d = DescrptSeA_tf( + rcut=self.rcut, + rcut_smth=self.rcut_smth, + sel=self.sel, + neuron=self.filter_neuron, + axis_neuron=self.axis_neuron, + seed=1, + ) + dp_embedding, dp_force, dp_vars = base_se_a( + descriptor=dp_d, + coord=self.np_batch["coord"], + atype=self.np_batch["atype"], + natoms=self.np_batch["natoms"], + box=self.np_batch["box"], + ) + + # Reproduced + old_impl = False + descriptor = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + neuron=self.filter_neuron, + axis_neuron=self.axis_neuron, + old_impl=old_impl, + ).to(DEVICE) + for name, param in descriptor.named_parameters(): + if old_impl: + ms = re.findall(r"(\d)\.deep_layers\.(\d)\.([a-z]+)", name) + else: + ms = re.findall(r"(\d)\.layers\.(\d)\.([a-z]+)", name) + if len(ms) == 1: + m = ms[0] + key = gen_key(worb=m[2], depth=int(m[1]) + 1, elemid=int(m[0])) + var = dp_vars[key] + with torch.no_grad(): + # Keep parameter value consistency between 2 implentations + param.data.copy_(torch.from_numpy(var)) + + pt_coord = self.torch_batch["coord"] + pt_coord.requires_grad_(True) + index = self.torch_batch["mapping"].unsqueeze(-1).expand(-1, -1, 3) + extended_coord = torch.gather(pt_coord, dim=1, index=index) + extended_coord = extended_coord - self.torch_batch["shift"] + extended_atype = torch.gather( + self.torch_batch["atype"], dim=1, index=self.torch_batch["mapping"] + ) + descriptor_out, _, _, _, _ = descriptor( + extended_coord, + extended_atype, + self.torch_batch["nlist"], + ) + my_embedding = descriptor_out.cpu().detach().numpy() + fake_energy = torch.sum(descriptor_out) + fake_energy.backward() + my_force = -pt_coord.grad.cpu().numpy() + + # Check + np.testing.assert_allclose(dp_embedding, my_embedding) + dp_force = dp_force.reshape(*my_force.shape) + np.testing.assert_allclose(dp_force, my_force) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_env_mat.py b/source/tests/pt/test_env_mat.py new file mode 100644 index 0000000000..f4931e9ecc --- /dev/null +++ b/source/tests/pt/test_env_mat.py @@ -0,0 +1,84 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np +import torch + +try: + from deepmd.model_format import ( + EnvMat, + ) + + support_env_mat = True +except ModuleNotFoundError: + support_env_mat = False +except ImportError: + support_env_mat = False + +from deepmd.pt.model.descriptor.env_mat import ( + prod_env_mat_se_a, +) +from deepmd.pt.utils import ( + env, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION + + +class TestCaseSingleFrameWithNlist: + def setUp(self): + # nloc == 3, nall == 4 + self.nloc = 3 + self.nall = 4 + self.nf, self.nt = 1, 2 + self.coord_ext = np.array( + [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 1], + [0, -2, 0], + ], + dtype=np.float64, + ).reshape([1, self.nall * 3]) + self.atype_ext = np.array([0, 0, 1, 0], dtype=int).reshape([1, self.nall]) + # sel = [5, 2] + self.sel = [5, 2] + self.nlist = np.array( + [ + [1, 3, -1, -1, -1, 2, -1], + [0, -1, -1, -1, -1, 2, -1], + [0, 1, -1, -1, -1, 0, -1], + ], + dtype=int, + ).reshape([1, self.nloc, sum(self.sel)]) + self.rcut = 0.4 + self.rcut_smth = 2.2 + + +# to be merged with the tf test case +@unittest.skipIf(not support_env_mat, "EnvMat not supported") +class TestEnvMat(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_consistency( + self, + ): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + davg = rng.normal(size=(self.nt, nnei, 4)) + dstd = rng.normal(size=(self.nt, nnei, 4)) + dstd = 0.1 + np.abs(dstd) + em0 = EnvMat(self.rcut, self.rcut_smth) + mm0, ww0 = em0.call(self.coord_ext, self.atype_ext, self.nlist, davg, dstd) + mm1, _, ww1 = prod_env_mat_se_a( + torch.tensor(self.coord_ext, dtype=dtype), + torch.tensor(self.nlist, dtype=int), + torch.tensor(self.atype_ext[:, :nloc], dtype=int), + davg, + dstd, + self.rcut, + self.rcut_smth, + ) + np.testing.assert_allclose(mm0, mm1) + np.testing.assert_allclose(ww0, ww1) diff --git a/source/tests/pt/test_fitting_net.py b/source/tests/pt/test_fitting_net.py new file mode 100644 index 0000000000..3feb4f4739 --- /dev/null +++ b/source/tests/pt/test_fitting_net.py @@ -0,0 +1,139 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import re +import unittest + +import numpy as np +import tensorflow.compat.v1 as tf +import torch + +tf.disable_eager_execution() + +from deepmd.pt.model.task import ( + EnergyFittingNet, +) +from deepmd.pt.utils.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) +from deepmd.tf.fit.ener import ( + EnerFitting, +) + + +class FakeDescriptor: + def __init__(self, ntypes, embedding_width): + self._ntypes = ntypes + self._dim_out = embedding_width + + def get_ntypes(self): + return self._ntypes + + def get_dim_out(self): + return self._dim_out + + +def gen_key(type_id, layer_id, w_or_b): + return (type_id, layer_id, w_or_b) + + +def base_fitting_net(dp_fn, embedding, natoms, atype): + g = tf.Graph() + with g.as_default(): + t_embedding = tf.placeholder(GLOBAL_NP_FLOAT_PRECISION, [None, None]) + t_natoms = tf.placeholder(tf.int32, [None]) + t_atype = tf.placeholder(tf.int32, [None, None]) + t_energy = dp_fn.build(t_embedding, t_natoms, {"atype": t_atype}) + init_op = tf.global_variables_initializer() + t_vars = {} + for var in tf.global_variables(): + key = None + matched = re.match(r"layer_(\d)_type_(\d)/([a-z]+)", var.name) + if matched: + key = gen_key( + type_id=matched.group(2), + layer_id=matched.group(1), + w_or_b=matched.group(3), + ) + else: + matched = re.match(r"final_layer_type_(\d)/([a-z]+)", var.name) + if matched: + key = gen_key( + type_id=matched.group(1), layer_id=-1, w_or_b=matched.group(2) + ) + if key is not None: + t_vars[key] = var + + with tf.Session(graph=g) as sess: + sess.run(init_op) + energy, values = sess.run( + [t_energy, t_vars], + feed_dict={ + t_embedding: embedding, + t_natoms: natoms, + t_atype: atype, + }, + ) + tf.reset_default_graph() + return energy, values + + +class TestFittingNet(unittest.TestCase): + def setUp(self): + nloc = 7 + self.embedding_width = 30 + self.natoms = np.array([nloc, nloc, 2, 5], dtype=np.int32) + rng = np.random.default_rng() + self.embedding = rng.uniform(size=[4, nloc * self.embedding_width]) + self.ntypes = self.natoms.size - 2 + self.n_neuron = [32, 32, 32] + self.atype = np.zeros([4, nloc], dtype=np.int32) + cnt = 0 + for i in range(self.ntypes): + self.atype[:, cnt : cnt + self.natoms[i + 2]] = i + cnt += self.natoms[i + 2] + + fake_d = FakeDescriptor(2, 30) + self.dp_fn = EnerFitting(fake_d, self.n_neuron) + self.dp_fn.bias_atom_e = rng.uniform(size=[self.ntypes]) + + def test_consistency(self): + dp_energy, values = base_fitting_net( + self.dp_fn, self.embedding, self.natoms, self.atype + ) + my_fn = EnergyFittingNet( + self.ntypes, + self.embedding_width, + self.n_neuron, + self.dp_fn.bias_atom_e, + use_tebd=False, + ) + for name, param in my_fn.named_parameters(): + matched = re.match("filter_layers\.(\d).deep_layers\.(\d)\.([a-z]+)", name) + key = None + if matched: + key = gen_key( + type_id=matched.group(1), + layer_id=matched.group(2), + w_or_b=matched.group(3), + ) + else: + matched = re.match("filter_layers\.(\d).final_layer\.([a-z]+)", name) + if matched: + key = gen_key( + type_id=matched.group(1), layer_id=-1, w_or_b=matched.group(2) + ) + assert key is not None + var = values[key] + with torch.no_grad(): + # Keep parameter value consistency between 2 implentations + param.data.copy_(torch.from_numpy(var)) + embedding = torch.from_numpy(self.embedding) + embedding = embedding.view(4, -1, self.embedding_width) + atype = torch.from_numpy(self.atype) + ret = my_fn(embedding, atype) + my_energy = ret["energy"] + my_energy = my_energy.detach() + self.assertTrue(np.allclose(dp_energy, my_energy.numpy().reshape([-1]))) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_force_grad.py b/source/tests/pt/test_force_grad.py new file mode 100644 index 0000000000..1ea4321d21 --- /dev/null +++ b/source/tests/pt/test_force_grad.py @@ -0,0 +1,123 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import json +import unittest +from pathlib import ( + Path, +) +from typing import ( + List, + Optional, +) + +import numpy as np +import torch + +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.dataloader import ( + DpLoaderSet, +) +from deepmd.pt.utils.dataset import ( + DeepmdDataSystem, +) +from deepmd.pt.utils.stat import ( + make_stat_input, +) + + +class CheckSymmetry(DeepmdDataSystem): + def __init__( + self, + sys_path: str, + rcut, + sec, + type_map: Optional[List[str]] = None, + type_split=True, + ): + super().__init__(sys_path, rcut, sec, type_map, type_split) + + def get_disturb(self, index, atom_index, axis_index, delta): + for i in range( + 0, len(self._dirs) + 1 + ): # note: if different sets can be merged, prefix sum is unused to calculate + if index < self.prefix_sum[i]: + break + frames = self._load_set(self._dirs[i - 1]) + tmp = copy.deepcopy(frames["coord"].reshape(self.nframes, -1, 3)) + tmp[:, atom_index, axis_index] += delta + frames["coord"] = tmp + frame = self.single_preprocess(frames, index - self.prefix_sum[i - 1]) + return frame + + +def get_data(batch): + inputs = {} + for key in ["coord", "atype", "box"]: + inputs[key] = batch[key].unsqueeze(0).to(env.DEVICE) + return inputs + + +class TestForceGrad(unittest.TestCase): + def setUp(self): + with open(str(Path(__file__).parent / "water/se_e2_a.json")) as fin: + self.config = json.load(fin) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.system_index = 0 + self.batch_index = 0 + self.get_dataset(self.system_index, self.batch_index) + self.get_model() + + def get_model(self): + training_systems = self.config["training"]["training_data"]["systems"] + model_params = self.config["model"] + data_stat_nbatch = model_params.get("data_stat_nbatch", 10) + train_data = DpLoaderSet( + training_systems, + self.config["training"]["training_data"]["batch_size"], + model_params, + ) + sampled = make_stat_input( + train_data.systems, train_data.dataloaders, data_stat_nbatch + ) + self.model = get_model(self.config["model"], sampled).to(env.DEVICE) + + def get_dataset(self, system_index=0, batch_index=0): + systems = self.config["training"]["training_data"]["systems"] + rcut = self.config["model"]["descriptor"]["rcut"] + sel = self.config["model"]["descriptor"]["sel"] + sec = torch.cumsum(torch.tensor(sel), dim=0) + type_map = self.config["model"]["type_map"] + self.dpdatasystem = CheckSymmetry( + sys_path=systems[system_index], rcut=rcut, sec=sec, type_map=type_map + ) + self.origin_batch = self.dpdatasystem._get_item(batch_index) + + @unittest.skip("it can be replaced by autodiff") + def test_force_grad(self, threshold=1e-2, delta0=1e-6, seed=20): + result0 = self.model(**get_data(self.origin_batch)) + np.random.default_rng(seed) + errors = np.zeros((self.dpdatasystem._natoms, 3)) + for atom_index in range(self.dpdatasystem._natoms): + for axis_index in range(3): + delta = np.random.random() * delta0 + disturb_batch = self.dpdatasystem.get_disturb( + self.batch_index, atom_index, axis_index, delta + ) + disturb_result = self.model(**get_data(disturb_batch)) + disturb_force = -(disturb_result["energy"] - result0["energy"]) / delta + disturb_error = ( + result0["force"][0, atom_index, axis_index] - disturb_force + ) + errors[atom_index, axis_index] = disturb_error.detach().cpu().numpy() + self.assertTrue(np.abs(errors).max() < threshold, msg=str(np.abs(errors).max())) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_jit.py b/source/tests/pt/test_jit.py new file mode 100644 index 0000000000..f13dade183 --- /dev/null +++ b/source/tests/pt/test_jit.py @@ -0,0 +1,140 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import os +import shutil +import unittest +from copy import ( + deepcopy, +) +from pathlib import ( + Path, +) + +import torch + +from deepmd.pt.entrypoints.main import ( + get_trainer, +) +from deepmd.pt.infer import ( + inference, +) + +from .test_permutation import ( + model_dpa1, + model_dpa2, + model_hybrid, + model_se_e2_a, +) + + +class JITTest: + def test_jit(self): + trainer = get_trainer(deepcopy(self.config)) + trainer.run() + model = torch.jit.script(inference.Tester("./model.pt", numb_test=1).model) + torch.jit.save(model, "./frozen_model.pth", {}) + + def tearDown(self): + for f in os.listdir("."): + if f.startswith("model") and f.endswith("pt"): + os.remove(f) + if f in ["lcurve.out", "frozen_model.pth"]: + os.remove(f) + if f in ["stat_files"]: + shutil.rmtree(f) + + +class TestEnergyModelSeA(unittest.TestCase, JITTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_se_e2_a) + self.config["training"]["numb_steps"] = 10 + self.config["training"]["save_freq"] = 10 + + def tearDown(self): + JITTest.tearDown(self) + + +class TestEnergyModelDPA1(unittest.TestCase, JITTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_dpa1) + self.config["training"]["numb_steps"] = 10 + self.config["training"]["save_freq"] = 10 + + def tearDown(self): + JITTest.tearDown(self) + + +class TestEnergyModelDPA2(unittest.TestCase, JITTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_dpa2) + self.config["model"]["descriptor"]["rcut"] = self.config["model"]["descriptor"][ + "repinit_rcut" + ] + self.config["model"]["descriptor"]["rcut_smth"] = self.config["model"][ + "descriptor" + ]["repinit_rcut_smth"] + self.config["model"]["descriptor"]["sel"] = self.config["model"]["descriptor"][ + "repinit_nsel" + ] + self.config["training"]["numb_steps"] = 10 + self.config["training"]["save_freq"] = 10 + + def tearDown(self): + JITTest.tearDown(self) + + +@unittest.skip("hybrid not supported at the moment") +class TestEnergyModelHybrid(unittest.TestCase, JITTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_hybrid) + self.config["training"]["numb_steps"] = 10 + self.config["training"]["save_freq"] = 10 + + def tearDown(self): + JITTest.tearDown(self) + + +@unittest.skip("hybrid not supported at the moment") +class TestEnergyModelHybrid2(unittest.TestCase, JITTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_hybrid) + self.config["model"]["descriptor"]["hybrid_mode"] = "sequential" + self.config["training"]["numb_steps"] = 10 + self.config["training"]["save_freq"] = 10 + + def tearDown(self): + JITTest.tearDown(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_loss.py b/source/tests/pt/test_loss.py new file mode 100644 index 0000000000..14934c7be0 --- /dev/null +++ b/source/tests/pt/test_loss.py @@ -0,0 +1,189 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import os +import unittest + +import numpy as np +import tensorflow.compat.v1 as tf +import torch + +tf.disable_eager_execution() +from pathlib import ( + Path, +) + +from deepmd.pt.loss import ( + EnergyStdLoss, +) +from deepmd.pt.utils.dataset import ( + DeepmdDataSet, +) +from deepmd.tf.common import ( + expand_sys_str, +) +from deepmd.tf.loss.ener import ( + EnerStdLoss, +) + +CUR_DIR = os.path.dirname(__file__) + + +def get_batch(): + with open(str(Path(__file__).parent / "water/se_e2_a.json")) as fin: + content = fin.read() + config = json.loads(content) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + config["training"]["training_data"]["systems"] = data_file + config["training"]["validation_data"]["systems"] = data_file + model_config = config["model"] + rcut = model_config["descriptor"]["rcut"] + # self.rcut_smth = model_config['descriptor']['rcut_smth'] + sel = model_config["descriptor"]["sel"] + batch_size = config["training"]["training_data"]["batch_size"] + systems = config["training"]["validation_data"]["systems"] + if isinstance(systems, str): + systems = expand_sys_str(systems) + dataset = DeepmdDataSet(systems, batch_size, model_config["type_map"], rcut, sel) + np_batch, pt_batch = dataset.get_batch() + return np_batch, pt_batch + + +class TestLearningRate(unittest.TestCase): + def setUp(self): + self.start_lr = 1.1 + self.start_pref_e = 0.02 + self.limit_pref_e = 1.0 + self.start_pref_f = 1000.0 + self.limit_pref_f = 1.0 + self.start_pref_v = 0.02 + self.limit_pref_v = 1.0 + self.cur_lr = 1.2 + # data + np_batch, pt_batch = get_batch() + natoms = np_batch["natoms"] + self.nloc = natoms[0] + l_energy, l_force, l_virial = ( + np_batch["energy"], + np_batch["force"], + np_batch["virial"], + ) + p_energy, p_force, p_virial = ( + np.ones_like(l_energy), + np.ones_like(l_force), + np.ones_like(l_virial), + ) + nloc = natoms[0] + batch_size = pt_batch["coord"].shape[0] + atom_energy = np.zeros(shape=[batch_size, nloc]) + atom_pref = np.zeros(shape=[batch_size, nloc * 3]) + # tf + base = EnerStdLoss( + self.start_lr, + self.start_pref_e, + self.limit_pref_e, + self.start_pref_f, + self.limit_pref_f, + self.start_pref_v, + self.limit_pref_v, + ) + self.g = tf.Graph() + with self.g.as_default(): + t_cur_lr = tf.placeholder(shape=[], dtype=tf.float64) + t_natoms = tf.placeholder(shape=[None], dtype=tf.int32) + t_penergy = tf.placeholder(shape=[None, 1], dtype=tf.float64) + t_pforce = tf.placeholder(shape=[None, None], dtype=tf.float64) + t_pvirial = tf.placeholder(shape=[None, 9], dtype=tf.float64) + t_patom_energy = tf.placeholder(shape=[None, None], dtype=tf.float64) + t_lenergy = tf.placeholder(shape=[None, 1], dtype=tf.float64) + t_lforce = tf.placeholder(shape=[None, None], dtype=tf.float64) + t_lvirial = tf.placeholder(shape=[None, 9], dtype=tf.float64) + t_latom_energy = tf.placeholder(shape=[None, None], dtype=tf.float64) + t_atom_pref = tf.placeholder(shape=[None, None], dtype=tf.float64) + find_energy = tf.constant(1.0, dtype=tf.float64) + find_force = tf.constant(1.0, dtype=tf.float64) + find_virial = tf.constant(1.0, dtype=tf.float64) + find_atom_energy = tf.constant(0.0, dtype=tf.float64) + find_atom_pref = tf.constant(0.0, dtype=tf.float64) + model_dict = { + "energy": t_penergy, + "force": t_pforce, + "virial": t_pvirial, + "atom_ener": t_patom_energy, + } + label_dict = { + "energy": t_lenergy, + "force": t_lforce, + "virial": t_lvirial, + "atom_ener": t_latom_energy, + "atom_pref": t_atom_pref, + "find_energy": find_energy, + "find_force": find_force, + "find_virial": find_virial, + "find_atom_ener": find_atom_energy, + "find_atom_pref": find_atom_pref, + } + self.base_loss_sess = base.build( + t_cur_lr, t_natoms, model_dict, label_dict, "" + ) + # torch + self.feed_dict = { + t_cur_lr: self.cur_lr, + t_natoms: natoms, + t_penergy: p_energy, + t_pforce: p_force, + t_pvirial: p_virial.reshape(-1, 9), + t_patom_energy: atom_energy, + t_lenergy: l_energy, + t_lforce: l_force, + t_lvirial: l_virial.reshape(-1, 9), + t_latom_energy: atom_energy, + t_atom_pref: atom_pref, + } + self.model_pred = { + "energy": torch.from_numpy(p_energy), + "force": torch.from_numpy(p_force), + "virial": torch.from_numpy(p_virial), + } + self.label = { + "energy": torch.from_numpy(l_energy), + "force": torch.from_numpy(l_force), + "virial": torch.from_numpy(l_virial), + } + self.natoms = pt_batch["natoms"] + + def tearDown(self) -> None: + tf.reset_default_graph() + return super().tearDown() + + def test_consistency(self): + with tf.Session(graph=self.g) as sess: + base_loss, base_more_loss = sess.run( + self.base_loss_sess, feed_dict=self.feed_dict + ) + mine = EnergyStdLoss( + self.start_lr, + self.start_pref_e, + self.limit_pref_e, + self.start_pref_f, + self.limit_pref_f, + self.start_pref_v, + self.limit_pref_v, + ) + my_loss, my_more_loss = mine( + self.label, + self.model_pred, + self.nloc, + self.cur_lr, + ) + my_loss = my_loss.detach().cpu() + self.assertTrue(np.allclose(base_loss, my_loss.numpy())) + for key in ["ener", "force", "virial"]: + self.assertTrue( + np.allclose( + base_more_loss["l2_%s_loss" % key], my_more_loss["l2_%s_loss" % key] + ) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_lr.py b/source/tests/pt/test_lr.py new file mode 100644 index 0000000000..ca1ec7e490 --- /dev/null +++ b/source/tests/pt/test_lr.py @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np +import tensorflow.compat.v1 as tf + +tf.disable_eager_execution() + +from deepmd.pt.utils.learning_rate import ( + LearningRateExp, +) +from deepmd.tf.utils import ( + learning_rate, +) + + +class TestLearningRate(unittest.TestCase): + def setUp(self): + self.start_lr = 0.001 + self.stop_lr = 3.51e-8 + self.decay_steps = np.arange(400, 601, 100) + self.stop_steps = np.arange(500, 1600, 500) + + def test_consistency(self): + for decay_step in self.decay_steps: + for stop_step in self.stop_steps: + self.decay_step = decay_step + self.stop_step = stop_step + self.judge_it() + + def judge_it(self): + base_lr = learning_rate.LearningRateExp( + self.start_lr, self.stop_lr, self.decay_step + ) + g = tf.Graph() + with g.as_default(): + global_step = tf.placeholder(shape=[], dtype=tf.int32) + t_lr = base_lr.build(global_step, self.stop_step) + + my_lr = LearningRateExp( + self.start_lr, self.stop_lr, self.decay_step, self.stop_step + ) + with tf.Session(graph=g) as sess: + base_vals = [ + sess.run(t_lr, feed_dict={global_step: step_id}) + for step_id in range(self.stop_step) + if step_id % self.decay_step != 0 + ] + my_vals = [ + my_lr.value(step_id) + for step_id in range(self.stop_step) + if step_id % self.decay_step != 0 + ] + self.assertTrue(np.allclose(base_vals, my_vals)) + tf.reset_default_graph() + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_mlp.py b/source/tests/pt/test_mlp.py new file mode 100644 index 0000000000..c06047b2a5 --- /dev/null +++ b/source/tests/pt/test_mlp.py @@ -0,0 +1,321 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import itertools +import unittest + +import numpy as np +import torch + +from deepmd.pt.utils.env import ( + PRECISION_DICT, +) + +try: + from deepmd.pt.model.network.mlp import ( + MLP, + MLPLayer, + ) + + support_native_net = True +except ModuleNotFoundError: + support_native_net = False + +try: + from deepmd.pt.model.network.mlp import ( + EmbeddingNet, + ) + + support_embedding_net = True +except ModuleNotFoundError: + support_embedding_net = False + +try: + from deepmd.pt.model.network.mlp import ( + FittingNet, + ) + + support_fitting_net = True +except ModuleNotFoundError: + support_fitting_net = False + + +try: + from deepmd.model_format import ( + NativeLayer, + NativeNet, + ) + + support_native_net = True +except ModuleNotFoundError: + support_native_net = False +except ImportError: + support_native_net = False + +try: + from deepmd.model_format import EmbeddingNet as DPEmbeddingNet + + support_embedding_net = True +except ModuleNotFoundError: + support_embedding_net = False +except ImportError: + support_embedding_net = False + +try: + from deepmd.model_format import FittingNet as DPFittingNet + + support_fitting_net = True +except ModuleNotFoundError: + support_fitting_net = False +except ImportError: + support_fitting_net = False + + +def get_tols(prec): + if prec in ["single", "float32"]: + rtol, atol = 0.0, 1e-4 + elif prec in ["double", "float64"]: + rtol, atol = 0.0, 1e-12 + # elif prec in ["half", "float16"]: + # rtol, atol=1e-2, 0 + else: + raise ValueError(f"unknown prec {prec}") + return rtol, atol + + +@unittest.skipIf(not support_native_net, "NativeLayer not supported") +class TestMLPLayer(unittest.TestCase): + def setUp(self): + self.test_cases = itertools.product( + [(5, 5), (5, 10), (5, 8), (8, 5)], # inp, out + [True, False], # bias + [True, False], # use time step + ["tanh", "none"], # activation + [True, False], # resnet + [None, [4], [3, 2]], # prefix shapes + ["float32", "double"], # precision + ) + + def test_match_native_layer( + self, + ): + for (ninp, nout), bias, ut, ac, resnet, ashp, prec in self.test_cases: + # input + inp_shap = [ninp] + if ashp is not None: + inp_shap = ashp + inp_shap + rtol, atol = get_tols(prec) + dtype = PRECISION_DICT[prec] + xx = torch.arange(np.prod(inp_shap), dtype=dtype).view(inp_shap) + # def mlp layer + ml = MLPLayer(ninp, nout, bias, ut, ac, resnet, precision=prec) + # check consistency + nl = NativeLayer.deserialize(ml.serialize()) + np.testing.assert_allclose( + ml.forward(xx).detach().numpy(), + nl.call(xx.detach().numpy()), + rtol=rtol, + atol=atol, + err_msg=f"(i={ninp}, o={nout}) bias={bias} use_dt={ut} act={ac} resnet={resnet} prec={prec}", + ) + # check self-consistency + ml1 = MLPLayer.deserialize(ml.serialize()) + np.testing.assert_allclose( + ml.forward(xx).detach().numpy(), + ml1.forward(xx).detach().numpy(), + rtol=rtol, + atol=atol, + err_msg=f"(i={ninp}, o={nout}) bias={bias} use_dt={ut} act={ac} resnet={resnet} prec={prec}", + ) + + def test_jit(self): + for (ninp, nout), bias, ut, ac, resnet, _, prec in self.test_cases: + ml = MLPLayer(ninp, nout, bias, ut, ac, resnet, precision=prec) + model = torch.jit.script(ml) + ml1 = MLPLayer.deserialize(ml.serialize()) + model = torch.jit.script(ml1) + + +@unittest.skipIf(not support_native_net, "NativeLayer not supported") +class TestMLP(unittest.TestCase): + def setUp(self): + self.test_cases = itertools.product( + [[2, 2, 4, 8], [1, 3, 3]], # inp and hiddens + [True, False], # bias + [True, False], # use time step + ["tanh", "none"], # activation + [True, False], # resnet + [None, [4], [3, 2]], # prefix shapes + ["float32", "double"], # precision + ) + + def test_match_native_net( + self, + ): + for ndims, bias, ut, ac, resnet, ashp, prec in self.test_cases: + # input + inp_shap = [ndims[0]] + if ashp is not None: + inp_shap = ashp + inp_shap + rtol, atol = get_tols(prec) + dtype = PRECISION_DICT[prec] + xx = torch.arange(np.prod(inp_shap), dtype=dtype).view(inp_shap) + # def MLP + layers = [] + for ii in range(1, len(ndims)): + layers.append( + MLPLayer( + ndims[ii - 1], ndims[ii], bias, ut, ac, resnet, precision=prec + ).serialize() + ) + ml = MLP(layers) + # check consistency + nl = NativeNet.deserialize(ml.serialize()) + np.testing.assert_allclose( + ml.forward(xx).detach().numpy(), + nl.call(xx.detach().numpy()), + rtol=rtol, + atol=atol, + err_msg=f"net={ndims} bias={bias} use_dt={ut} act={ac} resnet={resnet} prec={prec}", + ) + # check self-consistency + ml1 = MLP.deserialize(ml.serialize()) + np.testing.assert_allclose( + ml.forward(xx).detach().numpy(), + ml1.forward(xx).detach().numpy(), + rtol=rtol, + atol=atol, + err_msg=f"net={ndims} bias={bias} use_dt={ut} act={ac} resnet={resnet} prec={prec}", + ) + + def test_jit(self): + for ndims, bias, ut, ac, resnet, _, prec in self.test_cases: + layers = [] + for ii in range(1, len(ndims)): + ml = layers.append( + MLPLayer( + ndims[ii - 1], ndims[ii], bias, ut, ac, resnet, precision=prec + ).serialize() + ) + ml = MLP(ml) + model = torch.jit.script(ml) + ml1 = MLP.deserialize(ml.serialize()) + model = torch.jit.script(ml1) + + +@unittest.skipIf(not support_embedding_net, "EmbeddingNet not supported") +class TestEmbeddingNet(unittest.TestCase): + def setUp(self): + self.test_cases = itertools.product( + [1, 3], # inp + [[24, 48, 96], [24, 36]], # and hiddens + ["tanh", "none"], # activation + [True, False], # resnet_dt + ["float32", "double"], # precision + ) + + def test_match_embedding_net( + self, + ): + for idim, nn, act, idt, prec in self.test_cases: + # input + rtol, atol = get_tols(prec) + dtype = PRECISION_DICT[prec] + xx = torch.arange(idim, dtype=dtype) + # def MLP + ml = EmbeddingNet(idim, nn, act, idt, prec) + # check consistency + nl = DPEmbeddingNet.deserialize(ml.serialize()) + np.testing.assert_allclose( + ml.forward(xx).detach().numpy(), + nl.call(xx.detach().numpy()), + rtol=rtol, + atol=atol, + err_msg=f"idim={idim} nn={nn} use_dt={idt} act={act} prec={prec}", + ) + # check self-consistency + ml1 = EmbeddingNet.deserialize(ml.serialize()) + np.testing.assert_allclose( + ml.forward(xx).detach().numpy(), + ml1.forward(xx).detach().numpy(), + rtol=rtol, + atol=atol, + err_msg=f"idim={idim} nn={nn} use_dt={idt} act={act} prec={prec}", + ) + + def test_jit( + self, + ): + for idim, nn, act, idt, prec in self.test_cases: + # def MLP + ml = EmbeddingNet(idim, nn, act, idt, prec) + ml1 = EmbeddingNet.deserialize(ml.serialize()) + model = torch.jit.script(ml) + model = torch.jit.script(ml1) + + +@unittest.skipIf(not support_fitting_net, "FittingNet not supported") +class TestFittingNet(unittest.TestCase): + def setUp(self): + self.test_cases = itertools.product( + [1, 3], # inp + [1, 5], # out + [[24, 48, 96], [24, 36]], # and hiddens + ["tanh", "none"], # activation + [True, False], # resnet_dt + ["float32", "double"], # precision + [True, False], # bias_out + ) + + def test_match_fitting_net( + self, + ): + for idim, odim, nn, act, idt, prec, ob in self.test_cases: + # input + rtol, atol = get_tols(prec) + dtype = PRECISION_DICT[prec] + xx = torch.arange(idim, dtype=dtype) + # def MLP + ml = FittingNet( + idim, + odim, + neuron=nn, + activation_function=act, + resnet_dt=idt, + precision=prec, + bias_out=ob, + ) + # check consistency + nl = DPFittingNet.deserialize(ml.serialize()) + np.testing.assert_allclose( + ml.forward(xx).detach().numpy(), + nl.call(xx.detach().numpy()), + rtol=rtol, + atol=atol, + err_msg=f"idim={idim} nn={nn} use_dt={idt} act={act} prec={prec}", + ) + # check self-consistency + ml1 = FittingNet.deserialize(ml.serialize()) + np.testing.assert_allclose( + ml.forward(xx).detach().numpy(), + ml1.forward(xx).detach().numpy(), + rtol=rtol, + atol=atol, + err_msg=f"idim={idim} nn={nn} use_dt={idt} act={act} prec={prec}", + ) + + def test_jit( + self, + ): + for idim, odim, nn, act, idt, prec, ob in self.test_cases: + # def MLP + ml = FittingNet( + idim, + odim, + neuron=nn, + activation_function=act, + resnet_dt=idt, + precision=prec, + bias_out=ob, + ) + ml1 = FittingNet.deserialize(ml.serialize()) + model = torch.jit.script(ml) + model = torch.jit.script(ml1) diff --git a/source/tests/pt/test_model.py b/source/tests/pt/test_model.py new file mode 100644 index 0000000000..5bbbc9e352 --- /dev/null +++ b/source/tests/pt/test_model.py @@ -0,0 +1,415 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import collections +import json +import unittest + +import numpy as np +import tensorflow.compat.v1 as tf +import torch + +tf.disable_eager_execution() + +from pathlib import ( + Path, +) + +from deepmd.pt.loss import ( + EnergyStdLoss, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils.dataloader import ( + DpLoaderSet, +) +from deepmd.pt.utils.env import ( + DEVICE, +) +from deepmd.pt.utils.learning_rate import LearningRateExp as MyLRExp +from deepmd.pt.utils.stat import ( + make_stat_input, +) +from deepmd.tf.common import ( + data_requirement, + expand_sys_str, +) +from deepmd.tf.descriptor import DescrptSeA as DescrptSeA_tf +from deepmd.tf.fit import ( + EnerFitting, +) +from deepmd.tf.loss import ( + EnerStdLoss, +) +from deepmd.tf.model import ( + EnerModel, +) +from deepmd.tf.utils.data_system import ( + DeepmdDataSystem, +) +from deepmd.tf.utils.learning_rate import ( + LearningRateExp, +) + +VariableState = collections.namedtuple("VariableState", ["value", "gradient"]) + + +def torch2tf(torch_name): + fields = torch_name.split(".") + offset = int(fields[2] == "networks") + element_id = int(fields[2 + offset]) + if fields[0] == "descriptor": + layer_id = int(fields[4 + offset]) + 1 + weight_type = fields[5 + offset] + return "filter_type_all/%s_%d_%d:0" % (weight_type, layer_id, element_id) + elif fields[3] == "deep_layers": + layer_id = int(fields[4]) + weight_type = fields[5] + return "layer_%d_type_%d/%s:0" % (layer_id, element_id, weight_type) + elif fields[3] == "final_layer": + weight_type = fields[4] + return "final_layer_type_%d/%s:0" % (element_id, weight_type) + else: + raise RuntimeError("Unexpected parameter name: %s" % torch_name) + + +class DpTrainer: + def __init__(self): + with open(str(Path(__file__).parent / "water/se_e2_a.json")) as fin: + content = fin.read() + config = json.loads(content) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + config["training"]["training_data"]["systems"] = data_file + config["training"]["validation_data"]["systems"] = data_file + model_config = config["model"] + self.rcut = model_config["descriptor"]["rcut"] + self.rcut_smth = model_config["descriptor"]["rcut_smth"] + self.sel = model_config["descriptor"]["sel"] + self.systems = config["training"]["validation_data"]["systems"] + if isinstance(self.systems, str): + self.systems = expand_sys_str(self.systems) + self.batch_size = config["training"]["training_data"]["batch_size"] + self.type_map = model_config["type_map"] + self.filter_neuron = model_config["descriptor"]["neuron"] + self.axis_neuron = model_config["descriptor"]["axis_neuron"] + self.n_neuron = model_config["fitting_net"]["neuron"] + self.data_stat_nbatch = 3 + self.start_lr = 0.001 + self.stop_lr = 3.51e-8 + self.decay_steps = 500 + self.stop_steps = 1600 + self.start_pref_e = 1.0 + self.limit_pref_e = 2.0 + self.start_pref_f = 2.0 + self.limit_pref_f = 1.0 + self.ntypes = len(self.type_map) + + def get_intermediate_state(self, num_steps=1): + dp_model = self._get_dp_model() + dp_loss = self._get_dp_loss() + dp_lr = self._get_dp_lr() + dp_ds = self._get_dp_dataset() + dp_model.data_stat(dp_ds) + + # Build graph + g = tf.Graph() + with g.as_default(): + place_holders = self._get_dp_placeholders(dp_ds) + model_pred = dp_model.build( + coord_=place_holders["coord"], + atype_=place_holders["type"], + natoms=place_holders["natoms_vec"], + box=place_holders["box"], + mesh=place_holders["default_mesh"], + input_dict=place_holders, + ) + global_step = tf.train.get_or_create_global_step() + learning_rate = dp_lr.build(global_step, self.stop_steps) + l2_l, _ = dp_loss.build( + learning_rate=learning_rate, + natoms=place_holders["natoms_vec"], + model_dict=model_pred, + label_dict=place_holders, + suffix="test", + ) + t_vars = tf.trainable_variables() + optimizer = tf.train.AdamOptimizer(learning_rate) + t_grad_and_vars = optimizer.compute_gradients(l2_l, t_vars) + train_op = optimizer.apply_gradients(t_grad_and_vars, global_step) + init_op = tf.global_variables_initializer() + t_heads = { + "loss": l2_l, + "energy": model_pred["energy"], + "force": model_pred["force"], + "virial": model_pred["virial"], + "atomic_virial": model_pred["atom_virial"], + } + + # Get statistics of each component + stat_dict = { + "descriptor.mean": dp_model.descrpt.davg, + "descriptor.stddev": dp_model.descrpt.dstd, + "fitting_net.bias_atom_e": dp_model.fitting.bias_atom_e, + } + + # Get variables and their gradients + with tf.Session(graph=g) as sess: + sess.run(init_op) + for _ in range(num_steps): + batch = dp_ds.get_batch() + feeds = self._get_feed_dict(batch, place_holders) + sess.run(train_op, feed_dict=feeds) + + batch = dp_ds.get_batch() + feeds = self._get_feed_dict(batch, place_holders) + grads_and_vars, head_dict = sess.run( + [t_grad_and_vars, t_heads], feed_dict=feeds + ) + vs_dict = {} + for idx, one in enumerate(t_vars): + grad, var = grads_and_vars[idx] + vs_dict[one.name] = VariableState(var, grad) + + tf.reset_default_graph() + # Used for reproducing + return batch, head_dict, stat_dict, vs_dict + + def _get_dp_dataset(self): + data = DeepmdDataSystem( + systems=self.systems, + batch_size=self.batch_size, + test_size=1, + rcut=self.rcut, + type_map=self.type_map, + trn_all_set=True, + ) + data.add_dict(data_requirement) + return data + + def _get_dp_model(self): + dp_descrpt = DescrptSeA_tf( + rcut=self.rcut, + rcut_smth=self.rcut_smth, + sel=self.sel, + neuron=self.filter_neuron, + axis_neuron=self.axis_neuron, + ) + dp_fitting = EnerFitting(descrpt=dp_descrpt, neuron=self.n_neuron) + return EnerModel( + dp_descrpt, + dp_fitting, + type_map=self.type_map, + data_stat_nbatch=self.data_stat_nbatch, + ) + + def _get_dp_loss(self): + return EnerStdLoss( + starter_learning_rate=self.start_lr, + start_pref_e=self.start_pref_e, + limit_pref_e=self.limit_pref_e, + start_pref_f=self.start_pref_f, + limit_pref_f=self.limit_pref_f, + ) + + def _get_dp_lr(self): + return LearningRateExp( + start_lr=self.start_lr, stop_lr=self.stop_lr, decay_steps=self.decay_steps + ) + + def _get_dp_placeholders(self, dataset): + place_holders = {} + data_dict = dataset.get_data_dict() + for kk in data_dict.keys(): + if kk == "type": + continue + prec = tf.float64 + place_holders[kk] = tf.placeholder(prec, [None], name="t_" + kk) + place_holders["find_" + kk] = tf.placeholder( + tf.float32, name="t_find_" + kk + ) + place_holders["type"] = tf.placeholder(tf.int32, [None], name="t_type") + place_holders["natoms_vec"] = tf.placeholder( + tf.int32, [self.ntypes + 2], name="t_natoms" + ) + place_holders["default_mesh"] = tf.placeholder(tf.int32, [None], name="t_mesh") + place_holders["is_training"] = tf.placeholder(tf.bool) + return place_holders + + def _get_feed_dict(self, batch, place_holders): + feed_dict = {} + for kk in batch.keys(): + if kk == "find_type" or kk == "type": + continue + if "find_" in kk: + feed_dict[place_holders[kk]] = batch[kk] + else: + feed_dict[place_holders[kk]] = np.reshape(batch[kk], [-1]) + for ii in ["type"]: + feed_dict[place_holders[ii]] = np.reshape(batch[ii], [-1]) + for ii in ["natoms_vec", "default_mesh"]: + feed_dict[place_holders[ii]] = batch[ii] + feed_dict[place_holders["is_training"]] = True + return feed_dict + + +class TestEnergy(unittest.TestCase): + def setUp(self): + self.dp_trainer = DpTrainer() + self.wanted_step = 0 + for key in dir(self.dp_trainer): + if not key.startswith("_") or key == "get_intermediate_state": + value = getattr(self.dp_trainer, key) + setattr(self, key, value) + + def test_consistency(self): + batch, head_dict, stat_dict, vs_dict = self.dp_trainer.get_intermediate_state( + self.wanted_step + ) + # Build DeePMD graph + my_ds = DpLoaderSet( + self.systems, + self.batch_size, + model_params={ + "descriptor": { + "type": "se_e2_a", + "sel": self.sel, + "rcut": self.rcut, + }, + "type_map": self.type_map, + }, + ) + sampled = make_stat_input( + my_ds.systems, my_ds.dataloaders, self.data_stat_nbatch + ) + my_model = get_model( + model_params={ + "descriptor": { + "type": "se_e2_a", + "sel": self.sel, + "rcut_smth": self.rcut_smth, + "rcut": self.rcut, + "neuron": self.filter_neuron, + "axis_neuron": self.axis_neuron, + }, + "fitting_net": {"neuron": self.n_neuron}, + "data_stat_nbatch": self.data_stat_nbatch, + "type_map": self.type_map, + }, + sampled=sampled, + ) + my_model.to(DEVICE) + my_lr = MyLRExp(self.start_lr, self.stop_lr, self.decay_steps, self.stop_steps) + my_loss = EnergyStdLoss( + starter_learning_rate=self.start_lr, + start_pref_e=self.start_pref_e, + limit_pref_e=self.limit_pref_e, + start_pref_f=self.start_pref_f, + limit_pref_f=self.limit_pref_f, + ) + + # Keep statistics consistency between 2 implentations + my_em = my_model.descriptor + mean = stat_dict["descriptor.mean"].reshape([self.ntypes, my_em.get_nsel(), 4]) + stddev = stat_dict["descriptor.stddev"].reshape( + [self.ntypes, my_em.get_nsel(), 4] + ) + my_em.set_stat_mean_and_stddev( + torch.tensor(mean, device=DEVICE), + torch.tensor(stddev, device=DEVICE), + ) + my_model.fitting_net.bias_atom_e = torch.tensor( + stat_dict["fitting_net.bias_atom_e"], device=DEVICE + ) + + # Keep parameter value consistency between 2 implentations + for name, param in my_model.named_parameters(): + name = name.replace("sea.", "") + var_name = torch2tf(name) + var = vs_dict[var_name].value + with torch.no_grad(): + src = torch.from_numpy(var) + dst = param.data + # print(name) + # print(src.mean(), src.std()) + # print(dst.mean(), dst.std()) + dst.copy_(src) + # Start forward computing + batch = my_ds.systems[0]._data_system.preprocess(batch) + batch["coord"].requires_grad_(True) + batch["natoms"] = torch.tensor( + batch["natoms_vec"], device=batch["coord"].device + ).unsqueeze(0) + model_predict = my_model( + batch["coord"], batch["atype"], batch["box"], do_atomic_virial=True + ) + model_predict_1 = my_model( + batch["coord"], batch["atype"], batch["box"], do_atomic_virial=False + ) + p_energy, p_force, p_virial, p_atomic_virial = ( + model_predict["energy"], + model_predict["force"], + model_predict["virial"], + model_predict["atomic_virial"], + ) + cur_lr = my_lr.value(self.wanted_step) + model_pred = { + "energy": p_energy, + "force": p_force, + } + label = { + "energy": batch["energy"], + "force": batch["force"], + } + loss, _ = my_loss(model_pred, label, int(batch["natoms"][0, 0]), cur_lr) + np.testing.assert_allclose( + head_dict["energy"], p_energy.view(-1).cpu().detach().numpy() + ) + np.testing.assert_allclose( + head_dict["force"], + p_force.view(*head_dict["force"].shape).cpu().detach().numpy(), + ) + rtol = 1e-5 + atol = 1e-8 + np.testing.assert_allclose( + head_dict["loss"], loss.cpu().detach().numpy(), rtol=rtol, atol=atol + ) + np.testing.assert_allclose( + head_dict["virial"], + p_virial.view(*head_dict["virial"].shape).cpu().detach().numpy(), + ) + np.testing.assert_allclose( + head_dict["virial"], + model_predict_1["virial"] + .view(*head_dict["virial"].shape) + .cpu() + .detach() + .numpy(), + ) + self.assertIsNone(model_predict_1.get("atomic_virial", None)) + np.testing.assert_allclose( + head_dict["atomic_virial"], + p_atomic_virial.view(*head_dict["atomic_virial"].shape) + .cpu() + .detach() + .numpy(), + ) + optimizer = torch.optim.Adam(my_model.parameters(), lr=cur_lr) + optimizer.zero_grad() + + def step(step_id): + bdata = self.training_data.get_trainning_batch() + optimizer.zero_grad() + + # Compare gradient for consistency + loss.backward() + + for name, param in my_model.named_parameters(): + name = name.replace("sea.", "") + var_name = torch2tf(name) + var_grad = vs_dict[var_name].gradient + param_grad = param.grad.cpu() + var_grad = torch.tensor(var_grad) + assert np.allclose(var_grad, param_grad, rtol=rtol, atol=atol) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_nlist.py b/source/tests/pt/test_nlist.py new file mode 100644 index 0000000000..27c03acfaa --- /dev/null +++ b/source/tests/pt/test_nlist.py @@ -0,0 +1,212 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import torch + +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.nlist import ( + build_multiple_neighbor_list, + build_neighbor_list, + extend_coord_with_ghosts, + get_multiple_nlist_key, +) +from deepmd.pt.utils.region import ( + inter2phys, +) + +dtype = torch.float64 + + +class TestNeighList(unittest.TestCase): + def setUp(self): + self.nf = 3 + self.nloc = 2 + self.ns = 5 * 5 * 3 + self.nall = self.ns * self.nloc + self.cell = torch.tensor( + [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype + ).to(env.DEVICE) + self.icoord = torch.tensor([[0, 0, 0], [0.5, 0.5, 0.1]], dtype=dtype).to( + env.DEVICE + ) + self.atype = torch.tensor([0, 1], dtype=torch.int).to(env.DEVICE) + [self.cell, self.icoord, self.atype] = [ + ii.unsqueeze(0) for ii in [self.cell, self.icoord, self.atype] + ] + self.coord = inter2phys(self.icoord, self.cell).view([-1, self.nloc * 3]) + self.cell = self.cell.view([-1, 9]) + [self.cell, self.coord, self.atype] = [ + torch.tile(ii, [self.nf, 1]) for ii in [self.cell, self.coord, self.atype] + ] + self.rcut = 1.01 + self.prec = 1e-10 + self.nsel = [10, 10] + # genrated by preprocess.build_neighbor_list + # ref_nlist, _, _ = legacy_build_neighbor_list( + # 2, ecoord[0], eatype[0], + # self.rcut, + # torch.tensor([10,20], dtype=torch.long), + # mapping[0], type_split=True, ) + self.ref_nlist = torch.tensor( + [ + [0, 0, 0, 0, 0, 0, -1, -1, -1, -1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1], + [0, 0, 0, 0, -1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1], + ] + ).to(env.DEVICE) + + def test_build_notype(self): + ecoord, eatype, mapping = extend_coord_with_ghosts( + self.coord, self.atype, self.cell, self.rcut + ) + nlist = build_neighbor_list( + ecoord, + eatype, + self.nloc, + self.rcut, + sum(self.nsel), + distinguish_types=False, + ) + torch.testing.assert_close(nlist[0], nlist[1]) + nlist_mask = nlist[0] == -1 + nlist_loc = mapping[0][nlist[0]] + nlist_loc[nlist_mask] = -1 + torch.testing.assert_close( + torch.sort(nlist_loc, dim=-1)[0], + torch.sort(self.ref_nlist, dim=-1)[0], + ) + + def test_build_type(self): + ecoord, eatype, mapping = extend_coord_with_ghosts( + self.coord, self.atype, self.cell, self.rcut + ) + nlist = build_neighbor_list( + ecoord, + eatype, + self.nloc, + self.rcut, + self.nsel, + distinguish_types=True, + ) + torch.testing.assert_close(nlist[0], nlist[1]) + nlist_mask = nlist[0] == -1 + nlist_loc = mapping[0][nlist[0]] + nlist_loc[nlist_mask] = -1 + for ii in range(2): + torch.testing.assert_close( + torch.sort(torch.split(nlist_loc, self.nsel, dim=-1)[ii], dim=-1)[0], + torch.sort(torch.split(self.ref_nlist, self.nsel, dim=-1)[ii], dim=-1)[ + 0 + ], + ) + + def test_build_multiple_nlist(self): + rcuts = [1.01, 2.01] + nsels = [20, 80] + ecoord, eatype, mapping = extend_coord_with_ghosts( + self.coord, self.atype, self.cell, max(rcuts) + ) + nlist1 = build_neighbor_list( + ecoord, + eatype, + self.nloc, + rcuts[1], + nsels[1] - 1, + distinguish_types=False, + ) + pad = -1 * torch.ones( + [self.nf, self.nloc, 1], dtype=nlist1.dtype, device=nlist1.device + ) + nlist2 = torch.cat([nlist1, pad], dim=-1) + nlist0 = build_neighbor_list( + ecoord, + eatype, + self.nloc, + rcuts[0], + nsels[0], + distinguish_types=False, + ) + nlists = build_multiple_neighbor_list(ecoord, nlist1, rcuts, nsels) + for dd in range(2): + self.assertEqual( + nlists[get_multiple_nlist_key(rcuts[dd], nsels[dd])].shape[-1], + nsels[dd], + ) + torch.testing.assert_close( + nlists[get_multiple_nlist_key(rcuts[0], nsels[0])], + nlist0, + ) + torch.testing.assert_close( + nlists[get_multiple_nlist_key(rcuts[1], nsels[1])], + nlist2, + ) + + def test_extend_coord(self): + ecoord, eatype, mapping = extend_coord_with_ghosts( + self.coord, self.atype, self.cell, self.rcut + ) + # expected ncopy x nloc + self.assertEqual(list(ecoord.shape), [self.nf, self.nall * 3]) + self.assertEqual(list(eatype.shape), [self.nf, self.nall]) + self.assertEqual(list(mapping.shape), [self.nf, self.nall]) + # check the nloc part is identical with original coord + torch.testing.assert_close( + ecoord[:, : self.nloc * 3], self.coord, rtol=self.prec, atol=self.prec + ) + # check the shift vectors are aligned with grid + shift_vec = ( + ecoord.view([-1, self.ns, self.nloc, 3]) + - self.coord.view([-1, self.nloc, 3])[:, None, :, :] + ) + shift_vec = shift_vec.view([-1, self.nall, 3]) + # hack!!! assumes identical cell across frames + shift_vec = torch.matmul( + shift_vec, torch.linalg.inv(self.cell.view([self.nf, 3, 3])[0]) + ) + # nf x nall x 3 + shift_vec = torch.round(shift_vec) + # check: identical shift vecs + torch.testing.assert_close( + shift_vec[0], shift_vec[1], rtol=self.prec, atol=self.prec + ) + # check: shift idx aligned with grid + mm, cc = torch.unique(shift_vec[0][:, 0], dim=-1, return_counts=True) + torch.testing.assert_close( + mm, + torch.tensor([-2, -1, 0, 1, 2], dtype=dtype).to(env.DEVICE), + rtol=self.prec, + atol=self.prec, + ) + torch.testing.assert_close( + cc, + torch.tensor([30, 30, 30, 30, 30], dtype=torch.long).to(env.DEVICE), + rtol=self.prec, + atol=self.prec, + ) + mm, cc = torch.unique(shift_vec[1][:, 1], dim=-1, return_counts=True) + torch.testing.assert_close( + mm, + torch.tensor([-2, -1, 0, 1, 2], dtype=dtype).to(env.DEVICE), + rtol=self.prec, + atol=self.prec, + ) + torch.testing.assert_close( + cc, + torch.tensor([30, 30, 30, 30, 30], dtype=torch.long).to(env.DEVICE), + rtol=self.prec, + atol=self.prec, + ) + mm, cc = torch.unique(shift_vec[1][:, 2], dim=-1, return_counts=True) + torch.testing.assert_close( + mm, + torch.tensor([-1, 0, 1], dtype=dtype).to(env.DEVICE), + rtol=self.prec, + atol=self.prec, + ) + torch.testing.assert_close( + cc, + torch.tensor([50, 50, 50], dtype=torch.long).to(env.DEVICE), + rtol=self.prec, + atol=self.prec, + ) diff --git a/source/tests/pt/test_permutation.py b/source/tests/pt/test_permutation.py new file mode 100644 index 0000000000..b9724bb2af --- /dev/null +++ b/source/tests/pt/test_permutation.py @@ -0,0 +1,322 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest +from pathlib import ( + Path, +) + +import torch + +from deepmd.pt.infer.deep_eval import ( + eval_model, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.dataloader import ( + DpLoaderSet, +) +from deepmd.pt.utils.stat import ( + make_stat_input, +) + +dtype = torch.float64 + +model_se_e2_a = { + "type_map": ["O", "H", "B"], + "descriptor": { + "type": "se_e2_a", + "sel": [46, 92, 4], + "rcut_smth": 0.50, + "rcut": 6.00, + "neuron": [25, 50, 100], + "resnet_dt": False, + "axis_neuron": 16, + "seed": 1, + }, + "fitting_net": { + "neuron": [24, 24, 24], + "resnet_dt": True, + "seed": 1, + }, + "data_stat_nbatch": 20, +} + +model_dpa2 = { + "type_map": ["O", "H", "B"], + "descriptor": { + "type": "dpa2", + "repinit_rcut": 6.0, + "repinit_rcut_smth": 2.0, + "repinit_nsel": 30, + "repformer_rcut": 4.0, + "repformer_rcut_smth": 0.5, + "repformer_nsel": 20, + "repinit_neuron": [2, 4, 8], + "repinit_axis_neuron": 4, + "repinit_activation": "tanh", + "repformer_nlayers": 12, + "repformer_g1_dim": 8, + "repformer_g2_dim": 5, + "repformer_attn2_hidden": 3, + "repformer_attn2_nhead": 1, + "repformer_attn1_hidden": 5, + "repformer_attn1_nhead": 1, + "repformer_axis_dim": 4, + "repformer_update_h2": False, + "repformer_update_g1_has_conv": True, + "repformer_update_g1_has_grrg": True, + "repformer_update_g1_has_drrd": True, + "repformer_update_g1_has_attn": True, + "repformer_update_g2_has_g1g1": True, + "repformer_update_g2_has_attn": True, + "repformer_attn2_has_gate": True, + "repformer_add_type_ebd_to_seq": False, + }, + "fitting_net": { + "neuron": [24, 24], + "resnet_dt": True, + "seed": 1, + }, +} + +model_dpa1 = { + "type_map": ["O", "H", "B"], + "descriptor": { + "type": "se_atten", + "sel": 40, + "rcut_smth": 0.5, + "rcut": 4.0, + "neuron": [25, 50, 100], + "axis_neuron": 16, + "attn": 64, + "attn_layer": 2, + "attn_dotr": True, + "attn_mask": False, + "post_ln": True, + "ffn": False, + "ffn_embed_dim": 512, + "activation": "tanh", + "scaling_factor": 1.0, + "head_num": 1, + "normalize": False, + "temperature": 1.0, + "set_davg_zero": True, + }, + "fitting_net": { + "neuron": [24, 24, 24], + "resnet_dt": True, + "seed": 1, + }, +} + + +model_hybrid = { + "type_map": ["O", "H", "B"], + "descriptor": { + "type": "hybrid", + "list": [ + { + "type": "se_atten", + "sel": 120, + "rcut_smth": 0.5, + "rcut": 6.0, + "neuron": [25, 50, 100], + "axis_neuron": 16, + "attn": 128, + "attn_layer": 0, + "attn_dotr": True, + "attn_mask": False, + "post_ln": True, + "ffn": False, + "ffn_embed_dim": 1024, + "activation": "tanh", + "scaling_factor": 1.0, + "head_num": 1, + "normalize": True, + "temperature": 1.0, + }, + { + "type": "dpa2", + "repinit_rcut": 6.0, + "repinit_rcut_smth": 2.0, + "repinit_nsel": 30, + "repformer_rcut": 4.0, + "repformer_rcut_smth": 0.5, + "repformer_nsel": 10, + "repinit_neuron": [2, 4, 8], + "repinit_axis_neuron": 4, + "repinit_activation": "tanh", + "repformer_nlayers": 12, + "repformer_g1_dim": 8, + "repformer_g2_dim": 5, + "repformer_attn2_hidden": 3, + "repformer_attn2_nhead": 1, + "repformer_attn1_hidden": 5, + "repformer_attn1_nhead": 1, + "repformer_axis_dim": 4, + "repformer_update_h2": False, + "repformer_update_g1_has_conv": True, + "repformer_update_g1_has_grrg": True, + "repformer_update_g1_has_drrd": True, + "repformer_update_g1_has_attn": True, + "repformer_update_g2_has_g1g1": True, + "repformer_update_g2_has_attn": True, + "repformer_attn2_has_gate": True, + "repformer_add_type_ebd_to_seq": False, + }, + ], + }, + "fitting_net": { + "neuron": [240, 240, 240], + "resnet_dt": True, + "seed": 1, + "_comment": " that's all", + }, + "_comment": " that's all", +} + + +def make_sample(model_params): + training_systems = [ + str(Path(__file__).parent / "water/data/data_0"), + ] + data_stat_nbatch = model_params.get("data_stat_nbatch", 10) + train_data = DpLoaderSet( + training_systems, + batch_size=4, + model_params=model_params.copy(), + ) + sampled = make_stat_input( + train_data.systems, train_data.dataloaders, data_stat_nbatch + ) + return sampled + + +class PermutationTest: + def test( + self, + ): + natoms = 5 + cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) + cell = (cell + cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) + coord = torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) + coord = torch.matmul(coord, cell) + atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + idx_perm = [1, 0, 4, 3, 2] + e0, f0, v0 = eval_model( + self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype + ) + ret0 = { + "energy": e0.squeeze(0), + "force": f0.squeeze(0), + "virial": v0.squeeze(0), + } + e1, f1, v1 = eval_model( + self.model, coord[idx_perm].unsqueeze(0), cell.unsqueeze(0), atype[idx_perm] + ) + ret1 = { + "energy": e1.squeeze(0), + "force": f1.squeeze(0), + "virial": v1.squeeze(0), + } + prec = 1e-10 + torch.testing.assert_close(ret0["energy"], ret1["energy"], rtol=prec, atol=prec) + torch.testing.assert_close( + ret0["force"][idx_perm], ret1["force"], rtol=prec, atol=prec + ) + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + ret0["virial"], ret1["virial"], rtol=prec, atol=prec + ) + + +class TestEnergyModelSeA(unittest.TestCase, PermutationTest): + def setUp(self): + model_params = copy.deepcopy(model_se_e2_a) + sampled = make_sample(model_params) + self.type_split = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestEnergyModelDPA1(unittest.TestCase, PermutationTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa1) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestEnergyModelDPA2(unittest.TestCase, PermutationTest): + def setUp(self): + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + model_params = copy.deepcopy(model_dpa2) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestForceModelDPA2(unittest.TestCase, PermutationTest): + def setUp(self): + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + model_params = copy.deepcopy(model_dpa2) + model_params["fitting_net"]["type"] = "direct_force_ener" + self.type_split = True + self.test_virial = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +@unittest.skip("hybrid not supported at the moment") +class TestEnergyModelHybrid(unittest.TestCase, PermutationTest): + def setUp(self): + model_params = copy.deepcopy(model_hybrid) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +@unittest.skip("hybrid not supported at the moment") +class TestForceModelHybrid(unittest.TestCase, PermutationTest): + def setUp(self): + model_params = copy.deepcopy(model_hybrid) + model_params["fitting_net"]["type"] = "direct_force_ener" + sampled = make_sample(model_params) + self.type_split = True + self.test_virial = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +# class TestEnergyFoo(unittest.TestCase): +# def test(self): +# model_params = model_dpau +# sampled = make_sample(model_params) +# self.model = EnergyModelDPAUni(model_params, sampled).to(env.DEVICE) + +# natoms = 5 +# cell = torch.rand([3, 3], dtype=dtype) +# cell = (cell + cell.T) + 5. * torch.eye(3) +# coord = torch.rand([natoms, 3], dtype=dtype) +# coord = torch.matmul(coord, cell) +# atype = torch.IntTensor([0, 0, 0, 1, 1]) +# idx_perm = [1, 0, 4, 3, 2] +# ret0 = infer_model(self.model, coord, cell, atype, type_split=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_permutation_denoise.py b/source/tests/pt/test_permutation_denoise.py new file mode 100644 index 0000000000..47bd0360f2 --- /dev/null +++ b/source/tests/pt/test_permutation_denoise.py @@ -0,0 +1,102 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import torch + +from deepmd.pt.infer.deep_eval import ( + eval_model, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) + +from .test_permutation import ( # model_dpau, + make_sample, + model_dpa1, + model_dpa2, + model_hybrid, +) + +dtype = torch.float64 + +model_dpa1 = copy.deepcopy(model_dpa1) +model_dpa2 = copy.deepcopy(model_dpa2) +model_hybrid = copy.deepcopy(model_hybrid) +model_dpa1["type_map"] = ["O", "H", "B", "MASKED_TOKEN"] +model_dpa1.pop("fitting_net") +model_dpa2["type_map"] = ["O", "H", "B", "MASKED_TOKEN"] +model_dpa2.pop("fitting_net") +model_hybrid["type_map"] = ["O", "H", "B", "MASKED_TOKEN"] +model_hybrid.pop("fitting_net") + + +class PermutationDenoiseTest: + def test( + self, + ): + natoms = 5 + cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) + cell = (cell + cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) + coord = torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) + coord = torch.matmul(coord, cell) + atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + idx_perm = [1, 0, 4, 3, 2] + updated_c0, logits0 = eval_model( + self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype, denoise=True + ) + ret0 = {"updated_coord": updated_c0.squeeze(0), "logits": logits0.squeeze(0)} + updated_c1, logits1 = eval_model( + self.model, + coord[idx_perm].unsqueeze(0), + cell.unsqueeze(0), + atype[idx_perm], + denoise=True, + ) + ret1 = {"updated_coord": updated_c1.squeeze(0), "logits": logits1.squeeze(0)} + prec = 1e-10 + torch.testing.assert_close( + ret0["updated_coord"][idx_perm], ret1["updated_coord"], rtol=prec, atol=prec + ) + torch.testing.assert_close( + ret0["logits"][idx_perm], ret1["logits"], rtol=prec, atol=prec + ) + + +class TestDenoiseModelDPA1(unittest.TestCase, PermutationDenoiseTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa1) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestDenoiseModelDPA2(unittest.TestCase, PermutationDenoiseTest): + def setUp(self): + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + model_params = copy.deepcopy(model_dpa2) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +# @unittest.skip("hybrid not supported at the moment") +# class TestDenoiseModelHybrid(unittest.TestCase, TestPermutationDenoise): +# def setUp(self): +# model_params = copy.deepcopy(model_hybrid_denoise) +# sampled = make_sample(model_params) +# self.type_split = True +# self.model = get_model(model_params, sampled).to(env.DEVICE) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_region.py b/source/tests/pt/test_region.py new file mode 100644 index 0000000000..e8a3346562 --- /dev/null +++ b/source/tests/pt/test_region.py @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np +import torch + +from deepmd.pt.utils.preprocess import ( + Region3D, +) +from deepmd.pt.utils.region import ( + inter2phys, + to_face_distance, +) + +dtype = torch.float64 + + +class TestRegion(unittest.TestCase): + def setUp(self): + self.cell = torch.tensor( + [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype + ) + self.cell = self.cell.unsqueeze(0).unsqueeze(0) + self.cell = torch.tile(self.cell, [4, 5, 1, 1]) + self.prec = 1e-8 + + def test_inter_to_phys(self): + inter = torch.rand([4, 5, 3, 3], dtype=dtype) + phys = inter2phys(inter, self.cell) + for ii in range(4): + for jj in range(5): + expected_phys = torch.matmul(inter[ii, jj], self.cell[ii, jj]) + torch.testing.assert_close( + phys[ii, jj], expected_phys, rtol=self.prec, atol=self.prec + ) + + def test_to_face_dist(self): + cell0 = self.cell[0][0].numpy() + vol = np.linalg.det(cell0) + # area of surfaces xy, xz, yz + sxy = np.linalg.norm(np.cross(cell0[0], cell0[1])) + sxz = np.linalg.norm(np.cross(cell0[0], cell0[2])) + syz = np.linalg.norm(np.cross(cell0[1], cell0[2])) + # vol / area gives distance + dz = vol / sxy + dy = vol / sxz + dx = vol / syz + expected = torch.tensor([dx, dy, dz]) + dists = to_face_distance(self.cell) + for ii in range(4): + for jj in range(5): + torch.testing.assert_close( + dists[ii][jj], expected, rtol=self.prec, atol=self.prec + ) + + +class TestLegacyRegion(unittest.TestCase): + def setUp(self): + self.cell = torch.tensor( + [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype + ) + self.prec = 1e-6 + + def test_inter_to_phys(self): + inter = torch.rand([3, 3], dtype=dtype) + reg = Region3D(self.cell) + phys = reg.inter2phys(inter) + expected_phys = torch.matmul(inter, self.cell) + torch.testing.assert_close(phys, expected_phys, rtol=self.prec, atol=self.prec) + + def test_inter_to_inter(self): + inter = torch.rand([3, 3], dtype=dtype) + reg = Region3D(self.cell) + new_inter = reg.phys2inter(reg.inter2phys(inter)) + torch.testing.assert_close(inter, new_inter, rtol=self.prec, atol=self.prec) + + def test_to_face_dist(self): + pass diff --git a/source/tests/pt/test_rot.py b/source/tests/pt/test_rot.py new file mode 100644 index 0000000000..b5d9d9b64b --- /dev/null +++ b/source/tests/pt/test_rot.py @@ -0,0 +1,181 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import torch + +from deepmd.pt.infer.deep_eval import ( + eval_model, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) + +from .test_permutation import ( # model_dpau, + make_sample, + model_dpa1, + model_dpa2, + model_hybrid, + model_se_e2_a, +) + +dtype = torch.float64 + + +class RotTest: + def test( + self, + ): + prec = 1e-10 + natoms = 5 + cell = 10.0 * torch.eye(3, dtype=dtype).to(env.DEVICE) + coord = 2 * torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) + shift = torch.tensor([4, 4, 4], dtype=dtype).to(env.DEVICE) + atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + from scipy.stats import ( + special_ortho_group, + ) + + rmat = torch.tensor(special_ortho_group.rvs(3), dtype=dtype).to(env.DEVICE) + + # rotate only coord and shift to the center of cell + coord_rot = torch.matmul(coord, rmat) + e0, f0, v0 = eval_model( + self.model, (coord + shift).unsqueeze(0), cell.unsqueeze(0), atype + ) + ret0 = { + "energy": e0.squeeze(0), + "force": f0.squeeze(0), + "virial": v0.squeeze(0), + } + e1, f1, v1 = eval_model( + self.model, (coord_rot + shift).unsqueeze(0), cell.unsqueeze(0), atype + ) + ret1 = { + "energy": e1.squeeze(0), + "force": f1.squeeze(0), + "virial": v1.squeeze(0), + } + torch.testing.assert_close(ret0["energy"], ret1["energy"], rtol=prec, atol=prec) + torch.testing.assert_close( + torch.matmul(ret0["force"], rmat), ret1["force"], rtol=prec, atol=prec + ) + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + torch.matmul(rmat.T, torch.matmul(ret0["virial"], rmat)), + ret1["virial"], + rtol=prec, + atol=prec, + ) + + # rotate coord and cell + torch.manual_seed(0) + cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) + cell = (cell + cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) + coord = torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) + coord = torch.matmul(coord, cell) + atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + coord_rot = torch.matmul(coord, rmat) + cell_rot = torch.matmul(cell, rmat) + e0, f0, v0 = eval_model( + self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype + ) + ret0 = { + "energy": e0.squeeze(0), + "force": f0.squeeze(0), + "virial": v0.squeeze(0), + } + e1, f1, v1 = eval_model( + self.model, coord_rot.unsqueeze(0), cell_rot.unsqueeze(0), atype + ) + ret1 = { + "energy": e1.squeeze(0), + "force": f1.squeeze(0), + "virial": v1.squeeze(0), + } + torch.testing.assert_close(ret0["energy"], ret1["energy"], rtol=prec, atol=prec) + torch.testing.assert_close( + torch.matmul(ret0["force"], rmat), ret1["force"], rtol=prec, atol=prec + ) + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + torch.matmul(rmat.T, torch.matmul(ret0["virial"], rmat)), + ret1["virial"], + rtol=prec, + atol=prec, + ) + + +class TestEnergyModelSeA(unittest.TestCase, RotTest): + def setUp(self): + model_params = copy.deepcopy(model_se_e2_a) + sampled = make_sample(model_params) + self.type_split = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestEnergyModelDPA1(unittest.TestCase, RotTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa1) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestEnergyModelDPA2(unittest.TestCase, RotTest): + def setUp(self): + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + model_params = copy.deepcopy(model_dpa2) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestForceModelDPA2(unittest.TestCase, RotTest): + def setUp(self): + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + model_params = copy.deepcopy(model_dpa2) + model_params["fitting_net"]["type"] = "direct_force_ener" + self.type_split = True + self.test_virial = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +@unittest.skip("hybrid not supported at the moment") +class TestEnergyModelHybrid(unittest.TestCase, RotTest): + def setUp(self): + model_params = copy.deepcopy(model_hybrid) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +@unittest.skip("hybrid not supported at the moment") +class TestForceModelHybrid(unittest.TestCase, RotTest): + def setUp(self): + model_params = copy.deepcopy(model_hybrid) + model_params["fitting_net"]["type"] = "direct_force_ener" + sampled = make_sample(model_params) + self.type_split = True + self.test_virial = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_rot_denoise.py b/source/tests/pt/test_rot_denoise.py new file mode 100644 index 0000000000..cab8de7bec --- /dev/null +++ b/source/tests/pt/test_rot_denoise.py @@ -0,0 +1,133 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import torch + +from deepmd.pt.infer.deep_eval import ( + eval_model, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) + +from .test_permutation_denoise import ( + make_sample, + model_dpa1, + model_dpa2, +) + +dtype = torch.float64 + + +class RotDenoiseTest: + def test( + self, + ): + prec = 1e-10 + natoms = 5 + cell = 10.0 * torch.eye(3, dtype=dtype).to(env.DEVICE) + coord = 2 * torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) + shift = torch.tensor([4, 4, 4], dtype=dtype).to(env.DEVICE) + atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + from scipy.stats import ( + special_ortho_group, + ) + + rmat = torch.tensor(special_ortho_group.rvs(3), dtype=dtype).to(env.DEVICE) + + # rotate only coord and shift to the center of cell + coord_rot = torch.matmul(coord, rmat) + update_c0, logits0 = eval_model( + self.model, + (coord + shift).unsqueeze(0), + cell.unsqueeze(0), + atype, + denoise=True, + ) + update_c0 = update_c0 - (coord + shift).unsqueeze(0) + ret0 = {"updated_coord": update_c0.squeeze(0), "logits": logits0.squeeze(0)} + update_c1, logits1 = eval_model( + self.model, + (coord_rot + shift).unsqueeze(0), + cell.unsqueeze(0), + atype, + denoise=True, + ) + update_c1 = update_c1 - (coord_rot + shift).unsqueeze(0) + ret1 = {"updated_coord": update_c1.squeeze(0), "logits": logits1.squeeze(0)} + torch.testing.assert_close( + torch.matmul(ret0["updated_coord"], rmat), + ret1["updated_coord"], + rtol=prec, + atol=prec, + ) + torch.testing.assert_close(ret0["logits"], ret1["logits"], rtol=prec, atol=prec) + + # rotate coord and cell + torch.manual_seed(0) + cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) + cell = (cell + cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) + coord = torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) + coord = torch.matmul(coord, cell) + atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + coord_rot = torch.matmul(coord, rmat) + cell_rot = torch.matmul(cell, rmat) + update_c0, logits0 = eval_model( + self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype, denoise=True + ) + ret0 = {"updated_coord": update_c0.squeeze(0), "logits": logits0.squeeze(0)} + update_c1, logits1 = eval_model( + self.model, + coord_rot.unsqueeze(0), + cell_rot.unsqueeze(0), + atype, + denoise=True, + ) + ret1 = {"updated_coord": update_c1.squeeze(0), "logits": logits1.squeeze(0)} + torch.testing.assert_close(ret0["logits"], ret1["logits"], rtol=prec, atol=prec) + torch.testing.assert_close( + torch.matmul(ret0["updated_coord"], rmat), + ret1["updated_coord"], + rtol=prec, + atol=prec, + ) + + +class TestDenoiseModelDPA1(unittest.TestCase, RotDenoiseTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa1) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestDenoiseModelDPA2(unittest.TestCase, RotDenoiseTest): + def setUp(self): + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + model_params = copy.deepcopy(model_dpa2) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +# @unittest.skip("hybrid not supported at the moment") +# class TestEnergyModelHybrid(unittest.TestCase, TestRotDenoise): +# def setUp(self): +# model_params = copy.deepcopy(model_hybrid_denoise) +# sampled = make_sample(model_params) +# self.type_split = True +# self.model = get_model(model_params, sampled).to(env.DEVICE) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_rotation.py b/source/tests/pt/test_rotation.py new file mode 100644 index 0000000000..4b49377a27 --- /dev/null +++ b/source/tests/pt/test_rotation.py @@ -0,0 +1,133 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import unittest +from pathlib import ( + Path, +) +from typing import ( + List, + Optional, +) + +import numpy as np +import torch +from scipy.stats import ( + special_ortho_group, +) + +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.dataloader import ( + DpLoaderSet, +) +from deepmd.pt.utils.dataset import ( + DeepmdDataSystem, +) +from deepmd.pt.utils.stat import ( + make_stat_input, +) + + +class CheckSymmetry(DeepmdDataSystem): + def __init__( + self, + sys_path: str, + rcut, + sec, + type_map: Optional[List[str]] = None, + type_split=True, + ): + super().__init__(sys_path, rcut, sec, type_map, type_split) + + def get_rotation(self, index, rotation_matrix): + for i in range( + 0, len(self._dirs) + 1 + ): # note: if different sets can be merged, prefix sum is unused to calculate + if index < self.prefix_sum[i]: + break + frames = self._load_set(self._dirs[i - 1]) + frames["coord"] = np.dot( + rotation_matrix, frames["coord"].reshape(-1, 3).T + ).T.reshape(self.nframes, -1) + frames["box"] = np.dot( + rotation_matrix, frames["box"].reshape(-1, 3).T + ).T.reshape(self.nframes, -1) + frames["force"] = np.dot( + rotation_matrix, frames["force"].reshape(-1, 3).T + ).T.reshape(self.nframes, -1) + frame = self.single_preprocess(frames, index - self.prefix_sum[i - 1]) + return frame + + +def get_data(batch): + inputs = {} + for key in ["coord", "atype", "box"]: + inputs[key] = batch[key].unsqueeze(0).to(env.DEVICE) + return inputs + + +class TestRotation(unittest.TestCase): + def setUp(self): + with open(str(Path(__file__).parent / "water/se_e2_a.json")) as fin: + self.config = json.load(fin) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.rotation = special_ortho_group.rvs(3) + self.get_dataset(0) + self.get_model() + + def get_model(self): + training_systems = self.config["training"]["training_data"]["systems"] + model_params = self.config["model"] + data_stat_nbatch = model_params.get("data_stat_nbatch", 10) + train_data = DpLoaderSet( + training_systems, + self.config["training"]["training_data"]["batch_size"], + model_params, + ) + sampled = make_stat_input( + train_data.systems, train_data.dataloaders, data_stat_nbatch + ) + self.model = get_model(self.config["model"], sampled).to(env.DEVICE) + + def get_dataset(self, system_index=0, batch_index=0): + systems = self.config["training"]["training_data"]["systems"] + rcut = self.config["model"]["descriptor"]["rcut"] + sel = self.config["model"]["descriptor"]["sel"] + sec = torch.cumsum(torch.tensor(sel), dim=0) + type_map = self.config["model"]["type_map"] + dpdatasystem = CheckSymmetry( + sys_path=systems[system_index], rcut=rcut, sec=sec, type_map=type_map + ) + self.origin_batch = dpdatasystem._get_item(batch_index) + self.rotated_batch = dpdatasystem.get_rotation(batch_index, self.rotation) + + def test_rotation(self): + result1 = self.model(**get_data(self.origin_batch)) + result2 = self.model(**get_data(self.rotated_batch)) + rotation = torch.from_numpy(self.rotation).to(env.DEVICE) + self.assertTrue(result1["energy"] == result2["energy"]) + if "force" in result1: + self.assertTrue( + torch.allclose( + result2["force"][0], torch.matmul(rotation, result1["force"][0].T).T + ) + ) + if "virial" in result1: + self.assertTrue( + torch.allclose( + result2["virial"][0], + torch.matmul( + torch.matmul(rotation, result1["virial"][0].T), rotation.T + ), + ) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_sampler.py b/source/tests/pt/test_sampler.py new file mode 100644 index 0000000000..0ff16ed7c7 --- /dev/null +++ b/source/tests/pt/test_sampler.py @@ -0,0 +1,115 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import os +import unittest +from pathlib import ( + Path, +) + +import numpy as np +from torch.utils.data import ( + DataLoader, +) + +from deepmd.pt.utils.dataloader import ( + DpLoaderSet, + get_weighted_sampler, +) +from deepmd.tf.common import ( + expand_sys_str, +) +from deepmd.tf.utils import random as tf_random +from deepmd.tf.utils.data_system import ( + DeepmdDataSystem, +) + +CUR_DIR = os.path.dirname(__file__) + + +class TestSampler(unittest.TestCase): + def setUp(self): + with open(str(Path(__file__).parent / "water/se_e2_a.json")) as fin: + content = fin.read() + config = json.loads(content) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + config["training"]["training_data"]["systems"] = data_file + config["training"]["validation_data"]["systems"] = data_file + model_config = config["model"] + self.rcut = model_config["descriptor"]["rcut"] + self.rcut_smth = model_config["descriptor"]["rcut_smth"] + self.sel = model_config["descriptor"]["sel"] + self.batch_size = config["training"]["training_data"]["batch_size"] + self.systems = config["training"]["validation_data"]["systems"] + if isinstance(self.systems, str): + self.systems = expand_sys_str(self.systems) + self.my_dataset = DpLoaderSet( + self.systems, + self.batch_size, + model_params={ + "descriptor": { + "type": "se_e2_a", + "sel": self.sel, + "rcut": self.rcut, + }, + "type_map": model_config["type_map"], + }, + seed=10, + shuffle=False, + ) + + tf_random.seed(10) + self.dp_dataset = DeepmdDataSystem(self.systems, self.batch_size, 1, self.rcut) + + def test_sampler_debug_info(self): + dataloader = DataLoader( + self.my_dataset, + sampler=get_weighted_sampler(self.my_dataset, prob_style="prob_sys_size"), + batch_size=None, + num_workers=0, # setting to 0 diverges the behavior of its iterator; should be >=1 + drop_last=False, + pin_memory=True, + ) + batch_data = next(iter(dataloader)) + sid = batch_data["sid"] + fid = batch_data["fid"][0] + coord = batch_data["coord"].squeeze(0) + frame = self.my_dataset.systems[sid].__getitem__(fid) + self.assertTrue(np.allclose(coord, frame["coord"])) + + def test_auto_prob_uniform(self): + auto_prob_style = "prob_uniform" + sampler = get_weighted_sampler(self.my_dataset, prob_style=auto_prob_style) + my_probs = np.array(sampler.weights) + self.dp_dataset.set_sys_probs(auto_prob_style=auto_prob_style) + dp_probs = np.array(self.dp_dataset.sys_probs) + self.assertTrue(np.allclose(my_probs, dp_probs)) + + def test_auto_prob_sys_size(self): + auto_prob_style = "prob_sys_size" + sampler = get_weighted_sampler(self.my_dataset, prob_style=auto_prob_style) + my_probs = np.array(sampler.weights) + self.dp_dataset.set_sys_probs(auto_prob_style=auto_prob_style) + dp_probs = np.array(self.dp_dataset.sys_probs) + self.assertTrue(np.allclose(my_probs, dp_probs)) + + def test_auto_prob_sys_size_ext(self): + auto_prob_style = "prob_sys_size;0:1:0.2;1:3:0.8" + sampler = get_weighted_sampler(self.my_dataset, prob_style=auto_prob_style) + my_probs = np.array(sampler.weights) + self.dp_dataset.set_sys_probs(auto_prob_style=auto_prob_style) + dp_probs = np.array(self.dp_dataset.sys_probs) + self.assertTrue(np.allclose(my_probs, dp_probs)) + + def test_sys_probs(self): + sys_probs = [0.1, 0.4, 0.5] + sampler = get_weighted_sampler( + self.my_dataset, prob_style=sys_probs, sys_prob=True + ) + my_probs = np.array(sampler.weights) + self.dp_dataset.set_sys_probs(sys_probs=sys_probs) + dp_probs = np.array(self.dp_dataset.sys_probs) + self.assertTrue(np.allclose(my_probs, dp_probs)) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_saveload_dpa1.py b/source/tests/pt/test_saveload_dpa1.py new file mode 100644 index 0000000000..d1043f7029 --- /dev/null +++ b/source/tests/pt/test_saveload_dpa1.py @@ -0,0 +1,151 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import json +import os +import unittest +from pathlib import ( + Path, +) + +import torch +from torch.utils.data import ( + DataLoader, +) + +from deepmd.pt.loss import ( + EnergyStdLoss, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.train.wrapper import ( + ModelWrapper, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.dataloader import ( + BufferedIterator, + DpLoaderSet, +) +from deepmd.pt.utils.stat import ( + make_stat_input, +) +from deepmd.tf.common import ( + expand_sys_str, +) + + +def get_dataset(config): + model_config = config["model"] + rcut = model_config["descriptor"]["rcut"] + sel = model_config["descriptor"]["sel"] + systems = config["training"]["validation_data"]["systems"] + if isinstance(systems, str): + systems = expand_sys_str(systems) + batch_size = config["training"]["training_data"]["batch_size"] + type_map = model_config["type_map"] + + dataset = DpLoaderSet( + systems, + batch_size, + model_params={ + "descriptor": { + "type": "dpa1", + "sel": sel, + "rcut": rcut, + }, + "type_map": type_map, + }, + ) + data_stat_nbatch = model_config.get("data_stat_nbatch", 10) + sampled = make_stat_input(dataset.systems, dataset.dataloaders, data_stat_nbatch) + return dataset, sampled + + +class TestSaveLoadDPA1(unittest.TestCase): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as fin: + self.config = json.load(fin) + self.config["loss"]["starter_learning_rate"] = self.config["learning_rate"][ + "start_lr" + ] + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.dataset, self.sampled = get_dataset(self.config) + self.training_dataloader = DataLoader( + self.dataset, + sampler=torch.utils.data.RandomSampler(self.dataset), + batch_size=None, + num_workers=0, # setting to 0 diverges the behavior of its iterator; should be >=1 + drop_last=False, + pin_memory=True, + ) + self.training_data = BufferedIterator(iter(self.training_dataloader)) + self.loss = EnergyStdLoss(**self.config["loss"]) + self.cur_lr = 1 + self.task_key = "Default" + self.input_dict, self.label_dict = self.get_data() + self.start_lr = self.config["learning_rate"]["start_lr"] + + def get_model_result(self, read=False, model_file="tmp_model.pt"): + wrapper = self.create_wrapper(read) + optimizer = torch.optim.Adam(wrapper.parameters(), lr=self.start_lr) + optimizer.zero_grad() + if read: + wrapper.load_state_dict(torch.load(model_file, map_location=env.DEVICE)) + os.remove(model_file) + else: + torch.save(wrapper.state_dict(), model_file) + result = wrapper( + **self.input_dict, + cur_lr=self.cur_lr, + label=self.label_dict, + task_key=self.task_key, + )[0] + return result + + def create_wrapper(self, read: bool): + model_config = copy.deepcopy(self.config["model"]) + sampled = copy.deepcopy(self.sampled) + model_config["resuming"] = read + model_config["stat_file_dir"] = "stat_files" + model_config["stat_file"] = "stat.npz" + model_config["stat_file_path"] = os.path.join( + model_config["stat_file_dir"], model_config["stat_file"] + ) + model = get_model(model_config, sampled).to(env.DEVICE) + return ModelWrapper(model, self.loss) + + def get_data(self): + try: + batch_data = next(iter(self.training_data)) + except StopIteration: + # Refresh the status of the dataloader to start from a new epoch + self.training_data = BufferedIterator(iter(self.training_dataloader)) + batch_data = next(iter(self.training_data)) + input_dict = {} + for item in ["coord", "atype", "box"]: + if item in batch_data: + input_dict[item] = batch_data[item] + else: + input_dict[item] = None + label_dict = {} + for item in ["energy", "force", "virial"]: + if item in batch_data: + label_dict[item] = batch_data[item] + return input_dict, label_dict + + def test_saveload(self): + result1 = self.get_model_result() + result2 = self.get_model_result(read=True) + final_result = all( + torch.allclose(result1[item], result2[item]) for item in result1 + ) + self.assertTrue(final_result) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_saveload_se_e2_a.py b/source/tests/pt/test_saveload_se_e2_a.py new file mode 100644 index 0000000000..95d7f97a88 --- /dev/null +++ b/source/tests/pt/test_saveload_se_e2_a.py @@ -0,0 +1,145 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import json +import os +import unittest +from pathlib import ( + Path, +) + +import torch +from torch.utils.data import ( + DataLoader, +) + +from deepmd.pt.loss import ( + EnergyStdLoss, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.train.wrapper import ( + ModelWrapper, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.dataloader import ( + BufferedIterator, + DpLoaderSet, +) +from deepmd.pt.utils.stat import ( + make_stat_input, +) +from deepmd.tf.common import ( + expand_sys_str, +) + + +def get_dataset(config): + model_config = config["model"] + rcut = model_config["descriptor"]["rcut"] + sel = model_config["descriptor"]["sel"] + systems = config["training"]["validation_data"]["systems"] + if isinstance(systems, str): + systems = expand_sys_str(systems) + batch_size = config["training"]["training_data"]["batch_size"] + type_map = model_config["type_map"] + + dataset = DpLoaderSet( + systems, + batch_size, + model_params={ + "descriptor": { + "type": "se_e2_a", + "sel": sel, + "rcut": rcut, + }, + "type_map": type_map, + }, + ) + data_stat_nbatch = model_config.get("data_stat_nbatch", 10) + sampled = make_stat_input(dataset.systems, dataset.dataloaders, data_stat_nbatch) + return dataset, sampled + + +class TestSaveLoadSeA(unittest.TestCase): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_e2_a.json") + with open(input_json) as fin: + self.config = json.load(fin) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["loss"]["starter_learning_rate"] = self.config["learning_rate"][ + "start_lr" + ] + self.dataset, self.sampled = get_dataset(self.config) + self.training_dataloader = DataLoader( + self.dataset, + sampler=torch.utils.data.RandomSampler(self.dataset), + batch_size=None, + num_workers=0, # setting to 0 diverges the behavior of its iterator; should be >=1 + drop_last=False, + pin_memory=True, + ) + self.training_data = BufferedIterator(iter(self.training_dataloader)) + self.loss = EnergyStdLoss(**self.config["loss"]) + self.cur_lr = 1 + self.task_key = "Default" + self.input_dict, self.label_dict = self.get_data() + self.start_lr = self.config["learning_rate"]["start_lr"] + + def get_model_result(self, read=False, model_file="tmp_model.pt"): + wrapper = self.create_wrapper() + optimizer = torch.optim.Adam(wrapper.parameters(), lr=self.start_lr) + optimizer.zero_grad() + if read: + wrapper.load_state_dict(torch.load(model_file, map_location=env.DEVICE)) + os.remove(model_file) + else: + torch.save(wrapper.state_dict(), model_file) + result = wrapper( + **self.input_dict, + cur_lr=self.cur_lr, + label=self.label_dict, + task_key=self.task_key, + )[0] + return result + + def create_wrapper(self): + model_config = copy.deepcopy(self.config["model"]) + sampled = copy.deepcopy(self.sampled) + model = get_model(model_config, sampled).to(env.DEVICE) + return ModelWrapper(model, self.loss) + + def get_data(self): + try: + batch_data = next(iter(self.training_data)) + except StopIteration: + # Refresh the status of the dataloader to start from a new epoch + self.training_data = BufferedIterator(iter(self.training_dataloader)) + batch_data = next(iter(self.training_data)) + input_dict = {} + for item in ["coord", "atype", "box"]: + if item in batch_data: + input_dict[item] = batch_data[item] + else: + input_dict[item] = None + label_dict = {} + for item in ["energy", "force", "virial"]: + if item in batch_data: + label_dict[item] = batch_data[item] + return input_dict, label_dict + + def test_saveload(self): + result1 = self.get_model_result() + result2 = self.get_model_result(read=True) + final_result = all( + torch.allclose(result1[item], result2[item]) for item in result1 + ) + self.assertTrue(final_result) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_se_e2_a.py b/source/tests/pt/test_se_e2_a.py new file mode 100644 index 0000000000..96a17c2bad --- /dev/null +++ b/source/tests/pt/test_se_e2_a.py @@ -0,0 +1,199 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import itertools +import unittest + +import numpy as np +import torch + +try: + # from deepmd.model_format import PRECISION_DICT as DP_PRECISION_DICT + from deepmd.model_format import DescrptSeA as DPDescrptSeA + + support_se_e2_a = True +except ModuleNotFoundError: + support_se_e2_a = False +except ImportError: + support_se_e2_a = False + +from deepmd.pt.model.descriptor.se_a import ( + DescrptSeA, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.env import ( + PRECISION_DICT, +) + +from .test_mlp import ( + get_tols, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION + + +class TestCaseSingleFrameWithNlist: + def setUp(self): + # nloc == 3, nall == 4 + self.nloc = 3 + self.nall = 4 + self.nf, self.nt = 1, 2 + self.coord_ext = np.array( + [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 1], + [0, -2, 0], + ], + dtype=np.float64, + ).reshape([1, self.nall * 3]) + self.atype_ext = np.array([0, 0, 1, 0], dtype=int).reshape([1, self.nall]) + # sel = [5, 2] + self.sel = [5, 2] + self.nlist = np.array( + [ + [1, 3, -1, -1, -1, 2, -1], + [0, -1, -1, -1, -1, 2, -1], + [0, 1, -1, -1, -1, 0, -1], + ], + dtype=int, + ).reshape([1, self.nloc, sum(self.sel)]) + self.rcut = 0.4 + self.rcut_smth = 2.2 + + +# to be merged with the tf test case +@unittest.skipIf(not support_se_e2_a, "EnvMat not supported") +class TestDescrptSeA(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_consistency( + self, + ): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + davg = rng.normal(size=(self.nt, nnei, 4)) + dstd = rng.normal(size=(self.nt, nnei, 4)) + dstd = 0.1 + np.abs(dstd) + + for idt, prec in itertools.product( + [False, True], + ["float64", "float32"], + ): + dtype = PRECISION_DICT[prec] + rtol, atol = get_tols(prec) + err_msg = f"idt={idt} prec={prec}" + # sea new impl + dd0 = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + precision=prec, + resnet_dt=idt, + old_impl=False, + ).to(env.DEVICE) + dd0.sea.mean = torch.tensor(davg, dtype=dtype, device=env.DEVICE) + dd0.sea.dstd = torch.tensor(dstd, dtype=dtype, device=env.DEVICE) + rd0, _, _, _, _ = dd0( + torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), + torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), + torch.tensor(self.nlist, dtype=int, device=env.DEVICE), + ) + # serialization + dd1 = DescrptSeA.deserialize(dd0.serialize()) + rd1, _, _, _, _ = dd1( + torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), + torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), + torch.tensor(self.nlist, dtype=int, device=env.DEVICE), + ) + np.testing.assert_allclose( + rd0.detach().cpu().numpy(), + rd1.detach().cpu().numpy(), + rtol=rtol, + atol=atol, + err_msg=err_msg, + ) + # dp impl + dd2 = DPDescrptSeA.deserialize(dd0.serialize()) + rd2 = dd2.call( + self.coord_ext, + self.atype_ext, + self.nlist, + ) + np.testing.assert_allclose( + rd0.detach().cpu().numpy(), + rd2, + rtol=rtol, + atol=atol, + err_msg=err_msg, + ) + # old impl + if idt is False and prec == "float64": + dd3 = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + precision=prec, + resnet_dt=idt, + old_impl=True, + ).to(env.DEVICE) + dd0_state_dict = dd0.sea.state_dict() + dd3_state_dict = dd3.sea.state_dict() + for i in dd3_state_dict: + dd3_state_dict[i] = ( + dd0_state_dict[ + i.replace(".deep_layers.", ".layers.").replace( + "filter_layers_old.", "filter_layers.networks." + ) + ] + .detach() + .clone() + ) + if ".bias" in i: + dd3_state_dict[i] = dd3_state_dict[i].unsqueeze(0) + dd3.sea.load_state_dict(dd3_state_dict) + + rd3, _, _, _, _ = dd3( + torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), + torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), + torch.tensor(self.nlist, dtype=int, device=env.DEVICE), + ) + np.testing.assert_allclose( + rd0.detach().cpu().numpy(), + rd3.detach().cpu().numpy(), + rtol=rtol, + atol=atol, + err_msg=err_msg, + ) + + def test_jit( + self, + ): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + davg = rng.normal(size=(self.nt, nnei, 4)) + dstd = rng.normal(size=(self.nt, nnei, 4)) + dstd = 0.1 + np.abs(dstd) + + for idt, prec in itertools.product( + [False, True], + ["float64", "float32"], + ): + dtype = PRECISION_DICT[prec] + rtol, atol = get_tols(prec) + err_msg = f"idt={idt} prec={prec}" + # sea new impl + dd0 = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + precision=prec, + resnet_dt=idt, + old_impl=False, + ) + dd0.sea.mean = torch.tensor(davg, dtype=dtype, device=env.DEVICE) + dd0.sea.dstd = torch.tensor(dstd, dtype=dtype, device=env.DEVICE) + dd1 = DescrptSeA.deserialize(dd0.serialize()) + model = torch.jit.script(dd0) + model = torch.jit.script(dd1) diff --git a/source/tests/pt/test_smooth.py b/source/tests/pt/test_smooth.py new file mode 100644 index 0000000000..2e3bf61d10 --- /dev/null +++ b/source/tests/pt/test_smooth.py @@ -0,0 +1,230 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import torch + +from deepmd.pt.infer.deep_eval import ( + eval_model, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) + +from .test_permutation import ( # model_dpau, + make_sample, + model_dpa1, + model_dpa2, + model_hybrid, + model_se_e2_a, +) + +dtype = torch.float64 + + +class SmoothTest: + def test( + self, + ): + # displacement of atoms + epsilon = 1e-5 if self.epsilon is None else self.epsilon + # required prec. relative prec is not checked. + rprec = 0 + aprec = 1e-5 if self.aprec is None else self.aprec + + natoms = 10 + cell = 8.6 * torch.eye(3, dtype=dtype).to(env.DEVICE) + atype = torch.randint(0, 3, [natoms]) + coord0 = ( + torch.tensor( + [ + 0.0, + 0.0, + 0.0, + 4.0 - 0.5 * epsilon, + 0.0, + 0.0, + 0.0, + 4.0 - 0.5 * epsilon, + 0.0, + ], + dtype=dtype, + ) + .view([-1, 3]) + .to(env.DEVICE) + ) + coord1 = torch.rand([natoms - coord0.shape[0], 3], dtype=dtype).to(env.DEVICE) + coord1 = torch.matmul(coord1, cell) + coord = torch.concat([coord0, coord1], dim=0) + + coord0 = torch.clone(coord) + coord1 = torch.clone(coord) + coord1[1][0] += epsilon + coord2 = torch.clone(coord) + coord2[2][1] += epsilon + coord3 = torch.clone(coord) + coord3[1][0] += epsilon + coord3[2][1] += epsilon + + e0, f0, v0 = eval_model( + self.model, coord0.unsqueeze(0), cell.unsqueeze(0), atype + ) + ret0 = { + "energy": e0.squeeze(0), + "force": f0.squeeze(0), + "virial": v0.squeeze(0), + } + e1, f1, v1 = eval_model( + self.model, coord1.unsqueeze(0), cell.unsqueeze(0), atype + ) + ret1 = { + "energy": e1.squeeze(0), + "force": f1.squeeze(0), + "virial": v1.squeeze(0), + } + e2, f2, v2 = eval_model( + self.model, coord2.unsqueeze(0), cell.unsqueeze(0), atype + ) + ret2 = { + "energy": e2.squeeze(0), + "force": f2.squeeze(0), + "virial": v2.squeeze(0), + } + e3, f3, v3 = eval_model( + self.model, coord3.unsqueeze(0), cell.unsqueeze(0), atype + ) + ret3 = { + "energy": e3.squeeze(0), + "force": f3.squeeze(0), + "virial": v3.squeeze(0), + } + + def compare(ret0, ret1): + torch.testing.assert_close( + ret0["energy"], ret1["energy"], rtol=rprec, atol=aprec + ) + # plus 1. to avoid the divided-by-zero issue + torch.testing.assert_close( + 1.0 + ret0["force"], 1.0 + ret1["force"], rtol=rprec, atol=aprec + ) + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + 1.0 + ret0["virial"], 1.0 + ret1["virial"], rtol=rprec, atol=aprec + ) + + compare(ret0, ret1) + compare(ret1, ret2) + compare(ret0, ret3) + + +class TestEnergyModelSeA(unittest.TestCase, SmoothTest): + def setUp(self): + model_params = copy.deepcopy(model_se_e2_a) + sampled = make_sample(model_params) + self.type_split = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + self.epsilon, self.aprec = None, None + + +# @unittest.skip("dpa-1 not smooth at the moment") +class TestEnergyModelDPA1(unittest.TestCase, SmoothTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa1) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + # less degree of smoothness, + # error can be systematically removed by reducing epsilon + self.epsilon = 1e-5 + self.aprec = 1e-5 + + +class TestEnergyModelDPA2(unittest.TestCase, SmoothTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa2) + model_params["descriptor"]["repinit_rcut"] = 8 + model_params["descriptor"]["repinit_rcut_smth"] = 3.5 + model_params_sample = copy.deepcopy(model_params) + ####################################################### + # dirty hack here! the interface of dataload should be + # redesigned to support specifying rcut and sel + ####################################################### + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + self.epsilon, self.aprec = 1e-5, 1e-4 + + +class TestEnergyModelDPA2_1(unittest.TestCase, SmoothTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa2) + model_params["fitting_net"]["type"] = "ener" + model_params_sample = copy.deepcopy(model_params) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + self.type_split = True + self.test_virial = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + self.epsilon, self.aprec = None, None + + +class TestEnergyModelDPA2_2(unittest.TestCase, SmoothTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa2) + model_params["fitting_net"]["type"] = "ener" + model_params_sample = copy.deepcopy(model_params) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + self.type_split = True + self.test_virial = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + self.epsilon, self.aprec = None, None + + +@unittest.skip("hybrid not supported at the moment") +class TestEnergyModelHybrid(unittest.TestCase, SmoothTest): + def setUp(self): + model_params = copy.deepcopy(model_hybrid) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + self.epsilon, self.aprec = None, None + + +# class TestEnergyFoo(unittest.TestCase): +# def test(self): +# model_params = model_dpau +# sampled = make_sample(model_params) +# self.model = EnergyModelDPAUni(model_params, sampled).to(env.DEVICE) + +# natoms = 5 +# cell = torch.rand([3, 3], dtype=dtype) +# cell = (cell + cell.T) + 5. * torch.eye(3) +# coord = torch.rand([natoms, 3], dtype=dtype) +# coord = torch.matmul(coord, cell) +# atype = torch.IntTensor([0, 0, 0, 1, 1]) +# idx_perm = [1, 0, 4, 3, 2] +# ret0 = infer_model(self.model, coord, cell, atype, type_split=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_smooth_denoise.py b/source/tests/pt/test_smooth_denoise.py new file mode 100644 index 0000000000..a66e5df957 --- /dev/null +++ b/source/tests/pt/test_smooth_denoise.py @@ -0,0 +1,151 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import torch + +from deepmd.pt.infer.deep_eval import ( + eval_model, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) + +from .test_permutation_denoise import ( + make_sample, + model_dpa2, +) + +dtype = torch.float64 + + +class SmoothDenoiseTest: + def test( + self, + ): + # displacement of atoms + epsilon = 1e-5 if self.epsilon is None else self.epsilon + # required prec. relative prec is not checked. + rprec = 0 + aprec = 1e-5 if self.aprec is None else self.aprec + + natoms = 10 + cell = 8.6 * torch.eye(3, dtype=dtype).to(env.DEVICE) + atype = torch.randint(0, 3, [natoms]) + coord0 = ( + torch.tensor( + [ + 0.0, + 0.0, + 0.0, + 4.0 - 0.5 * epsilon, + 0.0, + 0.0, + 0.0, + 4.0 - 0.5 * epsilon, + 0.0, + ], + dtype=dtype, + ) + .view([-1, 3]) + .to(env.DEVICE) + ) + coord1 = torch.rand([natoms - coord0.shape[0], 3], dtype=dtype).to(env.DEVICE) + coord1 = torch.matmul(coord1, cell) + coord = torch.concat([coord0, coord1], dim=0) + + coord0 = torch.clone(coord) + coord1 = torch.clone(coord) + coord1[1][0] += epsilon + coord2 = torch.clone(coord) + coord2[2][1] += epsilon + coord3 = torch.clone(coord) + coord3[1][0] += epsilon + coord3[2][1] += epsilon + + update_c0, logits0 = eval_model( + self.model, coord0.unsqueeze(0), cell.unsqueeze(0), atype, denoise=True + ) + ret0 = {"updated_coord": update_c0.squeeze(0), "logits": logits0.squeeze(0)} + update_c1, logits1 = eval_model( + self.model, coord1.unsqueeze(0), cell.unsqueeze(0), atype, denoise=True + ) + ret1 = {"updated_coord": update_c1.squeeze(0), "logits": logits1.squeeze(0)} + update_c2, logits2 = eval_model( + self.model, coord2.unsqueeze(0), cell.unsqueeze(0), atype, denoise=True + ) + ret2 = {"updated_coord": update_c2.squeeze(0), "logits": logits2.squeeze(0)} + update_c3, logits3 = eval_model( + self.model, coord3.unsqueeze(0), cell.unsqueeze(0), atype, denoise=True + ) + ret3 = {"updated_coord": update_c3.squeeze(0), "logits": logits3.squeeze(0)} + + def compare(ret0, ret1): + torch.testing.assert_close( + ret0["updated_coord"], ret1["updated_coord"], rtol=rprec, atol=aprec + ) + torch.testing.assert_close( + ret0["logits"], ret1["logits"], rtol=rprec, atol=aprec + ) + + compare(ret0, ret1) + compare(ret1, ret2) + compare(ret0, ret3) + + +class TestDenoiseModelDPA2(unittest.TestCase, SmoothDenoiseTest): + def setUp(self): + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + model_params = copy.deepcopy(model_dpa2) + model_params["descriptor"]["sel"] = 8 + model_params["descriptor"]["rcut_smth"] = 3.5 + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + self.epsilon, self.aprec = None, None + self.epsilon = 1e-7 + self.aprec = 1e-5 + + +class TestDenoiseModelDPA2_1(unittest.TestCase, SmoothDenoiseTest): + def setUp(self): + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + model_params = copy.deepcopy(model_dpa2) + # model_params["descriptor"]["combine_grrg"] = True + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + self.epsilon, self.aprec = None, None + self.epsilon = 1e-7 + self.aprec = 1e-5 + + +# @unittest.skip("hybrid not supported at the moment") +# class TestDenoiseModelHybrid(unittest.TestCase, TestSmoothDenoise): +# def setUp(self): +# model_params = copy.deepcopy(model_hybrid_denoise) +# sampled = make_sample(model_params) +# self.type_split = True +# self.model = get_model(model_params, sampled).to(env.DEVICE) +# self.epsilon, self.aprec = None, None +# self.epsilon = 1e-7 +# self.aprec = 1e-5 + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_stat.py b/source/tests/pt/test_stat.py new file mode 100644 index 0000000000..08fc12ff11 --- /dev/null +++ b/source/tests/pt/test_stat.py @@ -0,0 +1,194 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import os +import unittest +from pathlib import ( + Path, +) + +import numpy as np +import torch + +from deepmd.pt.model.descriptor import ( + DescrptSeA, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.dataloader import ( + DpLoaderSet, +) +from deepmd.pt.utils.stat import ( + compute_output_stats, +) +from deepmd.pt.utils.stat import make_stat_input as my_make +from deepmd.tf.common import ( + expand_sys_str, +) +from deepmd.tf.descriptor.se_a import DescrptSeA as DescrptSeA_tf +from deepmd.tf.fit.ener import ( + EnerFitting, +) +from deepmd.tf.model.model_stat import make_stat_input as dp_make +from deepmd.tf.model.model_stat import merge_sys_stat as dp_merge +from deepmd.tf.utils import random as tf_random +from deepmd.tf.utils.data_system import ( + DeepmdDataSystem, +) + +CUR_DIR = os.path.dirname(__file__) + + +def compare(ut, base, given): + if isinstance(base, list): + ut.assertEqual(len(base), len(given)) + for idx in range(len(base)): + compare(ut, base[idx], given[idx]) + elif isinstance(base, np.ndarray): + ut.assertTrue(np.allclose(base.reshape(-1), given.reshape(-1))) + else: + ut.assertEqual(base, given) + + +class TestDataset(unittest.TestCase): + def setUp(self): + with open(str(Path(__file__).parent / "water/se_e2_a.json")) as fin: + content = fin.read() + config = json.loads(content) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + config["training"]["training_data"]["systems"] = data_file + config["training"]["validation_data"]["systems"] = data_file + model_config = config["model"] + self.rcut = model_config["descriptor"]["rcut"] + self.rcut_smth = model_config["descriptor"]["rcut_smth"] + self.sel = model_config["descriptor"]["sel"] + self.batch_size = config["training"]["training_data"]["batch_size"] + self.systems = config["training"]["validation_data"]["systems"] + if isinstance(self.systems, str): + self.systems = expand_sys_str(self.systems) + self.my_dataset = DpLoaderSet( + self.systems, + self.batch_size, + model_params={ + "descriptor": { + "type": "se_e2_a", + "sel": self.sel, + "rcut": self.rcut, + }, + "type_map": model_config["type_map"], + }, + seed=10, + ) + self.filter_neuron = model_config["descriptor"]["neuron"] + self.axis_neuron = model_config["descriptor"]["axis_neuron"] + self.data_stat_nbatch = 2 + self.filter_neuron = model_config["descriptor"]["neuron"] + self.axis_neuron = model_config["descriptor"]["axis_neuron"] + self.n_neuron = model_config["fitting_net"]["neuron"] + + self.my_sampled = my_make( + self.my_dataset.systems, self.my_dataset.dataloaders, self.data_stat_nbatch + ) + + tf_random.seed(10) + dp_dataset = DeepmdDataSystem(self.systems, self.batch_size, 1, self.rcut) + dp_dataset.add("energy", 1, atomic=False, must=False, high_prec=True) + dp_dataset.add("force", 3, atomic=True, must=False, high_prec=False) + self.dp_sampled = dp_make(dp_dataset, self.data_stat_nbatch, False) + self.dp_merged = dp_merge(self.dp_sampled) + self.dp_mesh = self.dp_merged.pop("default_mesh") + self.dp_d = DescrptSeA_tf( + rcut=self.rcut, + rcut_smth=self.rcut_smth, + sel=self.sel, + neuron=self.filter_neuron, + axis_neuron=self.axis_neuron, + ) + + def test_stat_output(self): + def my_merge(energy, natoms): + energy_lst = [] + natoms_lst = [] + for i in range(len(energy)): + for j in range(len(energy[i])): + energy_lst.append(torch.tensor(energy[i][j])) + natoms_lst.append( + torch.tensor(natoms[i][j]) + .unsqueeze(0) + .expand(energy[i][j].shape[0], -1) + ) + return energy_lst, natoms_lst + + energy = self.dp_sampled["energy"] + natoms = self.dp_sampled["natoms_vec"] + energy, natoms = my_merge(energy, natoms) + dp_fn = EnerFitting(self.dp_d, self.n_neuron) + dp_fn.compute_output_stats(self.dp_sampled) + bias_atom_e = compute_output_stats(energy, natoms) + self.assertTrue(np.allclose(dp_fn.bias_atom_e, bias_atom_e[:, 0])) + + # temporarily delete this function for performance of seeds in tf and pytorch may be different + """ + def test_stat_input(self): + my_sampled = self.my_sampled + # list of dicts, each dict contains samples from a system + dp_keys = set(self.dp_merged.keys()) # dict of list of batches + self.dp_merged['natoms'] = self.dp_merged['natoms_vec'] + for key in dp_keys: + if not key in my_sampled[0] or key in 'coord': + # coord is pre-normalized + continue + lst = [] + for item in my_sampled: + bsz = item['energy'].shape[0]//self.data_stat_nbatch + for j in range(self.data_stat_nbatch): + lst.append(item[key][j*bsz:(j+1)*bsz].cpu().numpy()) + compare(self, self.dp_merged[key], lst) + """ + + def test_descriptor(self): + coord = self.dp_merged["coord"] + atype = self.dp_merged["type"] + natoms = self.dp_merged["natoms_vec"] + box = self.dp_merged["box"] + self.dp_d.compute_input_stats(coord, box, atype, natoms, self.dp_mesh, {}) + + my_en = DescrptSeA( + self.rcut, self.rcut_smth, self.sel, self.filter_neuron, self.axis_neuron + ) + my_en = my_en.sea # get the block who has stat as private vars + sampled = self.my_sampled + for sys in sampled: + for key in [ + "coord", + "force", + "energy", + "atype", + "natoms", + "extended_coord", + "nlist", + "shift", + "mapping", + ]: + if key in sys.keys(): + sys[key] = sys[key].to(env.DEVICE) + sumr, suma, sumn, sumr2, suma2 = my_en.compute_input_stats(sampled) + my_en.init_desc_stat(sumr, suma, sumn, sumr2, suma2) + my_en.mean = my_en.mean + my_en.stddev = my_en.stddev + self.assertTrue( + np.allclose( + self.dp_d.davg.reshape([-1]), my_en.mean.cpu().reshape([-1]), rtol=0.01 + ) + ) + self.assertTrue( + np.allclose( + self.dp_d.dstd.reshape([-1]), + my_en.stddev.cpu().reshape([-1]), + rtol=0.01, + ) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py new file mode 100644 index 0000000000..574ca8688e --- /dev/null +++ b/source/tests/pt/test_training.py @@ -0,0 +1,116 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import os +import shutil +import unittest +from copy import ( + deepcopy, +) +from pathlib import ( + Path, +) + +from deepmd.pt.entrypoints.main import ( + get_trainer, +) + +from .test_permutation import ( + model_dpa1, + model_dpa2, + model_hybrid, + model_se_e2_a, +) + + +class DPTrainTest: + def test_dp_train(self): + trainer = get_trainer(deepcopy(self.config)) + trainer.run() + self.tearDown() + + def tearDown(self): + for f in os.listdir("."): + if f.startswith("model") and f.endswith(".pt"): + os.remove(f) + if f in ["lcurve.out"]: + os.remove(f) + if f in ["stat_files"]: + shutil.rmtree(f) + + +class TestEnergyModelSeA(unittest.TestCase, DPTrainTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_se_e2_a) + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + + def tearDown(self) -> None: + DPTrainTest.tearDown(self) + + +class TestEnergyModelDPA1(unittest.TestCase, DPTrainTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_dpa1) + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + + def tearDown(self) -> None: + DPTrainTest.tearDown(self) + + +class TestEnergyModelDPA2(unittest.TestCase, DPTrainTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_dpa2) + self.config["model"]["descriptor"]["rcut"] = self.config["model"]["descriptor"][ + "repinit_rcut" + ] + self.config["model"]["descriptor"]["rcut_smth"] = self.config["model"][ + "descriptor" + ]["repinit_rcut_smth"] + self.config["model"]["descriptor"]["sel"] = self.config["model"]["descriptor"][ + "repinit_nsel" + ] + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + + def tearDown(self) -> None: + DPTrainTest.tearDown(self) + + +@unittest.skip("hybrid not supported at the moment") +class TestEnergyModelHybrid(unittest.TestCase, DPTrainTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_hybrid) + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + + def tearDown(self) -> None: + DPTrainTest.tearDown(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_trans.py b/source/tests/pt/test_trans.py new file mode 100644 index 0000000000..e5d379b9ff --- /dev/null +++ b/source/tests/pt/test_trans.py @@ -0,0 +1,137 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import torch + +from deepmd.pt.infer.deep_eval import ( + eval_model, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) + +from .test_permutation import ( # model_dpau, + make_sample, + model_dpa1, + model_dpa2, + model_hybrid, + model_se_e2_a, +) + +dtype = torch.float64 + + +class TransTest: + def test( + self, + ): + natoms = 5 + cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) + cell = (cell + cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) + coord = torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) + coord = torch.matmul(coord, cell) + atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + shift = (torch.rand([3], dtype=dtype) - 0.5).to(env.DEVICE) * 2.0 + coord_s = torch.matmul( + torch.remainder(torch.matmul(coord + shift, torch.linalg.inv(cell)), 1.0), + cell, + ) + e0, f0, v0 = eval_model( + self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype + ) + ret0 = { + "energy": e0.squeeze(0), + "force": f0.squeeze(0), + "virial": v0.squeeze(0), + } + e1, f1, v1 = eval_model( + self.model, coord_s.unsqueeze(0), cell.unsqueeze(0), atype + ) + ret1 = { + "energy": e1.squeeze(0), + "force": f1.squeeze(0), + "virial": v1.squeeze(0), + } + prec = 1e-10 + torch.testing.assert_close(ret0["energy"], ret1["energy"], rtol=prec, atol=prec) + torch.testing.assert_close(ret0["force"], ret1["force"], rtol=prec, atol=prec) + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + ret0["virial"], ret1["virial"], rtol=prec, atol=prec + ) + + +class TestEnergyModelSeA(unittest.TestCase, TransTest): + def setUp(self): + model_params = copy.deepcopy(model_se_e2_a) + sampled = make_sample(model_params) + self.type_split = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestEnergyModelDPA1(unittest.TestCase, TransTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa1) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestEnergyModelDPA2(unittest.TestCase, TransTest): + def setUp(self): + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + model_params = copy.deepcopy(model_dpa2) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestForceModelDPA2(unittest.TestCase, TransTest): + def setUp(self): + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + model_params = copy.deepcopy(model_dpa2) + model_params["fitting_net"]["type"] = "direct_force_ener" + self.type_split = True + self.test_virial = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +@unittest.skip("hybrid not supported at the moment") +class TestEnergyModelHybrid(unittest.TestCase, TransTest): + def setUp(self): + model_params = copy.deepcopy(model_hybrid) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +@unittest.skip("hybrid not supported at the moment") +class TestForceModelHybrid(unittest.TestCase, TransTest): + def setUp(self): + model_params = copy.deepcopy(model_hybrid) + model_params["fitting_net"]["type"] = "direct_force_ener" + sampled = make_sample(model_params) + self.type_split = True + self.test_virial = False + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_trans_denoise.py b/source/tests/pt/test_trans_denoise.py new file mode 100644 index 0000000000..360633278c --- /dev/null +++ b/source/tests/pt/test_trans_denoise.py @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import torch + +from deepmd.pt.infer.deep_eval import ( + eval_model, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) + +from .test_permutation_denoise import ( + make_sample, + model_dpa1, + model_dpa2, + model_hybrid, +) + +dtype = torch.float64 + + +class TransDenoiseTest: + def test( + self, + ): + natoms = 5 + cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) + cell = (cell + cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) + coord = torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) + coord = torch.matmul(coord, cell) + atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + shift = (torch.rand([3], dtype=dtype) - 0.5).to(env.DEVICE) * 2.0 + coord_s = torch.matmul( + torch.remainder(torch.matmul(coord + shift, torch.linalg.inv(cell)), 1.0), + cell, + ) + updated_c0, logits0 = eval_model( + self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype, denoise=True + ) + updated_c0 = updated_c0 - coord.unsqueeze(0) + ret0 = {"updated_coord": updated_c0.squeeze(0), "logits": logits0.squeeze(0)} + updated_c1, logits1 = eval_model( + self.model, coord_s.unsqueeze(0), cell.unsqueeze(0), atype, denoise=True + ) + updated_c1 = updated_c1 - coord_s.unsqueeze(0) + ret1 = {"updated_coord": updated_c1.squeeze(0), "logits": logits1.squeeze(0)} + prec = 1e-10 + torch.testing.assert_close( + ret0["updated_coord"], ret1["updated_coord"], rtol=prec, atol=prec + ) + torch.testing.assert_close(ret0["logits"], ret1["logits"], rtol=prec, atol=prec) + + +class TestDenoiseModelDPA1(unittest.TestCase, TransDenoiseTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa1) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestDenoiseModelDPA2(unittest.TestCase, TransDenoiseTest): + def setUp(self): + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + sampled = make_sample(model_params_sample) + model_params = copy.deepcopy(model_dpa2) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +@unittest.skip("hybrid not supported at the moment") +class TestDenoiseModelHybrid(unittest.TestCase, TransDenoiseTest): + def setUp(self): + model_params = copy.deepcopy(model_hybrid) + sampled = make_sample(model_params) + self.type_split = True + self.model = get_model(model_params, sampled).to(env.DEVICE) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_unused_params.py b/source/tests/pt/test_unused_params.py new file mode 100644 index 0000000000..a924979466 --- /dev/null +++ b/source/tests/pt/test_unused_params.py @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import torch + +from deepmd.pt.infer.deep_eval import ( + eval_model, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) + +from .test_permutation import ( + make_sample, + model_dpa2, +) + +dtype = torch.float64 + + +class TestUnusedParamsDPA2(unittest.TestCase): + def test_unused(self): + import itertools + + for conv, drrd, grrg, attn1, g1g1, attn2, h2 in itertools.product( + [True], + [True], + [True], + [True], + [True], + [True], + [True], + ): + if (not drrd) and (not grrg) and h2: + # skip the case h2 is not envolved + continue + if (not grrg) and (not conv): + # skip the case g2 is not envolved + continue + model = copy.deepcopy(model_dpa2) + model["descriptor"]["rcut"] = model["descriptor"]["repinit_rcut"] + model["descriptor"]["sel"] = model["descriptor"]["repinit_nsel"] + model["descriptor"]["repformer_nlayers"] = 2 + # model["descriptor"]["combine_grrg"] = cmbg2 + model["descriptor"]["repformer_update_g1_has_conv"] = conv + model["descriptor"]["repformer_update_g1_has_drrd"] = drrd + model["descriptor"]["repformer_update_g1_has_grrg"] = grrg + model["descriptor"]["repformer_update_g1_has_attn"] = attn1 + model["descriptor"]["repformer_update_g2_has_g1g1"] = g1g1 + model["descriptor"]["repformer_update_g2_has_attn"] = attn2 + model["descriptor"]["repformer_update_h2"] = h2 + model["fitting_net"]["neuron"] = [12, 12, 12] + self._test_unused(model) + + def _test_unused(self, model_params): + sampled = make_sample(model_params) + self.model = get_model(model_params, sampled).to(env.DEVICE) + natoms = 5 + cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) + cell = (cell + cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) + coord = torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) + coord = torch.matmul(coord, cell) + atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + idx_perm = [1, 0, 4, 3, 2] + e0, f0, v0 = eval_model( + self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype + ) + ret0 = { + "energy": e0.squeeze(0), + "force": f0.squeeze(0), + "virial": v0.squeeze(0), + } + + # use computation graph to find all contributing tensors + def get_contributing_params(y, top_level=True): + nf = y.grad_fn.next_functions if top_level else y.next_functions + for f, _ in nf: + try: + yield f.variable + except AttributeError: + pass # node has no tensor + if f is not None: + yield from get_contributing_params(f, top_level=False) + + contributing_parameters = set(get_contributing_params(ret0["energy"])) + all_parameters = set(self.model.parameters()) + non_contributing = all_parameters - contributing_parameters + for ii in non_contributing: + print(ii.shape) + self.assertEqual(len(non_contributing), 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/water/data/data_0/set.000/box.npy b/source/tests/pt/water/data/data_0/set.000/box.npy new file mode 100644 index 0000000000000000000000000000000000000000..6ad2de625b40040a2d13248dd8b197a0f885bdc0 GIT binary patch literal 3008 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I#i2099snmP)#3giN=P+50J1|)!uk4+3o3j;`gR1G3Tu!RLSF@z3=(J&lM zhw$WpEv;Y^gKGdXK=Pw%5FvssEU<|obc}}KX!syf1GcchCWg>4ntn#Z2ay`Eg#|V- MgpSelGZe!I08k$V_W%F@ literal 0 HcmV?d00001 diff --git a/source/tests/pt/water/data/data_0/set.000/coord.npy b/source/tests/pt/water/data/data_0/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..8bd448b1254784551c11c2c238af183a8dc0a4f3 GIT binary patch literal 184448 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

8_D17ES@Pzxp;`@_~) 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

)*OElAOC#W~T87Hl|+ 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%0Aa1S;y~x!3JuS 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

E`A>seQw}y3QWG>*%j~)z< zc#kpcuJ!bs$6V2|eBQpphQe;O@+rs78RM`XT?v~}bx)rVXhzGHmLe!~1of%xL!-i0 z^t#$oUY|W3ikU*MM}82Mq%4NiNhOkr_oGG!C$bv)lB+cqWAeCZ2p-B@KjC|X0I7f*zLf(j*Zf)hq{#>C*~!>^-P4Onp8ANTam1;J+AoqlZAUO%qv$?I@@2~pP|or%89tf zF)#Sc!R`27t|4xodIygE*87_G&D-ZC43U%2QB(u{B2X8SiO!R43mukYV{VqDM5}jP;hg#_pZlq*BrXP3# zRdp>g+|`WYt>e*cwTatk4E zg8Zpr8{^(EU&P`SY9wLji(UJ`Nj8Jomu~C~+Hg&k z%KzJgo!cu#6SWBAS@wGMFe&;fUCg@B-?>RI)C9GX3!<0<1Nf}^FZk52mMaZP!2#!X zEYV%dy`OuG`*~_IrO3X+g*WW)f4+c|W!y{djUt`~iqQX^2$4tUq2HS+^kmcn7+=+- zuXB5lJxI)*IG+xkZdd-}Qb)3HR)O8bw^(#1m~qFWJ82>k;nPvSutC z?SaqpG%5YCGv#aF!Prc7vTi;Dr|_w0TCYcKjRtg}Gms8#-O6n*&Vb*bUyQ}##apEf zuDG@)ih>S5!J5EOOq=-!r}LLXSvM2&mlz2`-d6l#=cc=zjA!unG2fEPdc!;C^AZ6P zv~y+}e5BkN7iuCkzH}6R@A!(yX;QQ@BZ_;rq7Z=}y09~@g_kH;PoXsuw7ZaHLseY3 zU9IM{MbLs&=~hhf(xsu#XTcy^gZyTOk}2bV>ULJ3*zkPC4~-7AT%0AG`@lM>R)Z)+ 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 literal 0 HcmV?d00001 diff --git a/source/tests/pt/water/data/data_0/set.000/force.npy b/source/tests/pt/water/data/data_0/set.000/force.npy new file mode 100644 index 0000000000000000000000000000000000000000..10b2ab83a233e3e9cd9d3ce05cda586295edadf4 GIT binary patch literal 184448 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

Jbo~0y36oal zV{%*=yB1XdUf&~N;r*|o%*oT~km)z-%uc6m**cQ+H_y4o#8&fe^7kIvohR31}o);&RC{sc|4mUIYdkoYnywC_kkA}1Vvh|h4!2>Oz#E&+FC%r25>g88#p7+MeB6`0)Lxl`P3lv` z6<$Z#)dMNCS~d&a?GEDmfq!wS)E)eCH3@?MU7%Wb4Gfu}A- zxV=q@I8d0kT1J26$|eXo^>H28ZwjK+pBrqM=`Mcckk4q_R)z|b#!+~pF*xlxA-Kg_ zC~TnzU4GR8zGrXo<2~n#HlNGJn1{8rV}T}IY}rN;kB8B|+xqx#WgV3_s8P$j*ZdpB zYXE)<=v@Cw*sVLz36*G?CB22;|16mwke7?=M}NV9_(HV0xtS?$bApBJBYysP4vq~A zBkzL8Ec}Wp8?eKJ#+kLV%sZdB*!;o#lt+V@gLEz}D?iQxOBa(4KL#8oCBQQeYe-wX z7XzO*)!3zsVH4BS*q%4er00`GMqlFDfX*oP_U|#gXdA=Zto?*jxBcKE3Lavp!1gWB z&=!hr$zV~^pye}njOiuDXGwzeMv0yQWCsm z#dvqnIrc-@8tn2_VA+ZfIQqqEcqiV?st^3c!H;j?%}Wtz+Pa7BiTlc<)(Q4_{0(+< z!9u1HlgbvQ$-t~xwXC4P2zRgjO&XS=tYK^&Zn|W{RHMXfkk@5?^@g>~W?vKyb2-M; znjZ0!4Uci157x8myA?@5LKZgN0}PySpy{i`Lzzq)(f$_P|ky-+mmq zm!-^H)Xb7cyu*{#Ls?$V2>~cp!bA@O;7Y9reH7d*)~3;NOIIisdQGIB$XHZ; z=}ao`F0%VaOIe-TZ9e?_T#Pwt2n`nckSCSKrl!5BQ8}ytTl>kth0H}P{`wGbvwY28 ziYZ_}`;TfH;n@z7Z1upm(;r{N{3CJ|P4u9>31B;tLv4U@S#{4+k z<)MtOcd~(P^F^u2gJ_)XP<-I@1SE&eSZEt#zjnXpRY{MvwzuN#veWoOVAv`t45m>V z4{-fDx3UX%=UK()c$BZ*#*`j@Ws814VN;Zr;-C)UnVD5t6F#ShzqU3K{mfK3_wqnm zJYBexjg@CY!%Fn**E726^@#PWe1g$2yEjUtk1d!4=S&wQ9+|?KI(kHUkfX^sv|>5e~2yr^|X*@d$fsNc|HQ3zEPzp z>lmi-_CG4h-^)A{=7QnplbnXq712WnbDa3?9lNQZUvoe$l>hN4gF3e>vxJvrf@7>0 zPMc&x;LK4Flvab!HbRU^Ff+3A1l2*qFxPzn1QaO1%8WAtV=n`z`xkMRZ|#Khb8^k8uiJ6) znW^AqUqmJvtym_T#xFA(O$+oVvxf&x3ZB9U%4(Yj|0%e0o2TTFqpLN16x)*l&SaX) z2chk_QWCw}4&x`a;u@!=uw3C1%C8;8+ddl%Z|Wv<+}j>dpg5mp89-L*~u~g z{%_g+YQjUB_2`n($Il8BxN>%;6g|5i-&%HnJ-^Wp6kf&C(X^$|<8T#S>au8j={?fl zX(V3YXGOzIY^iXEEbQ?=3~M?ZVTY)mR~aOn+YYs3*eGrGW#?b~@-6_rt5)GmB^%CX zcPvyajbVLq((HB5FMRNO2t7?WBno`$N{Uy+z^D7Mo4Rgz!6bxLKAA|r!GdgW52D9Y zWO-ZFQK;S+#GI1^#@XyY!nx7|I~;^={Dc8;-SHzzukd54AEtrYcLQGM!49_hmI3@+ zph<6r=+K6A54bt!JU4!bnk0SmZ*uE@j-73DY z|GEA%PF_=wTYQ@^pz}KD{#Xq8>vd??B!OS=x)7#E%Swvp|6|LqxRCUdMYLECcz=9I z`|E;5GpZUv^XYkLjR_)~pn>Gm-XyRnbkT5IA^uY1=(h1Y>{_;irrh%-%j!TFQvHyM zo;$tRiT^PYuWCG@J*|J)y-!zguDv3@ zjvNisugHSQNik)uj0b&PFCM==NuWhcQ*o8kIJmxxc)Qy6JQPo z5j>P%O(T=s*I?iKj*e~K28Rx$u&%o@u;{^i-237RpIx&LQ>@-#=K^1tIXaK_#l^r3 z-Tq{0kO)hSl)+wJjg%%!v-;L`xIq6QKT*iW-c}na8E+;_`xMv1oKR)9v3nX*ozl&Z za7tncwId)lP9OgG9HP%{H*0(*YqP!fFW8&)AF(LX2p7iruwcD`R6Y6=`}Vel9sZHT zwNDZjn>$|M(3^(b5%VT?vv3s^3LV9>b8R8}$#l4LpM%+XVW<-tN@s4gV|Y$Fm%rQ! zGis%&zEj9%xs7Mbhug!Q00aJ0)O}Hw#GICnDPb>L-!hNP>6rDk3qQU+%U^vVINT{WD3OTHKaSrdUCkxY69q>_Z z6+SH+&9vG&*y;I$(Kw?-N}^w2=aDVoa`7hXoA4jxG?+r%k~Nh2J`v13>qNJXy`i^imxP|? zC^mVN0sAdCuDa9JiY=-N;Z2_GNB8Inl<>=&1;@|gwjb%lwufgy&fm6Xqs34XPaa5T zs{U~+1z70A8|NUtL;1HM-ex0MJE1=iU8$FE?5*U_z9OKprierniWIF^I3J8 zaOqDJ%}F=lciUC7!jD^Em4+4>EWgWU{>h*Q<6QP+)F4otK8(AYuw2w)R?nSqwx=^Y zU1(*A2L&1F!i&D)RJ-Ocg}FZA5=#0*T+?IJZ*5_ffqDX`>jIvd;Y5GuFQD??SU6KT zAM55xVB69_>Jjeih8+lml<7zDPg597>8)k07qa2zEjOlArwwD)YE$-?bgWuki10TS zBmOKG8QkB4;dXf-9wu@rpaHs8Qig>a5AiKJ0DqbFcndOda#DNO; zpmg|qT;_9v^XtCF54W$DiyaLa>D%NP1Kmm~4Dfi^$( zy*De&@T5n>P2fpwCl~%=9c(>OK(jSJ<7T%N{1|0>dTygkMPu*c+T3#dUEzv-n}BXe{ovEA(wJFL5P?Z(@6gm7rD7W+vnF72|I8)>tb% z=51On>Ct&hIK3c3_#LyxonE)N0SCfCDd0NGUml8*&whMhQ!T5pe#4G$Tg=d4An2TT zL#I(SY*A+@oVrmAYqQJ&yvD)(NeigHMV%8;h@yS**%*7ZlwDE(f^!bUveN4#sU&M9 zJo(z6DQXA8Mj0P!zv7LJ*Hf|ZnGd{cG>5qciZE-BlIY)-4m7nJOeu9n@a9)CRcyS$ zoCY3*ir}5Fv{{dhT%iEY@6K}8Hwtl&>ISwd`Xzo*-a(r(i<$n|pF%(4ENQQ=W4hZ6 z;ac@WKJ>mVe01*>x-k}P>85_=F9z4SL6ozimA}_B3X6~L!)v>TgNls`*}PJaDCLHd zaISw90ALf_b&;#>5&GU-x_IsSn{Rv(1%^L4n$-(fU-e?0HiyPx_^{=y7%)giU&HG3{SPvm<(2f7X% zgG#UA=+^dzJACFNZ+^BvtTS-9fr2%D~5@Ey4>->`Q z8I*EG6HF`o!NAX#K97<>{M}EEO2;PRyqnVElCfrzIUTC-;jb*YOi2;3QXN<{!i1(S zl)#9J+ks0c@>bgpcPO z0Ev=O^)vOathaMIwaqD{`@$Q1@9<)BkW@H2OU8iOuHCF@i#!|k_$24&Kb|ezh1`^; z8KUXaw>S>JV8~vTyv8WQO!_;ckeXs-`Z$CPfL5dp?a@8=XiWqe}4F%>ru6*Js^lukv4XLvcsJbarAv6#n+iL*r?L>9TUn z_wgN;Vd=<=cRH|9t5>p{;WaFKkQv-6sG$7*muP8Qa`iL)M1EdR2Q+sZl2`x?bjBTn z)*?f=mve*ta|^_9*=MZd^GQ~0CMEf#*93t>8exq|07h8~b8EH+9bLn752roB4b{i+ zk@pLjzibZ;OV|bvGoG^hm(Q`jTLM?UTBF9w?lNo5)`P%IUwSyvnr`bnK?4OZru=aj zU*&d@_kNj;5zSBWYU+G4Tatpln`&!}bfl>-SP{R^H{k0XQ*cpGgTtdw2jB)9gYh#> z=;;zEJf^#hq|eFWBz+DdMaq&VmUiO)K8i3zO&(564yT7l!l2dr3U6(8knLB|qm5^) z1eaqdG`OcS|K~0w_;**MG7iPlZDN4AzEFUz6FM!vxWN`HBSXziP;pAF(-1p!W zdzrA3$uI6l3aMw=rd8wN-Jg@-B~^>9Tf9+mXcGE1h0yz3qoLr3@DCT{)`LeXqwnPN z?EBBVkbO==TrKooOy!qx<3*W_*S3-zyb%Le%H%1%XE(QH=jECTfsH&%a}l^_$g?|Y z2az{crjsIL^!VMwS_bz}OV(Ysvb2uXEztzA#&67g)DI4s_GivFi0eEkP5-^?`yWN; z9Z=){#qowTNF}4JhLnmZQPgwJMP$oJQRGW@c9E3^(vCFAEGaEXr2Cw6jYL*5Bc+V& z5!vbY{Qj*!x~=>9JmSElhBnJUyL_2yh9KT5dlf|D(l!gOgUxNH77(Isypj_&2bGNr{F z+-)noUe!*M#1m zJ@In(Qrts{Xy+d;D7%>R`n~>q<-Ww0yR(O$eSHK!yRQa|ZBqX3%M8g~w?YV**9A5# zs1QFb4Tn>&Qk~!5?JoW;9!$EmbHMYGnnEM03%8n%!SNPrsZ*5^wtRmN$F`dZ-qMaG z&iy90j_@Hf!yIApNB%D8vA1;nQ?8hB;NiL}g<$`ZX102_W#k)A0l#ll0wHMT7 zxhu}$+7SXCO;A-FY0j4ROG}-H?&z#2_z_I55jQ|Fpc|SVF-EtlCK_vChyJa_@`5Ko zE0a5t`oNF$Wb9#Hc}GKGWqSfcPegIg0atjRm*mR0IT=1bai@_NBE@W$iPT3K=iMay>|9Q3cp3uYG3j1 z)Q{kzyPb8UbL7kiT`INh4RLn)+?;Bp*p<>w_Vjai+!3kD=3Vzgx9hQ3DYrx0&9<=k z#cEzYlfdNYAqsVjB7?E>>BWm=93=VVvVz9&n!5S&ZTdgxWUCRsv$~Hx9h-Q`ty1o0 zeS!lV2cg%0w`lW1=^kWU$BT=W;(=i%;@YqpRvI@1ODBI5-y zuL4sk`xF#7t1~0YP?oF&;IZ3FC+U<8_|Vd}pf`T&S7O zYcD>***)%w3G)VX(vcpl^I#A9KOc*^UQ#xDuNq-Ui+HzW8m{pR=OND|yiQ{Tw!fG` z2Zrq7YRfZ}HC~G^_H7be_hyPK*0xjNqinJ2b9Z6Qf!E^8yALGB$VR*}Y9(6wL~vWo za%#S4M`KcBxM}26u_pbS(^c1A-06H4BuU8NEXlQJUy!;f+1nz=Z^^eJuL zR4d+W(wCi2(Z~DUJ5$O{4aj=o1j!E@1%s@6;>3v`V7QGmL-I?84H`aTai%VYp1Omf zJyJpQ-VwBz9)bTpH$&vP17Iecqm2uO@{RS+;Py`yuHSNl=DFkxHEhb`-(7R^8{&dX z-UqX;olPm?lxLCn zrpyiFJqsbB#h)^p+r-7&o;n+^zD&l!{osx`3br=5h_indfVH`%(AaY?oYIJe*6=-) zw#S?`Hhtl(|Lte(`r|y$_aye&auoY?ScGmTj*JyM`;VMb{8K!otqau{44#^?HLqOL=z^S}0m(oSg_cec3(cQ?Nl zuDM3g`IN`vx3A$aHlP%XJC<_Pe*qw1ie&t~Z8nD_x{w-$Q_RN_T=aAWEQ&Kj&&U&$y*q~rUbyq2f%aH9{I#H})m;=Y z1RTa{!Pi~|yh7EGkJ@Ak%NoOJQKKBLD>05%SL2(Ff8o7hBBbee7bH@)!6W&ZmWH6Iu@&^UNDidsCSaRUjoGF$ytH=#%EH{i zZuLAE)_fh!4gBSk>V8nc5Nn=4qz>x+X42@)c>W%8Uz|0-Mo{_`3hTp_(c?-gnaW>K ze!abz3(2U_xgJjIO~mP5H#zL#9bWeDHoWM19A`;;jM}S5p;{+ahz!0>%Kz{E-X6#A zHD+?t2R{rPXvFI`=bJU$$!5Pw%oGn8^8bhhk(y`@~V_tEp@Zd?RwR{F8bcQ~7HSCqL;W1+_c zr&kurm{r%8XIzTGhz$>L^(k+R{8K2@bO;b{6-j8D`?|79$zeVD-eUUsGFM{e^}*f~ zPKxpCa%n(W0jamvP{6|%&`X*(KKICz|GWK5%+!@~WN&uK2YCm>=h4ODe?=W}@s1OC z%WEn6^?db=f&E~wqT3vG3dGRm%9Py6p4c6YI!5LbE@ul8PZ0lG=mxnb- zKCwqabKz}pKktaIHRsTE*U7AAu8PHu1No8aTH(|5A!6yKZ22RTKr!I)IQVV$7^o(M zygs>b(3uWm{?p-D9eW2`Jy$Rf+l$8~Zf(ta5q4dRhnQ*`@C(x5{LM+UF|dHz+^U3` zRu(iu<&s!j8i&)5r10MFJGpsKm=IKPM0io#OldPdON`J=SQ>ZNS-X1{tgbl%=ifBK zy;sj5aOn+nupP-t_G~6-_n5`PK@q{@{v=wU1yxuOWPB@_&McQX|b@b^=C9 z`_EwQC7iypl9lGyab2(#76)#@FMlRD_Z##b?NZA@S7{2SNAKieeOrnCJ`|%>!f9>B z3?7~5p;*6v5?$HQ32&a6gfq*-@IgD2bx$45D;Iag7&k3mk=Bn?XBUcPzYGMQ-cRK% zFRHkoW32pM*nIf)p_X?1&7}@E|G}NJbJ*wYCiF4TfP!U#GQ(B5;!J%*-u)nk&q=*; zjZSabwN*pWyuC@7klL9aCdHs<+Dm!JfF@p9yo3Lql~|Db+Hs%t#tNHfOW|M3WB%o! z&3}wD$XVKB=i0QR*Z#+7xyN^QA8{Jsc+jptxUvJ!Y?=K8F0IV_85xYe^6oTR> zV3J_X!Nu*_)$s*5yzj~t23s-Wup?gYkRqLrv#B!2nb*8>=9}_ly1p@(roZkWo4M*C znl5ev>WgojkbP)zt`@-_{}V-NR=0Zlf1^SwLI9H zMQ?oHVIPH!b)kgTIhc}t2Y>B}XUjtAPE~&ePM|iF`kvyAe{SJN)nTar@+4mGsIG8- zz7OZ!NJQ0zajdU44!?~J!&Ji=xNu*K+~oCUNKomH=L4S$W3@&KL5^=Z+oA?OzL?3U z-xkr+gb7%6B%b}>Npt1t#p0<+%|c}C3{X?ZVfPXlYiinXkBz<1A>lBWdr4fhPjgYI zNW<%8ouJuPO{{V<78jMcP@mmB=;^i-@W^xn;q0ZP68s*jr;P!(iV$%@coq!bk}Oa+ zBYZpA1P}c_B-VDR5`NCu0{%5?;QXwYsFtoR#tcu#%omgBagmZN?MNY>v5tXx*8REt z%=PR#`yyX^Q_60g$KuSQEqvvIFZS^s%OxI4wA!>cO`YRG8*08mVmAlg_%2O|dUgit zpDqJc<8m&adzFI5DsiW#1LU>023~)uhw9PNj9c@LFv2yQ(z{$f5*!ts@6x{z>2M{@D13$I_53*MP3tU9!(XzAFUvlgzVuCJouvGEEXkk=V! zM-Sv#Ru5=czb}+iumpC^UWi8rrwL{`!4y(hD_%d_ftQ|*=HEZ14(6JD|5rWoqvx}E z$nZqr$u<^BC1z)8<084E<|3M?-HCnIPQu`elZA6thxyI442m3P%!M;M8n*KrSrrt}_A&Z!?bXBcpLU1f(}Aba(c6n@LQzQliFyx>LXbA zAe_F5s^X6~W@zxLH(oxmM}AkjTQ&uqgAemsX`TC1=a?i<{wpLRf1kn+R=;H3O_o&u zWTMdf!75Uy8?gB#T{71_fYUAep-!JR@ExtM$f>^0pJqpZ@BBy_0EdLzDTm3&GzT`l z%n}o9bD_PfJ*7>)gm<@E@wkR=WHS3aSx;yLzkiO9Q{zUyYk;>}rOAJ8h!ikZn|%_V z;0cq?+{r(kigquB<37r0AZr6-yN-M)xd&7Oj98N_WkgI9FZ@v8B8 zx;5QUsJpvc@qae&wjR?l-^B}W)|z0nRV+NO?Sy}`f-&K@D$Xq1OD_+^(;$yy5_YFO zE-}gE_XhT0VWxy}Hr`xo9N{#`TZA5{205njtX%M!WNaq*=(Hk5g$! zc_7WLn~Y7DTcJP}4F;}vx$~AbGD)f-HxGaC9JmTP^;1Xpje-14S15}KlFpCi%Q<(} z3HKuBQ($843Dk;Mh!-*$_muCHCe&F%zRyz{+jNN+EgAtkUd_UQ(FM3-g*6H} zFKO!7Afe~c2UPs77guD|QuC29;^eySeDc~-JpOwEN7}R#LwlVD*WTwr|7RagJ8h3K zGjmYo;9kk^ejWQQT8`K2@6*<&J}95zE;!{sfq?^BDLtb}NGo5&()ozm`AS$h_HIh8%K-PpC_HxN8{JYnhJ)HY0UM1C;IM5VtKDtD(I?)4=$y=n&o|(m zM`2*)(-Y&0Z+2qI|i%akJ^vWTDF}FbzRs^Lvmyu`o$)XDIZG&cNS;Ftvi0ufJ#^X zynPo1W`oRK;%0vu;>AmcCErj^sg7XMepXB zPGN$Ya|YJE8-)9A7PDHmoc`#i(2A{6_iue?#jOF7@9O9q*zsiwYxfD}7Qb0|!KOR4 z$xo2)Q*Z8FX%B9x=g~eafjjQC=QzL7SoEY9=4qW4I@PVfHf4K7?WYm^rOk=E>ZS^d z7Ej{swIy)rzcO0$doIr%*^i4CnJBb7zD3CCB|E3{42K%5qwron&h<*JJfl7sb#%)h z>(P76e(y~V_UY*Tw+AoV=EL%>dpJb8)-DP!rpUIjXtBeByLT`X_DMM)yC6%}KXRTA zsobYGikITn6T#R|V?6&I*aSOkYlVb~-f(H!J9=n$*m+%FEycO<$rScO@=t~yVD)X= zp-+4i4~|hsv$$pqs&(TI>iU$cX3A#SUZhku4eO!}6nYj9;q?q(_6k@IMTdRPdsz*_ zEa$a6y6!j4x+ie`EGOY@X)})O7R~FFrz`GwXA4t9Qt9GxHJtLzlqO&$y_oeD2G91w zu%=^R`e`A#4~@e6y_fL1qFM}Xx0H4UEQP^gDIC9im|~d4btqk%4Og_|@N4h=xTt9z zf5>Hid@q?D`~z7buEpx?e4HXL#PL6OkjKdq*gaXFk32dLYX=q3HkXTVY}Rh7cvC{B zVwcdT8wbH|vAz)0do)eo7eHSNV!8bBI#{tX2Ir4HBF~q4e|82_ILjqU9`XGoEUaxu z&Vivcs*3}i_}&-qPs_B%kE$LpM7UoS1qu#gcDWdqp`OS?T zzhF)|GZJ_tPhOl%mXsjG|~sk8Sj1XD0YuY#_Z2HzBY%i4_BDpnuUp zp0J`Tc0HwyZ*%tu`|<-o-zQ2)lza2BrHPw`;t`)vXGmrmn|Aa?w30yp9 z9Q)2q5I_FxObZ%gX?brmXkESlJ~WIFj5c2uGY%hx=a=Kc2&r4P-Zy zP3Cd2Pm-A|KG=i%mss%Fl!t8Axe@H+U*e(0Rpg+kq3UxZ`J=@nxYrsfn3{LRe;t%? zLWmQNKDdW>e)=nH8QB>Q6h9R@_t?Wf#+6Cg1zQkRdeE_DPsNLS8tK~Lxm^CY3x4Tw zh&^Yt(7!I;!q5m?Z2fqKCNCIBZ~Ffh;*BC$v+GY7a9N$a{_O+JbL&_uU?|`5FQc~b zofszX!?wOTG<&KAe#z2B_v&cscOX`qthCP*#Eaa%#!!!4G!0!`a>g36h+7%vH}nK zi5yzr%DsnA;-6Yi;i9q}y4yd1-VvQ>^|&6K_s5kr>>6mdsvn5!mcrvF`^8PK6LFA3 zFy2eelXyDg#3{vYI92tL5YcFY!$m-~{e$u9)=Dv}Za)UtNDQ14mN+5(tnkIGPU!Hl zjMgi(@S9nlxMiNPOy!v`gsy88hfYgDI&xKRkZ6SWJ*526h|S!2eE@#f9td{nr@+rY zNbDeW@lPk^8=XheyYO(m63HiIYBdLF?_i zuI$E6jr{(I^Bd^a6c;z#4CQ>UG$Bt8KZwX58HbrJ9C zI0n?WWPz&0d(#@>1V)Qj;cd8r+55K2Z0DR|cvS=X)?HY2MX>0-KonL>EMfNx{&>|o z4Qrq8Nqi>%f5V!jPMAk3I1Bo|a-kApO zfAbPpOWv^E$qs;CN14tV!`)Nr1=3WYS%abtBI{;U@yYTZ@Z{bqZ zeDs|E3X9Hl;a+~H$z`8(Pg`(K>=tlPV(%Xn4(P|=fO}~e#9?^gN&uv(#>3&`0VrF$ zPu%x;GL^;`!xr0J==ymi7ltQNIqaZT-F4`a(hnC!t>&j7j9WJQ)THC%ECcF;u)r!3~pFP|cL3SijgAx3t`XLhn4- zv{>S}X^*15t&`B@g$v%;dIJ_;%7KB)|H9xoS~3m6TOl>-kkk4pK6G+p;Qp z(v3royeGGkCT_3MOvcl!xVTQ2=VWHFPmmhV5nhq<#7XkIrFyc@q$OLwq=sUpXY_$@ zpE$S6u;;6f^`ZZnhje)FSyml!0Y?rTitf8_kfWEn;?B-=yl&A~u_2`wUv5~AI}Xi8 zwYVLaQPo*7!sej(ad|P+O|};|$0yPUDU-fmx(8@CX7IkUZTS0CHBL{^mbJ|Dhf!l! zN#3x*+}$gScK)!S4Ot^#WnW{?hqZiZ-84?S<$&8u?C^%fkP0gHrvsa^q4SxE{5Bv0 zyANB<{f3lb;V6B+s=Xflge>@Xr-=@9+)ZwKbNKUd%=w$KqDy z(R@S4oT77gpr_?FWXBb_VX7-`ubPFcyd9R3L84c_JKWlxEGh4XD&GOteco zfqI2Og5I9ZY;!1xX75m9C#e^ATr&uIT84sE&s(Bpzc5-kyjmbPkrvJ$%`22#xNb(7 z@WF2leGQP@nq8)|S;T5_rZquDNGrY6T?}nLJNV@0P1O2Sj}0HMlOK|eLyw+&NwYy> z{rB%ozwD>+PUZi=?;bgM@g{LWsS387dIDXRrph1duM-Y7^~Cfit7!XO2WUJKN5>Xv ziov_0gtv8rc}DL9I9d|!e5fV^Z2sDDXBQvZyHgG4P!ua0E(gaqTX}I*Ht2=8L$l{X zP<=lK%Ad#4Nlyh0oc)5WwTH>x+FYV}o>sDnj``qfkciu)8AHLf_6j2%qFP^dJ`(Vg zZOl@@@u@GGe0v4_s8vu(j-s{^ePHdO-WWe^DF1GXqrQE%VM}xZ{P_?pocg_yj?|j7 z^1gbCZv7#ICwZWg)dy$CiG49M-(BocKc1b;U&~K+@`v^7=ZJGU`wJCPjQt6I#0@)ivELC>CQ$C zl6Ckcqq`{jSlFrJjSzTgJ1xr5AU3p(*h`=CWNwyr^kE0d4-I3cK@MxmUkG z;$PpzFlFjlSlc>9SiiV8bxX0M631f5y4jcJ)Grl=Syxi8B16U8heH+P+J}PIH63^; zu@>WIYw{bPYG`V=(Agp8usrW{CQUvVg*S?FF#l~iq%ChF(s0JJKXvfswOD!Zk=ydB zGi7AH&R5#;nSrHqd(bSg!7A?)G`^f*znHt2Tib!RTN$Fko@zQaYnm85 zHA4cFIP>?s8?^C(8ZY>Gjb2&)gFah2a68Y_bh#$Xd5ZpUyz1~B;vc*7n(r^+hm$G& zQ}>{f{l$1(ae+&Bbmd)2A(YUj&26>U$)?qnd%jJ>-vjyZ^qDVS&o@eqb@6Aae@cQ z-Busw!Rb*by!lEmzDnoblYaQpvKP1C_n2lzZI{QXcHp0#9JxbD8XefXgl>ITh3k(x z^Ows9h47b7oEo^0R;opF>g*XHt*hwMoPi(^GJKVYtCSF zdpdnE9EWrN9A@48_KGtt4tWy!hF<|G?g}D@8t3LyZNO z$nyC^x_Y<0VyHA__R4sLcf0E2hd3jwadKtyZgH-jGl#dNnR8=cD^4;Lz}s;IEBBp7 zh6{rEVscO6_?slWcKiiu$?UPSY!Z%h?!_7@0b<|d-DT5)!g1QHD74vUNE^bJ;R3}; z>{v5JoLRD*f|L8BZ^23$+FzMe9FlRUUk~nhE{JbpVodpP6g$?tIBR8MxFdJMC);SWu1whYpYKcm!0A^v7E& z`qVp5lU=Ty$J_d|smqii+L_&x^eG*Il(zc z2PwvM8;#!UPe2LG;2N6|u<&1RVSZf#s#ag%Gf&K9u6weu&%B%5^Q-|J$TsBldqPuE8C8{baXx+40q}Rq`QjU2sxI6NO*q3_h{1i1cHk;q0kzFzjx+^!Xj!-+C+^ z+c--YXp)3+nLgZ3ntvKw45q6ew~^6?COXzN1h3cVunG<2lz>W@{q_~Lm?gqohhnUp z`GGw3964OSO-Qe}j+zz|s7T7%7e-G;=NIavIo}W)TZ$mZ@FD7S{R^jNDe#8cQ`i#w zf{&L!qc!G{Tppf7op+34k2Zfkf29kizscYgnZHCAk4>~UUYTQZrz&veJ|WOVZNclLK!!5#2b zi3`JkM>I0GJ3Rawfir3rioM6xi2GYjc(wc;={z6G_u74iwz7BPwsFQ(*}0hx*~@9l zq}?1B9m&lb)@C!0jbQ#)l^rY-*i~+xxh2i1fG8 zHcSR_&WqvFvEPD`|8cgGyoguwc4B^RI&5mZ4QJ&S;eAmmdB0i@2fH69|KdHo@WgVY z=WW87uBu}B`#<6;b62*TbWE_1en)m87G#o=$C0^Gf7MbKPpn=Diks<>TNFtaYk$Fj z<2j(;wvaFS+Td3-MCTvTU^-p$!A8&G2eYe%l(wbxcz--q&J)l~9wYd=g^9nmoP}D+ zt+1xf3O8pMkg>NBTnt-|EALi|13zbp&mFX3;GrSd_~aCgjbF<<_En36I^LvR-*?gj z#dV4@n~RJ63G8!kL+wc|VO3rtj(RH1EK>Wj_OVZ}$gmKP?n@7p^`Nrp5xge&5U#p*kH1)jQSt~S*)Xl1>|UsfNsbk`(RLwh=+uwr`x+=-oK8jM z96`7i^paL+$?>sUB{qCkrH1$ts1MYq;T}?7eAQXdk{_fcTNA_&?!J8cd^Q*x<>K_( zJi*=Chh`OMV4{yQTOOJwDiyopwT@e9slPNg=r|6-=A5DY3VCTn*QLDZ|2cZ?XPVxVN72^JxUKZwv`G2Iw_!ZF-yMpRZmD< z^j)~-|4j69oG4B@+Kw;WuOU6@^)8+#`85(x!nC>1u{paNHhD@+ttm?Mz^{O(?+74| zK^o9YNfQ^y&SI@)DW8083+sFE5ysU#RWm+nt8+Sffso8>#|M4EzZ( zAKIa%pCMPYS#V>>PjZM$qki9P_+WDb`5ROat@DceXtW;N7?d7+aSF2 zI15jI-9jz*QfMt6p(zuFaE0z5)X=nqwyTEpv6~xRF6aUA&%4oyZ*R%Ks8VoDse{GP zi@+%b9F}hrmmVtRypKEKN^OPwqfRhAm?+W!^*osWCxn*UjUt=sN`Io zpo^{%pBg9i;)DiReR=@3FUfG$tlWVX&3)LxQ^ZweM2 zwsQEYIh?RBT#OK^;ZtrJbbQ{63r3pLIm>1I-2NFoEEvUyYTTR-?Jhk3juzpTGG{(A z#{oT-yd@4;K@SJjK)Uq=Ea`U>Mhz+z0#!eYo+aPJOy%Bu`kpPVU*-dr11>|#)xjJ& zGC=Ydg$Zkq?4|3o4q^F*PHevK2*)HC)0}G;@O(!jTHor$!@J68`j}LFEcG_qJ0@|~ zh7p*4;w~9hH}X59qtxw}24_ZlaPsPG{A6)Se4TKfMr}nLXx$%HUOh=Ic8;7szYKeM z*OL0)Ef9Mv34K>jkllEhMOP{}g0@93raQLySzBT{dkW&cQ%1ZmFAWTb^vA1S71-{w z9qUb8!Is5_oc2PAKeXNA13wO-e)(<;?^lj5-(BL^Iq%U-`v2-b)QWrOuMmIj>V|i} zpK;F8%qN=Nj@Np|VbwNmP`)=yw%7SKUa*Mc)23F`W7;ve^Po3&%c^(!(5nJsl79#r zFB&KYRS=l|?0^NP+MNC968JZ7A@$eQP$RLwpK}2}Fjc{AC!=6{nGxgpP^@yhK)0m) z{@{wUV#n+?{7S45HmaZGOAAbR@uGH$u_Iz>+rS8VYp_g;$)@q313|pD(_LKPx=HL2 z`j8`fdvfLMgYGJkS$cAB|!c*RF!D)B{+a zm4xSy+v5aM!7jhsal*|^{!cZHXZYR{-}aBkcRm?w%Xeg5Q8%(H3@8|bJD1$&Z7y*vPn^Y{dbO812U>G+$Satv z{uflfs8PeJSET5pMX&Ff;&kc%JMH&bIMVepC!RYk)IRjXefQj1EjN$H{kMA3jmXExsgKo(Kpvd+hOt8$OJHwaIlu-|btF~tHE$ephga1;|KX@n}F-nB^mG&^V zXC(R#X%)s>xYC@MXH>U!A6A|@!Bv%mK~>5XCRyJCx!xu5LP0)R{TGWKlV8HEg$F>} z#8mv4W}rCrs~bQ26o}t!0H1xyDqYGBb0Y5#=iF=@R+j|c5$n6R#+?H3y_Y&n8rG9X58Ma(fkVVkoksGjw|~iMc_@@*?WOm9cEh^r zSmfpQvU8hbF|$}%_C)du=FFCMsJ88><%1fZ-4Y73+Eu}$i-&l1a4x@?q|0t?+CoOi zC@ilJ#q`bNFmqih*j^flAx(`!zq5|m<$3`RT+spMs_qwl_unT@h&)V1CzRRu#al6U zW*SWDYK#AM(cys|-RSwo-EekmoKVx;MO0m)%Y{G9xL}eCW~_ZjjxCpj##2EYn^Y+p zEgL|e)4u|f4tJM0dDo7VgWI%pbntG6&p$+?*Uol|OZ~^-i{v?EG5x$aZU#Y>-)0&d z@mVLBIz9dIO_?z+k(e@KZE8=1FUL^*D2dlpT~}wqvdHf+^xz8)ZwllC z$=VW+@Edgc*pX+r3}IQg!z^n_B3+=ANb|`%b8z1}=FCWvOkj!G9f8i=7ZC{KJ2Hh1K>kpvs z%9*&VpRySFu^+F^nt|P_e*(t_@}iCBou|0=cE$4!ztc=5mzl8~8 zALk@}Kk4Gz0un{7ySTsI9BkCTO)qK>&`O%9q+@!=haWej*CY^42|RmQa`cx@iHFN_zqGYAA*_LUm)AG@m)p`7JWuDqI7ftFMJ5iSzSlM>v`csg%o(oQBf5>tWWPV9`kJC470LO-ena z9#Hja)|2ul-pSJ6NSC8jc3w#<%BF(+~6R?{OG^J>F@F-9!#F3+0_lVtM=oeU2WGFZmrjK}+n<8hRS+-6hm{ zq+$wf8Y>D7MJDw9hXEF;I-}&9qKhRVe9Ms0IAjaw_sFKZ56YbusOj*+L?d2P<%r_m z#niRKOq^~ro|fc21#yZ@%sFUFiO)Jgd`mdGw%dS5RCfu72N>~j`i0G2Gw|C@CrBEg zBCgwdm4X^aBovZl>4FZGZA`3EW{&2n) z8ZM-8_kmqxZ=ZVcr?9LjTTv5nVnsTv24|7H0@3VL_Rq84J)8Mk}UHHb5J=Azi zQ!(dXI6nBRk0q;P@RP|$dOh+WJ%72A!^ic&amnfYUVE_OrZ}ACvre&Tk^|~&enLy6 zyPcK7i^mRn&xH-$owl`G00-}9^B0VxgZU{^Ph>bRb&-kxmPowEE*p8ni0y2vYlA!5 zn)p%F1Rk9rQ~0;2;#9MZETwud{wHC-IgV&&kibA4SsoGvnrr&=>Ut0P_l<@4J-v;E z&Wq{$*Nxmb_#6(b(j$i{@$}%wLCC3c;N-r2S&~MQak{hfQ9T>}kk~{jk`H0@sC?)& zYdW6ZVu90ABtGD=C6WhZAZhvDq6NL9FzT>^qIU;VW;pS|jbE9PJcL=dHu1^leeu~I z3)ab7Ep9w*O#^>jqP2NP`9M+!JW_AZ>y}4wnRj>oC)X$EQXh%;6z-f6m_=UevdWe$ zwE$TLvcb1%@o2d@w^#c`)lC)jGY2zq4H15z%`_qzZMIX}#5DIF55?*MpAJaE!CHrS{G$xc+J`ludnI zR90i{Klr#7Kgv(>W}ggEN$QQyZaOC#ejJ9al`qBgO|vNTW?QM{pI`E~5sT$= ziJPU^`~1v)A*$#gS637wk0z$wI-WO$(IdSvYf~}b9{&<|x9f}_a{t4!d6#)o(NYL- z><{y#xtxVf3p7054cC?%^Yye{w0UQtXo5G$b40$hdngwChi>8=i4ESEogg-9KLr<3 z#^t`{Bn%6o{Q=!EzV@6vXq5rZ`FU0BccqWqdjA4cifbi}uuS-8e_8&aIsv?F`h&;z z-5BF5<1sZxczx+=YE*dv8zq*~u8@6@v^JXpJ}!g-NR=Za)-uo#3E%6&LR>nP{!5jaLI8ZwiQP*C7lw9O;I zWXEK1$o&CdrW&H~&zp44{2()n0tk#R;6pc670N#}@ey~RyZ|-4zCq%J{<%#pR-xRI zJccVxpUHE6zJUl5ozIoCH;Hp*dPVl{R)AZ@A6^n>PwKv%4{#I#T~0kuxf50 zjP%=u-V3YACd`&T!Ur)<;!4%exhEVuu5H{ah z3hO_0!S}ac)A3p@`P?a)P9Y5?v`#FbbK7bJtIwCkSA`jH<-L>}{Q44xc95R?J^SOA ze-UDrfT1`@HU@2y;~@Bqy|^s!mwedSr8M7QHaEKHLwR%$QSk4Nym<^iF+WVrqq9Iy z;yKw_zrbfgAYEM-N8oo>__#a?vgVzK4Uck#@O$UP_NS!}FG>g5e~F~~ISMVUUqG2& zs&lgQSmF88{^;Oa1y8^40MGsFsCO|?vW~gv<1Vp&q)fBhj4QCnPLFT)+Kz8@6Ul5_ z6jTjX7h;=-Q7`L!vD~XageJsNyO>@)gfm&Y<{fIRSjummw4|L_SMccbRGi+F4izUR zQSUp~A+F;D5(EVm$^&3i#V{d0>m1zg6eG9!9zx4huY=C%@px0Z2M4)_;-{iI2pH)O z32Ro6g!PiRxO4e?whx+|vE$;ob{t$5k9V7a)?Q1dw`qo$sv)O4G9PT2+7(LneH2_~ zETZc*=HTpNf_lr&!M!Cj=#p_9r0ZS*v;K#nZL$vb9G@(9OqIix#Hm=BGK4owiUq%j zI&iwD;AeXahI=-C6b2e4ib7(JIHZ3o9mOJp?*~b&=qu^Bb zRLFUB9hRP#x^G`Pus!U9a--2aWRsgrE&mO~1{MnQCGPjBzn7t4RU(Jaj^}r;d^qX2 z2CwXX9hOhrC-}ZFmrbhH;-jhC(8XFV7WR%|eOn(;TX%&FI*j5mXD8zT=T*4bX$AUu zy(2v#jXfp0@yI_GY|P`@%WTB?*5#QI~Dp;)pq%NOC7N z?3noyF3Ki=_L2>xdD|3Tt!)`Mb>`r7izUMdS8R z!ZQ;}FTdm*@?Rf#b+|={mCn3@N|F4kWiJ@#yK<&hB(088Ht=qm>9KKV7 zw*+aR(O@|R`OKi-BMjJG*_y7{e1JFOC&H=L9>V*rrShN(X-BDV!@;R5Bqn)1PIO&` ze$A;Im0-hC*^TGWVO&-`7DsLKXZv1N@Zz00MlJIYH$DC)dNeJ8!4jwI$)znUyFL*^ z3U^9At9X$cm6cb0gi0Vj^u>Fcx3^y(nd}zmr|>TKcfgm+RVE;Qbd( zUM=M)ebz_PruGtXgxngejX8?lE=dlmSxb5M-jRsE^zH1Xo`s}^vq-<^Lzw*TIDBpE zN>6qaa_n~LcGLO|BJ2(LQNcW{Ez3c(ysl`ia8Y;~pv;4eyk&BC6>!|~z4$1|*1Kdw%%s|OJu@EMG z9?3h`1PeVg?5Nq!fg&e8fJd64TOFpwk;KRIv5s-5&dK<#CB8cGsHa_I{x67pjEOn+v$_TrK`zf2ya$ zeoDWi$p%{M#5!+%9yo=-XKofb=J`SV%xrKQCeO#Rd}w>rTe^b>ICPi3Q0LJDYYkpY zo#zvz-Y1Cc*LIM%vKifoEENNk){yAd1#=`m9(G^BOwYM6^)a2tzQ(N-*A~AWhiaF0|DjFEM@awQ-ERU4f z-@4Plynl*JXU}(A?R&38&&1o55!6R!vZNbYUpp%FJHn#Tn>`TM+yt5d3b?8vU08q; z1F|w&;{N79$cD9|W{?_J49*izCCKRC<<~&IU+Kb|!4$mGiz0fDlbA9&cN+9^KfA@2Ghj}rn_YwizF!BQy;{#xxM zj?L}Q_NNrEe9BN<3Ih0lyb7ze_lpC}%Ak8|GRy0vu|v}Zx;^hKTt9S*c8v|AjdMrB z;U3*^9*iM-gRXYhruXA2K3_fEC(g#A{0m?9+yuM*mHCNAH#F{e2fw~tCrwyNb9>+X z|Gl=0j~)g^Z3Mgaf%tWK9KCBT5Q>|1x#z=q(z|0fw1269RS`w>;B#kMeS0rCUbca! z{ZiSa$PW$v+Sa6fzf849KhU&5s6Me}74=!v3)bvgfQ1@4!UpAA^w+PL931Rvls(dvF66ZRMij=PFpb_Cr@ zPnxtmmpZPNl7sGMTz6!ka8TlkuKFR+N{L-*@x8O49H+tB<{L2jo;q#XQ^)%+=flT} zG`^uaU8pbUONU$I#k|fEXGt(Y*FZzuu)mY|clSlxbqNF5uW%AwJ-3LQg420b;dW3> zn!wvmgrb4^YhnIIZDIH)2fSYKfnM$#kI#A>gvuH2;4?3R?!_C^*QphBc!dUz9^DVC z!Vgf^f`} z{}U9}Bx6fZrD#3B6Fr!GntNSHg3c#C^TdfJxZZz*B()sDEhZX#r&0@*)LpTmN`=$) zBW=nxD@Cy_j#^*crD^ka;eZJeZ~y*Va$jCU3ab^w^Im=VcApbCS{6+dH3<$Yeu`)M0+qY&P@W3)Mvf*hpS-+-%)Sa@*73nByWa>taG%hMa>tJ3omR z&Hsr$lm8RfZG6fd6Qg;=i~e>Y^&f=l%tDFRu3A%CQ;u0t-D+CkIvo2gc>=q25f;Zb z@SDBgK$a_``F-teV=i82y&g$8<9-S!78cR`1E)|mFNHoAxk7{EMb5jnnnym-sHuH) zk<$8I6?$BDV9#5d;MKKPAln)!MkS1-AQyG)d*~eg=h|5~+jTh3c)A6=-!6k{kIsA|{k*gbB&T<5mk8(8{&5t>M_$b&A;e*xv)HqgY z3)?hw<29e$dFGcn@Sz*i89GW6XL<6SGGk~^Val$4MtOG9&ShT)-MzP;MoC%lf;bgS zf98VlG?>B;$V)r2W*AWT$TsD~Q~WL056^Xb24@Bj=k5^7_L5Vk=Y3Bc;k+BxCoZOw z(r(iGp)&rwd<5TYS%IFTHi}6b6>&#z8~XY9vBV^4mS*&$661dbcy$RQ*OpA~ZtjZa zKMgp^VKR)lKZ3Vrg}?@jLEIPfh21~);+V0+dBmg%c>a4VmT%C5+_41^KCe~C&ELXz zr_BPZ<|CIJM#y!Sq)03)S$VO)E%<(1jEk;Y%ga5)Ax=B=sX^>J6kcDEEi6PP83b| zMdQ3RU+JF28nHZb9NfA}dtA4p;Jrp)c%lA=bc^Ou+d&OZ_*7yu?%yZt1=jVj^j0G+ zXqTfIl3OP0$Qzm$;7lJrPm%JsQcm7qOWMg53v~l+VaoT%aO6-=)@pLWw-WEkXMZdw zwoAU^v#nfuDHz|IBycUhMfnhEZ#v+tc(d8jI`F;~JnbR*OZ^d)6-VPv?L)Tf0_Di# zuqplCMf7^kQ`sFCU3xWcqxf*g6;3?-tGeogC5!(RkiWMgOq??r(kq8!qV!I%U1o_R zbCh_sVm_T-oX1~ORP2nOF6NCDwK!1hfz+Ui`*rnEZTkzHc3=hXF{u?3UKp~9#GD=x zP{n_5cw(-%9?TrsCZ?E2isy_ngdcC7WAV*y*m+bNw#=)Ls!(C{d)XrV)z^()x+}r$ zmx?&7&tp&@e3K5B`tsxCPOPGNj7>ErvWk)}t_v18&NK%nJ4AB#pJ(`jMYb5m-oof_ znJ}+nCJpT#CCF;DXn3&_h&GontVEOht_tVGt~=;}|94QY-bi*6_kdA?FKzd`42nTN z#8sJ5FycuP==o)0@s2QXIem~8pK1X8`jNO`fsd7R%(DJ`=5x*gWdQ*W)HG0J^)jO41>lEoiHr(DTNJLBYxfC zh#?EFQnQbpY=rkSzO-Qu)o-~D+r4*^jn5tO{oM;!;2fH=dIDa_dna5^d@H6+7%E

=WuLe9@KPd6GJ{!P^T7ch#6}NYPGg_;C&68Yv=?rgfvu*SJ%#4)mg%al5yEg7`zEAN;NTE?d}pgz*6$#k)A=PVc)bLMZNGv8M@zf+ zCW+movlpx_&%@3tCbJWclCL!zgTCa@iA6bdVPQ739aY0)Kf?LqK6~yjv5=N^FM|*E z8IW$M$-6$e@*VF2q36≪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%0A#Q@mkkizN`27U-(50`$kYVrH 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�(Y@f_>L0XCV;4>T=`M_(*(4S# zCP3l%8^V9R3aI&IPqNF^2X*s<^flED6JOsDI>zeov<4fljc0yW_0s9l+y2l?94Yh- zIYBJ>Ds+Z5LZ`6;esVQoU)7b=+rXR8AD=J2m>x|Z!*rB|pZ8*MsTm=`C_Q1%}&$9eEP0-CmnyLKM zfIqpr#W6pF zt+;kn0ffF(<^G*?uuHWiKU~y~@2_})d$X3{hkP%r$sQo(w55v{e*eUuu3`95@=K08 zmrkdZb;zs2xXdqiJS8kN3XMizob%4O+|6O05ZKZPIk}TDLg_MO`FG*J z6T0)~h(*|K%~ks9ZvgG@h}8CJ80)V*0Yk63;VQ?|Sli8ndV5q-cVji~|65 z%cXR?gC+Sch=wCgI-u$QQSmM0p5PeQm3$XwqV4!jxKLpt7ToI#aCR!r4vbZJo!BTG z?hr|zwFh$D)3L%m(;2wJd@(wQbjC+K0H>}hhk@!3$X5DJ`rbVXQ*&~0)5eQ@=7gv4 b&MKUje>noCsq0~*vKHCg%qvrw*9-m+vO>aZ literal 0 HcmV?d00001 diff --git a/source/tests/pt/water/data/data_0/type.raw b/source/tests/pt/water/data/data_0/type.raw new file mode 100644 index 0000000000..97e8fdfcf8 --- /dev/null +++ b/source/tests/pt/water/data/data_0/type.raw @@ -0,0 +1,192 @@ +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 diff --git a/source/tests/pt/water/data/data_0/type_map.raw b/source/tests/pt/water/data/data_0/type_map.raw new file mode 100644 index 0000000000..e900768b1d --- /dev/null +++ b/source/tests/pt/water/data/data_0/type_map.raw @@ -0,0 +1,2 @@ +O +H diff --git a/source/tests/pt/water/data/single/set.000/box.npy b/source/tests/pt/water/data/single/set.000/box.npy new file mode 100644 index 0000000000000000000000000000000000000000..65897e0f9c5ec79e1a3182fc9b9bd6e00799244f GIT binary patch literal 164 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= gXCxM+0{I$-ItrGWItsN4WCN~HS$9VUB!G<%0BRf_UjP6A literal 0 HcmV?d00001 diff --git a/source/tests/pt/water/data/single/set.000/coord.npy b/source/tests/pt/water/data/single/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..6e0594a8030bf6c9d7e3e2a4d1c7b9e5b421af1f GIT binary patch literal 2432 zcmbW0`yKSLG75FhocSMPePwHe zDG}%MkyhDAaw(@}QcH=XEYf6s|Ap`Kx98*6$Mbo}ea$LQ-wAR%4G4^>keQ2TcsCu|QUiNSf~zKtfgx(c`|H{sc96>7buPIJ#z zpzPcOmZ79gU#n@*lNTcNu2Do^iw>1Pe+Jzh3%HgJUl=YGk-6nWy0|J7)|wyKPT3Z| zQ`RT-TXL2ecU;AbQVn5sWgRjX+tc^Fn2BVzi0p8pyPG<3Niu<=6+>Ky7ZQzqQs)=u zZi4HGgx#t<%3kEHr4_xq(74Q)I29!dOX*-zEgPhm_DLWAZAm&#xmepcgVd%iCm+Rg z5V$$e#%reZXsZO;GNPKOV#x2>1Frvfj9z!c&8qo?&Hb=azljoA1*AIlxGVn#G1=9e z?m|u4q+-kO|LQW+xpM<^gY6MtFprcZj@0{YHJ0vHqa?L6NG@&06g?w49BD>&i?pTt zEdFGtxK><`nMtL?JzUa&FNHl6z})8wekpy)epN6g+t>`ePTdEu(Ow=_Dfm&UMwyRC zuwSOn2gE%Af60qqRel|pJ>5`g?MB%jJV^h@EZ%uu6W%|aO;eXzayd+iK1Ghf?xPQX z^;a>i(UYf{!&Nx&bTcyx)f7fc0`Pry3tSS$kP#^lnOz{+`bE>Re-xZ~a9=Qk!UKzNKeY-EOMFOFG7P&G1u8BXL&2oo z^mhA6WVKpR_RUb(8jiAD-vmg-%P2f+8yvaYMkd#c<)SFE9zmVEVSvBVF)?2I?+(0 zJAOM`2G1+1MNYq+zZXbCRE+r!;PNVLTwe*s9q3BH*BhK z?tVWKY9>-@;&k$rt4H>7UHEhfsATC>!mjP~`cV!m66@2Bh)x6s26O2h0<=9OGHa5f z2Y>&F`KHg=&S6z@X}rKC9Gi;B?PX{fua~moix@mUk5so!WFt4jNY~entdBgzhQ~AL zQ`IESKYlSy4jkalxjAELatG_r$Yjcgy(nedQRvBAQQK-|5_Zf+;-A{s_n!i;^cOR7 z&^eBjB00Kv)r~9-%h020MkcNr{CIK>4lVH@xA#9|!Ru5+saIg7TOxEWnb8cJHk^(B z9oA0y2PHGHvA9{0T$Q1^HNjIQj3 zOLr@8q>ziD=4rI?uo4+wdBh#mZ-VQT2)?KD3KT?pU{>Zy-@G9DBvIxCjdjqf)+K+* z1fge%3LV=#hJXqUUanhA(|hIVK)w{O5@*+N8$L($~brivU{;y-q1lLr3CR4Oj3jo{H*EVCOJ5AYw7YIdtiD$fgHCep{u!B7=JB8 zn(?fV(;t3{mzh6sE?XO+qUk{S>Kz!pYfqjDdrA54hC-Jd&8p9#$^jk{ z-LJ_$>Iw8OyhV5H2fWMQOOu?7k@A-|d4R)1hdpEo?!EN*ywEr_r*-vnU|%2+aF7u!4L6&5}*0RqBS=eR4C^%~GMD z3!gWX?aO^}WH$!C^Psd-sKPFg8rCA;8B|wnL}TJTIKm0Ze7qV0Z4q>e z?_mBDXJpN>oA#)8(XDn0Vdg1YXe4DJmZ7S_UdaEI=llEXRa1QB;QfOgaDhUmW z`H3|Sa6hj_LVpR2x@$4hID`hCPo{S92c&o_khgIPdD)~vwRRq@ZcT)Njy>K?-iyDc uuOO#;rYP*6E9Cv|<7JitZD`h|thghHADP2$ukfd>(&eo3oe~yBzs0}CO%F=| literal 0 HcmV?d00001 diff --git a/source/tests/pt/water/data/single/set.000/energy.npy b/source/tests/pt/water/data/single/set.000/energy.npy new file mode 100644 index 0000000000000000000000000000000000000000..a0a88fb78ae09feb17e41593d6d8f60084479320 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= aXCxM+0{I$-I+{8PwF(pfu7z)39s>Z!l^&G< literal 0 HcmV?d00001 diff --git a/source/tests/pt/water/data/single/set.000/force.npy b/source/tests/pt/water/data/single/set.000/force.npy new file mode 100644 index 0000000000000000000000000000000000000000..d5b847a86e3a5eea0476a8cd93210965b3cb44b9 GIT binary patch literal 2432 zcmbWr`6HEi8-Q_J$WjbRqE1PNk|>It=e`v&Dq&icNE~IdG$aivOIeaFh7`&cO_sC_ za-Q#fM50Nwn50rusD@FQrlv*By#K;`{r0)CRM0X|tRlS1yxo z-V9@3F3L*O+z?Z39`n}XeQc;dGwD{f@e=<;_QT{AA zN4n7_Baz+5bwN0Cu!>%k#IeG}zhPPJ5bS-aMKLWYSabL!##vd~owXg|?OVLjsM~|} zXRc+D3**sMuZ{eRJE*Ah1Oi@Ni~dW z^JA4;FVGE>a@^#;4TF0H(i~qM(iXhKcjisJ(^1!B7=dn(9yxob*sW{Cwh!!Xg;dj@Qs41=vT6D{C!efq2^*6=Int2!< z{*AQv(g)bpQ%%*YWI(v-0e7H83KbFs(#<=>MxD~w@^emX$3MehRwl$=K`*>%i(_8- z?Ibg!j$0w#fP3D@u#k)qR<+)rRq3QKeQ6Co9BSw4-YQGG_gPBWOItg&fajb!*Je9> zFNobsE5$T)WTsng!s35U0<*q`>g~?B7`n|j9RverqoHU9^wdGeT446j4`&P?Q{ zWRquLH7V`?z|9|?3Q;{Fe5OS>n-y`HY?aS*&Fj@j{-83sL^q3`?^gzW<%O_jrve&0 zJx!MiqR@rg1>)2y9PFGZ?U-XOZIMf&q@ur|eVz{uZ~_b-8slDtIbr>cQn+--i!ax7 zpzv#xQFqK7Cstl$b9OYsohEORQz#;v4XyAjhKF^2*ZJX(GHC3g%mUK7eBISLR@& zhVNotfE;tCEfGg4?|n0Ca99W>nh{j4Q$&yZf|$bcO7c`V#H_6DH{>MSaAvhHDP3g+ zx}C9vThU=q-j+<3Dgsi?f6KgAb;BfkTghJMpU53+sbp>mJwIeiXQZFuk&_w-)*CW8 zhf=0eG=aPWvmv)&7M&PAK=g42+2~KFl#W9qFi}F3-!lf6pprB=PN0rc^R{kiYVy0-D?0 zfIdkdtQt0?nvH++%a1<6h(uMp%+?^Tf3A>rE52cO4z|Lb_$WHR`2vesQ^7pfYQllc zCK{>lW%4Hqkkfw7cdaB4tK5Y_%{+GLQx5IVXk>*oIn3tjS2lL@Fh**ng3vUX*ECn* zj2=yc>kg&_R$2UFgC3M=TZxfM+88Yr(d&*r5Z87t4Dg#;h% z&tmVaJlTfiCFB>-53R@6v+_wZM6u1`&^j-SSuU7&c`Ela(|Gw6H1BSOH#)I2Yr`4d zG@^qQ`(&_n(cPkvv?}&u!hC#ZuR%J`uhZz=6ww;{Jj^c^ql#fa>!0rk16s-SYflgUlZpE8& zwp^@02FlC0Fg1=8FCaXt{=`(TnxgYuT`I4)WMvj{@a&Q-o47L=Zf{nFKUUm=WY2i| zB8tcEor)9|Vu|nd5-~R?8%|l>=cl*trWn6su7gveBcmE<^T8Vb{H2s`#ASfm>@H?z zUBYdgW6dq{TY|Z^^D*+}1AMsIf|Mo3*fuI-_t8cIxl`U#&`<~7bTh|FN;E4ujY zp2oDN*AY!zGPsAYs_|=rE_u5KQs~Nagt{RRKF=Q8|8k+pPsgENX)J4a9l{2m_Os5E zwN%zoNR=0+;IniUS~{)_HhPRBk8c`bY?}e^`|Kfat-2JX&F7$Pw3ZJX>0q_9)mh4( zqa;eW&DCx0z#0=-iKO^Wg8pyLv2%f2c=bnxlHxmc%)4U=`xlJPcc%aV literal 0 HcmV?d00001 diff --git a/source/tests/pt/water/data/single/type.raw b/source/tests/pt/water/data/single/type.raw new file mode 100644 index 0000000000..97e8fdfcf8 --- /dev/null +++ b/source/tests/pt/water/data/single/type.raw @@ -0,0 +1,192 @@ +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 diff --git a/source/tests/pt/water/data/single/type_map.raw b/source/tests/pt/water/data/single/type_map.raw new file mode 100644 index 0000000000..e900768b1d --- /dev/null +++ b/source/tests/pt/water/data/single/type_map.raw @@ -0,0 +1,2 @@ +O +H diff --git a/source/tests/pt/water/lkf.json b/source/tests/pt/water/lkf.json new file mode 100644 index 0000000000..4385d02136 --- /dev/null +++ b/source/tests/pt/water/lkf.json @@ -0,0 +1,79 @@ +{ + "model": { + "type_map": [ + "O", + "H" + ], + "descriptor": { + "type": "se_e2_a", + "sel": [ + 46, + 92 + ], + "rcut_smth": 0.50, + "rcut": 6.00, + "neuron": [ + 25, + 25, + 25 + ], + "resnet_dt": false, + "axis_neuron": 16, + "seed": 1, + "_comment": " that's all" + }, + "fitting_net": { + "neuron": [ + 100, + 100, + 100 + ], + "resnet_dt": true, + "seed": 1, + "_comment": " that's all" + }, + "data_stat_nbatch": 20, + "_comment": " that's all" + }, + "learning_rate": { + "type": "exp", + "decay_steps": 5000, + "start_lr": 0.001, + "stop_lr": 3.51e-8, + "_comment": "that's all" + }, + "loss": { + "type": "ener", + "start_pref_e": 0.02, + "limit_pref_e": 1, + "start_pref_f": 1000, + "limit_pref_f": 1, + "_comment": " that's all" + }, + "training": { + "training_data": { + "systems": [ + "pt/water/data/data_0" + ], + "batch_size": 3, + "_comment": "that's all" + }, + "validation_data": { + "systems": [ + "pt/water/data/data_0" + ], + "batch_size": 1, + "numb_btch": 3, + "_comment": "that's all" + }, + "numb_steps": 1, + "seed": 10, + "disp_file": "lcurve.out", + "disp_freq": 1, + "save_freq": 1, + "opt_type": "LKF", + "kf_blocksize": 1024, + "_comment": "that's all" + }, + "_comment": "that's all" +} diff --git a/source/tests/pt/water/se_atten.json b/source/tests/pt/water/se_atten.json new file mode 100644 index 0000000000..8867e0db41 --- /dev/null +++ b/source/tests/pt/water/se_atten.json @@ -0,0 +1,84 @@ +{ + "_comment": "that's all", + "model": { + "type_map": [ + "O", + "H" + ], + "descriptor": { + "type": "se_atten", + "sel": 40, + "rcut_smth": 0.5, + "rcut": 4.0, + "neuron": [ + 25, + 50, + 100 + ], + "axis_neuron": 16, + "attn": 64, + "attn_layer": 2, + "attn_dotr": true, + "attn_mask": false, + "post_ln": true, + "ffn": false, + "ffn_embed_dim": 512, + "activation": "tanh", + "scaling_factor": 1.0, + "head_num": 1, + "normalize": false, + "temperature": 1.0 + }, + "fitting_net": { + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "seed": 1, + "_comment": " that's all" + }, + "_comment": " that's all" + }, + "learning_rate": { + "type": "exp", + "decay_steps": 5000, + "start_lr": 0.001, + "stop_lr": 3.51e-08, + "_comment": "that's all" + }, + "loss": { + "type": "ener", + "start_pref_e": 0.02, + "limit_pref_e": 1, + "start_pref_f": 1000, + "limit_pref_f": 1, + "start_pref_v": 0, + "limit_pref_v": 0, + "_comment": " that's all" + }, + "training": { + "training_data": { + "systems": [ + "pt/water/data/data_0" + ], + "batch_size": 1, + "_comment": "that's all" + }, + "validation_data": { + "systems": [ + "pt/water/data/data_0" + ], + "batch_size": 1, + "numb_btch": 1, + "_comment": "that's all" + }, + "numb_steps": 1000000, + "seed": 10, + "disp_file": "lcurve.out", + "disp_freq": 100, + "save_freq": 1000, + "_comment": "that's all" + } +} diff --git a/source/tests/pt/water/se_e2_a.json b/source/tests/pt/water/se_e2_a.json new file mode 100644 index 0000000000..425ca3cbf5 --- /dev/null +++ b/source/tests/pt/water/se_e2_a.json @@ -0,0 +1,77 @@ +{ + "model": { + "type_map": [ + "O", + "H" + ], + "descriptor": { + "type": "se_e2_a", + "sel": [ + 46, + 92 + ], + "rcut_smth": 0.50, + "rcut": 6.00, + "neuron": [ + 25, + 50, + 100 + ], + "resnet_dt": false, + "axis_neuron": 16, + "seed": 1, + "_comment": " that's all" + }, + "fitting_net": { + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "seed": 1, + "_comment": " that's all" + }, + "data_stat_nbatch": 20, + "_comment": " that's all" + }, + "learning_rate": { + "type": "exp", + "decay_steps": 5000, + "start_lr": 0.001, + "stop_lr": 3.51e-8, + "_comment": "that's all" + }, + "loss": { + "type": "ener", + "start_pref_e": 0.02, + "limit_pref_e": 1, + "start_pref_f": 1000, + "limit_pref_f": 1, + "_comment": " that's all" + }, + "training": { + "training_data": { + "systems": [ + "pt/water/data/data_0" + ], + "batch_size": 1, + "_comment": "that's all" + }, + "validation_data": { + "systems": [ + "pt/water/data/data_0" + ], + "batch_size": 1, + "numb_btch": 3, + "_comment": "that's all" + }, + "numb_steps": 100000, + "seed": 10, + "disp_file": "lcurve.out", + "disp_freq": 100, + "save_freq": 10000, + "_comment": "that's all" + }, + "_comment": "that's all" +} diff --git a/source/tests/test_adjust_sel.py b/source/tests/test_adjust_sel.py index b1cbdc5afc..9bed3606fd 100644 --- a/source/tests/test_adjust_sel.py +++ b/source/tests/test_adjust_sel.py @@ -82,12 +82,10 @@ def _init_models(): return INPUT, frozen_model, decreased_model, increased_model -INPUT, FROZEN_MODEL, DECREASED_MODEL, INCREASED_MODEL = _init_models() - - class TestDeepPotAAdjustSel(unittest.TestCase): @classmethod def setUpClass(self): + INPUT, FROZEN_MODEL, DECREASED_MODEL, INCREASED_MODEL = _init_models() self.dp_original = DeepPot(FROZEN_MODEL) self.dp_decreased = DeepPot(DECREASED_MODEL) self.dp_increased = DeepPot(INCREASED_MODEL) diff --git a/source/tests/test_finetune_se_atten.py b/source/tests/test_finetune_se_atten.py index 3614fcb13a..47fedcf685 100644 --- a/source/tests/test_finetune_se_atten.py +++ b/source/tests/test_finetune_se_atten.py @@ -147,67 +147,77 @@ def _init_models(setup_model, i): ) -if not parse_version(tf.__version__) < parse_version("1.15"): - - def previous_se_atten(jdata): - jdata["model"]["descriptor"]["stripped_type_embedding"] = False - jdata["model"]["descriptor"]["attn_layer"] = 2 - - def stripped_model(jdata): - jdata["model"]["descriptor"]["stripped_type_embedding"] = True - jdata["model"]["descriptor"]["attn_layer"] = 2 - - def compressible_model(jdata): - jdata["model"]["descriptor"]["stripped_type_embedding"] = True - jdata["model"]["descriptor"]["attn_layer"] = 0 - - models = [previous_se_atten, stripped_model, compressible_model] - INPUT_PRES = [] - INPUT_FINETUNES = [] - INPUT_FINETUNE_MIXS = [] - PRE_MODELS = [] - FINETUNED_MODELS = [] - FINETUNED_MODEL_MIXS = [] - PRE_MAPS = [] - FINETUNED_MAPS = [] - VALID_DATAS = [] - for i, model in enumerate(models): - ( - INPUT_PRE, - INPUT_FINETUNE, - INPUT_FINETUNE_MIX, - PRE_MODEL, - FINETUNED_MODEL, - FINETUNED_MODEL_MIX, - PRE_MAP, - FINETUNED_MAP, - VALID_DATA, - ) = _init_models(model, i) - INPUT_PRES.append(INPUT_PRE) - INPUT_FINETUNES.append(INPUT_FINETUNE) - INPUT_FINETUNE_MIXS.append(INPUT_FINETUNE_MIX) - PRE_MODELS.append(PRE_MODEL) - FINETUNED_MODELS.append(FINETUNED_MODEL) - FINETUNED_MODEL_MIXS.append(FINETUNED_MODEL_MIX) - PRE_MAPS.append(PRE_MAP) - FINETUNED_MAPS.append(FINETUNED_MAP) - VALID_DATAS.append(VALID_DATA) - - @unittest.skipIf( parse_version(tf.__version__) < parse_version("1.15"), f"The current tf version {tf.__version__} is too low to run the new testing model.", ) class TestFinetuneSeAtten(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + if not parse_version(tf.__version__) < parse_version("1.15"): + + def previous_se_atten(jdata): + jdata["model"]["descriptor"]["stripped_type_embedding"] = False + jdata["model"]["descriptor"]["attn_layer"] = 2 + + def stripped_model(jdata): + jdata["model"]["descriptor"]["stripped_type_embedding"] = True + jdata["model"]["descriptor"]["attn_layer"] = 2 + + def compressible_model(jdata): + jdata["model"]["descriptor"]["stripped_type_embedding"] = True + jdata["model"]["descriptor"]["attn_layer"] = 0 + + models = [previous_se_atten, stripped_model, compressible_model] + INPUT_PRES = [] + INPUT_FINETUNES = [] + INPUT_FINETUNE_MIXS = [] + PRE_MODELS = [] + FINETUNED_MODELS = [] + FINETUNED_MODEL_MIXS = [] + PRE_MAPS = [] + FINETUNED_MAPS = [] + VALID_DATAS = [] + for i, model in enumerate(models): + ( + INPUT_PRE, + INPUT_FINETUNE, + INPUT_FINETUNE_MIX, + PRE_MODEL, + FINETUNED_MODEL, + FINETUNED_MODEL_MIX, + PRE_MAP, + FINETUNED_MAP, + VALID_DATA, + ) = _init_models(model, i) + INPUT_PRES.append(INPUT_PRE) + INPUT_FINETUNES.append(INPUT_FINETUNE) + INPUT_FINETUNE_MIXS.append(INPUT_FINETUNE_MIX) + PRE_MODELS.append(PRE_MODEL) + FINETUNED_MODELS.append(FINETUNED_MODEL) + FINETUNED_MODEL_MIXS.append(FINETUNED_MODEL_MIX) + PRE_MAPS.append(PRE_MAP) + FINETUNED_MAPS.append(FINETUNED_MAP) + VALID_DATAS.append(VALID_DATA) + cls.INPUT_PRES = INPUT_PRES + cls.INPUT_FINETUNES = INPUT_FINETUNES + cls.INPUT_FINETUNE_MIXS = INPUT_FINETUNE_MIXS + cls.PRE_MODELS = PRE_MODELS + cls.FINETUNED_MODELS = FINETUNED_MODELS + cls.FINETUNED_MODEL_MIXS = FINETUNED_MODEL_MIXS + cls.PRE_MAPS = PRE_MAPS + cls.FINETUNED_MAPS = FINETUNED_MAPS + cls.VALID_DATAS = VALID_DATAS + @classmethod def tearDownClass(self): - for i in range(len(INPUT_PRES)): - _file_delete(INPUT_PRES[i]) - _file_delete(INPUT_FINETUNES[i]) - _file_delete(INPUT_FINETUNE_MIXS[i]) - _file_delete(PRE_MODELS[i]) - _file_delete(FINETUNED_MODELS[i]) - _file_delete(FINETUNED_MODEL_MIXS[i]) + for i in range(len(self.INPUT_PRES)): + _file_delete(self.INPUT_PRES[i]) + _file_delete(self.INPUT_FINETUNES[i]) + _file_delete(self.INPUT_FINETUNE_MIXS[i]) + _file_delete(self.PRE_MODELS[i]) + _file_delete(self.FINETUNED_MODELS[i]) + _file_delete(self.FINETUNED_MODEL_MIXS[i]) _file_delete("out.json") _file_delete("model.ckpt.meta") _file_delete("model.ckpt.index") @@ -223,22 +233,22 @@ def tearDownClass(self): _file_delete("lcurve.out") def test_finetune_standard(self): - for i in range(len(INPUT_PRES)): - self.valid_data = VALID_DATAS[i] + for i in range(len(self.INPUT_PRES)): + self.valid_data = self.VALID_DATAS[i] pretrained_bias = get_tensor_by_name( - PRE_MODELS[i], "fitting_attr/t_bias_atom_e" + self.PRE_MODELS[i], "fitting_attr/t_bias_atom_e" ) finetuned_bias = get_tensor_by_name( - FINETUNED_MODELS[i], "fitting_attr/t_bias_atom_e" + self.FINETUNED_MODELS[i], "fitting_attr/t_bias_atom_e" ) - sorter = np.argsort(PRE_MAPS[i]) + sorter = np.argsort(self.PRE_MAPS[i]) idx_type_map = sorter[ - np.searchsorted(PRE_MAPS[i], FINETUNED_MAPS[i], sorter=sorter) + np.searchsorted(self.PRE_MAPS[i], self.FINETUNED_MAPS[i], sorter=sorter) ] test_data = self.valid_data.get_test() atom_nums = np.tile(np.bincount(test_data["type"][0])[idx_type_map], (4, 1)) - dp = DeepPotential(PRE_MODELS[i]) + dp = DeepPotential(self.PRE_MODELS[i]) energy = dp.eval( test_data["coord"], test_data["box"], test_data["type"][0] )[0] @@ -250,7 +260,7 @@ def test_finetune_standard(self): 0 ].reshape(-1) - dp_finetuned = DeepPotential(FINETUNED_MODELS[i]) + dp_finetuned = DeepPotential(self.FINETUNED_MODELS[i]) energy_finetuned = dp_finetuned.eval( test_data["coord"], test_data["box"], test_data["type"][0] )[0] @@ -266,22 +276,22 @@ def test_finetune_standard(self): np.testing.assert_almost_equal(finetune_results, 0.0, default_places) def test_finetune_mixed_type(self): - for i in range(len(INPUT_PRES)): - self.valid_data = VALID_DATAS[i] + for i in range(len(self.INPUT_PRES)): + self.valid_data = self.VALID_DATAS[i] pretrained_bias = get_tensor_by_name( - PRE_MODELS[i], "fitting_attr/t_bias_atom_e" + self.PRE_MODELS[i], "fitting_attr/t_bias_atom_e" ) finetuned_bias_mixed_type = get_tensor_by_name( - FINETUNED_MODEL_MIXS[i], "fitting_attr/t_bias_atom_e" + self.FINETUNED_MODEL_MIXS[i], "fitting_attr/t_bias_atom_e" ) - sorter = np.argsort(PRE_MAPS[i]) + sorter = np.argsort(self.PRE_MAPS[i]) idx_type_map = sorter[ - np.searchsorted(PRE_MAPS[i], FINETUNED_MAPS[i], sorter=sorter) + np.searchsorted(self.PRE_MAPS[i], self.FINETUNED_MAPS[i], sorter=sorter) ] test_data = self.valid_data.get_test() atom_nums = np.tile(np.bincount(test_data["type"][0])[idx_type_map], (4, 1)) - dp = DeepPotential(PRE_MODELS[i]) + dp = DeepPotential(self.PRE_MODELS[i]) energy = dp.eval( test_data["coord"], test_data["box"], test_data["type"][0] )[0] @@ -293,7 +303,7 @@ def test_finetune_mixed_type(self): 0 ].reshape(-1) - dp_finetuned_mixed_type = DeepPotential(FINETUNED_MODEL_MIXS[i]) + dp_finetuned_mixed_type = DeepPotential(self.FINETUNED_MODEL_MIXS[i]) energy_finetuned = dp_finetuned_mixed_type.eval( test_data["coord"], test_data["box"], test_data["type"][0] )[0] diff --git a/source/tests/test_init_frz_model_multi.py b/source/tests/test_init_frz_model_multi.py index e5e5733c7d..fc37d82397 100644 --- a/source/tests/test_init_frz_model_multi.py +++ b/source/tests/test_init_frz_model_multi.py @@ -180,20 +180,19 @@ def _init_models(): return INPUT, ckpt, frozen_model, model_ckpt, model_frz, data, stop_batch -( - INPUT, - CKPT, - FROZEN_MODEL, - CKPT_TRAINER, - FRZ_TRAINER, - VALID_DATA, - STOP_BATCH, -) = _init_models() - - class TestInitFrzModelMulti(unittest.TestCase): @classmethod def setUpClass(cls): + ( + cls.INPUT, + cls.CKPT, + cls.FROZEN_MODEL, + CKPT_TRAINER, + FRZ_TRAINER, + VALID_DATA, + STOP_BATCH, + ) = _init_models() + cls.dp_ckpt = CKPT_TRAINER cls.dp_frz = FRZ_TRAINER cls.valid_data_dict = {"water_ener": VALID_DATA} @@ -205,19 +204,19 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - _file_delete(INPUT) - _file_delete(FROZEN_MODEL) + _file_delete(cls.INPUT) + _file_delete(cls.FROZEN_MODEL) _file_delete("out.json") _file_delete(str(tests_path / "checkpoint")) - _file_delete(CKPT + ".meta") - _file_delete(CKPT + ".index") - _file_delete(CKPT + ".data-00000-of-00001") - _file_delete(CKPT + "-0.meta") - _file_delete(CKPT + "-0.index") - _file_delete(CKPT + "-0.data-00000-of-00001") - _file_delete(CKPT + "-1.meta") - _file_delete(CKPT + "-1.index") - _file_delete(CKPT + "-1.data-00000-of-00001") + _file_delete(cls.CKPT + ".meta") + _file_delete(cls.CKPT + ".index") + _file_delete(cls.CKPT + ".data-00000-of-00001") + _file_delete(cls.CKPT + "-0.meta") + _file_delete(cls.CKPT + "-0.index") + _file_delete(cls.CKPT + "-0.data-00000-of-00001") + _file_delete(cls.CKPT + "-1.meta") + _file_delete(cls.CKPT + "-1.index") + _file_delete(cls.CKPT + "-1.data-00000-of-00001") _file_delete("input_v2_compat.json") _file_delete("lcurve.out") diff --git a/source/tests/test_init_frz_model_se_a.py b/source/tests/test_init_frz_model_se_a.py index d98c2bc14f..7545e3aae9 100644 --- a/source/tests/test_init_frz_model_se_a.py +++ b/source/tests/test_init_frz_model_se_a.py @@ -128,20 +128,18 @@ def _init_models(): return INPUT, ckpt, frozen_model, model_ckpt, model_frz, data, stop_batch -( - INPUT, - CKPT, - FROZEN_MODEL, - CKPT_TRAINER, - FRZ_TRAINER, - VALID_DATA, - STOP_BATCH, -) = _init_models() - - class TestInitFrzModelA(unittest.TestCase): @classmethod def setUpClass(cls): + ( + cls.INPUT, + cls.CKPT, + cls.FROZEN_MODEL, + CKPT_TRAINER, + FRZ_TRAINER, + VALID_DATA, + STOP_BATCH, + ) = _init_models() cls.dp_ckpt = CKPT_TRAINER cls.dp_frz = FRZ_TRAINER cls.valid_data = VALID_DATA @@ -149,19 +147,19 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - _file_delete(INPUT) - _file_delete(FROZEN_MODEL) + _file_delete(cls.INPUT) + _file_delete(cls.FROZEN_MODEL) _file_delete("out.json") _file_delete(str(tests_path / "checkpoint")) - _file_delete(CKPT + ".meta") - _file_delete(CKPT + ".index") - _file_delete(CKPT + ".data-00000-of-00001") - _file_delete(CKPT + "-0.meta") - _file_delete(CKPT + "-0.index") - _file_delete(CKPT + "-0.data-00000-of-00001") - _file_delete(CKPT + "-1.meta") - _file_delete(CKPT + "-1.index") - _file_delete(CKPT + "-1.data-00000-of-00001") + _file_delete(cls.CKPT + ".meta") + _file_delete(cls.CKPT + ".index") + _file_delete(cls.CKPT + ".data-00000-of-00001") + _file_delete(cls.CKPT + "-0.meta") + _file_delete(cls.CKPT + "-0.index") + _file_delete(cls.CKPT + "-0.data-00000-of-00001") + _file_delete(cls.CKPT + "-1.meta") + _file_delete(cls.CKPT + "-1.index") + _file_delete(cls.CKPT + "-1.data-00000-of-00001") _file_delete("input_v2_compat.json") _file_delete("lcurve.out") diff --git a/source/tests/test_init_frz_model_se_a_tebd.py b/source/tests/test_init_frz_model_se_a_tebd.py index 594bf83085..1b282c00d5 100644 --- a/source/tests/test_init_frz_model_se_a_tebd.py +++ b/source/tests/test_init_frz_model_se_a_tebd.py @@ -129,20 +129,19 @@ def _init_models(): return INPUT, ckpt, frozen_model, model_ckpt, model_frz, data, stop_batch -( - INPUT, - CKPT, - FROZEN_MODEL, - CKPT_TRAINER, - FRZ_TRAINER, - VALID_DATA, - STOP_BATCH, -) = _init_models() - - class TestInitFrzModelA(unittest.TestCase): @classmethod def setUpClass(cls): + ( + cls.INPUT, + cls.CKPT, + cls.FROZEN_MODEL, + CKPT_TRAINER, + FRZ_TRAINER, + VALID_DATA, + STOP_BATCH, + ) = _init_models() + cls.dp_ckpt = CKPT_TRAINER cls.dp_frz = FRZ_TRAINER cls.valid_data = VALID_DATA @@ -150,19 +149,19 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - _file_delete(INPUT) - _file_delete(FROZEN_MODEL) + _file_delete(cls.INPUT) + _file_delete(cls.FROZEN_MODEL) _file_delete("out.json") _file_delete(str(tests_path / "checkpoint")) - _file_delete(CKPT + ".meta") - _file_delete(CKPT + ".index") - _file_delete(CKPT + ".data-00000-of-00001") - _file_delete(CKPT + "-0.meta") - _file_delete(CKPT + "-0.index") - _file_delete(CKPT + "-0.data-00000-of-00001") - _file_delete(CKPT + "-1.meta") - _file_delete(CKPT + "-1.index") - _file_delete(CKPT + "-1.data-00000-of-00001") + _file_delete(cls.CKPT + ".meta") + _file_delete(cls.CKPT + ".index") + _file_delete(cls.CKPT + ".data-00000-of-00001") + _file_delete(cls.CKPT + "-0.meta") + _file_delete(cls.CKPT + "-0.index") + _file_delete(cls.CKPT + "-0.data-00000-of-00001") + _file_delete(cls.CKPT + "-1.meta") + _file_delete(cls.CKPT + "-1.index") + _file_delete(cls.CKPT + "-1.data-00000-of-00001") _file_delete("input_v2_compat.json") _file_delete("lcurve.out") diff --git a/source/tests/test_init_frz_model_se_a_type.py b/source/tests/test_init_frz_model_se_a_type.py index 3221245065..b356dbf6d0 100644 --- a/source/tests/test_init_frz_model_se_a_type.py +++ b/source/tests/test_init_frz_model_se_a_type.py @@ -132,20 +132,18 @@ def _init_models(): return INPUT, ckpt, frozen_model, model_ckpt, model_frz, data, stop_batch -( - INPUT, - CKPT, - FROZEN_MODEL, - CKPT_TRAINER, - FRZ_TRAINER, - VALID_DATA, - STOP_BATCH, -) = _init_models() - - class TestInitFrzModelAType(unittest.TestCase): @classmethod def setUpClass(cls): + ( + cls.INPUT, + cls.CKPT, + cls.FROZEN_MODEL, + CKPT_TRAINER, + FRZ_TRAINER, + VALID_DATA, + STOP_BATCH, + ) = _init_models() cls.dp_ckpt = CKPT_TRAINER cls.dp_frz = FRZ_TRAINER cls.valid_data = VALID_DATA @@ -153,19 +151,19 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - _file_delete(INPUT) - _file_delete(FROZEN_MODEL) + _file_delete(cls.INPUT) + _file_delete(cls.FROZEN_MODEL) _file_delete("out.json") _file_delete(str(tests_path / "checkpoint")) - _file_delete(CKPT + ".meta") - _file_delete(CKPT + ".index") - _file_delete(CKPT + ".data-00000-of-00001") - _file_delete(CKPT + "-0.meta") - _file_delete(CKPT + "-0.index") - _file_delete(CKPT + "-0.data-00000-of-00001") - _file_delete(CKPT + "-1.meta") - _file_delete(CKPT + "-1.index") - _file_delete(CKPT + "-1.data-00000-of-00001") + _file_delete(cls.CKPT + ".meta") + _file_delete(cls.CKPT + ".index") + _file_delete(cls.CKPT + ".data-00000-of-00001") + _file_delete(cls.CKPT + "-0.meta") + _file_delete(cls.CKPT + "-0.index") + _file_delete(cls.CKPT + "-0.data-00000-of-00001") + _file_delete(cls.CKPT + "-1.meta") + _file_delete(cls.CKPT + "-1.index") + _file_delete(cls.CKPT + "-1.data-00000-of-00001") _file_delete("input_v2_compat.json") _file_delete("lcurve.out") diff --git a/source/tests/test_init_frz_model_se_atten.py b/source/tests/test_init_frz_model_se_atten.py index 5554ae415c..7889440cd3 100644 --- a/source/tests/test_init_frz_model_se_atten.py +++ b/source/tests/test_init_frz_model_se_atten.py @@ -146,32 +146,6 @@ def compressible_model(jdata): jdata["model"]["descriptor"]["stripped_type_embedding"] = True jdata["model"]["descriptor"]["attn_layer"] = 0 - models = [previous_se_atten, stripped_model, compressible_model] - INPUTS = [] - CKPTS = [] - FROZEN_MODELS = [] - CKPT_TRAINERS = [] - FRZ_TRAINERS = [] - VALID_DATAS = [] - STOP_BATCHS = [] - for i, model in enumerate(models): - ( - INPUT, - CKPT, - FROZEN_MODEL, - CKPT_TRAINER, - FRZ_TRAINER, - VALID_DATA, - STOP_BATCH, - ) = _init_models(model, i) - INPUTS.append(INPUT) - CKPTS.append(CKPT) - FROZEN_MODELS.append(FROZEN_MODEL) - CKPT_TRAINERS.append(CKPT_TRAINER) - FRZ_TRAINERS.append(FRZ_TRAINER) - VALID_DATAS.append(VALID_DATA) - STOP_BATCHS.append(STOP_BATCH) - @unittest.skipIf( parse_version(tf.__version__) < parse_version("1.15"), @@ -180,6 +154,38 @@ def compressible_model(jdata): class TestInitFrzModelAtten(unittest.TestCase): @classmethod def setUpClass(cls): + models = [previous_se_atten, stripped_model, compressible_model] + INPUTS = [] + CKPTS = [] + FROZEN_MODELS = [] + CKPT_TRAINERS = [] + FRZ_TRAINERS = [] + VALID_DATAS = [] + STOP_BATCHS = [] + for i, model in enumerate(models): + ( + INPUT, + CKPT, + FROZEN_MODEL, + CKPT_TRAINER, + FRZ_TRAINER, + VALID_DATA, + STOP_BATCH, + ) = _init_models(model, i) + INPUTS.append(INPUT) + CKPTS.append(CKPT) + FROZEN_MODELS.append(FROZEN_MODEL) + CKPT_TRAINERS.append(CKPT_TRAINER) + FRZ_TRAINERS.append(FRZ_TRAINER) + VALID_DATAS.append(VALID_DATA) + STOP_BATCHS.append(STOP_BATCH) + cls.INPUTS = INPUTS + cls.CKPTS = CKPTS + cls.FROZEN_MODELS = FROZEN_MODELS + cls.CKPT_TRAINERS = CKPT_TRAINERS + cls.FRZ_TRAINERS = FRZ_TRAINERS + cls.VALID_DATAS = VALID_DATAS + cls.STOP_BATCHS = STOP_BATCHS cls.dp_ckpts = CKPT_TRAINERS cls.dp_frzs = FRZ_TRAINERS cls.valid_datas = VALID_DATAS @@ -188,28 +194,28 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): for i in range(len(cls.dp_ckpts)): - _file_delete(INPUTS[i]) - _file_delete(FROZEN_MODELS[i]) + _file_delete(cls.INPUTS[i]) + _file_delete(cls.FROZEN_MODELS[i]) _file_delete("out.json") _file_delete(str(tests_path / "checkpoint")) - _file_delete(CKPT[i] + ".meta") - _file_delete(CKPT[i] + ".index") - _file_delete(CKPT[i] + ".data-00000-of-00001") - _file_delete(CKPT[i] + "-0.meta") - _file_delete(CKPT[i] + "-0.index") - _file_delete(CKPT[i] + "-0.data-00000-of-00001") - _file_delete(CKPT[i] + "-1.meta") - _file_delete(CKPT[i] + "-1.index") - _file_delete(CKPT[i] + "-1.data-00000-of-00001") + _file_delete(cls.CKPTS[i] + ".meta") + _file_delete(cls.CKPTS[i] + ".index") + _file_delete(cls.CKPTS[i] + ".data-00000-of-00001") + _file_delete(cls.CKPTS[i] + "-0.meta") + _file_delete(cls.CKPTS[i] + "-0.index") + _file_delete(cls.CKPTS[i] + "-0.data-00000-of-00001") + _file_delete(cls.CKPTS[i] + "-1.meta") + _file_delete(cls.CKPTS[i] + "-1.index") + _file_delete(cls.CKPTS[i] + "-1.data-00000-of-00001") _file_delete(f"input_v2_compat{i}.json") _file_delete("lcurve.out") def test_single_frame(self): for i in range(len(self.dp_ckpts)): - self.dp_ckpt = CKPT_TRAINERS[i] - self.dp_frz = FRZ_TRAINERS[i] - self.valid_data = VALID_DATAS[i] - self.stop_batch = STOP_BATCHS[i] + self.dp_ckpt = self.CKPT_TRAINERS[i] + self.dp_frz = self.FRZ_TRAINERS[i] + self.valid_data = self.VALID_DATAS[i] + self.stop_batch = self.STOP_BATCHS[i] valid_batch = self.valid_data.get_batch() natoms = valid_batch["natoms_vec"] diff --git a/source/tests/test_init_frz_model_se_r.py b/source/tests/test_init_frz_model_se_r.py index 84d109bcfd..fd916b3fdc 100644 --- a/source/tests/test_init_frz_model_se_r.py +++ b/source/tests/test_init_frz_model_se_r.py @@ -136,20 +136,19 @@ def _init_models(): return INPUT, ckpt, frozen_model, model_ckpt, model_frz, data, stop_batch -( - INPUT, - CKPT, - FROZEN_MODEL, - CKPT_TRAINER, - FRZ_TRAINER, - VALID_DATA, - STOP_BATCH, -) = _init_models() - - class TestInitFrzModelR(unittest.TestCase): @classmethod def setUpClass(cls): + ( + cls.INPUT, + cls.CKPT, + cls.FROZEN_MODEL, + CKPT_TRAINER, + FRZ_TRAINER, + VALID_DATA, + STOP_BATCH, + ) = _init_models() + cls.dp_ckpt = CKPT_TRAINER cls.dp_frz = FRZ_TRAINER cls.valid_data = VALID_DATA @@ -157,19 +156,19 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - _file_delete(INPUT) - _file_delete(FROZEN_MODEL) + _file_delete(cls.INPUT) + _file_delete(cls.FROZEN_MODEL) _file_delete("out.json") _file_delete(str(tests_path / "checkpoint")) - _file_delete(CKPT + ".meta") - _file_delete(CKPT + ".index") - _file_delete(CKPT + ".data-00000-of-00001") - _file_delete(CKPT + "-0.meta") - _file_delete(CKPT + "-0.index") - _file_delete(CKPT + "-0.data-00000-of-00001") - _file_delete(CKPT + "-1.meta") - _file_delete(CKPT + "-1.index") - _file_delete(CKPT + "-1.data-00000-of-00001") + _file_delete(cls.CKPT + ".meta") + _file_delete(cls.CKPT + ".index") + _file_delete(cls.CKPT + ".data-00000-of-00001") + _file_delete(cls.CKPT + "-0.meta") + _file_delete(cls.CKPT + "-0.index") + _file_delete(cls.CKPT + "-0.data-00000-of-00001") + _file_delete(cls.CKPT + "-1.meta") + _file_delete(cls.CKPT + "-1.index") + _file_delete(cls.CKPT + "-1.data-00000-of-00001") _file_delete("input_v2_compat.json") _file_delete("lcurve.out") diff --git a/source/tests/test_init_frz_model_spin.py b/source/tests/test_init_frz_model_spin.py index 7aa3d514dc..b5c480c2ba 100644 --- a/source/tests/test_init_frz_model_spin.py +++ b/source/tests/test_init_frz_model_spin.py @@ -140,20 +140,19 @@ def _init_models(): return INPUT, ckpt, frozen_model, model_ckpt, model_frz, data, stop_batch -( - INPUT, - CKPT, - FROZEN_MODEL, - CKPT_TRAINER, - FRZ_TRAINER, - VALID_DATA, - STOP_BATCH, -) = _init_models() - - class TestInitFrzModelR(unittest.TestCase): @classmethod def setUpClass(cls): + ( + cls.INPUT, + cls.CKPT, + cls.FROZEN_MODEL, + CKPT_TRAINER, + FRZ_TRAINER, + VALID_DATA, + STOP_BATCH, + ) = _init_models() + cls.dp_ckpt = CKPT_TRAINER cls.dp_frz = FRZ_TRAINER cls.valid_data = VALID_DATA @@ -161,19 +160,19 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - _file_delete(INPUT) - _file_delete(FROZEN_MODEL) + _file_delete(cls.INPUT) + _file_delete(cls.FROZEN_MODEL) _file_delete("out.json") _file_delete(str(tests_path / "checkpoint")) - _file_delete(CKPT + ".meta") - _file_delete(CKPT + ".index") - _file_delete(CKPT + ".data-00000-of-00001") - _file_delete(CKPT + "-0.meta") - _file_delete(CKPT + "-0.index") - _file_delete(CKPT + "-0.data-00000-of-00001") - _file_delete(CKPT + "-1.meta") - _file_delete(CKPT + "-1.index") - _file_delete(CKPT + "-1.data-00000-of-00001") + _file_delete(cls.CKPT + ".meta") + _file_delete(cls.CKPT + ".index") + _file_delete(cls.CKPT + ".data-00000-of-00001") + _file_delete(cls.CKPT + "-0.meta") + _file_delete(cls.CKPT + "-0.index") + _file_delete(cls.CKPT + "-0.data-00000-of-00001") + _file_delete(cls.CKPT + "-1.meta") + _file_delete(cls.CKPT + "-1.index") + _file_delete(cls.CKPT + "-1.data-00000-of-00001") _file_delete("input_v2_compat.json") _file_delete("lcurve.out") diff --git a/source/tests/test_model_compression_se_a_ebd_type_one_side.py b/source/tests/test_model_compression_se_a_ebd_type_one_side.py index 9ad1970e9b..741c95b26e 100644 --- a/source/tests/test_model_compression_se_a_ebd_type_one_side.py +++ b/source/tests/test_model_compression_se_a_ebd_type_one_side.py @@ -98,7 +98,6 @@ def _init_models_exclude_types(): INPUT, FROZEN_MODEL, COMPRESSED_MODEL = _init_models() -INPUT_ET, FROZEN_MODEL_ET, COMPRESSED_MODEL_ET = _init_models_exclude_types() class TestDeepPotAPBC(unittest.TestCase): @@ -444,8 +443,13 @@ def test_ase(self): class TestDeepPotAPBCExcludeTypes(unittest.TestCase): @classmethod def setUpClass(self): - self.dp_original = DeepPot(FROZEN_MODEL_ET) - self.dp_compressed = DeepPot(COMPRESSED_MODEL_ET) + ( + self.INPUT_ET, + self.FROZEN_MODEL_ET, + self.COMPRESSED_MODEL_ET, + ) = _init_models_exclude_types() + self.dp_original = DeepPot(self.FROZEN_MODEL_ET) + self.dp_compressed = DeepPot(self.COMPRESSED_MODEL_ET) self.coords = np.array( [ 12.83, @@ -473,9 +477,9 @@ def setUpClass(self): @classmethod def tearDownClass(self): - _file_delete(INPUT_ET) - _file_delete(FROZEN_MODEL_ET) - _file_delete(COMPRESSED_MODEL_ET) + _file_delete(self.INPUT_ET) + _file_delete(self.FROZEN_MODEL_ET) + _file_delete(self.COMPRESSED_MODEL_ET) _file_delete("out.json") _file_delete("compress.json") _file_delete("checkpoint") diff --git a/source/tests/test_model_compression_se_a_type_one_side_exclude_types.py b/source/tests/test_model_compression_se_a_type_one_side_exclude_types.py index 5b6ac4e13e..bdf09cf3e8 100644 --- a/source/tests/test_model_compression_se_a_type_one_side_exclude_types.py +++ b/source/tests/test_model_compression_se_a_type_one_side_exclude_types.py @@ -66,12 +66,11 @@ def _init_models(): return INPUT, frozen_model, compressed_model -INPUT, FROZEN_MODEL, COMPRESSED_MODEL = _init_models() - - class TestDeepPotAPBCTypeOneSideExcludeTypes(unittest.TestCase): @classmethod def setUpClass(self): + INPUT, FROZEN_MODEL, COMPRESSED_MODEL = _init_models() + self.dp_original = DeepPot(FROZEN_MODEL) self.dp_compressed = DeepPot(COMPRESSED_MODEL) self.coords = np.array( From 2e5333d4ff04bcdc52277f34ca06edfdaa3b6834 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Sat, 27 Jan 2024 18:12:09 +0800 Subject: [PATCH 015/686] atomic model is not required to provide the fitting net (#3184) Co-authored-by: Han Wang --- deepmd/pt/model/model/atomic_model.py | 7 ------- deepmd/pt/model/model/dp_atomic_model.py | 6 ------ 2 files changed, 13 deletions(-) diff --git a/deepmd/pt/model/model/atomic_model.py b/deepmd/pt/model/model/atomic_model.py index 47fd463fc9..9720bfa57d 100644 --- a/deepmd/pt/model/model/atomic_model.py +++ b/deepmd/pt/model/model/atomic_model.py @@ -14,16 +14,9 @@ from deepmd.model_format import ( FittingOutputDef, ) -from deepmd.pt.model.task import ( - Fitting, -) class AtomicModel(ABC): - @abstractmethod - def get_fitting_net(self) -> Fitting: - raise NotImplementedError - @abstractmethod def get_fitting_output_def(self) -> FittingOutputDef: raise NotImplementedError diff --git a/deepmd/pt/model/model/dp_atomic_model.py b/deepmd/pt/model/model/dp_atomic_model.py index ffeeeda660..a0f9b25765 100644 --- a/deepmd/pt/model/model/dp_atomic_model.py +++ b/deepmd/pt/model/model/dp_atomic_model.py @@ -128,12 +128,6 @@ def __init__( self.descriptor.dim_out, self.ntypes - 1, self.descriptor.dim_emb ) - def get_fitting_net(self) -> Fitting: - """Get the fitting net.""" - return ( - self.fitting_net if self.fitting_net is not None else self.coord_denoise_net - ) - def get_fitting_output_def(self) -> FittingOutputDef: """Get the output def of the fitting net.""" return ( From f900f3a1320ac42369789288d8b9b56fa40ee42f Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 27 Jan 2024 05:12:41 -0500 Subject: [PATCH 016/686] cc: fix returning type of sel_types (#3181) Fix the following compiler warning: ``` /home/runner/work/deepmd-kit/deepmd-kit/source/api_c/src/c_api.cc:1336:17: warning: returning address of local temporary object [-Wreturn-stack-address] return (int*)&(dcm->dcm.sel_types())[0]; ^~~~~~~~~~~~~~~~~~~~~~ 1 warning generated. ``` by returning the reference of `sel_type`. `DataChargeModifier.sel_types` is not used anywhere, even in the test, so we don't have a chance to determine if there is a possible segfault, and this warning has no actual impact. It seems `DeepTensor` has returned a reference since the beginning (https://github.com/deepmodeling/deepmd-kit/pull/137). (perhaps because `DeepTensor.sel_types` is used) `DeepTensor` and `DataChargeModifier` have different returned types. --- source/api_cc/include/DataModifier.h | 4 ++-- source/api_cc/include/DataModifierTF.h | 2 +- source/api_cc/src/DataModifier.cc | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/source/api_cc/include/DataModifier.h b/source/api_cc/include/DataModifier.h index 1e611a3930..6d443d9b9c 100644 --- a/source/api_cc/include/DataModifier.h +++ b/source/api_cc/include/DataModifier.h @@ -84,7 +84,7 @@ class DipoleChargeModifierBase { * @brief Get the list of sel types. * @return The list of sel types. */ - virtual std::vector sel_types() const = 0; + virtual const std::vector& sel_types() const = 0; }; /** @@ -161,7 +161,7 @@ class DipoleChargeModifier { * @brief Get the list of sel types. * @return The list of sel types. */ - std::vector sel_types() const; + const std::vector& sel_types() const; private: bool inited; diff --git a/source/api_cc/include/DataModifierTF.h b/source/api_cc/include/DataModifierTF.h index c0021c6947..cd1d696c3c 100644 --- a/source/api_cc/include/DataModifierTF.h +++ b/source/api_cc/include/DataModifierTF.h @@ -84,7 +84,7 @@ class DipoleChargeModifierTF : public DipoleChargeModifierBase { * @brief Get the list of sel types. * @return The list of sel types. */ - std::vector sel_types() const { + const std::vector& sel_types() const { assert(inited); return sel_type; }; diff --git a/source/api_cc/src/DataModifier.cc b/source/api_cc/src/DataModifier.cc index 38d1fc879a..bac2e13da5 100644 --- a/source/api_cc/src/DataModifier.cc +++ b/source/api_cc/src/DataModifier.cc @@ -92,6 +92,6 @@ double DipoleChargeModifier::cutoff() const { return dcm->cutoff(); } int DipoleChargeModifier::numb_types() const { return dcm->numb_types(); } -std::vector DipoleChargeModifier::sel_types() const { +const std::vector& DipoleChargeModifier::sel_types() const { return dcm->sel_types(); } From 2631ce265b548b3ff8593599a2de74aed664c620 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 27 Jan 2024 05:19:15 -0500 Subject: [PATCH 017/686] breaking: drop Python 3.7 support (#3185) ... per discussion. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- backend/find_tensorflow.py | 6 +++--- backend/read_env.py | 2 +- deepmd/tf/utils/tabulate.py | 4 ++-- doc/development/coding-conventions.rst | 2 +- doc/install/easy-install.md | 4 ++++ doc/install/install-from-source.md | 5 +++-- pyproject.toml | 4 ++-- source/install/build_tf.py | 22 +++++++++++----------- 8 files changed, 27 insertions(+), 22 deletions(-) diff --git a/backend/find_tensorflow.py b/backend/find_tensorflow.py index 08a73f7252..32ae62469c 100644 --- a/backend/find_tensorflow.py +++ b/backend/find_tensorflow.py @@ -28,7 +28,7 @@ ) -@lru_cache() +@lru_cache def find_tensorflow() -> Tuple[Optional[str], List[str]]: """Find TensorFlow library. @@ -111,7 +111,7 @@ def find_tensorflow() -> Tuple[Optional[str], List[str]]: return tf_install_dir, requires -@lru_cache() +@lru_cache def get_tf_requirement(tf_version: str = "") -> dict: """Get TensorFlow requirement (CPU) when TF is not installed. @@ -189,7 +189,7 @@ def get_tf_requirement(tf_version: str = "") -> dict: } -@lru_cache() +@lru_cache def get_tf_version(tf_path: Union[str, Path]) -> str: """Get TF version from a TF Python library path. diff --git a/backend/read_env.py b/backend/read_env.py index ba6bf5f9f3..2cf433181a 100644 --- a/backend/read_env.py +++ b/backend/read_env.py @@ -19,7 +19,7 @@ ) -@lru_cache() +@lru_cache def get_argument_from_env() -> Tuple[str, list, list, dict, str]: """Get the arguments from environment variables. diff --git a/deepmd/tf/utils/tabulate.py b/deepmd/tf/utils/tabulate.py index 4ade5962e0..ff5e2b9e09 100644 --- a/deepmd/tf/utils/tabulate.py +++ b/deepmd/tf/utils/tabulate.py @@ -770,12 +770,12 @@ def _get_layer_size(self): return layer_size @property - @lru_cache() + @lru_cache def _n_all_excluded(self) -> int: """Then number of types excluding all types.""" return sum(int(self._all_excluded(ii)) for ii in range(0, self.ntypes)) - @lru_cache() + @lru_cache def _all_excluded(self, ii: int) -> bool: """Check if type ii excluds all types. diff --git a/doc/development/coding-conventions.rst b/doc/development/coding-conventions.rst index ad4203ee4f..137b0d0d51 100644 --- a/doc/development/coding-conventions.rst +++ b/doc/development/coding-conventions.rst @@ -30,7 +30,7 @@ Rules ----- The code must be compatible with the oldest supported version of python -which is 3.7 +which is 3.8. The project follows the generic coding conventions as specified in the `Style Guide for Python Code`_, `Docstring diff --git a/doc/install/easy-install.md b/doc/install/easy-install.md index 3bc1f4b944..2d0972c8be 100644 --- a/doc/install/easy-install.md +++ b/doc/install/easy-install.md @@ -8,6 +8,10 @@ After your easy installation, DeePMD-kit (`dp`) and LAMMPS (`lmp`) will be avail Note: The off-line packages and conda packages require the [GNU C Library](https://www.gnu.org/software/libc/) 2.17 or above. The GPU version requires [compatible NVIDIA driver](https://docs.nvidia.com/deploy/cuda-compatibility/index.html#minor-version-compatibility) to be installed in advance. It is possible to force conda to [override detection](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-virtual.html#overriding-detected-packages) when installation, but these requirements are still necessary during runtime. ::: +:::{note} +Python 3.8 or above is required for Python interface. +::: + - [Install off-line packages](#install-off-line-packages) - [Install with conda](#install-with-conda) - [Install with docker](#install-with-docker) diff --git a/doc/install/install-from-source.md b/doc/install/install-from-source.md index 4f94b9c793..51d1f4c1e5 100644 --- a/doc/install/install-from-source.md +++ b/doc/install/install-from-source.md @@ -16,12 +16,13 @@ deepmd_source_dir=`pwd` ## Install the python interface ### Install Tensorflow's python interface -First, check the python version on your machine +First, check the python version on your machine. +Python 3.8 or above is required. ```bash python --version ``` -We follow the virtual environment approach to install TensorFlow's Python interface. The full instruction can be found on the official [TensorFlow website](https://www.tensorflow.org/install/pip). TensorFlow 1.8 or later is supported. Now we assume that the Python interface will be installed to the virtual environment directory `$tensorflow_venv` +We follow the virtual environment approach to install TensorFlow's Python interface. The full instruction can be found on the official [TensorFlow website](https://www.tensorflow.org/install/pip). TensorFlow 2.2 or later is supported. Now we assume that the Python interface will be installed to the virtual environment directory `$tensorflow_venv` ```bash virtualenv -p python3 $tensorflow_venv source $tensorflow_venv/bin/activate diff --git a/pyproject.toml b/pyproject.toml index 8b8da65aaf..6c8632ddb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", "Environment :: GPU :: NVIDIA CUDA :: 12 :: 12.2", "Intended Audience :: Science/Research", - "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Scientific/Engineering :: Physics", @@ -46,7 +46,7 @@ dependencies = [ 'wcmatch', 'packaging', ] -requires-python = ">=3.7" +requires-python = ">=3.8" keywords = ["deepmd"] [project.entry-points."lammps.plugins"] diff --git a/source/install/build_tf.py b/source/install/build_tf.py index 15847d2c21..3e3700b9ac 100755 --- a/source/install/build_tf.py +++ b/source/install/build_tf.py @@ -423,14 +423,14 @@ def __init__(self, version="1.11.0") -> None: self.version = version @property - @lru_cache() + @lru_cache def resources(self) -> Dict[str, OnlineResource]: return { "bazelisk": RESOURCES["bazelisk-" + self.version], } @property - @lru_cache() + @lru_cache def dependencies(self) -> Dict[str, Build]: return {} @@ -449,12 +449,12 @@ class BuildNumpy(Build): """Build NumPy.""" @property - @lru_cache() + @lru_cache def resources(self) -> Dict[str, OnlineResource]: return {} @property - @lru_cache() + @lru_cache def dependencies(self) -> Dict[str, Build]: return {} @@ -481,12 +481,12 @@ class BuildCUDA(Build): """Find CUDA.""" @property - @lru_cache() + @lru_cache def resources(self) -> Dict[str, OnlineResource]: return {} @property - @lru_cache() + @lru_cache def dependencies(self) -> Dict[str, Build]: return {} @@ -536,7 +536,7 @@ def cudnn_version(self): ) @property - @lru_cache() + @lru_cache def cuda_compute_capabilities(self): """Get cuda compute capabilities.""" cuda_version = tuple(map(int, self.cuda_version.split("."))) @@ -554,12 +554,12 @@ class BuildROCM(Build): """Find ROCm.""" @property - @lru_cache() + @lru_cache def resources(self) -> Dict[str, OnlineResource]: return {} @property - @lru_cache() + @lru_cache def dependencies(self) -> Dict[str, Build]: return {} @@ -599,14 +599,14 @@ def __init__( self.enable_rocm = enable_rocm @property - @lru_cache() + @lru_cache def resources(self) -> Dict[str, OnlineResource]: return { "tensorflow": RESOURCES["tensorflow-" + self.version], } @property - @lru_cache() + @lru_cache def dependencies(self) -> Dict[str, Build]: optional_dep = {} if self.enable_cuda: From 484bdc3becf711e94966debcf97a9cfeaa2dc7ba Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 27 Jan 2024 07:21:22 -0500 Subject: [PATCH 018/686] Merge TF and PT CLI (#3187) Just merge in form. Several options or subcommands are only supported by TensorFlow or PyTorch. Also, avoid import from `deepmd.tf` in `deepmd.utils.argcheck`. ``` Use --tf or --pt to choose the backend: dp --tf train input.json dp --pt train input.json ``` --------- Signed-off-by: Jinzhe Zeng --- deepmd/infer/deep_pot.py | 2 +- deepmd/main.py | 152 +++++++++++++++++++++++++++----- deepmd/pt/entrypoints/main.py | 159 +++++++++++----------------------- deepmd/tf/entrypoints/main.py | 8 ++ deepmd/utils/argcheck.py | 26 +++++- 5 files changed, 209 insertions(+), 138 deletions(-) diff --git a/deepmd/infer/deep_pot.py b/deepmd/infer/deep_pot.py index b863a7ddc2..546c0f3c7e 100644 --- a/deepmd/infer/deep_pot.py +++ b/deepmd/infer/deep_pot.py @@ -56,7 +56,7 @@ def __new__(cls, model_file: str, *args, **kwargs): return super().__new__(DeepPotTF) elif backend == DPBackend.PyTorch: - from deepmd_pt.infer.deep_eval import DeepPot as DeepPotPT + from deepmd.pt.infer.deep_eval import DeepPot as DeepPotPT return super().__new__(DeepPotPT) else: diff --git a/deepmd/main.py b/deepmd/main.py index 142bf860cb..30d2b293c0 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -6,6 +6,7 @@ """ import argparse import logging +import os import textwrap from typing import ( List, @@ -45,6 +46,21 @@ class RawTextArgumentDefaultsHelpFormatter( """This formatter is used to print multile-line help message with default value.""" +BACKEND_TABLE = { + "tensorflow": "tensorflow", + "tf": "tensorflow", + "pytorch": "pytorch", + "pt": "pytorch", +} + + +class BackendOption(argparse.Action): + """Map backend alias to unique name.""" + + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, BACKEND_TABLE[values]) + + def main_parser() -> argparse.ArgumentParser: """DeePMD-Kit commandline options argument parser. @@ -56,8 +72,51 @@ def main_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="DeePMD-kit: A deep learning package for many-body potential energy" " representation and molecular dynamics", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, + formatter_class=RawTextArgumentDefaultsHelpFormatter, + epilog=textwrap.dedent( + """\ + Use --tf or --pt to choose the backend: + dp --tf train input.json + dp --pt train input.json + """ + ), + ) + + # default backend is TF for compatibility + default_backend = os.environ.get("DP_BACKEND", "tensorflow").lower() + if default_backend not in BACKEND_TABLE.keys(): + raise ValueError( + f"Unknown backend {default_backend}. " + "Please set DP_BACKEND to either tensorflow or pytorch." + ) + + parser_backend = parser.add_mutually_exclusive_group() + parser_backend.add_argument( + "-b", + "--backend", + choices=list(BACKEND_TABLE.keys()), + action=BackendOption, + default=default_backend, + help=( + "The backend of the model. Default can be set by environment variable " + "DP_BACKEND." + ), ) + parser_backend.add_argument( + "--tf", + action="store_const", + dest="backend", + const="tensorflow", + help="Alias for --backend tensorflow", + ) + parser_backend.add_argument( + "--pt", + action="store_const", + dest="backend", + const="pytorch", + help="Alias for --backend pytorch", + ) + subparsers = parser.add_subparsers(title="Valid subcommands", dest="command") # * logging options parser ********************************************************* @@ -98,7 +157,9 @@ def main_parser() -> argparse.ArgumentParser: # * transfer script **************************************************************** parser_transfer = subparsers.add_parser( - "transfer", parents=[parser_log], help="pass parameters to another model" + "transfer", + parents=[parser_log], + help="(Supported backend: TensorFlow) pass parameters to another model", ) parser_transfer.add_argument( "-r", @@ -160,7 +221,7 @@ def main_parser() -> argparse.ArgumentParser: "--init-frz-model", type=str, default=None, - help="Initialize the training from the frozen model.", + help="(Supported backend: TensorFlow) Initialize the training from the frozen model.", ) parser_train_subgroup.add_argument( "-t", @@ -174,12 +235,24 @@ def main_parser() -> argparse.ArgumentParser: "--output", type=str, default="out.json", - help="The output file of the parameters used in training.", + help="(Supported backend: TensorFlow) The output file of the parameters used in training.", ) parser_train.add_argument( "--skip-neighbor-stat", action="store_true", - help="Skip calculating neighbor statistics. Sel checking, automatic sel, and model compression will be disabled.", + help="(Supported backend: TensorFlow) Skip calculating neighbor statistics. Sel checking, automatic sel, and model compression will be disabled.", + ) + parser_train.add_argument( + # -m has been used by mpi-log + "--model-branch", + type=str, + default="", + help="(Supported backend: PyTorch) Model branch chosen for fine-tuning if multi-task. If not specified, it will re-init the fitting net.", + ) + parser_train.add_argument( + "--force-load", + action="store_true", + help="(Supported backend: PyTorch) Force load from ckpt, other missing tensors will init from scratch", ) # * freeze script ****************************************************************** @@ -199,36 +272,43 @@ def main_parser() -> argparse.ArgumentParser: parser_frz.add_argument( "-c", "--checkpoint-folder", + "--checkpoint", type=str, default=".", - help="path to checkpoint folder", + help="Path to checkpoint. TensorFlow backend: a folder; PyTorch backend: either a folder containing model.pt, or a pt file", ) parser_frz.add_argument( "-o", "--output", type=str, - default="frozen_model.pb", - help="name of graph, will output to the checkpoint folder", + default="frozen_model", + help="Filename (prefix) of the output model file. TensorFlow backend: suffix is .pb; PyTorch backend: suffix is .pth", ) parser_frz.add_argument( "-n", "--node-names", type=str, default=None, - help="the frozen nodes, if not set, determined from the model type", + help="(Supported backend: TensorFlow) the frozen nodes, if not set, determined from the model type", ) parser_frz.add_argument( "-w", "--nvnmd-weight", type=str, default=None, - help="the name of weight file (.npy), if set, save the model's weight into the file", + help="(Supported backend: TensorFlow) the name of weight file (.npy), if set, save the model's weight into the file", ) parser_frz.add_argument( "--united-model", action="store_true", default=False, - help="When in multi-task mode, freeze all nodes into one united model", + help="(Supported backend: TensorFlow) When in multi-task mode, freeze all nodes into one united model", + ) + parser_frz.add_argument( + "--head", + default=None, + type=str, + help="(Supported backend: PyTorch) Task head to freeze if in multi-task mode.", ) # * test script ******************************************************************** @@ -247,9 +327,9 @@ def main_parser() -> argparse.ArgumentParser: parser_tst.add_argument( "-m", "--model", - default="frozen_model.pb", + default="frozen_model", type=str, - help="Frozen model file to import", + help="Frozen model file (prefix) to import. TensorFlow backend: suffix is .pb; PyTorch backend: suffix is .pt", ) parser_tst_subgroup = parser_tst.add_mutually_exclusive_group() parser_tst_subgroup.add_argument( @@ -267,7 +347,11 @@ def main_parser() -> argparse.ArgumentParser: help="The path to file of test list.", ) parser_tst.add_argument( - "-S", "--set-prefix", default="set", type=str, help="The set prefix" + "-S", + "--set-prefix", + default="set", + type=str, + help="(Supported backend: TensorFlow) The set prefix", ) parser_tst.add_argument( "-n", @@ -277,7 +361,11 @@ def main_parser() -> argparse.ArgumentParser: help="The number of data for test. 0 means all data.", ) parser_tst.add_argument( - "-r", "--rand-seed", type=int, default=None, help="The random seed" + "-r", + "--rand-seed", + type=int, + default=None, + help="(Supported backend: TensorFlow) The random seed", ) parser_tst.add_argument( "--shuffle-test", action="store_true", default=False, help="Shuffle test data" @@ -294,7 +382,19 @@ def main_parser() -> argparse.ArgumentParser: "--atomic", action="store_true", default=False, - help="Test the accuracy of atomic label, i.e. energy / tensor (dipole, polar)", + help="(Supported backend: TensorFlow) Test the accuracy of atomic label, i.e. energy / tensor (dipole, polar)", + ) + parser_tst.add_argument( + "-i", + "--input_script", + type=str, + help="(Supported backend: PyTorch) The input script of the model", + ) + parser_tst.add_argument( + "--head", + default=None, + type=str, + help="(Supported backend: PyTorch) Task head to test if in multi-task mode.", ) # * compress model ***************************************************************** @@ -308,7 +408,7 @@ def main_parser() -> argparse.ArgumentParser: parser_compress = subparsers.add_parser( "compress", parents=[parser_log, parser_mpi_log], - help="compress a model", + help="(Supported backend: TensorFlow) compress a model", formatter_class=RawTextArgumentDefaultsHelpFormatter, epilog=textwrap.dedent( """\ @@ -409,10 +509,10 @@ def main_parser() -> argparse.ArgumentParser: parser_model_devi.add_argument( "-m", "--models", - default=["graph.000.pb", "graph.001.pb", "graph.002.pb", "graph.003.pb"], + default=["graph.000", "graph.001", "graph.002", "graph.003"], nargs="+", type=str, - help="Frozen models file to import", + help="Frozen models file (prefix) to import. TensorFlow backend: suffix is .pb; PyTorch backend: suffix is .pt.", ) parser_model_devi.add_argument( "-s", @@ -465,7 +565,7 @@ def main_parser() -> argparse.ArgumentParser: parser_transform = subparsers.add_parser( "convert-from", parents=[parser_log], - help="convert lower model version to supported version", + help="(Supported backend: TensorFlow) convert lower model version to supported version", formatter_class=RawTextArgumentDefaultsHelpFormatter, epilog=textwrap.dedent( """\ @@ -503,7 +603,7 @@ def main_parser() -> argparse.ArgumentParser: parser_neighbor_stat = subparsers.add_parser( "neighbor-stat", parents=[parser_log], - help="Calculate neighbor statistics", + help="(Supported backend: TensorFlow) Calculate neighbor statistics", formatter_class=RawTextArgumentDefaultsHelpFormatter, epilog=textwrap.dedent( """\ @@ -550,7 +650,7 @@ def main_parser() -> argparse.ArgumentParser: parser_train_nvnmd = subparsers.add_parser( "train-nvnmd", parents=[parser_log], - help="train nvnmd model", + help="(Supported backend: TensorFlow) train nvnmd model", formatter_class=argparse.ArgumentDefaultsHelpFormatter, epilog=textwrap.dedent( """\ @@ -651,6 +751,12 @@ def main(): if no command was input """ args = parse_args() - from deepmd.tf.entrypoints.main import main as deepmd_main + + if args.backend == "tensorflow": + from deepmd.tf.entrypoints.main import main as deepmd_main + elif args.backend == "pytorch": + from deepmd.pt.entrypoints.main import main as deepmd_main + else: + raise ValueError(f"Unknown backend {args.backend}") deepmd_main(args) diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index f1cd7ae210..c5e551ebd8 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -3,6 +3,14 @@ import json import logging import os +from pathlib import ( + Path, +) +from typing import ( + List, + Optional, + Union, +) import torch import torch.distributed as dist @@ -13,6 +21,18 @@ from deepmd import ( __version__, ) +from deepmd.entrypoints.doc import ( + doc_train_input, +) +from deepmd.entrypoints.gui import ( + start_dpgui, +) +from deepmd.infer.model_devi import ( + make_model_devi, +) +from deepmd.main import ( + parse_args, +) from deepmd.pt.infer import ( inference, ) @@ -266,130 +286,49 @@ def clean_loggers(): @record -def main(args=None): +def main(args: Optional[Union[List[str], argparse.Namespace]] = None): clean_loggers() + + if not isinstance(args, argparse.Namespace): + FLAGS = parse_args(args=args) + else: + FLAGS = args + dict_args = vars(FLAGS) + logging.basicConfig( level=logging.WARNING if env.LOCAL_RANK else logging.INFO, format=f"%(asctime)-15s {os.environ.get('RANK') or ''} [%(filename)s:%(lineno)d] %(levelname)s %(message)s", ) logging.info("DeepMD version: %s", __version__) - parser = argparse.ArgumentParser( - description="A tool to manager deep models of potential energy surface." - ) - subparsers = parser.add_subparsers(dest="command") - train_parser = subparsers.add_parser("train", help="Train a model.") - train_parser.add_argument("INPUT", help="A Json-format configuration file.") - parser_train_subgroup = train_parser.add_mutually_exclusive_group() - parser_train_subgroup.add_argument( - "-i", - "--init-model", - type=str, - default=None, - help="Initialize the model by the provided checkpoint.", - ) - parser_train_subgroup.add_argument( - "-r", - "--restart", - type=str, - default=None, - help="Restart the training from the provided checkpoint.", - ) - parser_train_subgroup.add_argument( - "-t", - "--finetune", - type=str, - default=None, - help="Finetune the frozen pretrained model.", - ) - train_parser.add_argument( - "-m", - "--model-branch", - type=str, - default="", - help="Model branch chosen for fine-tuning if multi-task. If not specified, it will re-init the fitting net.", - ) - train_parser.add_argument( - "--force-load", - action="store_true", - help="Force load from ckpt, other missing tensors will init from scratch", - ) - - test_parser = subparsers.add_parser("test", help="Test a model.") - test_parser_subgroup = test_parser.add_mutually_exclusive_group() - test_parser_subgroup.add_argument( - "-s", - "--system", - default=None, - type=str, - help="The system dir. Recursively detect systems in this directory", - ) - test_parser_subgroup.add_argument( - "-f", - "--datafile", - default=None, - type=str, - help="The path to file of test list.", - ) - test_parser_subgroup.add_argument( - "-i", - "--input-script", - default=None, - type=str, - help="The path to the input script, the validation systems will be tested.", - ) - test_parser.add_argument( - "-m", - "--model", - default="model.pt", - type=str, - help="Model checkpoint to import", - ) - test_parser.add_argument( - "--head", - default=None, - type=str, - help="Task head to test if in multi-task mode.", - ) - test_parser.add_argument( - "-n", "--numb-test", default=100, type=int, help="The number of data for test" - ) - test_parser.add_argument( - "-d", - "--detail-file", - type=str, - default=None, - help="The prefix to files where details of energy, force and virial accuracy/accuracy per atom will be written", - ) - test_parser.add_argument( - "--shuffle-test", action="store_true", default=False, help="Shuffle test data" - ) - freeze_parser = subparsers.add_parser("freeze", help="Freeze a model.") - freeze_parser.add_argument("model", help="Resumes from checkpoint.") - freeze_parser.add_argument( - "-o", - "--output", - type=str, - default="frozen_model.pth", - help="The frozen model path", - ) - freeze_parser.add_argument( - "--head", - default=None, - type=str, - help="Task head to freeze if in multi-task mode.", - ) - - FLAGS = parser.parse_args(args) if FLAGS.command == "train": train(FLAGS) elif FLAGS.command == "test": + FLAGS.output = str(Path(FLAGS.model).with_suffix(".pt")) test(FLAGS) elif FLAGS.command == "freeze": + if Path(FLAGS.checkpoint_folder).is_dir(): + # TODO: automatically generate model.pt during training + # FLAGS.model = str(Path(FLAGS.checkpoint).joinpath("model.pt")) + raise NotImplementedError("Checkpoint should give a file") + else: + FLAGS.model = FLAGS.checkpoint_folder + FLAGS.output = str(Path(FLAGS.output).with_suffix(".pth")) freeze(FLAGS) + elif args.command == "doc-train-input": + doc_train_input(**dict_args) + elif args.command == "model-devi": + dict_args["models"] = [ + str(Path(mm).with_suffix(".pt")) + if Path(mm).suffix not in (".pb", ".pt") + else mm + for mm in dict_args["models"] + ] + make_model_devi(**dict_args) + elif args.command == "gui": + start_dpgui(**dict_args) else: - logging.error("Invalid command!") - parser.print_help() + raise RuntimeError(f"Invalid command {FLAGS.command}!") if __name__ == "__main__": diff --git a/deepmd/tf/entrypoints/main.py b/deepmd/tf/entrypoints/main.py index d9618e7498..d57b43fc7c 100644 --- a/deepmd/tf/entrypoints/main.py +++ b/deepmd/tf/entrypoints/main.py @@ -73,8 +73,10 @@ def main(args: Optional[Union[List[str], argparse.Namespace]] = None): if args.command == "train": train_dp(**dict_args) elif args.command == "freeze": + dict_args["output"] = str(Path(dict_args["output"]).with_suffix(".pb")) freeze(**dict_args) elif args.command == "test": + dict_args["model"] = str(Path(dict_args["model"]).with_suffix(".pb")) test(**dict_args) elif args.command == "transfer": transfer(**dict_args) @@ -83,6 +85,12 @@ def main(args: Optional[Union[List[str], argparse.Namespace]] = None): elif args.command == "doc-train-input": doc_train_input(**dict_args) elif args.command == "model-devi": + dict_args["models"] = [ + str(Path(mm).with_suffix(".pb")) + if Path(mm).suffix not in (".pb", ".pt") + else mm + for mm in dict_args["models"] + ] make_model_devi(**dict_args) elif args.command == "convert-from": convert(**dict_args) diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 2acf8ed80b..31b54b4d76 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -14,10 +14,6 @@ dargs, ) -from deepmd.tf.common import ( - ACTIVATION_FN_DICT, - PRECISION_DICT, -) from deepmd.utils.argcheck_nvnmd import ( nvnmd_args, ) @@ -28,6 +24,28 @@ log = logging.getLogger(__name__) +# TODO: import from a module outside tf/pt +ACTIVATION_FN_DICT = { + "relu": None, + "relu6": None, + "softplus": None, + "sigmoid": None, + "tanh": None, + "gelu": None, + "gelu_tf": None, + "None": None, + "none": None, +} +# TODO: import from a module outside tf/pt +PRECISION_DICT = { + "default": None, + "float16": None, + "float32": None, + "float64": None, + "bfloat16": None, +} + + def list_to_doc(xx): items = [] for ii in xx: From 3e4715f9f44827f35d274ab3309edd72ed6cf638 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 27 Jan 2024 07:23:45 -0500 Subject: [PATCH 019/686] Fix PT `DeepPot` and replace ASE calculator (#3186) - Set `deepmd.pt.utils.ase_calc.DPCalculator` as an alias of `deepmd.calculator.DP`; - Replace `deepmd_pt` with `deepmd.pt` in `deep_pot.py`; fix (atomic) virial output shape of `DeepPot`; add tests for them; - Set `pbc` in `pt/test_calculator.py` as it requests stress. --------- Signed-off-by: Jinzhe Zeng --- deepmd/pt/infer/deep_eval.py | 24 ++++++++--- deepmd/pt/utils/ase_calc.py | 67 ++---------------------------- source/tests/pt/test_calculator.py | 2 + source/tests/pt/test_deeppot.py | 16 +++++++ 4 files changed, 41 insertions(+), 68 deletions(-) diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index 79772b47ae..b5d596ed0f 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -195,13 +195,27 @@ def _eval_model( ) if isinstance(batch_output, tuple): batch_output = batch_output[0] - energy_out = batch_output["energy"].detach().cpu().numpy() + energy_out = batch_output["energy"].reshape(nframes, 1).detach().cpu().numpy() if "atom_energy" in batch_output: - atomic_energy_out = batch_output["atom_energy"].detach().cpu().numpy() - force_out = batch_output["force"].detach().cpu().numpy() - virial_out = batch_output["virial"].detach().cpu().numpy() + atomic_energy_out = ( + batch_output["atom_energy"] + .reshape(nframes, natoms, 1) + .detach() + .cpu() + .numpy() + ) + force_out = ( + batch_output["force"].reshape(nframes, natoms, 3).detach().cpu().numpy() + ) + virial_out = batch_output["virial"].reshape(nframes, 9).detach().cpu().numpy() if "atomic_virial" in batch_output: - atomic_virial_out = batch_output["atomic_virial"].detach().cpu().numpy() + atomic_virial_out = ( + batch_output["atomic_virial"] + .reshape(nframes, natoms, 9) + .detach() + .cpu() + .numpy() + ) if not atomic: return energy_out, force_out, virial_out diff --git a/deepmd/pt/utils/ase_calc.py b/deepmd/pt/utils/ase_calc.py index 8d5fe8bce9..6bcb9cdc5e 100644 --- a/deepmd/pt/utils/ase_calc.py +++ b/deepmd/pt/utils/ase_calc.py @@ -1,65 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from typing import ( - ClassVar, -) +from deepmd.calculator import DP as DPCalculator -import dpdata -import numpy as np -from ase import ( - Atoms, -) -from ase.calculators.calculator import ( - Calculator, - PropertyNotImplementedError, -) - -from deepmd.pt.infer.deep_eval import ( - DeepPot, -) - - -class DPCalculator(Calculator): - implemented_properties: ClassVar[list] = [ - "energy", - "free_energy", - "forces", - "virial", - "stress", - ] - - def __init__(self, model): - Calculator.__init__(self) - self.dp = DeepPot(model) - self.type_map = self.dp.type_map - - def calculate(self, atoms: Atoms, properties, system_changes) -> None: - Calculator.calculate(self, atoms, properties, system_changes) - system = dpdata.System(atoms, fmt="ase/structure") - type_trans = np.array( - [self.type_map.index(i) for i in system.data["atom_names"]] - ) - input_coords = system.data["coords"] - input_cells = system.data["cells"] - input_types = list(type_trans[system.data["atom_types"]]) - model_predict = self.dp.eval(input_coords, input_cells, input_types) - self.results = { - "energy": model_predict[0].item(), - "free_energy": model_predict[0].item(), - "forces": model_predict[1].reshape(-1, 3), - "virial": model_predict[2].reshape(3, 3), - } - - # convert virial into stress for lattice relaxation - if "stress" in properties: - if sum(atoms.get_pbc()) > 0 or (atoms.cell is not None): - # the usual convention (tensile stress is positive) - # stress = -virial / volume - stress = ( - -0.5 - * (self.results["virial"].copy() + self.results["virial"].copy().T) - / atoms.get_volume() - ) - # Voigt notation - self.results["stress"] = stress.flat[[0, 4, 8, 5, 2, 1]] - else: - raise PropertyNotImplementedError +__all__ = [ + "DPCalculator", +] diff --git a/source/tests/pt/test_calculator.py b/source/tests/pt/test_calculator.py index e8382b22b8..a35538250b 100644 --- a/source/tests/pt/test_calculator.py +++ b/source/tests/pt/test_calculator.py @@ -66,6 +66,7 @@ def test_calculator(self): # positions=[tuple(item) for item in coordinate], cell=cell, calculator=self.calculator, + pbc=True, ) e0, f0 = ase_atoms0.get_potential_energy(), ase_atoms0.get_forces() s0, v0 = ( @@ -79,6 +80,7 @@ def test_calculator(self): # positions=[tuple(item) for item in coordinate], cell=cell, calculator=self.calculator, + pbc=True, ) e1, f1 = ase_atoms1.get_potential_energy(), ase_atoms1.get_forces() s1, v1 = ( diff --git a/source/tests/pt/test_deeppot.py b/source/tests/pt/test_deeppot.py index 7f3ecf7d1b..d56f08d17c 100644 --- a/source/tests/pt/test_deeppot.py +++ b/source/tests/pt/test_deeppot.py @@ -10,6 +10,7 @@ import numpy as np +from deepmd.infer.deep_pot import DeepPot as DeepPotUni from deepmd.pt.entrypoints.main import ( get_trainer, ) @@ -79,3 +80,18 @@ def test_dp_test(self): atype = np.array([0, 0, 0, 1, 1]).reshape(1, -1) e, f, v, ae, av = dp.eval(coord, cell, atype, atomic=True) + self.assertEqual(e.shape, (1, 1)) + self.assertEqual(f.shape, (1, 5, 3)) + self.assertEqual(v.shape, (1, 9)) + self.assertEqual(ae.shape, (1, 5, 1)) + self.assertEqual(av.shape, (1, 5, 9)) + + self.assertEqual(dp.get_type_map(), ["O", "H"]) + self.assertEqual(dp.get_ntypes(), 2) + self.assertEqual(dp.get_dim_fparam(), 0) + self.assertEqual(dp.get_dim_aparam(), 0) + + def test_uni(self): + dp = DeepPotUni("model.pt") + self.assertIsInstance(dp, DeepPot) + # its methods has been tested in test_dp_test From 497c8ba338319e63a52fcd21eec76d6e6d2084d0 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Sun, 28 Jan 2024 16:30:47 +0800 Subject: [PATCH 020/686] breaking: pt: change the virial output dim to 9 (#3188) 1. compatible with tf 2. compatible with the input cell shape Co-authored-by: Han Wang --- deepmd/pt/model/model/transform_output.py | 24 +++++++++++++---------- source/tests/pt/test_autodiff.py | 8 +++++--- source/tests/pt/test_rot.py | 8 ++++---- source/tests/pt/test_rotation.py | 5 +++-- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/deepmd/pt/model/model/transform_output.py b/deepmd/pt/model/model/transform_output.py index 673491d788..a14518e8a0 100644 --- a/deepmd/pt/model/model/transform_output.py +++ b/deepmd/pt/model/model/transform_output.py @@ -70,6 +70,8 @@ def task_deriv_one( if do_atomic_virial: extended_virial_corr = atomic_virial_corr(extended_coord, atom_energy) extended_virial = extended_virial + extended_virial_corr + # to [...,3,3] -> [...,9] + extended_virial = extended_virial.view(list(extended_virial.shape[:-2]) + [9]) # noqa:RUF005 return extended_force, extended_virial @@ -106,18 +108,18 @@ def take_deriv( split_svv1 = torch.split(svv1, [1] * size, dim=-1) split_ff, split_avir = [], [] for vvi, svvi in zip(split_vv1, split_svv1): - # nf x nloc x 3, nf x nloc x 3 x 3 + # nf x nloc x 3, nf x nloc x 9 ffi, aviri = task_deriv_one( vvi, svvi, coord_ext, do_atomic_virial=do_atomic_virial ) - # nf x nloc x 1 x 3, nf x nloc x 1 x 3 x 3 + # nf x nloc x 1 x 3, nf x nloc x 1 x 9 ffi = ffi.unsqueeze(-2) - aviri = aviri.unsqueeze(-3) + aviri = aviri.unsqueeze(-2) split_ff.append(ffi) split_avir.append(aviri) - # nf x nloc x v_dim x 3, nf x nloc x v_dim x 3 x 3 + # nf x nloc x v_dim x 3, nf x nloc x v_dim x 9 ff = torch.concat(split_ff, dim=-2) - avir = torch.concat(split_avir, dim=-3) + avir = torch.concat(split_avir, dim=-2) return ff, avir @@ -185,7 +187,7 @@ def communicate_extended_output( force = torch.zeros( vldims + derv_r_ext_dims, dtype=vv.dtype, device=vv.device ) - # nf x nloc x 1 x 3 + # nf x nloc x nvar x 3 new_ret[kk_derv_r] = torch.scatter_reduce( force, 1, @@ -193,13 +195,15 @@ def communicate_extended_output( src=model_ret[kk_derv_r], reduce="sum", ) - mapping = mapping.unsqueeze(-1).expand( - [-1] * (len(mldims) + len(derv_r_ext_dims)) + [3] + derv_c_ext_dims = list(vdef.shape) + [9] # noqa:RUF005 + # nf x nloc x nvar x 3 -> nf x nloc x nvar x 9 + mapping = torch.tile( + mapping, [1] * (len(mldims) + len(vdef.shape)) + [3] ) virial = torch.zeros( - vldims + derv_r_ext_dims + [3], dtype=vv.dtype, device=vv.device + vldims + derv_c_ext_dims, dtype=vv.dtype, device=vv.device ) - # nf x nloc x 1 x 3 + # nf x nloc x nvar x 9 new_ret[kk_derv_c] = torch.scatter_reduce( virial, 1, diff --git a/source/tests/pt/test_autodiff.py b/source/tests/pt/test_autodiff.py index 4f303a8bb3..8840fbdd4c 100644 --- a/source/tests/pt/test_autodiff.py +++ b/source/tests/pt/test_autodiff.py @@ -121,9 +121,11 @@ def np_infer( def ff(bb): return np_infer(bb)["energy"] - fdv = -( - finite_difference(ff, cell, delta=delta).transpose(0, 2, 1) @ cell - ).squeeze() + fdv = ( + -(finite_difference(ff, cell, delta=delta).transpose(0, 2, 1) @ cell) + .squeeze() + .reshape(9) + ) rfv = np_infer(cell)["virial"] np.testing.assert_almost_equal(fdv, rfv, decimal=places) diff --git a/source/tests/pt/test_rot.py b/source/tests/pt/test_rot.py index b5d9d9b64b..7222fd6f69 100644 --- a/source/tests/pt/test_rot.py +++ b/source/tests/pt/test_rot.py @@ -65,8 +65,8 @@ def test( ) if not hasattr(self, "test_virial") or self.test_virial: torch.testing.assert_close( - torch.matmul(rmat.T, torch.matmul(ret0["virial"], rmat)), - ret1["virial"], + torch.matmul(rmat.T, torch.matmul(ret0["virial"].view([3, 3]), rmat)), + ret1["virial"].view([3, 3]), rtol=prec, atol=prec, ) @@ -102,8 +102,8 @@ def test( ) if not hasattr(self, "test_virial") or self.test_virial: torch.testing.assert_close( - torch.matmul(rmat.T, torch.matmul(ret0["virial"], rmat)), - ret1["virial"], + torch.matmul(rmat.T, torch.matmul(ret0["virial"].view([3, 3]), rmat)), + ret1["virial"].view([3, 3]), rtol=prec, atol=prec, ) diff --git a/source/tests/pt/test_rotation.py b/source/tests/pt/test_rotation.py index 4b49377a27..58ec80e0d6 100644 --- a/source/tests/pt/test_rotation.py +++ b/source/tests/pt/test_rotation.py @@ -121,9 +121,10 @@ def test_rotation(self): if "virial" in result1: self.assertTrue( torch.allclose( - result2["virial"][0], + result2["virial"][0].view([3, 3]), torch.matmul( - torch.matmul(rotation, result1["virial"][0].T), rotation.T + torch.matmul(rotation, result1["virial"][0].view([3, 3]).T), + rotation.T, ), ) ) From a8168b5addd2b91a5fed21f9db77019e0acec9ae Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 28 Jan 2024 03:38:07 -0500 Subject: [PATCH 021/686] PT: keep the same checkpoint behavior as TF (#3191) Set the default `save_ckpt` to `model.ckpt` as the prefix. When saving checkpoints, `model.ckpt-100.pt` will be saved, and `model.ckpt.pt` will be symlinked to `model.ckpt-100.pt`. A `checkpoint` file will be dedicated to record `model.ckpt-100.pt`. This keeps the same behavior as the TF backend. One can do the below using the PT backend just like the TF backend: ```sh dp --pt train input.json # one can cancel the training before it finishes dp --pt freeze ``` --------- Signed-off-by: Jinzhe Zeng --- deepmd/common.py | 31 +++++++++++++++++++++++++++++ deepmd/main.py | 2 +- deepmd/pt/entrypoints/main.py | 6 +++--- deepmd/pt/train/training.py | 19 +++++++++--------- deepmd/tf/train/trainer.py | 19 ++++-------------- source/tests/pt/water/se_atten.json | 1 + 6 files changed, 49 insertions(+), 29 deletions(-) diff --git a/deepmd/common.py b/deepmd/common.py index f950b50919..05d02234b4 100644 --- a/deepmd/common.py +++ b/deepmd/common.py @@ -1,5 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import glob import json +import os +import platform +import shutil import warnings from pathlib import ( Path, @@ -268,3 +272,30 @@ def get_np_precision(precision: "_PRECISION") -> np.dtype: return np.float64 else: raise RuntimeError(f"{precision} is not a valid precision") + + +def symlink_prefix_files(old_prefix: str, new_prefix: str): + """Create symlinks from old checkpoint prefix to new one. + + On Windows this function will copy files instead of creating symlinks. + + Parameters + ---------- + old_prefix : str + old checkpoint prefix, all files with this prefix will be symlinked + new_prefix : str + new checkpoint prefix + """ + original_files = glob.glob(old_prefix + ".*") + for ori_ff in original_files: + new_ff = new_prefix + ori_ff[len(old_prefix) :] + try: + # remove old one + os.remove(new_ff) + except OSError: + pass + if platform.system() != "Windows": + # by default one does not have access to create symlink on Windows + os.symlink(os.path.relpath(ori_ff, os.path.dirname(new_ff)), new_ff) + else: + shutil.copyfile(ori_ff, new_ff) diff --git a/deepmd/main.py b/deepmd/main.py index 30d2b293c0..ff7120c8e7 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -275,7 +275,7 @@ def main_parser() -> argparse.ArgumentParser: "--checkpoint", type=str, default=".", - help="Path to checkpoint. TensorFlow backend: a folder; PyTorch backend: either a folder containing model.pt, or a pt file", + help="Path to checkpoint. TensorFlow backend: a folder; PyTorch backend: either a folder containing checkpoint, or a pt file", ) parser_frz.add_argument( "-o", diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index c5e551ebd8..ad5e92d495 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -308,9 +308,9 @@ def main(args: Optional[Union[List[str], argparse.Namespace]] = None): test(FLAGS) elif FLAGS.command == "freeze": if Path(FLAGS.checkpoint_folder).is_dir(): - # TODO: automatically generate model.pt during training - # FLAGS.model = str(Path(FLAGS.checkpoint).joinpath("model.pt")) - raise NotImplementedError("Checkpoint should give a file") + checkpoint_path = Path(FLAGS.checkpoint_folder) + latest_ckpt_file = (checkpoint_path / "checkpoint").read_text() + FLAGS.model = str(checkpoint_path.joinpath(latest_ckpt_file)) else: FLAGS.model = FLAGS.checkpoint_folder FLAGS.output = str(Path(FLAGS.output).with_suffix(".pth")) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 049685a6e3..8ea69c8489 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging -import os import time from copy import ( deepcopy, @@ -22,6 +21,9 @@ logging_redirect_tqdm, ) +from deepmd.common import ( + symlink_prefix_files, +) from deepmd.pt.loss import ( DenoiseLoss, EnergyStdLoss, @@ -102,7 +104,7 @@ def __init__( self.num_steps = training_params["numb_steps"] self.disp_file = training_params.get("disp_file", "lcurve.out") self.disp_freq = training_params.get("disp_freq", 1000) - self.save_ckpt = training_params.get("save_ckpt", "model.pt") + self.save_ckpt = training_params.get("save_ckpt", "model.ckpt") self.save_freq = training_params.get("save_freq", 1000) self.lcurve_should_print_header = True @@ -650,13 +652,14 @@ def log_loss_valid(_task_key="Default"): or (_step_id + 1) == self.num_steps ) and (self.rank == 0 or dist.get_rank() == 0): # Handle the case if rank 0 aborted and re-assigned - self.latest_model = Path(self.save_ckpt) - self.latest_model = self.latest_model.with_name( - f"{self.latest_model.stem}_{_step_id + 1}{self.latest_model.suffix}" - ) + self.latest_model = Path(self.save_ckpt + f"-{_step_id + 1}.pt") + module = self.wrapper.module if dist.is_initialized() else self.wrapper self.save_model(self.latest_model, lr=cur_lr, step=_step_id) logging.info(f"Saved model to {self.latest_model}") + symlink_prefix_files(self.latest_model.stem, self.save_ckpt) + with open("checkpoint", "w") as f: + f.write(str(self.latest_model)) self.t0 = time.time() with logging_redirect_tqdm(): @@ -694,10 +697,6 @@ def log_loss_valid(_task_key="Default"): logging.info( f"Frozen model for inferencing has been saved to {pth_model_path}" ) - try: - os.symlink(self.latest_model, self.save_ckpt) - except OSError: - self.save_model(self.save_ckpt, lr=0, step=self.num_steps) logging.info(f"Trained model has been saved to: {self.save_ckpt}") if fout: diff --git a/deepmd/tf/train/trainer.py b/deepmd/tf/train/trainer.py index 19b81d7a13..2d29a1a1c1 100644 --- a/deepmd/tf/train/trainer.py +++ b/deepmd/tf/train/trainer.py @@ -1,9 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: LGPL-3.0-or-later -import glob import logging import os -import platform import shutil import time from typing import ( @@ -22,6 +20,9 @@ # load grad of force module import deepmd.tf.op # noqa: F401 +from deepmd.common import ( + symlink_prefix_files, +) from deepmd.tf.common import ( data_requirement, get_precision, @@ -830,19 +831,7 @@ def save_checkpoint(self, cur_batch: int): ) from e # make symlinks from prefix with step to that without step to break nothing # get all checkpoint files - original_files = glob.glob(ckpt_prefix + ".*") - for ori_ff in original_files: - new_ff = self.save_ckpt + ori_ff[len(ckpt_prefix) :] - try: - # remove old one - os.remove(new_ff) - except OSError: - pass - if platform.system() != "Windows": - # by default one does not have access to create symlink on Windows - os.symlink(os.path.relpath(ori_ff, os.path.dirname(new_ff)), new_ff) - else: - shutil.copyfile(ori_ff, new_ff) + symlink_prefix_files(ckpt_prefix, self.save_ckpt) log.info("saved checkpoint %s" % self.save_ckpt) def get_feed_dict(self, batch, is_training): diff --git a/source/tests/pt/water/se_atten.json b/source/tests/pt/water/se_atten.json index 8867e0db41..3ed80ae892 100644 --- a/source/tests/pt/water/se_atten.json +++ b/source/tests/pt/water/se_atten.json @@ -79,6 +79,7 @@ "disp_file": "lcurve.out", "disp_freq": 100, "save_freq": 1000, + "save_ckpt": "model", "_comment": "that's all" } } From 5b64d5ca485c4b174a1d7cc71e44186fb4951d21 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 28 Jan 2024 19:46:21 -0500 Subject: [PATCH 022/686] add size and replace arguments to deepmd.utils.random.choice (#3195) Fix https://github.com/deepmodeling/deepmd-kit/security/code-scanning/2096 --------- Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/pt/utils/dataset.py | 4 ++-- deepmd/utils/random.py | 27 +++++++++++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/deepmd/pt/utils/dataset.py b/deepmd/pt/utils/dataset.py index 24daa6e37e..3920499d3a 100644 --- a/deepmd/pt/utils/dataset.py +++ b/deepmd/pt/utils/dataset.py @@ -879,7 +879,7 @@ def __len__(self): def __getitem__(self, index=None): """Get a batch of frames from the selected system.""" if index is None: - index = dp_random.choice(np.arange(self.nsystems), self.probs) + index = dp_random.choice(np.arange(self.nsystems), p=self.probs) b_data = self._data_systems[index].get_batch(self._batch_size) b_data["natoms"] = torch.tensor( self._natoms_vec[index], device=env.PREPROCESS_DEVICE @@ -892,7 +892,7 @@ def __getitem__(self, index=None): def get_training_batch(self, index=None): """Get a batch of frames from the selected system.""" if index is None: - index = dp_random.choice(np.arange(self.nsystems), self.probs) + index = dp_random.choice(np.arange(self.nsystems), p=self.probs) b_data = self._data_systems[index].get_batch_for_train(self._batch_size) b_data["natoms"] = torch.tensor( self._natoms_vec[index], device=env.PREPROCESS_DEVICE diff --git a/deepmd/utils/random.py b/deepmd/utils/random.py index 8944419412..44ea6a1dac 100644 --- a/deepmd/utils/random.py +++ b/deepmd/utils/random.py @@ -1,6 +1,8 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( Optional, + Tuple, + Union, ) import numpy as np @@ -8,22 +10,35 @@ _RANDOM_GENERATOR = np.random.RandomState() -def choice(a: np.ndarray, p: Optional[np.ndarray] = None): +def choice( + a: Union[np.ndarray, int], + size: Optional[Union[int, Tuple[int, ...]]] = None, + replace: bool = True, + p: Optional[np.ndarray] = None, +): """Generates a random sample from a given 1-D array. Parameters ---------- - a : np.ndarray - A random sample is generated from its elements. - p : np.ndarray - The probabilities associated with each entry in a. + a : 1-D array-like or int + If an ndarray, a random sample is generated from its elements. If an int, + the random sample is generated as if it were np.arange(a) + size : int or tuple of ints, optional + Output shape. If the given shape is, e.g., (m, n, k), then m * n * k samples + are drawn. Default is None, in which case a single value is returned. + replace : boolean, optional + Whether the sample is with or without replacement. Default is True, meaning + that a value of a can be selected multiple times. + p : 1-D array-like, optional + The probabilities associated with each entry in a. If not given, the sample + assumes a uniform distribution over all entries in a. Returns ------- np.ndarray arrays with results and their shapes """ - return _RANDOM_GENERATOR.choice(a, p=p) + return _RANDOM_GENERATOR.choice(a, size=size, replace=replace, p=p) def random(size=None): From 0bb44f329b724ba002f8ab1ec8fff548fe70d3f1 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 28 Jan 2024 20:24:24 -0500 Subject: [PATCH 023/686] drop tqdm (#3194) per discussion. Signed-off-by: Jinzhe Zeng Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- backend/dynamic_metadata.py | 1 - deepmd/pt/train/training.py | 48 ++++++++++++++----------------------- deepmd/pt/utils/dataset.py | 5 +--- deepmd/pt/utils/env.py | 1 - deepmd/pt/utils/stat.py | 5 +--- 5 files changed, 20 insertions(+), 40 deletions(-) diff --git a/backend/dynamic_metadata.py b/backend/dynamic_metadata.py index e30c97bd98..a5817727f5 100644 --- a/backend/dynamic_metadata.py +++ b/backend/dynamic_metadata.py @@ -90,6 +90,5 @@ def dynamic_metadata( ], "torch": [ "torch>=2a", - "tqdm", ], } diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 8ea69c8489..e4c672765b 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -14,12 +14,6 @@ import numpy as np import torch -from tqdm import ( - tqdm, -) -from tqdm.contrib.logging import ( - logging_redirect_tqdm, -) from deepmd.common import ( symlink_prefix_files, @@ -47,7 +41,6 @@ ) from deepmd.pt.utils.env import ( DEVICE, - DISABLE_TQDM, JIT, LOCAL_RANK, NUM_WORKERS, @@ -662,29 +655,24 @@ def log_loss_valid(_task_key="Default"): f.write(str(self.latest_model)) self.t0 = time.time() - with logging_redirect_tqdm(): - for step_id in tqdm( - range(self.num_steps), - disable=(bool(dist.get_rank()) if dist.is_initialized() else False) - or DISABLE_TQDM, - ): # set to None to disable on non-TTY; disable on not rank 0 - if step_id < self.start_step: - continue - if self.multi_task: - chosen_index_list = dp_random.choice( - np.arange(self.num_model), - p=np.array(self.model_prob), - size=self.world_size, - replace=True, - ) - assert chosen_index_list.size == self.world_size - model_index = chosen_index_list[self.rank] - model_key = self.model_keys[model_index] - else: - model_key = "Default" - step(step_id, model_key) - if JIT: - break + for step_id in range(self.num_steps): + if step_id < self.start_step: + continue + if self.multi_task: + chosen_index_list = dp_random.choice( + np.arange(self.num_model), + p=np.array(self.model_prob), + size=self.world_size, + replace=True, + ) + assert chosen_index_list.size == self.world_size + model_index = chosen_index_list[self.rank] + model_key = self.model_keys[model_index] + else: + model_key = "Default" + step(step_id, model_key) + if JIT: + break if ( self.rank == 0 or dist.get_rank() == 0 diff --git a/deepmd/pt/utils/dataset.py b/deepmd/pt/utils/dataset.py index 3920499d3a..c104e64491 100644 --- a/deepmd/pt/utils/dataset.py +++ b/deepmd/pt/utils/dataset.py @@ -13,9 +13,6 @@ from torch.utils.data import ( Dataset, ) -from tqdm import ( - trange, -) from deepmd.pt.utils import ( dp_random, @@ -506,7 +503,7 @@ def preprocess(self, batch): assert batch["atype"].max() < len(self._type_map) nlist, nlist_loc, nlist_type, shift, mapping = [], [], [], [], [] - for sid in trange(n_frames, disable=env.DISABLE_TQDM): + for sid in range(n_frames): region = Region3D(box[sid]) nloc = atype[sid].shape[0] _coord = normalize_coord(coord[sid], region, nloc) diff --git a/deepmd/pt/utils/env.py b/deepmd/pt/utils/env.py index 5b6eaf7c14..6fa72943c7 100644 --- a/deepmd/pt/utils/env.py +++ b/deepmd/pt/utils/env.py @@ -8,7 +8,6 @@ GLOBAL_NP_FLOAT_PRECISION = getattr(np, PRECISION) GLOBAL_PT_FLOAT_PRECISION = getattr(torch, PRECISION) GLOBAL_ENER_FLOAT_PRECISION = getattr(np, PRECISION) -DISABLE_TQDM = os.environ.get("DISABLE_TQDM", False) SAMPLER_RECORD = os.environ.get("SAMPLER_RECORD", False) try: # only linux diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index 837a0104f9..18ee4d9abe 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -3,9 +3,6 @@ import numpy as np import torch -from tqdm import ( - trange, -) from deepmd.pt.utils import ( env, @@ -40,7 +37,7 @@ def make_stat_input(datasets, dataloaders, nbatches): if datasets[0].mixed_type: keys.append("real_natoms_vec") logging.info(f"Packing data for statistics from {len(datasets)} systems") - for i in trange(len(datasets), disable=env.DISABLE_TQDM): + for i in range(len(datasets)): sys_stat = {key: [] for key in keys} iterator = iter(dataloaders[i]) for _ in range(nbatches): From 890056165d381240d3122b18e89b3a3ef9b46af6 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 28 Jan 2024 21:13:24 -0500 Subject: [PATCH 024/686] reorganize tests directory (#3196) ``` - source - tests - common - tf - pt ``` --------- Signed-off-by: Jinzhe Zeng --- pyproject.toml | 2 +- source/tests/__init__.py | 1 + source/tests/common/__init__.py | 1 + source/tests/common/common.py | 5 +++ .../{ => common}/test_argument_parser.py | 2 +- source/tests/{ => common}/test_examples.py | 6 +-- source/tests/{ => common}/test_gui.py | 0 .../{ => common}/test_model_format_utils.py | 0 source/tests/{ => common}/test_output_def.py | 0 source/tests/{ => common}/test_sel_idx.py | 2 +- source/tests/{ => common}/test_uni_infer.py | 10 ++--- source/tests/tf/__init__.py | 1 + source/tests/{ => tf}/common.py | 1 + .../{ => tf}/compat_inputs/water_se_a_v0.json | 0 .../{ => tf}/compat_inputs/water_se_a_v1.json | 0 .../{ => tf}/compat_inputs/water_v0.json | 0 .../{ => tf}/compat_inputs/water_v1.json | 0 .../{ => tf}/compat_inputs/water_v2.json | 0 source/tests/{ => tf}/data_dp_mask/nopbc | 0 .../{ => tf}/data_dp_mask/set.000/aparam.npy | Bin .../data_dp_mask/set.000/atom_pref.npy | Bin .../{ => tf}/data_dp_mask/set.000/box.npy | Bin .../{ => tf}/data_dp_mask/set.000/coord.npy | Bin .../{ => tf}/data_dp_mask/set.000/energy.npy | Bin .../{ => tf}/data_dp_mask/set.000/force.npy | Bin source/tests/{ => tf}/data_dp_mask/type.raw | 0 .../tests/{ => tf}/data_dp_mask/type_map.raw | 0 .../tests/{ => tf}/data_modifier/dipole.json | 0 .../sys_10/set.000/atomic_dipole.npy | Bin .../data_modifier/sys_10/set.000/box.npy | Bin .../data_modifier/sys_10/set.000/coord.npy | Bin .../data_modifier/sys_10/set.000/energy.npy | Bin .../data_modifier/sys_10/set.000/force.npy | Bin .../{ => tf}/data_modifier/sys_10/type.raw | 0 .../data_modifier/sys_10/type_map.raw | 0 .../{ => tf}/finetune/data/set.000/box.npy | Bin .../{ => tf}/finetune/data/set.000/coord.npy | Bin .../{ => tf}/finetune/data/set.000/energy.npy | Bin .../{ => tf}/finetune/data/set.000/force.npy | Bin source/tests/{ => tf}/finetune/data/type.raw | 0 .../tests/{ => tf}/finetune/data/type_map.raw | 0 .../finetune/data_mixed_type/set.000/box.npy | Bin .../data_mixed_type/set.000/coord.npy | Bin .../data_mixed_type/set.000/energy.npy | Bin .../data_mixed_type/set.000/force.npy | Bin .../set.000/real_atom_types.npy | Bin .../finetune/data_mixed_type/type.raw | 0 .../finetune/data_mixed_type/type_map.raw | 0 .../{ => tf}/finetune/input_finetune.json | 0 .../{ => tf}/finetune/input_pretrain.json | 0 .../init_frz_model/data/set.000/box.npy | Bin .../init_frz_model/data/set.000/coord.npy | Bin .../init_frz_model/data/set.000/energy.npy | Bin .../init_frz_model/data/set.000/force.npy | Bin .../{ => tf}/init_frz_model/data/type.raw | 0 .../{ => tf}/init_frz_model/data/type_map.raw | 0 .../tests/{ => tf}/init_frz_model/input.json | 0 .../model_compression/data/set.000/box.npy | Bin .../model_compression/data/set.000/coord.npy | Bin .../model_compression/data/set.000/energy.npy | Bin .../model_compression/data/set.000/force.npy | Bin .../{ => tf}/model_compression/data/type.raw | 0 .../model_compression/data/type_map.raw | 0 .../{ => tf}/model_compression/input.json | 0 .../tests/{ => tf}/model_spin/set.000/box.npy | Bin .../{ => tf}/model_spin/set.000/coord.npy | Bin .../{ => tf}/model_spin/set.000/energy.npy | Bin .../{ => tf}/model_spin/set.000/force.npy | Bin source/tests/{ => tf}/model_spin/type.raw | 0 source/tests/{ => tf}/nvnmd/ref/box.npy | Bin .../{ => tf}/nvnmd/ref/config_v0_cnn.npy | Bin .../{ => tf}/nvnmd/ref/config_v1_cnn.npy | Bin source/tests/{ => tf}/nvnmd/ref/coord.npy | Bin source/tests/{ => tf}/nvnmd/ref/type.raw | 0 .../{ => tf}/nvnmd/ref/weight_v0_cnn.npy | Bin .../{ => tf}/nvnmd/ref/weight_v1_cnn.npy | Bin source/tests/{ => tf}/pairwise_dprc.json | 0 source/tests/{ => tf}/polar_se_a.json | 0 source/tests/{ => tf}/polar_se_a_tebd.json | 0 source/tests/{ => tf}/test.hdf5 | Bin .../tests/{ => tf}/test_activation_fn_gelu.py | 0 source/tests/{ => tf}/test_adjust_sel.py | 14 +++---- source/tests/{ => tf}/test_auto_batch_size.py | 0 source/tests/{ => tf}/test_cluster.py | 0 source/tests/{ => tf}/test_common.py | 0 source/tests/{ => tf}/test_compat_input.py | 8 ++-- .../{ => tf}/test_compressed_training.py | 13 ++++--- .../tests/{ => tf}/test_data_large_batch.py | 9 +++-- source/tests/{ => tf}/test_data_modifier.py | 11 +++--- .../{ => tf}/test_data_modifier_shuffle.py | 0 .../tests/{ => tf}/test_data_requirement.py | 0 source/tests/{ => tf}/test_deepdipole.py | 23 +++++------ source/tests/{ => tf}/test_deepdos.py | 9 +++-- source/tests/{ => tf}/test_deepmd_data.py | 7 ++-- source/tests/{ => tf}/test_deepmd_data_sys.py | 0 source/tests/{ => tf}/test_deeppolar.py | 17 +++++---- source/tests/{ => tf}/test_deeppot_a.py | 36 +++++++++--------- source/tests/{ => tf}/test_deeppot_r.py | 13 ++++--- source/tests/{ => tf}/test_deeppot_spin.py | 9 +++-- source/tests/{ => tf}/test_descrpt_hybrid.py | 11 +++--- source/tests/{ => tf}/test_descrpt_nonsmth.py | 15 ++++---- .../tests/{ => tf}/test_descrpt_se_a_mask.py | 26 ++++++++----- .../tests/{ => tf}/test_descrpt_se_a_type.py | 11 +++--- .../tests/{ => tf}/test_descrpt_se_atten.py | 11 +++--- source/tests/{ => tf}/test_descrpt_se_r.py | 15 ++++---- source/tests/{ => tf}/test_descrpt_sea_ef.py | 15 ++++---- .../{ => tf}/test_descrpt_sea_ef_para.py | 15 ++++---- .../tests/{ => tf}/test_descrpt_sea_ef_rot.py | 0 .../{ => tf}/test_descrpt_sea_ef_vert.py | 15 ++++---- source/tests/{ => tf}/test_descrpt_smooth.py | 15 ++++---- source/tests/{ => tf}/test_dipole_se_a.py | 15 ++++---- .../tests/{ => tf}/test_dipole_se_a_tebd.py | 15 ++++---- source/tests/{ => tf}/test_dipolecharge.py | 9 +++-- source/tests/{ => tf}/test_dp_test.py | 13 ++++--- source/tests/{ => tf}/test_embedding_net.py | 0 source/tests/{ => tf}/test_env.py | 0 source/tests/{ => tf}/test_ewald.py | 0 .../tests/{ => tf}/test_finetune_se_atten.py | 11 +++--- source/tests/{ => tf}/test_fitting_dos.py | 11 +++--- .../tests/{ => tf}/test_fitting_ener_type.py | 11 +++--- source/tests/{ => tf}/test_fitting_stat.py | 7 ++-- source/tests/{ => tf}/test_gen_stat_data.py | 0 source/tests/{ => tf}/test_get_potential.py | 9 +++-- .../{ => tf}/test_init_frz_model_multi.py | 11 +++--- .../{ => tf}/test_init_frz_model_se_a.py | 11 +++--- .../{ => tf}/test_init_frz_model_se_a_tebd.py | 11 +++--- .../{ => tf}/test_init_frz_model_se_a_type.py | 11 +++--- .../{ => tf}/test_init_frz_model_se_atten.py | 11 +++--- .../{ => tf}/test_init_frz_model_se_r.py | 11 +++--- .../{ => tf}/test_init_frz_model_spin.py | 11 +++--- source/tests/{ => tf}/test_lammps.py | 9 +++-- source/tests/{ => tf}/test_layer_name.py | 13 ++++--- source/tests/{ => tf}/test_linear_model.py | 10 ++--- source/tests/{ => tf}/test_loss_gf.py | 0 .../{ => tf}/test_mixed_prec_training.py | 14 +++---- .../{ => tf}/test_model_compression_se_a.py | 14 +++---- .../test_model_compression_se_a_ebd.py | 14 +++---- ...odel_compression_se_a_ebd_type_one_side.py | 14 +++---- ...ession_se_a_type_one_side_exclude_types.py | 14 +++---- .../test_model_compression_se_atten.py | 14 +++---- .../{ => tf}/test_model_compression_se_r.py | 14 +++---- .../{ => tf}/test_model_compression_se_t.py | 14 +++---- source/tests/{ => tf}/test_model_devi.py | 18 ++++----- source/tests/{ => tf}/test_model_devi_mix.py | 24 ++++++------ source/tests/{ => tf}/test_model_dos.py | 13 ++++--- source/tests/{ => tf}/test_model_loc_frame.py | 11 +++--- source/tests/{ => tf}/test_model_multi.py | 17 +++++---- source/tests/{ => tf}/test_model_pairtab.py | 11 +++--- source/tests/{ => tf}/test_model_se_a.py | 13 ++++--- .../tests/{ => tf}/test_model_se_a_aparam.py | 11 +++--- source/tests/{ => tf}/test_model_se_a_ebd.py | 11 +++--- .../tests/{ => tf}/test_model_se_a_ebd_v2.py | 11 +++--- .../tests/{ => tf}/test_model_se_a_fparam.py | 11 +++--- .../tests/{ => tf}/test_model_se_a_srtab.py | 11 +++--- source/tests/{ => tf}/test_model_se_a_type.py | 11 +++--- source/tests/{ => tf}/test_model_se_atten.py | 15 ++++---- source/tests/{ => tf}/test_model_se_r.py | 11 +++--- source/tests/{ => tf}/test_model_se_t.py | 11 +++--- source/tests/{ => tf}/test_model_spin.json | 0 source/tests/{ => tf}/test_model_spin.py | 17 +++++---- source/tests/{ => tf}/test_neighbor_stat.py | 0 .../tests/{ => tf}/test_nvnmd_entrypoints.py | 7 ++-- source/tests/{ => tf}/test_nvnmd_op.py | 0 source/tests/{ => tf}/test_pairwise_dprc.py | 9 +++-- .../tests/{ => tf}/test_parallel_training.py | 8 ++-- source/tests/{ => tf}/test_polar_se_a.py | 15 ++++---- source/tests/{ => tf}/test_polar_se_a_tebd.py | 15 ++++---- source/tests/{ => tf}/test_prod_env_mat.py | 0 source/tests/{ => tf}/test_prod_force.py | 0 source/tests/{ => tf}/test_prod_force_grad.py | 0 source/tests/{ => tf}/test_prod_virial.py | 0 .../tests/{ => tf}/test_prod_virial_grad.py | 0 source/tests/{ => tf}/test_tab_nonsmth.py | 21 +++++----- source/tests/{ => tf}/test_tab_smooth.py | 21 +++++----- source/tests/{ => tf}/test_tabulate.py | 0 source/tests/{ => tf}/test_train.py | 0 source/tests/{ => tf}/test_transfer.py | 14 ++++--- source/tests/{ => tf}/test_type_embed.py | 0 source/tests/{ => tf}/test_type_one_side.py | 11 +++--- source/tests/{ => tf}/test_virtual_type.py | 13 ++++--- source/tests/{ => tf}/train_dos.json | 0 source/tests/{ => tf}/water.json | 0 source/tests/{ => tf}/water_hybrid.json | 0 source/tests/{ => tf}/water_layer_name.json | 0 source/tests/{ => tf}/water_multi.json | 0 source/tests/{ => tf}/water_se_a.json | 0 source/tests/{ => tf}/water_se_a_afparam.json | 0 source/tests/{ => tf}/water_se_a_aparam.json | 0 source/tests/{ => tf}/water_se_a_ebd.json | 0 source/tests/{ => tf}/water_se_a_fparam.json | 0 source/tests/{ => tf}/water_se_a_srtab.json | 0 source/tests/{ => tf}/water_se_a_type.json | 0 source/tests/{ => tf}/water_se_atten.json | 0 ...ater_se_atten_compressible_mixed_type.json | 0 .../{ => tf}/water_se_atten_mixed_type.json | 0 source/tests/{ => tf}/water_se_r.json | 0 source/tests/{ => tf}/water_se_t.json | 0 source/tests/{ => tf}/wfc.json | 0 .../{ => tf}/yaml_inputs/water_se_a_v1.json | 0 .../{ => tf}/yaml_inputs/water_se_a_v1.yaml | 0 .../tests/{ => tf}/yaml_inputs/water_v1.json | 0 .../tests/{ => tf}/yaml_inputs/water_v1.yaml | 0 source/tests/{ => tf}/zinc_se_a_mask.json | 0 203 files changed, 552 insertions(+), 480 deletions(-) create mode 100644 source/tests/__init__.py create mode 100644 source/tests/common/__init__.py create mode 100644 source/tests/common/common.py rename source/tests/{ => common}/test_argument_parser.py (99%) rename source/tests/{ => common}/test_examples.py (93%) rename source/tests/{ => common}/test_gui.py (100%) rename source/tests/{ => common}/test_model_format_utils.py (100%) rename source/tests/{ => common}/test_output_def.py (100%) rename source/tests/{ => common}/test_sel_idx.py (94%) rename source/tests/{ => common}/test_uni_infer.py (81%) create mode 100644 source/tests/tf/__init__.py rename source/tests/{ => tf}/common.py (99%) rename source/tests/{ => tf}/compat_inputs/water_se_a_v0.json (100%) rename source/tests/{ => tf}/compat_inputs/water_se_a_v1.json (100%) rename source/tests/{ => tf}/compat_inputs/water_v0.json (100%) rename source/tests/{ => tf}/compat_inputs/water_v1.json (100%) rename source/tests/{ => tf}/compat_inputs/water_v2.json (100%) rename source/tests/{ => tf}/data_dp_mask/nopbc (100%) rename source/tests/{ => tf}/data_dp_mask/set.000/aparam.npy (100%) rename source/tests/{ => tf}/data_dp_mask/set.000/atom_pref.npy (100%) rename source/tests/{ => tf}/data_dp_mask/set.000/box.npy (100%) rename source/tests/{ => tf}/data_dp_mask/set.000/coord.npy (100%) rename source/tests/{ => tf}/data_dp_mask/set.000/energy.npy (100%) rename source/tests/{ => tf}/data_dp_mask/set.000/force.npy (100%) rename source/tests/{ => tf}/data_dp_mask/type.raw (100%) rename source/tests/{ => tf}/data_dp_mask/type_map.raw (100%) rename source/tests/{ => tf}/data_modifier/dipole.json (100%) rename source/tests/{ => tf}/data_modifier/sys_10/set.000/atomic_dipole.npy (100%) rename source/tests/{ => tf}/data_modifier/sys_10/set.000/box.npy (100%) rename source/tests/{ => tf}/data_modifier/sys_10/set.000/coord.npy (100%) rename source/tests/{ => tf}/data_modifier/sys_10/set.000/energy.npy (100%) rename source/tests/{ => tf}/data_modifier/sys_10/set.000/force.npy (100%) rename source/tests/{ => tf}/data_modifier/sys_10/type.raw (100%) rename source/tests/{ => tf}/data_modifier/sys_10/type_map.raw (100%) rename source/tests/{ => tf}/finetune/data/set.000/box.npy (100%) rename source/tests/{ => tf}/finetune/data/set.000/coord.npy (100%) rename source/tests/{ => tf}/finetune/data/set.000/energy.npy (100%) rename source/tests/{ => tf}/finetune/data/set.000/force.npy (100%) rename source/tests/{ => tf}/finetune/data/type.raw (100%) rename source/tests/{ => tf}/finetune/data/type_map.raw (100%) rename source/tests/{ => tf}/finetune/data_mixed_type/set.000/box.npy (100%) rename source/tests/{ => tf}/finetune/data_mixed_type/set.000/coord.npy (100%) rename source/tests/{ => tf}/finetune/data_mixed_type/set.000/energy.npy (100%) rename source/tests/{ => tf}/finetune/data_mixed_type/set.000/force.npy (100%) rename source/tests/{ => tf}/finetune/data_mixed_type/set.000/real_atom_types.npy (100%) rename source/tests/{ => tf}/finetune/data_mixed_type/type.raw (100%) rename source/tests/{ => tf}/finetune/data_mixed_type/type_map.raw (100%) rename source/tests/{ => tf}/finetune/input_finetune.json (100%) rename source/tests/{ => tf}/finetune/input_pretrain.json (100%) rename source/tests/{ => tf}/init_frz_model/data/set.000/box.npy (100%) rename source/tests/{ => tf}/init_frz_model/data/set.000/coord.npy (100%) rename source/tests/{ => tf}/init_frz_model/data/set.000/energy.npy (100%) rename source/tests/{ => tf}/init_frz_model/data/set.000/force.npy (100%) rename source/tests/{ => tf}/init_frz_model/data/type.raw (100%) rename source/tests/{ => tf}/init_frz_model/data/type_map.raw (100%) rename source/tests/{ => tf}/init_frz_model/input.json (100%) rename source/tests/{ => tf}/model_compression/data/set.000/box.npy (100%) rename source/tests/{ => tf}/model_compression/data/set.000/coord.npy (100%) rename source/tests/{ => tf}/model_compression/data/set.000/energy.npy (100%) rename source/tests/{ => tf}/model_compression/data/set.000/force.npy (100%) rename source/tests/{ => tf}/model_compression/data/type.raw (100%) rename source/tests/{ => tf}/model_compression/data/type_map.raw (100%) rename source/tests/{ => tf}/model_compression/input.json (100%) rename source/tests/{ => tf}/model_spin/set.000/box.npy (100%) rename source/tests/{ => tf}/model_spin/set.000/coord.npy (100%) rename source/tests/{ => tf}/model_spin/set.000/energy.npy (100%) rename source/tests/{ => tf}/model_spin/set.000/force.npy (100%) rename source/tests/{ => tf}/model_spin/type.raw (100%) rename source/tests/{ => tf}/nvnmd/ref/box.npy (100%) rename source/tests/{ => tf}/nvnmd/ref/config_v0_cnn.npy (100%) rename source/tests/{ => tf}/nvnmd/ref/config_v1_cnn.npy (100%) rename source/tests/{ => tf}/nvnmd/ref/coord.npy (100%) rename source/tests/{ => tf}/nvnmd/ref/type.raw (100%) rename source/tests/{ => tf}/nvnmd/ref/weight_v0_cnn.npy (100%) rename source/tests/{ => tf}/nvnmd/ref/weight_v1_cnn.npy (100%) rename source/tests/{ => tf}/pairwise_dprc.json (100%) rename source/tests/{ => tf}/polar_se_a.json (100%) rename source/tests/{ => tf}/polar_se_a_tebd.json (100%) rename source/tests/{ => tf}/test.hdf5 (100%) rename source/tests/{ => tf}/test_activation_fn_gelu.py (100%) rename source/tests/{ => tf}/test_adjust_sel.py (99%) rename source/tests/{ => tf}/test_auto_batch_size.py (100%) rename source/tests/{ => tf}/test_cluster.py (100%) rename source/tests/{ => tf}/test_common.py (100%) rename source/tests/{ => tf}/test_compat_input.py (98%) rename source/tests/{ => tf}/test_compressed_training.py (99%) rename source/tests/{ => tf}/test_data_large_batch.py (99%) rename source/tests/{ => tf}/test_data_modifier.py (99%) rename source/tests/{ => tf}/test_data_modifier_shuffle.py (100%) rename source/tests/{ => tf}/test_data_requirement.py (100%) rename source/tests/{ => tf}/test_deepdipole.py (98%) rename source/tests/{ => tf}/test_deepdos.py (99%) rename source/tests/{ => tf}/test_deepmd_data.py (99%) rename source/tests/{ => tf}/test_deepmd_data_sys.py (100%) rename source/tests/{ => tf}/test_deeppolar.py (99%) rename source/tests/{ => tf}/test_deeppot_a.py (97%) rename source/tests/{ => tf}/test_deeppot_r.py (98%) rename source/tests/{ => tf}/test_deeppot_spin.py (98%) rename source/tests/{ => tf}/test_descrpt_hybrid.py (99%) rename source/tests/{ => tf}/test_descrpt_nonsmth.py (99%) rename source/tests/{ => tf}/test_descrpt_se_a_mask.py (94%) rename source/tests/{ => tf}/test_descrpt_se_a_type.py (99%) rename source/tests/{ => tf}/test_descrpt_se_atten.py (99%) rename source/tests/{ => tf}/test_descrpt_se_r.py (99%) rename source/tests/{ => tf}/test_descrpt_sea_ef.py (99%) rename source/tests/{ => tf}/test_descrpt_sea_ef_para.py (99%) rename source/tests/{ => tf}/test_descrpt_sea_ef_rot.py (100%) rename source/tests/{ => tf}/test_descrpt_sea_ef_vert.py (99%) rename source/tests/{ => tf}/test_descrpt_smooth.py (99%) rename source/tests/{ => tf}/test_dipole_se_a.py (99%) rename source/tests/{ => tf}/test_dipole_se_a_tebd.py (99%) rename source/tests/{ => tf}/test_dipolecharge.py (98%) rename source/tests/{ => tf}/test_dp_test.py (97%) rename source/tests/{ => tf}/test_embedding_net.py (100%) rename source/tests/{ => tf}/test_env.py (100%) rename source/tests/{ => tf}/test_ewald.py (100%) rename source/tests/{ => tf}/test_finetune_se_atten.py (99%) rename source/tests/{ => tf}/test_fitting_dos.py (99%) rename source/tests/{ => tf}/test_fitting_ener_type.py (99%) rename source/tests/{ => tf}/test_fitting_stat.py (99%) rename source/tests/{ => tf}/test_gen_stat_data.py (100%) rename source/tests/{ => tf}/test_get_potential.py (96%) rename source/tests/{ => tf}/test_init_frz_model_multi.py (99%) rename source/tests/{ => tf}/test_init_frz_model_se_a.py (99%) rename source/tests/{ => tf}/test_init_frz_model_se_a_tebd.py (99%) rename source/tests/{ => tf}/test_init_frz_model_se_a_type.py (99%) rename source/tests/{ => tf}/test_init_frz_model_se_atten.py (99%) rename source/tests/{ => tf}/test_init_frz_model_se_r.py (99%) rename source/tests/{ => tf}/test_init_frz_model_spin.py (99%) rename source/tests/{ => tf}/test_lammps.py (87%) rename source/tests/{ => tf}/test_layer_name.py (99%) rename source/tests/{ => tf}/test_linear_model.py (94%) rename source/tests/{ => tf}/test_loss_gf.py (100%) rename source/tests/{ => tf}/test_mixed_prec_training.py (99%) rename source/tests/{ => tf}/test_model_compression_se_a.py (99%) rename source/tests/{ => tf}/test_model_compression_se_a_ebd.py (99%) rename source/tests/{ => tf}/test_model_compression_se_a_ebd_type_one_side.py (99%) rename source/tests/{ => tf}/test_model_compression_se_a_type_one_side_exclude_types.py (99%) rename source/tests/{ => tf}/test_model_compression_se_atten.py (99%) rename source/tests/{ => tf}/test_model_compression_se_r.py (99%) rename source/tests/{ => tf}/test_model_compression_se_t.py (99%) rename source/tests/{ => tf}/test_model_devi.py (97%) rename source/tests/{ => tf}/test_model_devi_mix.py (94%) rename source/tests/{ => tf}/test_model_dos.py (99%) rename source/tests/{ => tf}/test_model_loc_frame.py (99%) rename source/tests/{ => tf}/test_model_multi.py (99%) rename source/tests/{ => tf}/test_model_pairtab.py (99%) rename source/tests/{ => tf}/test_model_se_a.py (99%) rename source/tests/{ => tf}/test_model_se_a_aparam.py (99%) rename source/tests/{ => tf}/test_model_se_a_ebd.py (99%) rename source/tests/{ => tf}/test_model_se_a_ebd_v2.py (99%) rename source/tests/{ => tf}/test_model_se_a_fparam.py (99%) rename source/tests/{ => tf}/test_model_se_a_srtab.py (99%) rename source/tests/{ => tf}/test_model_se_a_type.py (99%) rename source/tests/{ => tf}/test_model_se_atten.py (99%) rename source/tests/{ => tf}/test_model_se_r.py (99%) rename source/tests/{ => tf}/test_model_se_t.py (99%) rename source/tests/{ => tf}/test_model_spin.json (100%) rename source/tests/{ => tf}/test_model_spin.py (99%) rename source/tests/{ => tf}/test_neighbor_stat.py (100%) rename source/tests/{ => tf}/test_nvnmd_entrypoints.py (99%) rename source/tests/{ => tf}/test_nvnmd_op.py (100%) rename source/tests/{ => tf}/test_pairwise_dprc.py (99%) rename source/tests/{ => tf}/test_parallel_training.py (98%) rename source/tests/{ => tf}/test_polar_se_a.py (99%) rename source/tests/{ => tf}/test_polar_se_a_tebd.py (99%) rename source/tests/{ => tf}/test_prod_env_mat.py (100%) rename source/tests/{ => tf}/test_prod_force.py (100%) rename source/tests/{ => tf}/test_prod_force_grad.py (100%) rename source/tests/{ => tf}/test_prod_virial.py (100%) rename source/tests/{ => tf}/test_prod_virial_grad.py (100%) rename source/tests/{ => tf}/test_tab_nonsmth.py (98%) rename source/tests/{ => tf}/test_tab_smooth.py (98%) rename source/tests/{ => tf}/test_tabulate.py (100%) rename source/tests/{ => tf}/test_train.py (100%) rename source/tests/{ => tf}/test_transfer.py (98%) rename source/tests/{ => tf}/test_type_embed.py (100%) rename source/tests/{ => tf}/test_type_one_side.py (99%) rename source/tests/{ => tf}/test_virtual_type.py (97%) rename source/tests/{ => tf}/train_dos.json (100%) rename source/tests/{ => tf}/water.json (100%) rename source/tests/{ => tf}/water_hybrid.json (100%) rename source/tests/{ => tf}/water_layer_name.json (100%) rename source/tests/{ => tf}/water_multi.json (100%) rename source/tests/{ => tf}/water_se_a.json (100%) rename source/tests/{ => tf}/water_se_a_afparam.json (100%) rename source/tests/{ => tf}/water_se_a_aparam.json (100%) rename source/tests/{ => tf}/water_se_a_ebd.json (100%) rename source/tests/{ => tf}/water_se_a_fparam.json (100%) rename source/tests/{ => tf}/water_se_a_srtab.json (100%) rename source/tests/{ => tf}/water_se_a_type.json (100%) rename source/tests/{ => tf}/water_se_atten.json (100%) rename source/tests/{ => tf}/water_se_atten_compressible_mixed_type.json (100%) rename source/tests/{ => tf}/water_se_atten_mixed_type.json (100%) rename source/tests/{ => tf}/water_se_r.json (100%) rename source/tests/{ => tf}/water_se_t.json (100%) rename source/tests/{ => tf}/wfc.json (100%) rename source/tests/{ => tf}/yaml_inputs/water_se_a_v1.json (100%) rename source/tests/{ => tf}/yaml_inputs/water_se_a_v1.yaml (100%) rename source/tests/{ => tf}/yaml_inputs/water_v1.json (100%) rename source/tests/{ => tf}/yaml_inputs/water_v1.yaml (100%) rename source/tests/{ => tf}/zinc_se_a_mask.json (100%) diff --git a/pyproject.toml b/pyproject.toml index 6c8632ddb9..1d37246a88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,7 +130,7 @@ test-command = [ "python -m deepmd -h", "dp -h", "dp_ipi", - "pytest {project}/source/tests/test_lammps.py" + "pytest {project}/source/tests/tf/test_lammps.py" ] test-extras = ["cpu", "test", "lmp", "ipi"] build = ["cp310-*"] diff --git a/source/tests/__init__.py b/source/tests/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/source/tests/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/source/tests/common/__init__.py b/source/tests/common/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/source/tests/common/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/source/tests/common/common.py b/source/tests/common/common.py new file mode 100644 index 0000000000..4736042150 --- /dev/null +++ b/source/tests/common/common.py @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import pathlib + +tests_path = pathlib.Path(__file__).parent.absolute() +infer_path = (tests_path.parent / "infer").absolute() diff --git a/source/tests/test_argument_parser.py b/source/tests/common/test_argument_parser.py similarity index 99% rename from source/tests/test_argument_parser.py rename to source/tests/common/test_argument_parser.py index 988199d1e4..0b4f053aed 100644 --- a/source/tests/test_argument_parser.py +++ b/source/tests/common/test_argument_parser.py @@ -21,7 +21,7 @@ Union, ) -from deepmd.tf.entrypoints.main import ( +from deepmd.main import ( get_ll, parse_args, ) diff --git a/source/tests/test_examples.py b/source/tests/common/test_examples.py similarity index 93% rename from source/tests/test_examples.py rename to source/tests/common/test_examples.py index ea087fbc9d..ad06925eab 100644 --- a/source/tests/test_examples.py +++ b/source/tests/common/test_examples.py @@ -7,14 +7,14 @@ Path, ) -from deepmd.tf.common import ( +from deepmd.common import ( j_loader, ) -from deepmd.tf.utils.argcheck import ( +from deepmd.utils.argcheck import ( normalize, ) -p_examples = Path(__file__).parent.parent.parent / "examples" +p_examples = Path(__file__).parent.parent.parent.parent / "examples" input_files = ( p_examples / "water" / "se_e2_a" / "input.json", diff --git a/source/tests/test_gui.py b/source/tests/common/test_gui.py similarity index 100% rename from source/tests/test_gui.py rename to source/tests/common/test_gui.py diff --git a/source/tests/test_model_format_utils.py b/source/tests/common/test_model_format_utils.py similarity index 100% rename from source/tests/test_model_format_utils.py rename to source/tests/common/test_model_format_utils.py diff --git a/source/tests/test_output_def.py b/source/tests/common/test_output_def.py similarity index 100% rename from source/tests/test_output_def.py rename to source/tests/common/test_output_def.py diff --git a/source/tests/test_sel_idx.py b/source/tests/common/test_sel_idx.py similarity index 94% rename from source/tests/test_sel_idx.py rename to source/tests/common/test_sel_idx.py index e340ba55e7..d6630e3e83 100644 --- a/source/tests/test_sel_idx.py +++ b/source/tests/common/test_sel_idx.py @@ -3,7 +3,7 @@ import numpy as np -from deepmd.tf.common import ( +from deepmd.common import ( select_idx_map, ) diff --git a/source/tests/test_uni_infer.py b/source/tests/common/test_uni_infer.py similarity index 81% rename from source/tests/test_uni_infer.py rename to source/tests/common/test_uni_infer.py index 4cff5e10d5..139d1f9ec9 100644 --- a/source/tests/test_uni_infer.py +++ b/source/tests/common/test_uni_infer.py @@ -4,22 +4,22 @@ import os import unittest -from common import ( - tests_path, -) - from deepmd.infer.deep_pot import DeepPot as DeepPot from deepmd.tf.infer.deep_pot import DeepPot as DeepPotTF from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) +from .common import ( + infer_path, +) + class TestUniversalInfer(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppot-r.pbtxt")), "deeppot.pb" + str(infer_path / os.path.join("deeppot-r.pbtxt")), "deeppot.pb" ) def test_deep_pot(self): diff --git a/source/tests/tf/__init__.py b/source/tests/tf/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/source/tests/tf/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/source/tests/common.py b/source/tests/tf/common.py similarity index 99% rename from source/tests/common.py rename to source/tests/tf/common.py index cb68e4d46d..a83397c11c 100644 --- a/source/tests/common.py +++ b/source/tests/tf/common.py @@ -28,6 +28,7 @@ global_default_places = 5 tests_path = pathlib.Path(__file__).parent.absolute() +infer_path = (tests_path.parent / "infer").absolute() def j_loader(filename): diff --git a/source/tests/compat_inputs/water_se_a_v0.json b/source/tests/tf/compat_inputs/water_se_a_v0.json similarity index 100% rename from source/tests/compat_inputs/water_se_a_v0.json rename to source/tests/tf/compat_inputs/water_se_a_v0.json diff --git a/source/tests/compat_inputs/water_se_a_v1.json b/source/tests/tf/compat_inputs/water_se_a_v1.json similarity index 100% rename from source/tests/compat_inputs/water_se_a_v1.json rename to source/tests/tf/compat_inputs/water_se_a_v1.json diff --git a/source/tests/compat_inputs/water_v0.json b/source/tests/tf/compat_inputs/water_v0.json similarity index 100% rename from source/tests/compat_inputs/water_v0.json rename to source/tests/tf/compat_inputs/water_v0.json diff --git a/source/tests/compat_inputs/water_v1.json b/source/tests/tf/compat_inputs/water_v1.json similarity index 100% rename from source/tests/compat_inputs/water_v1.json rename to source/tests/tf/compat_inputs/water_v1.json diff --git a/source/tests/compat_inputs/water_v2.json b/source/tests/tf/compat_inputs/water_v2.json similarity index 100% rename from source/tests/compat_inputs/water_v2.json rename to source/tests/tf/compat_inputs/water_v2.json diff --git a/source/tests/data_dp_mask/nopbc b/source/tests/tf/data_dp_mask/nopbc similarity index 100% rename from source/tests/data_dp_mask/nopbc rename to source/tests/tf/data_dp_mask/nopbc diff --git a/source/tests/data_dp_mask/set.000/aparam.npy b/source/tests/tf/data_dp_mask/set.000/aparam.npy similarity index 100% rename from source/tests/data_dp_mask/set.000/aparam.npy rename to source/tests/tf/data_dp_mask/set.000/aparam.npy diff --git a/source/tests/data_dp_mask/set.000/atom_pref.npy b/source/tests/tf/data_dp_mask/set.000/atom_pref.npy similarity index 100% rename from source/tests/data_dp_mask/set.000/atom_pref.npy rename to source/tests/tf/data_dp_mask/set.000/atom_pref.npy diff --git a/source/tests/data_dp_mask/set.000/box.npy b/source/tests/tf/data_dp_mask/set.000/box.npy similarity index 100% rename from source/tests/data_dp_mask/set.000/box.npy rename to source/tests/tf/data_dp_mask/set.000/box.npy diff --git a/source/tests/data_dp_mask/set.000/coord.npy b/source/tests/tf/data_dp_mask/set.000/coord.npy similarity index 100% rename from source/tests/data_dp_mask/set.000/coord.npy rename to source/tests/tf/data_dp_mask/set.000/coord.npy diff --git a/source/tests/data_dp_mask/set.000/energy.npy b/source/tests/tf/data_dp_mask/set.000/energy.npy similarity index 100% rename from source/tests/data_dp_mask/set.000/energy.npy rename to source/tests/tf/data_dp_mask/set.000/energy.npy diff --git a/source/tests/data_dp_mask/set.000/force.npy b/source/tests/tf/data_dp_mask/set.000/force.npy similarity index 100% rename from source/tests/data_dp_mask/set.000/force.npy rename to source/tests/tf/data_dp_mask/set.000/force.npy diff --git a/source/tests/data_dp_mask/type.raw b/source/tests/tf/data_dp_mask/type.raw similarity index 100% rename from source/tests/data_dp_mask/type.raw rename to source/tests/tf/data_dp_mask/type.raw diff --git a/source/tests/data_dp_mask/type_map.raw b/source/tests/tf/data_dp_mask/type_map.raw similarity index 100% rename from source/tests/data_dp_mask/type_map.raw rename to source/tests/tf/data_dp_mask/type_map.raw diff --git a/source/tests/data_modifier/dipole.json b/source/tests/tf/data_modifier/dipole.json similarity index 100% rename from source/tests/data_modifier/dipole.json rename to source/tests/tf/data_modifier/dipole.json diff --git a/source/tests/data_modifier/sys_10/set.000/atomic_dipole.npy b/source/tests/tf/data_modifier/sys_10/set.000/atomic_dipole.npy similarity index 100% rename from source/tests/data_modifier/sys_10/set.000/atomic_dipole.npy rename to source/tests/tf/data_modifier/sys_10/set.000/atomic_dipole.npy diff --git a/source/tests/data_modifier/sys_10/set.000/box.npy b/source/tests/tf/data_modifier/sys_10/set.000/box.npy similarity index 100% rename from source/tests/data_modifier/sys_10/set.000/box.npy rename to source/tests/tf/data_modifier/sys_10/set.000/box.npy diff --git a/source/tests/data_modifier/sys_10/set.000/coord.npy b/source/tests/tf/data_modifier/sys_10/set.000/coord.npy similarity index 100% rename from source/tests/data_modifier/sys_10/set.000/coord.npy rename to source/tests/tf/data_modifier/sys_10/set.000/coord.npy diff --git a/source/tests/data_modifier/sys_10/set.000/energy.npy b/source/tests/tf/data_modifier/sys_10/set.000/energy.npy similarity index 100% rename from source/tests/data_modifier/sys_10/set.000/energy.npy rename to source/tests/tf/data_modifier/sys_10/set.000/energy.npy diff --git a/source/tests/data_modifier/sys_10/set.000/force.npy b/source/tests/tf/data_modifier/sys_10/set.000/force.npy similarity index 100% rename from source/tests/data_modifier/sys_10/set.000/force.npy rename to source/tests/tf/data_modifier/sys_10/set.000/force.npy diff --git a/source/tests/data_modifier/sys_10/type.raw b/source/tests/tf/data_modifier/sys_10/type.raw similarity index 100% rename from source/tests/data_modifier/sys_10/type.raw rename to source/tests/tf/data_modifier/sys_10/type.raw diff --git a/source/tests/data_modifier/sys_10/type_map.raw b/source/tests/tf/data_modifier/sys_10/type_map.raw similarity index 100% rename from source/tests/data_modifier/sys_10/type_map.raw rename to source/tests/tf/data_modifier/sys_10/type_map.raw diff --git a/source/tests/finetune/data/set.000/box.npy b/source/tests/tf/finetune/data/set.000/box.npy similarity index 100% rename from source/tests/finetune/data/set.000/box.npy rename to source/tests/tf/finetune/data/set.000/box.npy diff --git a/source/tests/finetune/data/set.000/coord.npy b/source/tests/tf/finetune/data/set.000/coord.npy similarity index 100% rename from source/tests/finetune/data/set.000/coord.npy rename to source/tests/tf/finetune/data/set.000/coord.npy diff --git a/source/tests/finetune/data/set.000/energy.npy b/source/tests/tf/finetune/data/set.000/energy.npy similarity index 100% rename from source/tests/finetune/data/set.000/energy.npy rename to source/tests/tf/finetune/data/set.000/energy.npy diff --git a/source/tests/finetune/data/set.000/force.npy b/source/tests/tf/finetune/data/set.000/force.npy similarity index 100% rename from source/tests/finetune/data/set.000/force.npy rename to source/tests/tf/finetune/data/set.000/force.npy diff --git a/source/tests/finetune/data/type.raw b/source/tests/tf/finetune/data/type.raw similarity index 100% rename from source/tests/finetune/data/type.raw rename to source/tests/tf/finetune/data/type.raw diff --git a/source/tests/finetune/data/type_map.raw b/source/tests/tf/finetune/data/type_map.raw similarity index 100% rename from source/tests/finetune/data/type_map.raw rename to source/tests/tf/finetune/data/type_map.raw diff --git a/source/tests/finetune/data_mixed_type/set.000/box.npy b/source/tests/tf/finetune/data_mixed_type/set.000/box.npy similarity index 100% rename from source/tests/finetune/data_mixed_type/set.000/box.npy rename to source/tests/tf/finetune/data_mixed_type/set.000/box.npy diff --git a/source/tests/finetune/data_mixed_type/set.000/coord.npy b/source/tests/tf/finetune/data_mixed_type/set.000/coord.npy similarity index 100% rename from source/tests/finetune/data_mixed_type/set.000/coord.npy rename to source/tests/tf/finetune/data_mixed_type/set.000/coord.npy diff --git a/source/tests/finetune/data_mixed_type/set.000/energy.npy b/source/tests/tf/finetune/data_mixed_type/set.000/energy.npy similarity index 100% rename from source/tests/finetune/data_mixed_type/set.000/energy.npy rename to source/tests/tf/finetune/data_mixed_type/set.000/energy.npy diff --git a/source/tests/finetune/data_mixed_type/set.000/force.npy b/source/tests/tf/finetune/data_mixed_type/set.000/force.npy similarity index 100% rename from source/tests/finetune/data_mixed_type/set.000/force.npy rename to source/tests/tf/finetune/data_mixed_type/set.000/force.npy diff --git a/source/tests/finetune/data_mixed_type/set.000/real_atom_types.npy b/source/tests/tf/finetune/data_mixed_type/set.000/real_atom_types.npy similarity index 100% rename from source/tests/finetune/data_mixed_type/set.000/real_atom_types.npy rename to source/tests/tf/finetune/data_mixed_type/set.000/real_atom_types.npy diff --git a/source/tests/finetune/data_mixed_type/type.raw b/source/tests/tf/finetune/data_mixed_type/type.raw similarity index 100% rename from source/tests/finetune/data_mixed_type/type.raw rename to source/tests/tf/finetune/data_mixed_type/type.raw diff --git a/source/tests/finetune/data_mixed_type/type_map.raw b/source/tests/tf/finetune/data_mixed_type/type_map.raw similarity index 100% rename from source/tests/finetune/data_mixed_type/type_map.raw rename to source/tests/tf/finetune/data_mixed_type/type_map.raw diff --git a/source/tests/finetune/input_finetune.json b/source/tests/tf/finetune/input_finetune.json similarity index 100% rename from source/tests/finetune/input_finetune.json rename to source/tests/tf/finetune/input_finetune.json diff --git a/source/tests/finetune/input_pretrain.json b/source/tests/tf/finetune/input_pretrain.json similarity index 100% rename from source/tests/finetune/input_pretrain.json rename to source/tests/tf/finetune/input_pretrain.json diff --git a/source/tests/init_frz_model/data/set.000/box.npy b/source/tests/tf/init_frz_model/data/set.000/box.npy similarity index 100% rename from source/tests/init_frz_model/data/set.000/box.npy rename to source/tests/tf/init_frz_model/data/set.000/box.npy diff --git a/source/tests/init_frz_model/data/set.000/coord.npy b/source/tests/tf/init_frz_model/data/set.000/coord.npy similarity index 100% rename from source/tests/init_frz_model/data/set.000/coord.npy rename to source/tests/tf/init_frz_model/data/set.000/coord.npy diff --git a/source/tests/init_frz_model/data/set.000/energy.npy b/source/tests/tf/init_frz_model/data/set.000/energy.npy similarity index 100% rename from source/tests/init_frz_model/data/set.000/energy.npy rename to source/tests/tf/init_frz_model/data/set.000/energy.npy diff --git a/source/tests/init_frz_model/data/set.000/force.npy b/source/tests/tf/init_frz_model/data/set.000/force.npy similarity index 100% rename from source/tests/init_frz_model/data/set.000/force.npy rename to source/tests/tf/init_frz_model/data/set.000/force.npy diff --git a/source/tests/init_frz_model/data/type.raw b/source/tests/tf/init_frz_model/data/type.raw similarity index 100% rename from source/tests/init_frz_model/data/type.raw rename to source/tests/tf/init_frz_model/data/type.raw diff --git a/source/tests/init_frz_model/data/type_map.raw b/source/tests/tf/init_frz_model/data/type_map.raw similarity index 100% rename from source/tests/init_frz_model/data/type_map.raw rename to source/tests/tf/init_frz_model/data/type_map.raw diff --git a/source/tests/init_frz_model/input.json b/source/tests/tf/init_frz_model/input.json similarity index 100% rename from source/tests/init_frz_model/input.json rename to source/tests/tf/init_frz_model/input.json diff --git a/source/tests/model_compression/data/set.000/box.npy b/source/tests/tf/model_compression/data/set.000/box.npy similarity index 100% rename from source/tests/model_compression/data/set.000/box.npy rename to source/tests/tf/model_compression/data/set.000/box.npy diff --git a/source/tests/model_compression/data/set.000/coord.npy b/source/tests/tf/model_compression/data/set.000/coord.npy similarity index 100% rename from source/tests/model_compression/data/set.000/coord.npy rename to source/tests/tf/model_compression/data/set.000/coord.npy diff --git a/source/tests/model_compression/data/set.000/energy.npy b/source/tests/tf/model_compression/data/set.000/energy.npy similarity index 100% rename from source/tests/model_compression/data/set.000/energy.npy rename to source/tests/tf/model_compression/data/set.000/energy.npy diff --git a/source/tests/model_compression/data/set.000/force.npy b/source/tests/tf/model_compression/data/set.000/force.npy similarity index 100% rename from source/tests/model_compression/data/set.000/force.npy rename to source/tests/tf/model_compression/data/set.000/force.npy diff --git a/source/tests/model_compression/data/type.raw b/source/tests/tf/model_compression/data/type.raw similarity index 100% rename from source/tests/model_compression/data/type.raw rename to source/tests/tf/model_compression/data/type.raw diff --git a/source/tests/model_compression/data/type_map.raw b/source/tests/tf/model_compression/data/type_map.raw similarity index 100% rename from source/tests/model_compression/data/type_map.raw rename to source/tests/tf/model_compression/data/type_map.raw diff --git a/source/tests/model_compression/input.json b/source/tests/tf/model_compression/input.json similarity index 100% rename from source/tests/model_compression/input.json rename to source/tests/tf/model_compression/input.json diff --git a/source/tests/model_spin/set.000/box.npy b/source/tests/tf/model_spin/set.000/box.npy similarity index 100% rename from source/tests/model_spin/set.000/box.npy rename to source/tests/tf/model_spin/set.000/box.npy diff --git a/source/tests/model_spin/set.000/coord.npy b/source/tests/tf/model_spin/set.000/coord.npy similarity index 100% rename from source/tests/model_spin/set.000/coord.npy rename to source/tests/tf/model_spin/set.000/coord.npy diff --git a/source/tests/model_spin/set.000/energy.npy b/source/tests/tf/model_spin/set.000/energy.npy similarity index 100% rename from source/tests/model_spin/set.000/energy.npy rename to source/tests/tf/model_spin/set.000/energy.npy diff --git a/source/tests/model_spin/set.000/force.npy b/source/tests/tf/model_spin/set.000/force.npy similarity index 100% rename from source/tests/model_spin/set.000/force.npy rename to source/tests/tf/model_spin/set.000/force.npy diff --git a/source/tests/model_spin/type.raw b/source/tests/tf/model_spin/type.raw similarity index 100% rename from source/tests/model_spin/type.raw rename to source/tests/tf/model_spin/type.raw diff --git a/source/tests/nvnmd/ref/box.npy b/source/tests/tf/nvnmd/ref/box.npy similarity index 100% rename from source/tests/nvnmd/ref/box.npy rename to source/tests/tf/nvnmd/ref/box.npy diff --git a/source/tests/nvnmd/ref/config_v0_cnn.npy b/source/tests/tf/nvnmd/ref/config_v0_cnn.npy similarity index 100% rename from source/tests/nvnmd/ref/config_v0_cnn.npy rename to source/tests/tf/nvnmd/ref/config_v0_cnn.npy diff --git a/source/tests/nvnmd/ref/config_v1_cnn.npy b/source/tests/tf/nvnmd/ref/config_v1_cnn.npy similarity index 100% rename from source/tests/nvnmd/ref/config_v1_cnn.npy rename to source/tests/tf/nvnmd/ref/config_v1_cnn.npy diff --git a/source/tests/nvnmd/ref/coord.npy b/source/tests/tf/nvnmd/ref/coord.npy similarity index 100% rename from source/tests/nvnmd/ref/coord.npy rename to source/tests/tf/nvnmd/ref/coord.npy diff --git a/source/tests/nvnmd/ref/type.raw b/source/tests/tf/nvnmd/ref/type.raw similarity index 100% rename from source/tests/nvnmd/ref/type.raw rename to source/tests/tf/nvnmd/ref/type.raw diff --git a/source/tests/nvnmd/ref/weight_v0_cnn.npy b/source/tests/tf/nvnmd/ref/weight_v0_cnn.npy similarity index 100% rename from source/tests/nvnmd/ref/weight_v0_cnn.npy rename to source/tests/tf/nvnmd/ref/weight_v0_cnn.npy diff --git a/source/tests/nvnmd/ref/weight_v1_cnn.npy b/source/tests/tf/nvnmd/ref/weight_v1_cnn.npy similarity index 100% rename from source/tests/nvnmd/ref/weight_v1_cnn.npy rename to source/tests/tf/nvnmd/ref/weight_v1_cnn.npy diff --git a/source/tests/pairwise_dprc.json b/source/tests/tf/pairwise_dprc.json similarity index 100% rename from source/tests/pairwise_dprc.json rename to source/tests/tf/pairwise_dprc.json diff --git a/source/tests/polar_se_a.json b/source/tests/tf/polar_se_a.json similarity index 100% rename from source/tests/polar_se_a.json rename to source/tests/tf/polar_se_a.json diff --git a/source/tests/polar_se_a_tebd.json b/source/tests/tf/polar_se_a_tebd.json similarity index 100% rename from source/tests/polar_se_a_tebd.json rename to source/tests/tf/polar_se_a_tebd.json diff --git a/source/tests/test.hdf5 b/source/tests/tf/test.hdf5 similarity index 100% rename from source/tests/test.hdf5 rename to source/tests/tf/test.hdf5 diff --git a/source/tests/test_activation_fn_gelu.py b/source/tests/tf/test_activation_fn_gelu.py similarity index 100% rename from source/tests/test_activation_fn_gelu.py rename to source/tests/tf/test_activation_fn_gelu.py diff --git a/source/tests/test_adjust_sel.py b/source/tests/tf/test_adjust_sel.py similarity index 99% rename from source/tests/test_adjust_sel.py rename to source/tests/tf/test_adjust_sel.py index 9bed3606fd..c86bad45b7 100644 --- a/source/tests/test_adjust_sel.py +++ b/source/tests/tf/test_adjust_sel.py @@ -6,13 +6,6 @@ import numpy as np -# from deepmd.tf.entrypoints.compress import compress -from common import ( - j_loader, - run_dp, - tests_path, -) - from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) @@ -20,6 +13,13 @@ DeepPot, ) +# from deepmd.tf.entrypoints.compress import compress +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_auto_batch_size.py b/source/tests/tf/test_auto_batch_size.py similarity index 100% rename from source/tests/test_auto_batch_size.py rename to source/tests/tf/test_auto_batch_size.py diff --git a/source/tests/test_cluster.py b/source/tests/tf/test_cluster.py similarity index 100% rename from source/tests/test_cluster.py rename to source/tests/tf/test_cluster.py diff --git a/source/tests/test_common.py b/source/tests/tf/test_common.py similarity index 100% rename from source/tests/test_common.py rename to source/tests/tf/test_common.py diff --git a/source/tests/test_compat_input.py b/source/tests/tf/test_compat_input.py similarity index 98% rename from source/tests/test_compat_input.py rename to source/tests/tf/test_compat_input.py index e8a74e9c48..f7c605380c 100644 --- a/source/tests/test_compat_input.py +++ b/source/tests/tf/test_compat_input.py @@ -2,15 +2,15 @@ import os import unittest -from common import ( - j_loader, -) - from deepmd.tf.utils.compat import ( convert_input_v0_v1, convert_input_v1_v2, ) +from .common import ( + j_loader, +) + class TestConvertInput(unittest.TestCase): def test_convert_smth(self): diff --git a/source/tests/test_compressed_training.py b/source/tests/tf/test_compressed_training.py similarity index 99% rename from source/tests/test_compressed_training.py rename to source/tests/tf/test_compressed_training.py index c3d07762f8..998ef8cb59 100644 --- a/source/tests/test_compressed_training.py +++ b/source/tests/tf/test_compressed_training.py @@ -3,18 +3,19 @@ import os import unittest -# from deepmd.tf.entrypoints.compress import compress -from common import ( - j_loader, - run_dp, - tests_path, -) from packaging.version import parse as parse_version from deepmd.tf.env import ( tf, ) +# from deepmd.tf.entrypoints.compress import compress +from .common import ( + j_loader, + run_dp, + tests_path, +) + @unittest.skipIf( parse_version(tf.__version__) < parse_version("2"), diff --git a/source/tests/test_data_large_batch.py b/source/tests/tf/test_data_large_batch.py similarity index 99% rename from source/tests/test_data_large_batch.py rename to source/tests/tf/test_data_large_batch.py index 84e99591d5..c31a4c4a9a 100644 --- a/source/tests/test_data_large_batch.py +++ b/source/tests/tf/test_data_large_batch.py @@ -3,10 +3,6 @@ import unittest import numpy as np -from common import ( - gen_data, - j_loader, -) from packaging.version import parse as parse_version from deepmd.tf.common import ( @@ -31,6 +27,11 @@ TypeEmbedNet, ) +from .common import ( + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_data_modifier.py b/source/tests/tf/test_data_modifier.py similarity index 99% rename from source/tests/test_data_modifier.py rename to source/tests/tf/test_data_modifier.py index 01e3cdcb2d..98b6c41427 100644 --- a/source/tests/test_data_modifier.py +++ b/source/tests/tf/test_data_modifier.py @@ -2,11 +2,6 @@ import os import numpy as np -from common import ( - Data, - j_loader, - tests_path, -) from deepmd.tf.common import ( data_requirement, @@ -29,6 +24,12 @@ DeepmdDataSystem, ) +from .common import ( + Data, + j_loader, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: global_default_fv_hh = 1e-2 global_default_dw_hh = 1e-2 diff --git a/source/tests/test_data_modifier_shuffle.py b/source/tests/tf/test_data_modifier_shuffle.py similarity index 100% rename from source/tests/test_data_modifier_shuffle.py rename to source/tests/tf/test_data_modifier_shuffle.py diff --git a/source/tests/test_data_requirement.py b/source/tests/tf/test_data_requirement.py similarity index 100% rename from source/tests/test_data_requirement.py rename to source/tests/tf/test_data_requirement.py diff --git a/source/tests/test_deepdipole.py b/source/tests/tf/test_deepdipole.py similarity index 98% rename from source/tests/test_deepdipole.py rename to source/tests/tf/test_deepdipole.py index 6dffe59fe5..2c8ec7cc66 100644 --- a/source/tests/test_deepdipole.py +++ b/source/tests/tf/test_deepdipole.py @@ -4,12 +4,6 @@ import ase.neighborlist import numpy as np -from common import ( - finite_difference, - strerch_box, - tests_path, - tf, -) from packaging.version import parse as parse_version from deepmd.tf.env import ( @@ -22,6 +16,13 @@ convert_pbtxt_to_pb, ) +from .common import ( + finite_difference, + infer_path, + strerch_box, + tf, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: @@ -32,7 +33,7 @@ class TestDeepDipolePBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deepdipole.pbtxt")), "deepdipole.pb" + str(infer_path / os.path.join("deepdipole.pbtxt")), "deepdipole.pb" ) cls.dp = DeepDipole("deepdipole.pb") @@ -111,7 +112,7 @@ class TestDeepDipoleNoPBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deepdipole.pbtxt")), "deepdipole.pb" + str(infer_path / os.path.join("deepdipole.pbtxt")), "deepdipole.pb" ) cls.dp = DeepDipole("deepdipole.pb") @@ -185,7 +186,7 @@ class TestDeepDipoleNewPBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deepdipole_new.pbtxt")), + str(infer_path / os.path.join("deepdipole_new.pbtxt")), "deepdipole_new.pb", ) cls.dp = DeepDipole("deepdipole_new.pb") @@ -656,7 +657,7 @@ class TestDeepDipoleFakePBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deepdipole_fake.pbtxt")), + str(infer_path / os.path.join("deepdipole_fake.pbtxt")), "deepdipole_fake.pb", ) cls.dp = DeepDipole("deepdipole_fake.pb") @@ -1042,7 +1043,7 @@ class TestDeepDipoleNewPBCNeighborList(TestDeepDipoleNewPBC): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deepdipole_new.pbtxt")), + str(infer_path / os.path.join("deepdipole_new.pbtxt")), "deepdipole_new.pb", ) cls.dp = DeepDipole( diff --git a/source/tests/test_deepdos.py b/source/tests/tf/test_deepdos.py similarity index 99% rename from source/tests/test_deepdos.py rename to source/tests/tf/test_deepdos.py index 3f3d0cda7f..d94c2c3f2d 100644 --- a/source/tests/test_deepdos.py +++ b/source/tests/tf/test_deepdos.py @@ -3,9 +3,6 @@ import unittest import numpy as np -from common import ( - tests_path, -) from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, @@ -17,6 +14,10 @@ convert_pbtxt_to_pb, ) +from .common import ( + infer_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: @@ -27,7 +28,7 @@ class TestDeepDOS(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deepdos.pbtxt")), "deepdos.pb" + str(infer_path / os.path.join("deepdos.pbtxt")), "deepdos.pb" ) cls.dp = DeepDOS("deepdos.pb") diff --git a/source/tests/test_deepmd_data.py b/source/tests/tf/test_deepmd_data.py similarity index 99% rename from source/tests/test_deepmd_data.py rename to source/tests/tf/test_deepmd_data.py index e486446ab8..b1a0147771 100644 --- a/source/tests/test_deepmd_data.py +++ b/source/tests/tf/test_deepmd_data.py @@ -5,9 +5,6 @@ import unittest import numpy as np -from common import ( - tests_path, -) from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, @@ -16,6 +13,10 @@ DeepmdData, ) +from .common import ( + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: places = 6 else: diff --git a/source/tests/test_deepmd_data_sys.py b/source/tests/tf/test_deepmd_data_sys.py similarity index 100% rename from source/tests/test_deepmd_data_sys.py rename to source/tests/tf/test_deepmd_data_sys.py diff --git a/source/tests/test_deeppolar.py b/source/tests/tf/test_deeppolar.py similarity index 99% rename from source/tests/test_deeppolar.py rename to source/tests/tf/test_deeppolar.py index 18d9cb4ad9..cfa115c59f 100644 --- a/source/tests/test_deeppolar.py +++ b/source/tests/tf/test_deeppolar.py @@ -4,10 +4,6 @@ import ase.neighborlist import numpy as np -from common import ( - tests_path, - tf, -) from packaging.version import parse as parse_version from deepmd.tf.env import ( @@ -20,6 +16,11 @@ convert_pbtxt_to_pb, ) +from .common import ( + infer_path, + tf, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: @@ -30,7 +31,7 @@ class TestDeepPolarPBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppolar.pbtxt")), "deeppolar.pb" + str(infer_path / os.path.join("deeppolar.pbtxt")), "deeppolar.pb" ) cls.dp = DeepPolar("deeppolar.pb") @@ -121,7 +122,7 @@ class TestDeepPolarNoPBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppolar.pbtxt")), "deeppolar.pb" + str(infer_path / os.path.join("deeppolar.pbtxt")), "deeppolar.pb" ) cls.dp = DeepPolar("deeppolar.pb") @@ -207,7 +208,7 @@ class TestDeepPolarNewPBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppolar_new.pbtxt")), + str(infer_path / os.path.join("deeppolar_new.pbtxt")), "deeppolar_new.pb", ) cls.dp = DeepPolar("deeppolar_new.pb") @@ -1093,7 +1094,7 @@ class TestDeepPolarNewPBCNeighborList(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppolar_new.pbtxt")), + str(infer_path / os.path.join("deeppolar_new.pbtxt")), "deeppolar_new.pb", ) cls.dp = DeepPolar( diff --git a/source/tests/test_deeppot_a.py b/source/tests/tf/test_deeppot_a.py similarity index 97% rename from source/tests/test_deeppot_a.py rename to source/tests/tf/test_deeppot_a.py index 32e92cd8bd..af060aca1c 100644 --- a/source/tests/test_deeppot_a.py +++ b/source/tests/tf/test_deeppot_a.py @@ -6,10 +6,6 @@ import ase.neighborlist import dpdata import numpy as np -from common import ( - run_dp, - tests_path, -) from packaging.version import parse as parse_version from deepmd.tf.env import ( @@ -30,6 +26,12 @@ detect_model_version, ) +from .common import ( + infer_path, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: @@ -38,7 +40,7 @@ class TestModelMajorCompatability(unittest.TestCase): def setUp(self): - model_file = str(tests_path / os.path.join("infer", "deeppot.pbtxt")) + model_file = str(infer_path / os.path.join("deeppot.pbtxt")) with open(model_file) as fp: # data = fp.read().replace('\n', '') data = fp.read().split("\n") @@ -68,7 +70,7 @@ def test(self): class TestModelMinorCompatability(unittest.TestCase): def setUp(self): - model_file = str(tests_path / os.path.join("infer", "deeppot.pbtxt")) + model_file = str(infer_path / os.path.join("deeppot.pbtxt")) with open(model_file) as fp: # data = fp.read().replace('\n', '') data = fp.read().split("\n") @@ -100,7 +102,7 @@ class TestDeepPotAPBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppot.pbtxt")), "deeppot.pb" + str(infer_path / os.path.join("deeppot.pbtxt")), "deeppot.pb" ) cls.dp = DeepPot("deeppot.pb") @@ -278,7 +280,7 @@ def test_1frame_atm(self): def test_descriptor(self): descpt = self.dp.eval_descriptor(self.coords, self.box, self.atype) - expected_descpt = np.loadtxt(str(tests_path / "infer" / "deeppot_descpt.txt")) + expected_descpt = np.loadtxt(str(infer_path / "deeppot_descpt.txt")) np.testing.assert_almost_equal(descpt.ravel(), expected_descpt.ravel()) def test_2frame_atm(self): @@ -327,7 +329,7 @@ class TestDeepPotANoPBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppot.pbtxt")), "deeppot.pb" + str(infer_path / os.path.join("deeppot.pbtxt")), "deeppot.pb" ) cls.dp = DeepPot("deeppot.pb") @@ -550,7 +552,7 @@ class TestDeepPotALargeBoxNoPBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppot.pbtxt")), "deeppot.pb" + str(infer_path / os.path.join("deeppot.pbtxt")), "deeppot.pb" ) cls.dp = DeepPot("deeppot.pb") @@ -799,7 +801,7 @@ def setUp(self): def test_convert_012(self): old_model = "deeppot.pb" new_model = "deeppot-new.pb" - convert_pbtxt_to_pb(str(tests_path / "infer" / "sea_012.pbtxt"), old_model) + convert_pbtxt_to_pb(str(infer_path / "sea_012.pbtxt"), old_model) run_dp(f"dp convert-from 0.12 -i {old_model} -o {new_model}") dp = DeepPot(new_model) _, _, _, _, _ = dp.eval(self.coords, self.box, self.atype, atomic=True) @@ -809,7 +811,7 @@ def test_convert_012(self): def test_convert(self): old_model = "deeppot.pb" new_model = "deeppot-new.pb" - convert_pbtxt_to_pb(str(tests_path / "infer" / "sea_012.pbtxt"), old_model) + convert_pbtxt_to_pb(str(infer_path / "sea_012.pbtxt"), old_model) run_dp(f"dp convert-from -i {old_model} -o {new_model}") dp = DeepPot(new_model) _, _, _, _, _ = dp.eval(self.coords, self.box, self.atype, atomic=True) @@ -820,11 +822,11 @@ def test_detect(self): old_model = "deeppot.pb" new_model_txt = "deeppot_new.pbtxt" new_model_pb = "deeppot_new.pb" - convert_pbtxt_to_pb(str(tests_path / "infer" / "sea_012.pbtxt"), old_model) + convert_pbtxt_to_pb(str(infer_path / "sea_012.pbtxt"), old_model) version = detect_model_version(old_model) self.assertEqual(version, parse_version("0.12")) os.remove(old_model) - shutil.copyfile(str(tests_path / "infer" / "sea_012.pbtxt"), new_model_txt) + shutil.copyfile(str(infer_path / "sea_012.pbtxt"), new_model_txt) convert_dp012_to_dp10(new_model_txt) convert_pbtxt_to_pb(new_model_txt, new_model_pb) version = detect_model_version(new_model_pb) @@ -857,7 +859,7 @@ class TestTypeEmbed(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "se_e2_a_tebd.pbtxt")), + str(infer_path / os.path.join("se_e2_a_tebd.pbtxt")), "se_e2_a_tebd.pb", ) cls.dp = DeepPot("se_e2_a_tebd.pb") @@ -898,7 +900,7 @@ class TestFparamAparam(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "fparam_aparam.pbtxt")), + str(infer_path / os.path.join("fparam_aparam.pbtxt")), "fparam_aparam.pb", ) cls.dp = DeepPot("fparam_aparam.pb") @@ -1155,7 +1157,7 @@ class TestDeepPotAPBCNeighborList(TestDeepPotAPBC): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppot.pbtxt")), "deeppot.pb" + str(infer_path / os.path.join("deeppot.pbtxt")), "deeppot.pb" ) cls.dp = DeepPot( "deeppot.pb", diff --git a/source/tests/test_deeppot_r.py b/source/tests/tf/test_deeppot_r.py similarity index 98% rename from source/tests/test_deeppot_r.py rename to source/tests/tf/test_deeppot_r.py index 47f957d2cd..482a8c42ee 100644 --- a/source/tests/test_deeppot_r.py +++ b/source/tests/tf/test_deeppot_r.py @@ -3,9 +3,6 @@ import unittest import numpy as np -from common import ( - tests_path, -) from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, @@ -18,6 +15,10 @@ convert_pbtxt_to_pb, ) +from .common import ( + infer_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: @@ -28,7 +29,7 @@ class TestDeepPotRPBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppot-r.pbtxt")), "deeppot.pb" + str(infer_path / os.path.join("deeppot-r.pbtxt")), "deeppot.pb" ) cls.dp = DeepPot("deeppot.pb") @@ -239,7 +240,7 @@ class TestDeepPotRNoPBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppot-r.pbtxt")), "deeppot.pb" + str(infer_path / os.path.join("deeppot-r.pbtxt")), "deeppot.pb" ) cls.dp = DeepPot("deeppot.pb") @@ -453,7 +454,7 @@ class TestDeepPotRLargeBoxNoPBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppot-r.pbtxt")), "deeppot.pb" + str(infer_path / os.path.join("deeppot-r.pbtxt")), "deeppot.pb" ) cls.dp = DeepPot("deeppot.pb") diff --git a/source/tests/test_deeppot_spin.py b/source/tests/tf/test_deeppot_spin.py similarity index 98% rename from source/tests/test_deeppot_spin.py rename to source/tests/tf/test_deeppot_spin.py index b390fe6c79..d64cdf2dd6 100644 --- a/source/tests/test_deeppot_spin.py +++ b/source/tests/tf/test_deeppot_spin.py @@ -3,9 +3,6 @@ import unittest import numpy as np -from common import ( - tests_path, -) from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, @@ -17,6 +14,10 @@ convert_pbtxt_to_pb, ) +from .common import ( + infer_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: @@ -27,7 +28,7 @@ class TestDeepPotAPBC(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deepspin.pbtxt")), "deepspin.pb" + str(infer_path / os.path.join("deepspin.pbtxt")), "deepspin.pb" ) cls.dp = DeepPot("deepspin.pb") diff --git a/source/tests/test_descrpt_hybrid.py b/source/tests/tf/test_descrpt_hybrid.py similarity index 99% rename from source/tests/test_descrpt_hybrid.py rename to source/tests/tf/test_descrpt_hybrid.py index 08177f6a08..7c9d38cf7b 100644 --- a/source/tests/test_descrpt_hybrid.py +++ b/source/tests/tf/test_descrpt_hybrid.py @@ -2,11 +2,6 @@ import unittest import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from packaging.version import parse as parse_version from deepmd.tf.common import ( @@ -22,6 +17,12 @@ TypeEmbedNet, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_descrpt_nonsmth.py b/source/tests/tf/test_descrpt_nonsmth.py similarity index 99% rename from source/tests/test_descrpt_nonsmth.py rename to source/tests/tf/test_descrpt_nonsmth.py index 31f9da7ff2..63c5f15c85 100644 --- a/source/tests/test_descrpt_nonsmth.py +++ b/source/tests/tf/test_descrpt_nonsmth.py @@ -2,13 +2,6 @@ import unittest import numpy as np -from common import ( - Data, - force_dw_test, - force_test, - virial_dw_test, - virial_test, -) # load grad of force module from deepmd.tf.env import ( @@ -18,6 +11,14 @@ tf, ) +from .common import ( + Data, + force_dw_test, + force_test, + virial_dw_test, + virial_test, +) + class Inter: def setUp(self, data, comp=0, pbc=True, sess=None): diff --git a/source/tests/test_descrpt_se_a_mask.py b/source/tests/tf/test_descrpt_se_a_mask.py similarity index 94% rename from source/tests/test_descrpt_se_a_mask.py rename to source/tests/tf/test_descrpt_se_a_mask.py index b35bc75b04..b6488d88c6 100644 --- a/source/tests/test_descrpt_se_a_mask.py +++ b/source/tests/tf/test_descrpt_se_a_mask.py @@ -1,12 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import os -import pathlib import numpy as np -from common import ( - DataSystem, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -24,21 +19,26 @@ convert_pbtxt_to_pb, ) +from .common import ( + DataSystem, + infer_path, + j_loader, + tests_path, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 -tests_path = pathlib.Path(__file__).parent.absolute() - class TestModel(tf.test.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "dp4mask.pbtxt")), - str(tests_path / os.path.join("infer", "dp4mask.pb")), + str(infer_path / os.path.join("dp4mask.pbtxt")), + str(infer_path / os.path.join("dp4mask.pb")), ) - cls.dp = DeepPot(str(tests_path / os.path.join("infer", "dp4mask.pb"))) + cls.dp = DeepPot(str(infer_path / os.path.join("dp4mask.pb"))) def test_dp_mask_model(self): dcoord = np.array( @@ -225,6 +225,12 @@ def test_descriptor_se_a_mask(self): jfile = "zinc_se_a_mask.json" jdata = j_loader(jfile) + jdata["training"]["training_data"]["systems"] = [ + str(tests_path / "data_dp_mask") + ] + jdata["training"]["validation_data"]["systems"] = [ + str(tests_path / "data_dp_mask") + ] systems = j_must_have(jdata["training"]["validation_data"], "systems") # set_pfx = j_must_have(jdata['validation_data'], "set_prefix") set_pfx = "set" diff --git a/source/tests/test_descrpt_se_a_type.py b/source/tests/tf/test_descrpt_se_a_type.py similarity index 99% rename from source/tests/test_descrpt_se_a_type.py rename to source/tests/tf/test_descrpt_se_a_type.py index f5f294be35..43ed34dc92 100644 --- a/source/tests/test_descrpt_se_a_type.py +++ b/source/tests/tf/test_descrpt_se_a_type.py @@ -1,10 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -19,6 +14,12 @@ TypeEmbedNet, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_descrpt_se_atten.py b/source/tests/tf/test_descrpt_se_atten.py similarity index 99% rename from source/tests/test_descrpt_se_atten.py rename to source/tests/tf/test_descrpt_se_atten.py index d7e3c31f2c..d7ffc4bf8d 100644 --- a/source/tests/test_descrpt_se_atten.py +++ b/source/tests/tf/test_descrpt_se_atten.py @@ -3,11 +3,6 @@ import unittest import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from packaging.version import parse as parse_version from deepmd.tf.common import ( @@ -23,6 +18,12 @@ TypeEmbedNet, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_descrpt_se_r.py b/source/tests/tf/test_descrpt_se_r.py similarity index 99% rename from source/tests/test_descrpt_se_r.py rename to source/tests/tf/test_descrpt_se_r.py index 9e01bc83fd..d95c8fbb21 100644 --- a/source/tests/test_descrpt_se_r.py +++ b/source/tests/tf/test_descrpt_se_r.py @@ -2,13 +2,6 @@ import unittest import numpy as np -from common import ( - Data, - force_dw_test, - force_test, - virial_dw_test, - virial_test, -) # load grad of force module from deepmd.tf.env import ( @@ -18,6 +11,14 @@ tf, ) +from .common import ( + Data, + force_dw_test, + force_test, + virial_dw_test, + virial_test, +) + class Inter: def setUp(self, data, pbc=True, sess=None): diff --git a/source/tests/test_descrpt_sea_ef.py b/source/tests/tf/test_descrpt_sea_ef.py similarity index 99% rename from source/tests/test_descrpt_sea_ef.py rename to source/tests/tf/test_descrpt_sea_ef.py index 42f26da887..e9e990a659 100644 --- a/source/tests/test_descrpt_sea_ef.py +++ b/source/tests/tf/test_descrpt_sea_ef.py @@ -2,13 +2,6 @@ import unittest import numpy as np -from common import ( - Data, - force_dw_test, - force_test, - virial_dw_test, - virial_test, -) # load grad of force module from deepmd.tf.env import ( @@ -18,6 +11,14 @@ tf, ) +from .common import ( + Data, + force_dw_test, + force_test, + virial_dw_test, + virial_test, +) + class Inter: def setUp(self, data, pbc=True, sess=None): diff --git a/source/tests/test_descrpt_sea_ef_para.py b/source/tests/tf/test_descrpt_sea_ef_para.py similarity index 99% rename from source/tests/test_descrpt_sea_ef_para.py rename to source/tests/tf/test_descrpt_sea_ef_para.py index 16c92a5dc7..1af6ea648a 100644 --- a/source/tests/test_descrpt_sea_ef_para.py +++ b/source/tests/tf/test_descrpt_sea_ef_para.py @@ -2,13 +2,6 @@ import unittest import numpy as np -from common import ( - Data, - force_dw_test, - force_test, - virial_dw_test, - virial_test, -) # load grad of force module from deepmd.tf.env import ( @@ -18,6 +11,14 @@ tf, ) +from .common import ( + Data, + force_dw_test, + force_test, + virial_dw_test, + virial_test, +) + class Inter: def setUp(self, data, pbc=True, sess=None): diff --git a/source/tests/test_descrpt_sea_ef_rot.py b/source/tests/tf/test_descrpt_sea_ef_rot.py similarity index 100% rename from source/tests/test_descrpt_sea_ef_rot.py rename to source/tests/tf/test_descrpt_sea_ef_rot.py diff --git a/source/tests/test_descrpt_sea_ef_vert.py b/source/tests/tf/test_descrpt_sea_ef_vert.py similarity index 99% rename from source/tests/test_descrpt_sea_ef_vert.py rename to source/tests/tf/test_descrpt_sea_ef_vert.py index bc27a7c933..09bca9b754 100644 --- a/source/tests/test_descrpt_sea_ef_vert.py +++ b/source/tests/tf/test_descrpt_sea_ef_vert.py @@ -2,13 +2,6 @@ import unittest import numpy as np -from common import ( - Data, - force_dw_test, - force_test, - virial_dw_test, - virial_test, -) # load grad of force module from deepmd.tf.env import ( @@ -18,6 +11,14 @@ tf, ) +from .common import ( + Data, + force_dw_test, + force_test, + virial_dw_test, + virial_test, +) + class Inter: def setUp(self, data, pbc=True, sess=None): diff --git a/source/tests/test_descrpt_smooth.py b/source/tests/tf/test_descrpt_smooth.py similarity index 99% rename from source/tests/test_descrpt_smooth.py rename to source/tests/tf/test_descrpt_smooth.py index 206aca8d8a..91a3f7dbf0 100644 --- a/source/tests/test_descrpt_smooth.py +++ b/source/tests/tf/test_descrpt_smooth.py @@ -2,13 +2,6 @@ import unittest import numpy as np -from common import ( - Data, - force_dw_test, - force_test, - virial_dw_test, - virial_test, -) # load grad of force module from deepmd.tf.env import ( @@ -18,6 +11,14 @@ tf, ) +from .common import ( + Data, + force_dw_test, + force_test, + virial_dw_test, + virial_test, +) + class Inter: def setUp(self, data, pbc=True, sess=None): diff --git a/source/tests/test_dipole_se_a.py b/source/tests/tf/test_dipole_se_a.py similarity index 99% rename from source/tests/test_dipole_se_a.py rename to source/tests/tf/test_dipole_se_a.py index ca31ce87d5..f0e495ef21 100644 --- a/source/tests/test_dipole_se_a.py +++ b/source/tests/tf/test_dipole_se_a.py @@ -1,12 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - finite_difference, - gen_data, - j_loader, - strerch_box, -) from deepmd.tf.common import ( j_must_have, @@ -24,6 +17,14 @@ DipoleModel, ) +from .common import ( + DataSystem, + finite_difference, + gen_data, + j_loader, + strerch_box, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_dipole_se_a_tebd.py b/source/tests/tf/test_dipole_se_a_tebd.py similarity index 99% rename from source/tests/test_dipole_se_a_tebd.py rename to source/tests/tf/test_dipole_se_a_tebd.py index b211d0eb48..ed403bd047 100644 --- a/source/tests/test_dipole_se_a_tebd.py +++ b/source/tests/tf/test_dipole_se_a_tebd.py @@ -2,13 +2,6 @@ import unittest import numpy as np -from common import ( - DataSystem, - finite_difference, - gen_data, - j_loader, - strerch_box, -) from packaging.version import parse as parse_version from deepmd.tf.common import ( @@ -30,6 +23,14 @@ TypeEmbedNet, ) +from .common import ( + DataSystem, + finite_difference, + gen_data, + j_loader, + strerch_box, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_dipolecharge.py b/source/tests/tf/test_dipolecharge.py similarity index 98% rename from source/tests/test_dipolecharge.py rename to source/tests/tf/test_dipolecharge.py index e435eb431a..408b1bbdf2 100644 --- a/source/tests/test_dipolecharge.py +++ b/source/tests/tf/test_dipolecharge.py @@ -3,9 +3,6 @@ import unittest import numpy as np -from common import ( - tests_path, -) from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, @@ -17,6 +14,10 @@ convert_pbtxt_to_pb, ) +from .common import ( + infer_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: @@ -27,7 +28,7 @@ class TestDipoleCharge(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "dipolecharge_d.pbtxt")), + str(infer_path / os.path.join("dipolecharge_d.pbtxt")), "dipolecharge_d.pb", ) cls.dp = DipoleChargeModifier( diff --git a/source/tests/test_dp_test.py b/source/tests/tf/test_dp_test.py similarity index 97% rename from source/tests/test_dp_test.py rename to source/tests/tf/test_dp_test.py index 978cd95804..9a3dde3da0 100644 --- a/source/tests/test_dp_test.py +++ b/source/tests/tf/test_dp_test.py @@ -8,15 +8,16 @@ import dpdata import numpy as np -from common import ( - tests_path, -) from deepmd.tf.entrypoints.test import test as dp_test from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) +from .common import ( + infer_path, +) + default_places = 6 @@ -71,7 +72,7 @@ class TestDPTestEner(unittest.TestCase, TestDPTest): def setUpClass(cls): cls.model_name = "deeppot.pb" convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppot.pbtxt")), cls.model_name + str(infer_path / os.path.join("deeppot.pbtxt")), cls.model_name ) def setUp(self): @@ -207,7 +208,7 @@ class TestDPTestDipole(unittest.TestCase, TestDPTest): def setUpClass(cls): cls.model_name = "deepdipole.pb" convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deepdipole.pbtxt")), cls.model_name + str(infer_path / os.path.join("deepdipole.pbtxt")), cls.model_name ) def setUp(self): @@ -266,7 +267,7 @@ class TestDPTestPolar(unittest.TestCase, TestDPTest): def setUpClass(cls): cls.model_name = "deeppolar.pb" convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppolar.pbtxt")), cls.model_name + str(infer_path / os.path.join("deeppolar.pbtxt")), cls.model_name ) def setUp(self): diff --git a/source/tests/test_embedding_net.py b/source/tests/tf/test_embedding_net.py similarity index 100% rename from source/tests/test_embedding_net.py rename to source/tests/tf/test_embedding_net.py diff --git a/source/tests/test_env.py b/source/tests/tf/test_env.py similarity index 100% rename from source/tests/test_env.py rename to source/tests/tf/test_env.py diff --git a/source/tests/test_ewald.py b/source/tests/tf/test_ewald.py similarity index 100% rename from source/tests/test_ewald.py rename to source/tests/tf/test_ewald.py diff --git a/source/tests/test_finetune_se_atten.py b/source/tests/tf/test_finetune_se_atten.py similarity index 99% rename from source/tests/test_finetune_se_atten.py rename to source/tests/tf/test_finetune_se_atten.py index 47fedcf685..35eb994a46 100644 --- a/source/tests/test_finetune_se_atten.py +++ b/source/tests/tf/test_finetune_se_atten.py @@ -5,11 +5,6 @@ import unittest import numpy as np -from common import ( - j_loader, - run_dp, - tests_path, -) from packaging.version import parse as parse_version from deepmd.tf.env import ( @@ -32,6 +27,12 @@ get_tensor_by_name, ) +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_fitting_dos.py b/source/tests/tf/test_fitting_dos.py similarity index 99% rename from source/tests/test_fitting_dos.py rename to source/tests/tf/test_fitting_dos.py index 532bcfafbc..a2a54d6287 100644 --- a/source/tests/test_fitting_dos.py +++ b/source/tests/tf/test_fitting_dos.py @@ -1,10 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -19,6 +14,12 @@ DOSFitting, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_fitting_ener_type.py b/source/tests/tf/test_fitting_ener_type.py similarity index 99% rename from source/tests/test_fitting_ener_type.py rename to source/tests/tf/test_fitting_ener_type.py index 05a9d053ab..4dd6fb80a1 100644 --- a/source/tests/test_fitting_ener_type.py +++ b/source/tests/tf/test_fitting_ener_type.py @@ -1,10 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -19,6 +14,12 @@ EnerFitting, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_fitting_stat.py b/source/tests/tf/test_fitting_stat.py similarity index 99% rename from source/tests/test_fitting_stat.py rename to source/tests/tf/test_fitting_stat.py index 2b20dd5a4c..9e2408c57b 100644 --- a/source/tests/test_fitting_stat.py +++ b/source/tests/tf/test_fitting_stat.py @@ -5,9 +5,6 @@ ) import numpy as np -from common import ( - j_loader, -) from deepmd.tf.descriptor import ( DescrptSeA, @@ -16,6 +13,10 @@ EnerFitting, ) +from .common import ( + j_loader, +) + input_json = "water_se_a_afparam.json" diff --git a/source/tests/test_gen_stat_data.py b/source/tests/tf/test_gen_stat_data.py similarity index 100% rename from source/tests/test_gen_stat_data.py rename to source/tests/tf/test_gen_stat_data.py diff --git a/source/tests/test_get_potential.py b/source/tests/tf/test_get_potential.py similarity index 96% rename from source/tests/test_get_potential.py rename to source/tests/tf/test_get_potential.py index feb264afe0..47462a20a3 100644 --- a/source/tests/test_get_potential.py +++ b/source/tests/tf/test_get_potential.py @@ -2,9 +2,6 @@ """Test if `DeepPotential` facto function returns the right type of potential.""" import unittest -from pathlib import ( - Path, -) from deepmd.tf.infer import ( DeepDipole, @@ -16,10 +13,14 @@ convert_pbtxt_to_pb, ) +from .common import ( + infer_path, +) + class TestGetPotential(unittest.TestCase): def setUp(self): - self.work_dir = Path(__file__).parent / "infer" + self.work_dir = infer_path convert_pbtxt_to_pb( str(self.work_dir / "deeppot.pbtxt"), str(self.work_dir / "deep_pot.pb") diff --git a/source/tests/test_init_frz_model_multi.py b/source/tests/tf/test_init_frz_model_multi.py similarity index 99% rename from source/tests/test_init_frz_model_multi.py rename to source/tests/tf/test_init_frz_model_multi.py index fc37d82397..b723134ca1 100644 --- a/source/tests/test_init_frz_model_multi.py +++ b/source/tests/tf/test_init_frz_model_multi.py @@ -4,11 +4,6 @@ import unittest import numpy as np -from common import ( - j_loader, - run_dp, - tests_path, -) from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, @@ -33,6 +28,12 @@ replace_model_params_with_frz_multi_model, ) +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_init_frz_model_se_a.py b/source/tests/tf/test_init_frz_model_se_a.py similarity index 99% rename from source/tests/test_init_frz_model_se_a.py rename to source/tests/tf/test_init_frz_model_se_a.py index 7545e3aae9..5d4ed1063c 100644 --- a/source/tests/test_init_frz_model_se_a.py +++ b/source/tests/tf/test_init_frz_model_se_a.py @@ -4,11 +4,6 @@ import unittest import numpy as np -from common import ( - j_loader, - run_dp, - tests_path, -) from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, @@ -30,6 +25,12 @@ DeepmdDataSystem, ) +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_init_frz_model_se_a_tebd.py b/source/tests/tf/test_init_frz_model_se_a_tebd.py similarity index 99% rename from source/tests/test_init_frz_model_se_a_tebd.py rename to source/tests/tf/test_init_frz_model_se_a_tebd.py index 1b282c00d5..afc1e46ed8 100644 --- a/source/tests/test_init_frz_model_se_a_tebd.py +++ b/source/tests/tf/test_init_frz_model_se_a_tebd.py @@ -4,11 +4,6 @@ import unittest import numpy as np -from common import ( - j_loader, - run_dp, - tests_path, -) from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, @@ -30,6 +25,12 @@ DeepmdDataSystem, ) +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_init_frz_model_se_a_type.py b/source/tests/tf/test_init_frz_model_se_a_type.py similarity index 99% rename from source/tests/test_init_frz_model_se_a_type.py rename to source/tests/tf/test_init_frz_model_se_a_type.py index b356dbf6d0..48ff4eb294 100644 --- a/source/tests/test_init_frz_model_se_a_type.py +++ b/source/tests/tf/test_init_frz_model_se_a_type.py @@ -4,11 +4,6 @@ import unittest import numpy as np -from common import ( - j_loader, - run_dp, - tests_path, -) from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, @@ -30,6 +25,12 @@ DeepmdDataSystem, ) +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_init_frz_model_se_atten.py b/source/tests/tf/test_init_frz_model_se_atten.py similarity index 99% rename from source/tests/test_init_frz_model_se_atten.py rename to source/tests/tf/test_init_frz_model_se_atten.py index 7889440cd3..a114deffc8 100644 --- a/source/tests/test_init_frz_model_se_atten.py +++ b/source/tests/tf/test_init_frz_model_se_atten.py @@ -4,11 +4,6 @@ import unittest import numpy as np -from common import ( - j_loader, - run_dp, - tests_path, -) from packaging.version import parse as parse_version from deepmd.tf.env import ( @@ -31,6 +26,12 @@ DeepmdDataSystem, ) +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_init_frz_model_se_r.py b/source/tests/tf/test_init_frz_model_se_r.py similarity index 99% rename from source/tests/test_init_frz_model_se_r.py rename to source/tests/tf/test_init_frz_model_se_r.py index fd916b3fdc..100c09196e 100644 --- a/source/tests/test_init_frz_model_se_r.py +++ b/source/tests/tf/test_init_frz_model_se_r.py @@ -4,11 +4,6 @@ import unittest import numpy as np -from common import ( - j_loader, - run_dp, - tests_path, -) from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, @@ -30,6 +25,12 @@ DeepmdDataSystem, ) +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_init_frz_model_spin.py b/source/tests/tf/test_init_frz_model_spin.py similarity index 99% rename from source/tests/test_init_frz_model_spin.py rename to source/tests/tf/test_init_frz_model_spin.py index b5c480c2ba..c2c433cde0 100644 --- a/source/tests/test_init_frz_model_spin.py +++ b/source/tests/tf/test_init_frz_model_spin.py @@ -4,11 +4,6 @@ import unittest import numpy as np -from common import ( - j_loader, - run_dp, - tests_path, -) from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, @@ -30,6 +25,12 @@ DeepmdDataSystem, ) +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_lammps.py b/source/tests/tf/test_lammps.py similarity index 87% rename from source/tests/test_lammps.py rename to source/tests/tf/test_lammps.py index d235d6576e..b295d5212a 100644 --- a/source/tests/test_lammps.py +++ b/source/tests/tf/test_lammps.py @@ -2,14 +2,15 @@ import os import subprocess import unittest -from pathlib import ( - Path, -) from deepmd.tf.utils.convert import ( convert_pbtxt_to_pb, ) +from .common import ( + infer_path, +) + @unittest.skipIf( os.environ.get("CIBUILDWHEEL", "0") != "1", @@ -20,7 +21,7 @@ class TestLAMMPS(unittest.TestCase): @classmethod def setUpClass(cls): - cls.work_dir = (Path(__file__).parent / "infer").absolute() + cls.work_dir = infer_path convert_pbtxt_to_pb( str(cls.work_dir / "deeppot.pbtxt"), str(cls.work_dir / "deep_pot.pb") diff --git a/source/tests/test_layer_name.py b/source/tests/tf/test_layer_name.py similarity index 99% rename from source/tests/test_layer_name.py rename to source/tests/tf/test_layer_name.py index 71229b5ce7..8c2264315f 100644 --- a/source/tests/test_layer_name.py +++ b/source/tests/tf/test_layer_name.py @@ -1,11 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - del_data, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -24,6 +18,13 @@ MultiModel, ) +from .common import ( + DataSystem, + del_data, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_linear_model.py b/source/tests/tf/test_linear_model.py similarity index 94% rename from source/tests/test_linear_model.py rename to source/tests/tf/test_linear_model.py index ef0d324a69..95ece9c19f 100644 --- a/source/tests/test_linear_model.py +++ b/source/tests/tf/test_linear_model.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import os -import sys import numpy as np @@ -19,12 +18,11 @@ convert_pbtxt_to_pb, ) -sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) -from common import ( +from .common import ( DataSystem, del_data, gen_data, - tests_path, + infer_path, ) @@ -35,8 +33,8 @@ def setUp(self): with open(os.path.join(self.data_dir, "type_map.raw"), "w") as f: f.write("O\nH") self.pbtxts = [ - os.path.join(tests_path, "infer/deeppot.pbtxt"), - os.path.join(tests_path, "infer/deeppot-1.pbtxt"), + os.path.join(infer_path, "deeppot.pbtxt"), + os.path.join(infer_path, "deeppot-1.pbtxt"), ] self.graph_dirs = [pbtxt.replace("pbtxt", "pb") for pbtxt in self.pbtxts] for pbtxt, pb in zip(self.pbtxts, self.graph_dirs): diff --git a/source/tests/test_loss_gf.py b/source/tests/tf/test_loss_gf.py similarity index 100% rename from source/tests/test_loss_gf.py rename to source/tests/tf/test_loss_gf.py diff --git a/source/tests/test_mixed_prec_training.py b/source/tests/tf/test_mixed_prec_training.py similarity index 99% rename from source/tests/test_mixed_prec_training.py rename to source/tests/tf/test_mixed_prec_training.py index 620a9b9fd0..63504134af 100644 --- a/source/tests/test_mixed_prec_training.py +++ b/source/tests/tf/test_mixed_prec_training.py @@ -5,13 +5,6 @@ import unittest import numpy as np - -# from deepmd.tf.entrypoints.compress import compress -from common import ( - j_loader, - run_dp, - tests_path, -) from packaging.version import ( Version, ) @@ -20,6 +13,13 @@ TF_VERSION, ) +# from deepmd.tf.entrypoints.compress import compress +from .common import ( + j_loader, + run_dp, + tests_path, +) + def _file_delete(file): if os.path.isdir(file): diff --git a/source/tests/test_model_compression_se_a.py b/source/tests/tf/test_model_compression_se_a.py similarity index 99% rename from source/tests/test_model_compression_se_a.py rename to source/tests/tf/test_model_compression_se_a.py index 0a6b7c85cb..37d1857661 100644 --- a/source/tests/test_model_compression_se_a.py +++ b/source/tests/tf/test_model_compression_se_a.py @@ -6,13 +6,6 @@ import numpy as np -# from deepmd.tf.entrypoints.compress import compress -from common import ( - j_loader, - run_dp, - tests_path, -) - from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) @@ -20,6 +13,13 @@ DeepPot, ) +# from deepmd.tf.entrypoints.compress import compress +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_model_compression_se_a_ebd.py b/source/tests/tf/test_model_compression_se_a_ebd.py similarity index 99% rename from source/tests/test_model_compression_se_a_ebd.py rename to source/tests/tf/test_model_compression_se_a_ebd.py index 8b64117acd..1ab0cfe5cc 100644 --- a/source/tests/test_model_compression_se_a_ebd.py +++ b/source/tests/tf/test_model_compression_se_a_ebd.py @@ -6,13 +6,6 @@ import numpy as np -# from deepmd.tf.entrypoints.compress import compress -from common import ( - j_loader, - run_dp, - tests_path, -) - from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) @@ -20,6 +13,13 @@ DeepPot, ) +# from deepmd.tf.entrypoints.compress import compress +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_model_compression_se_a_ebd_type_one_side.py b/source/tests/tf/test_model_compression_se_a_ebd_type_one_side.py similarity index 99% rename from source/tests/test_model_compression_se_a_ebd_type_one_side.py rename to source/tests/tf/test_model_compression_se_a_ebd_type_one_side.py index 741c95b26e..5ae8ef4990 100644 --- a/source/tests/test_model_compression_se_a_ebd_type_one_side.py +++ b/source/tests/tf/test_model_compression_se_a_ebd_type_one_side.py @@ -6,13 +6,6 @@ import numpy as np -# from deepmd.tf.entrypoints.compress import compress -from common import ( - j_loader, - run_dp, - tests_path, -) - from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) @@ -20,6 +13,13 @@ DeepPot, ) +# from deepmd.tf.entrypoints.compress import compress +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_model_compression_se_a_type_one_side_exclude_types.py b/source/tests/tf/test_model_compression_se_a_type_one_side_exclude_types.py similarity index 99% rename from source/tests/test_model_compression_se_a_type_one_side_exclude_types.py rename to source/tests/tf/test_model_compression_se_a_type_one_side_exclude_types.py index bdf09cf3e8..3726fc2bda 100644 --- a/source/tests/test_model_compression_se_a_type_one_side_exclude_types.py +++ b/source/tests/tf/test_model_compression_se_a_type_one_side_exclude_types.py @@ -6,13 +6,6 @@ import numpy as np -# from deepmd.tf.entrypoints.compress import compress -from common import ( - j_loader, - run_dp, - tests_path, -) - from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) @@ -20,6 +13,13 @@ DeepPot, ) +# from deepmd.tf.entrypoints.compress import compress +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_model_compression_se_atten.py b/source/tests/tf/test_model_compression_se_atten.py similarity index 99% rename from source/tests/test_model_compression_se_atten.py rename to source/tests/tf/test_model_compression_se_atten.py index 2e250fe80e..dbc54dd51a 100644 --- a/source/tests/test_model_compression_se_atten.py +++ b/source/tests/tf/test_model_compression_se_atten.py @@ -5,13 +5,6 @@ import unittest import numpy as np - -# from deepmd.tf.entrypoints.compress import compress -from common import ( - j_loader, - run_dp, - tests_path, -) from packaging.version import parse as parse_version from deepmd.tf.env import ( @@ -21,6 +14,13 @@ DeepPot, ) +# from deepmd.tf.entrypoints.compress import compress +from .common import ( + j_loader, + run_dp, + tests_path, +) + def _file_delete(file): if os.path.isdir(file): diff --git a/source/tests/test_model_compression_se_r.py b/source/tests/tf/test_model_compression_se_r.py similarity index 99% rename from source/tests/test_model_compression_se_r.py rename to source/tests/tf/test_model_compression_se_r.py index 0c5912164f..4a5d9ad9f6 100644 --- a/source/tests/test_model_compression_se_r.py +++ b/source/tests/tf/test_model_compression_se_r.py @@ -6,13 +6,6 @@ import numpy as np -# from deepmd.tf.entrypoints.compress import compress -from common import ( - j_loader, - run_dp, - tests_path, -) - from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) @@ -20,6 +13,13 @@ DeepPot, ) +# from deepmd.tf.entrypoints.compress import compress +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_model_compression_se_t.py b/source/tests/tf/test_model_compression_se_t.py similarity index 99% rename from source/tests/test_model_compression_se_t.py rename to source/tests/tf/test_model_compression_se_t.py index eb33ce2b93..0cf1135f8a 100644 --- a/source/tests/test_model_compression_se_t.py +++ b/source/tests/tf/test_model_compression_se_t.py @@ -6,13 +6,6 @@ import numpy as np -# from deepmd.tf.entrypoints.compress import compress -from common import ( - j_loader, - run_dp, - tests_path, -) - from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, ) @@ -20,6 +13,13 @@ DeepPot, ) +# from deepmd.tf.entrypoints.compress import compress +from .common import ( + j_loader, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_model_devi.py b/source/tests/tf/test_model_devi.py similarity index 97% rename from source/tests/test_model_devi.py rename to source/tests/tf/test_model_devi.py index 21275ee2d1..58a6266ca9 100644 --- a/source/tests/test_model_devi.py +++ b/source/tests/tf/test_model_devi.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import os -import sys import unittest import numpy as np @@ -12,18 +11,17 @@ from deepmd.tf.infer.model_devi import ( make_model_devi, ) +from deepmd.tf.utils.convert import ( + convert_pbtxt_to_pb, +) -sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) -from common import ( +from .common import ( del_data, gen_data, + infer_path, tests_path, ) -from deepmd.tf.utils.convert import ( - convert_pbtxt_to_pb, -) - class TestMakeModelDevi(unittest.TestCase): def setUp(self): @@ -39,8 +37,8 @@ def setUp(self): self.freq = 10 self.pbtxts = [ - os.path.join(tests_path, "infer/deeppot.pbtxt"), - os.path.join(tests_path, "infer/deeppot-1.pbtxt"), + os.path.join(infer_path, "deeppot.pbtxt"), + os.path.join(infer_path, "deeppot-1.pbtxt"), ] self.graph_dirs = [pbtxt.replace("pbtxt", "pb") for pbtxt in self.pbtxts] for pbtxt, pb in zip(self.pbtxts, self.graph_dirs): @@ -215,7 +213,7 @@ class TestMakeModelDeviFparamAparam(unittest.TestCase): @classmethod def setUpClass(cls): cls.pbtxts = [ - os.path.join(tests_path, "infer/fparam_aparam.pbtxt"), + os.path.join(infer_path, "fparam_aparam.pbtxt"), ] cls.graph_dirs = [pbtxt.replace("pbtxt", "pb") for pbtxt in cls.pbtxts] for pbtxt, pb in zip(cls.pbtxts, cls.graph_dirs): diff --git a/source/tests/test_model_devi_mix.py b/source/tests/tf/test_model_devi_mix.py similarity index 94% rename from source/tests/test_model_devi_mix.py rename to source/tests/tf/test_model_devi_mix.py index 5715b49165..d9062e939a 100644 --- a/source/tests/test_model_devi_mix.py +++ b/source/tests/tf/test_model_devi_mix.py @@ -1,10 +1,13 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import os -import sys import unittest import numpy as np +from packaging.version import parse as parse_version +from deepmd.tf.env import ( + tf, +) from deepmd.tf.infer import ( DeepPotential, calc_model_devi, @@ -12,21 +15,16 @@ from deepmd.tf.infer.model_devi import ( make_model_devi, ) +from deepmd.tf.utils.convert import ( + convert_pbtxt_to_pb, +) -sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) -from common import ( +from .common import ( del_data, gen_data, + infer_path, tests_path, ) -from packaging.version import parse as parse_version - -from deepmd.tf.env import ( - tf, -) -from deepmd.tf.utils.convert import ( - convert_pbtxt_to_pb, -) @unittest.skipIf( @@ -56,8 +54,8 @@ def setUp(self): ) self.pbtxts = [ - os.path.join(tests_path, "infer/se_atten_no_atten_1.pbtxt"), - os.path.join(tests_path, "infer/se_atten_no_atten_2.pbtxt"), + os.path.join(infer_path, "se_atten_no_atten_1.pbtxt"), + os.path.join(infer_path, "se_atten_no_atten_2.pbtxt"), ] self.graph_dirs = [pbtxt.replace("pbtxt", "pb") for pbtxt in self.pbtxts] for pbtxt, pb in zip(self.pbtxts, self.graph_dirs): diff --git a/source/tests/test_model_dos.py b/source/tests/tf/test_model_dos.py similarity index 99% rename from source/tests/test_model_dos.py rename to source/tests/tf/test_model_dos.py index 72cd9db524..d88c81c332 100644 --- a/source/tests/test_model_dos.py +++ b/source/tests/tf/test_model_dos.py @@ -1,11 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - del_data, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -23,6 +17,13 @@ DOSModel, ) +from .common import ( + DataSystem, + del_data, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_loc_frame.py b/source/tests/tf/test_model_loc_frame.py similarity index 99% rename from source/tests/test_model_loc_frame.py rename to source/tests/tf/test_model_loc_frame.py index 035ffc868e..f97e349145 100644 --- a/source/tests/test_model_loc_frame.py +++ b/source/tests/tf/test_model_loc_frame.py @@ -1,10 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -22,6 +17,12 @@ EnerModel, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_multi.py b/source/tests/tf/test_model_multi.py similarity index 99% rename from source/tests/test_model_multi.py rename to source/tests/tf/test_model_multi.py index fa75951366..c526b479a6 100644 --- a/source/tests/test_model_multi.py +++ b/source/tests/tf/test_model_multi.py @@ -1,13 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - del_data, - finite_difference, - gen_data, - j_loader, - strerch_box, -) from deepmd.tf.common import ( j_must_have, @@ -26,6 +18,15 @@ MultiModel, ) +from .common import ( + DataSystem, + del_data, + finite_difference, + gen_data, + j_loader, + strerch_box, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_pairtab.py b/source/tests/tf/test_model_pairtab.py similarity index 99% rename from source/tests/test_model_pairtab.py rename to source/tests/tf/test_model_pairtab.py index 8a7ebd605c..e2c45ee50c 100644 --- a/source/tests/test_model_pairtab.py +++ b/source/tests/tf/test_model_pairtab.py @@ -1,11 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np import scipy.spatial.distance -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -17,6 +12,12 @@ Model, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_se_a.py b/source/tests/tf/test_model_se_a.py similarity index 99% rename from source/tests/test_model_se_a.py rename to source/tests/tf/test_model_se_a.py index f537452385..57a8f4af52 100644 --- a/source/tests/test_model_se_a.py +++ b/source/tests/tf/test_model_se_a.py @@ -1,12 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import dpdata import numpy as np -from common import ( - DataSystem, - del_data, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -27,6 +21,13 @@ TypeEmbedNet, ) +from .common import ( + DataSystem, + del_data, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_se_a_aparam.py b/source/tests/tf/test_model_se_a_aparam.py similarity index 99% rename from source/tests/test_model_se_a_aparam.py rename to source/tests/tf/test_model_se_a_aparam.py index aca2d8c63c..6b37dfd459 100644 --- a/source/tests/test_model_se_a_aparam.py +++ b/source/tests/tf/test_model_se_a_aparam.py @@ -1,10 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -22,6 +17,12 @@ EnerModel, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_se_a_ebd.py b/source/tests/tf/test_model_se_a_ebd.py similarity index 99% rename from source/tests/test_model_se_a_ebd.py rename to source/tests/tf/test_model_se_a_ebd.py index 2e133a9a63..b819c2ddc9 100644 --- a/source/tests/test_model_se_a_ebd.py +++ b/source/tests/tf/test_model_se_a_ebd.py @@ -1,10 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -22,6 +17,12 @@ EnerModel, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_se_a_ebd_v2.py b/source/tests/tf/test_model_se_a_ebd_v2.py similarity index 99% rename from source/tests/test_model_se_a_ebd_v2.py rename to source/tests/tf/test_model_se_a_ebd_v2.py index f302308a73..0cc89f5151 100644 --- a/source/tests/test_model_se_a_ebd_v2.py +++ b/source/tests/tf/test_model_se_a_ebd_v2.py @@ -1,10 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -25,6 +20,12 @@ TypeEmbedNet, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_se_a_fparam.py b/source/tests/tf/test_model_se_a_fparam.py similarity index 99% rename from source/tests/test_model_se_a_fparam.py rename to source/tests/tf/test_model_se_a_fparam.py index 46aac18fcb..806ae13582 100644 --- a/source/tests/test_model_se_a_fparam.py +++ b/source/tests/tf/test_model_se_a_fparam.py @@ -1,10 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -22,6 +17,12 @@ EnerModel, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_se_a_srtab.py b/source/tests/tf/test_model_se_a_srtab.py similarity index 99% rename from source/tests/test_model_se_a_srtab.py rename to source/tests/tf/test_model_se_a_srtab.py index 3fcb55050d..3a93349741 100644 --- a/source/tests/test_model_se_a_srtab.py +++ b/source/tests/tf/test_model_se_a_srtab.py @@ -2,11 +2,6 @@ import os import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -24,6 +19,12 @@ EnerModel, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_se_a_type.py b/source/tests/tf/test_model_se_a_type.py similarity index 99% rename from source/tests/test_model_se_a_type.py rename to source/tests/tf/test_model_se_a_type.py index bc2a2c3045..4b19378cf6 100644 --- a/source/tests/test_model_se_a_type.py +++ b/source/tests/tf/test_model_se_a_type.py @@ -1,10 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -25,6 +20,12 @@ TypeEmbedNet, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_se_atten.py b/source/tests/tf/test_model_se_atten.py similarity index 99% rename from source/tests/test_model_se_atten.py rename to source/tests/tf/test_model_se_atten.py index 592858db2a..ad6926e0da 100644 --- a/source/tests/test_model_se_atten.py +++ b/source/tests/tf/test_model_se_atten.py @@ -3,13 +3,6 @@ import unittest import numpy as np -from common import ( - DataSystem, - check_smooth_efv, - finite_difference_fv, - gen_data, - j_loader, -) from packaging.version import parse as parse_version from deepmd.tf.common import ( @@ -31,6 +24,14 @@ TypeEmbedNet, ) +from .common import ( + DataSystem, + check_smooth_efv, + finite_difference_fv, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_se_r.py b/source/tests/tf/test_model_se_r.py similarity index 99% rename from source/tests/test_model_se_r.py rename to source/tests/tf/test_model_se_r.py index acfe6e95dd..a635e6c3c4 100644 --- a/source/tests/test_model_se_r.py +++ b/source/tests/tf/test_model_se_r.py @@ -1,10 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -22,6 +17,12 @@ EnerModel, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_se_t.py b/source/tests/tf/test_model_se_t.py similarity index 99% rename from source/tests/test_model_se_t.py rename to source/tests/tf/test_model_se_t.py index cb8ed97833..881a0e06c4 100644 --- a/source/tests/test_model_se_t.py +++ b/source/tests/tf/test_model_se_t.py @@ -1,10 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -22,6 +17,12 @@ EnerModel, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_model_spin.json b/source/tests/tf/test_model_spin.json similarity index 100% rename from source/tests/test_model_spin.json rename to source/tests/tf/test_model_spin.json diff --git a/source/tests/test_model_spin.py b/source/tests/tf/test_model_spin.py similarity index 99% rename from source/tests/test_model_spin.py rename to source/tests/tf/test_model_spin.py index d1a6f59fe1..26100c19d0 100644 --- a/source/tests/test_model_spin.py +++ b/source/tests/tf/test_model_spin.py @@ -2,13 +2,6 @@ import unittest import numpy as np -from common import ( - DataSystem, - del_data, - gen_data, - j_loader, - tests_path, -) from deepmd.tf.common import ( j_must_have, @@ -29,6 +22,14 @@ Spin, ) +from .common import ( + DataSystem, + del_data, + gen_data, + j_loader, + tests_path, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 @@ -46,7 +47,6 @@ def test_model_spin(self): jdata = j_loader(jfile) # set system information - systems = j_must_have(jdata["training"]["training_data"], "systems") set_pfx = j_must_have(jdata["training"], "set_prefix") batch_size = j_must_have(jdata["training"]["training_data"], "batch_size") batch_size = 2 @@ -59,6 +59,7 @@ def test_model_spin(self): jdata["training"]["validation_data"]["systems"] = [ str(tests_path / "model_spin/") ] + systems = j_must_have(jdata["training"]["training_data"], "systems") data = DataSystem(systems, set_pfx, batch_size, test_size, rcut, run_opt=None) test_data = data.get_test() diff --git a/source/tests/test_neighbor_stat.py b/source/tests/tf/test_neighbor_stat.py similarity index 100% rename from source/tests/test_neighbor_stat.py rename to source/tests/tf/test_neighbor_stat.py diff --git a/source/tests/test_nvnmd_entrypoints.py b/source/tests/tf/test_nvnmd_entrypoints.py similarity index 99% rename from source/tests/test_nvnmd_entrypoints.py rename to source/tests/tf/test_nvnmd_entrypoints.py index b257f8fffa..cc7a92c032 100644 --- a/source/tests/test_nvnmd_entrypoints.py +++ b/source/tests/tf/test_nvnmd_entrypoints.py @@ -3,9 +3,6 @@ import numpy as np import pytest -from common import ( - tests_path, -) from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, @@ -44,6 +41,10 @@ update_deepmd_input, ) +from .common import ( + tests_path, +) + class TestNvnmdEntrypointsV0(tf.test.TestCase): @pytest.mark.run(order=0) diff --git a/source/tests/test_nvnmd_op.py b/source/tests/tf/test_nvnmd_op.py similarity index 100% rename from source/tests/test_nvnmd_op.py rename to source/tests/tf/test_nvnmd_op.py diff --git a/source/tests/test_pairwise_dprc.py b/source/tests/tf/test_pairwise_dprc.py similarity index 99% rename from source/tests/test_pairwise_dprc.py rename to source/tests/tf/test_pairwise_dprc.py index a38c856c26..afe6885542 100644 --- a/source/tests/test_pairwise_dprc.py +++ b/source/tests/tf/test_pairwise_dprc.py @@ -5,10 +5,6 @@ import dpdata import numpy as np -from common import ( - run_dp, - tests_path, -) from packaging.version import parse as parse_version from deepmd.tf import ( @@ -38,6 +34,11 @@ run_sess, ) +from .common import ( + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: diff --git a/source/tests/test_parallel_training.py b/source/tests/tf/test_parallel_training.py similarity index 98% rename from source/tests/test_parallel_training.py rename to source/tests/tf/test_parallel_training.py index 85311cf558..1f93c809a2 100644 --- a/source/tests/test_parallel_training.py +++ b/source/tests/tf/test_parallel_training.py @@ -3,14 +3,14 @@ import subprocess as sp import unittest -from common import ( - tests_path, -) - from deepmd.tf.cluster.local import ( get_gpus, ) +from .common import ( + tests_path, +) + class TestSingleMachine(unittest.TestCase): def setUp(self): diff --git a/source/tests/test_polar_se_a.py b/source/tests/tf/test_polar_se_a.py similarity index 99% rename from source/tests/test_polar_se_a.py rename to source/tests/tf/test_polar_se_a.py index 39c34f0a01..04487dec7b 100644 --- a/source/tests/test_polar_se_a.py +++ b/source/tests/tf/test_polar_se_a.py @@ -1,12 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - finite_difference, - gen_data, - j_loader, - strerch_box, -) from deepmd.tf.common import ( j_must_have, @@ -24,6 +17,14 @@ PolarModel, ) +from .common import ( + DataSystem, + finite_difference, + gen_data, + j_loader, + strerch_box, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_polar_se_a_tebd.py b/source/tests/tf/test_polar_se_a_tebd.py similarity index 99% rename from source/tests/test_polar_se_a_tebd.py rename to source/tests/tf/test_polar_se_a_tebd.py index 1c82488dca..38c3ae20ef 100644 --- a/source/tests/test_polar_se_a_tebd.py +++ b/source/tests/tf/test_polar_se_a_tebd.py @@ -2,13 +2,6 @@ import unittest import numpy as np -from common import ( - DataSystem, - finite_difference, - gen_data, - j_loader, - strerch_box, -) from packaging.version import parse as parse_version from deepmd.tf.common import ( @@ -30,6 +23,14 @@ TypeEmbedNet, ) +from .common import ( + DataSystem, + finite_difference, + gen_data, + j_loader, + strerch_box, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_prod_env_mat.py b/source/tests/tf/test_prod_env_mat.py similarity index 100% rename from source/tests/test_prod_env_mat.py rename to source/tests/tf/test_prod_env_mat.py diff --git a/source/tests/test_prod_force.py b/source/tests/tf/test_prod_force.py similarity index 100% rename from source/tests/test_prod_force.py rename to source/tests/tf/test_prod_force.py diff --git a/source/tests/test_prod_force_grad.py b/source/tests/tf/test_prod_force_grad.py similarity index 100% rename from source/tests/test_prod_force_grad.py rename to source/tests/tf/test_prod_force_grad.py diff --git a/source/tests/test_prod_virial.py b/source/tests/tf/test_prod_virial.py similarity index 100% rename from source/tests/test_prod_virial.py rename to source/tests/tf/test_prod_virial.py diff --git a/source/tests/test_prod_virial_grad.py b/source/tests/tf/test_prod_virial_grad.py similarity index 100% rename from source/tests/test_prod_virial_grad.py rename to source/tests/tf/test_prod_virial_grad.py diff --git a/source/tests/test_tab_nonsmth.py b/source/tests/tf/test_tab_nonsmth.py similarity index 98% rename from source/tests/test_tab_nonsmth.py rename to source/tests/tf/test_tab_nonsmth.py index 6b09b98428..7132d0c206 100644 --- a/source/tests/test_tab_nonsmth.py +++ b/source/tests/tf/test_tab_nonsmth.py @@ -3,16 +3,6 @@ import unittest import numpy as np -from common import ( - Data, - force_dw_test, - force_test, - virial_dw_test, - virial_test, -) -from test_descrpt_nonsmth import ( - Inter, -) # load grad of force module import deepmd.tf.op # noqa: F401 @@ -24,6 +14,17 @@ PairTab, ) +from .common import ( + Data, + force_dw_test, + force_test, + virial_dw_test, + virial_test, +) +from .test_descrpt_nonsmth import ( + Inter, +) + def _make_tab(ntype): xx = np.arange(0, 9, 0.001) diff --git a/source/tests/test_tab_smooth.py b/source/tests/tf/test_tab_smooth.py similarity index 98% rename from source/tests/test_tab_smooth.py rename to source/tests/tf/test_tab_smooth.py index f823b366c8..e0cf564cd6 100644 --- a/source/tests/test_tab_smooth.py +++ b/source/tests/tf/test_tab_smooth.py @@ -3,16 +3,6 @@ import unittest import numpy as np -from common import ( - Data, - force_dw_test, - force_test, - virial_dw_test, - virial_test, -) -from test_descrpt_smooth import ( - Inter, -) # load grad of force module from deepmd.tf.env import ( @@ -23,6 +13,17 @@ PairTab, ) +from .common import ( + Data, + force_dw_test, + force_test, + virial_dw_test, + virial_test, +) +from .test_descrpt_smooth import ( + Inter, +) + def _make_tab(ntype): xx = np.arange(0, 9, 0.001) diff --git a/source/tests/test_tabulate.py b/source/tests/tf/test_tabulate.py similarity index 100% rename from source/tests/test_tabulate.py rename to source/tests/tf/test_tabulate.py diff --git a/source/tests/test_train.py b/source/tests/tf/test_train.py similarity index 100% rename from source/tests/test_train.py rename to source/tests/tf/test_train.py diff --git a/source/tests/test_transfer.py b/source/tests/tf/test_transfer.py similarity index 98% rename from source/tests/test_transfer.py rename to source/tests/tf/test_transfer.py index f73c9eef66..e5b7f0a906 100644 --- a/source/tests/test_transfer.py +++ b/source/tests/tf/test_transfer.py @@ -4,10 +4,6 @@ import unittest import numpy as np -from common import ( - run_dp, - tests_path, -) from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, @@ -19,6 +15,12 @@ convert_pbtxt_to_pb, ) +from .common import ( + infer_path, + run_dp, + tests_path, +) + if GLOBAL_NP_FLOAT_PRECISION == np.float32: default_places = 4 else: @@ -48,10 +50,10 @@ def setUpClass(self): self.raw_model = str(tests_path / "dp-raw.pb") self.new_model = str(tests_path / "dp-new.pb") convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppot.pbtxt")), self.old_model + str(infer_path / os.path.join("deeppot.pbtxt")), self.old_model ) convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "deeppot-1.pbtxt")), self.raw_model + str(infer_path / os.path.join("deeppot-1.pbtxt")), self.raw_model ) ret = run_dp( "dp transfer -O " diff --git a/source/tests/test_type_embed.py b/source/tests/tf/test_type_embed.py similarity index 100% rename from source/tests/test_type_embed.py rename to source/tests/tf/test_type_embed.py diff --git a/source/tests/test_type_one_side.py b/source/tests/tf/test_type_one_side.py similarity index 99% rename from source/tests/test_type_one_side.py rename to source/tests/tf/test_type_one_side.py index d1c02981e7..5c71a41739 100644 --- a/source/tests/test_type_one_side.py +++ b/source/tests/tf/test_type_one_side.py @@ -1,10 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np -from common import ( - DataSystem, - gen_data, - j_loader, -) from deepmd.tf.common import ( j_must_have, @@ -16,6 +11,12 @@ tf, ) +from .common import ( + DataSystem, + gen_data, + j_loader, +) + GLOBAL_ENER_FLOAT_PRECISION = tf.float64 GLOBAL_TF_FLOAT_PRECISION = tf.float64 GLOBAL_NP_FLOAT_PRECISION = np.float64 diff --git a/source/tests/test_virtual_type.py b/source/tests/tf/test_virtual_type.py similarity index 97% rename from source/tests/test_virtual_type.py rename to source/tests/tf/test_virtual_type.py index 0aca54dfd6..5ceb1c7637 100644 --- a/source/tests/test_virtual_type.py +++ b/source/tests/tf/test_virtual_type.py @@ -4,11 +4,6 @@ import unittest import numpy as np -from common import ( - gen_data, - j_loader, - tests_path, -) from deepmd.tf.common import ( j_must_have, @@ -26,12 +21,18 @@ NeighborStat, ) +from .common import ( + gen_data, + infer_path, + j_loader, +) + class TestVirtualType(unittest.TestCase): @classmethod def setUpClass(cls): convert_pbtxt_to_pb( - str(tests_path / os.path.join("infer", "virtual_type.pbtxt")), + str(infer_path / os.path.join("virtual_type.pbtxt")), "virtual_type.pb", ) cls.dp = DeepPot("virtual_type.pb") diff --git a/source/tests/train_dos.json b/source/tests/tf/train_dos.json similarity index 100% rename from source/tests/train_dos.json rename to source/tests/tf/train_dos.json diff --git a/source/tests/water.json b/source/tests/tf/water.json similarity index 100% rename from source/tests/water.json rename to source/tests/tf/water.json diff --git a/source/tests/water_hybrid.json b/source/tests/tf/water_hybrid.json similarity index 100% rename from source/tests/water_hybrid.json rename to source/tests/tf/water_hybrid.json diff --git a/source/tests/water_layer_name.json b/source/tests/tf/water_layer_name.json similarity index 100% rename from source/tests/water_layer_name.json rename to source/tests/tf/water_layer_name.json diff --git a/source/tests/water_multi.json b/source/tests/tf/water_multi.json similarity index 100% rename from source/tests/water_multi.json rename to source/tests/tf/water_multi.json diff --git a/source/tests/water_se_a.json b/source/tests/tf/water_se_a.json similarity index 100% rename from source/tests/water_se_a.json rename to source/tests/tf/water_se_a.json diff --git a/source/tests/water_se_a_afparam.json b/source/tests/tf/water_se_a_afparam.json similarity index 100% rename from source/tests/water_se_a_afparam.json rename to source/tests/tf/water_se_a_afparam.json diff --git a/source/tests/water_se_a_aparam.json b/source/tests/tf/water_se_a_aparam.json similarity index 100% rename from source/tests/water_se_a_aparam.json rename to source/tests/tf/water_se_a_aparam.json diff --git a/source/tests/water_se_a_ebd.json b/source/tests/tf/water_se_a_ebd.json similarity index 100% rename from source/tests/water_se_a_ebd.json rename to source/tests/tf/water_se_a_ebd.json diff --git a/source/tests/water_se_a_fparam.json b/source/tests/tf/water_se_a_fparam.json similarity index 100% rename from source/tests/water_se_a_fparam.json rename to source/tests/tf/water_se_a_fparam.json diff --git a/source/tests/water_se_a_srtab.json b/source/tests/tf/water_se_a_srtab.json similarity index 100% rename from source/tests/water_se_a_srtab.json rename to source/tests/tf/water_se_a_srtab.json diff --git a/source/tests/water_se_a_type.json b/source/tests/tf/water_se_a_type.json similarity index 100% rename from source/tests/water_se_a_type.json rename to source/tests/tf/water_se_a_type.json diff --git a/source/tests/water_se_atten.json b/source/tests/tf/water_se_atten.json similarity index 100% rename from source/tests/water_se_atten.json rename to source/tests/tf/water_se_atten.json diff --git a/source/tests/water_se_atten_compressible_mixed_type.json b/source/tests/tf/water_se_atten_compressible_mixed_type.json similarity index 100% rename from source/tests/water_se_atten_compressible_mixed_type.json rename to source/tests/tf/water_se_atten_compressible_mixed_type.json diff --git a/source/tests/water_se_atten_mixed_type.json b/source/tests/tf/water_se_atten_mixed_type.json similarity index 100% rename from source/tests/water_se_atten_mixed_type.json rename to source/tests/tf/water_se_atten_mixed_type.json diff --git a/source/tests/water_se_r.json b/source/tests/tf/water_se_r.json similarity index 100% rename from source/tests/water_se_r.json rename to source/tests/tf/water_se_r.json diff --git a/source/tests/water_se_t.json b/source/tests/tf/water_se_t.json similarity index 100% rename from source/tests/water_se_t.json rename to source/tests/tf/water_se_t.json diff --git a/source/tests/wfc.json b/source/tests/tf/wfc.json similarity index 100% rename from source/tests/wfc.json rename to source/tests/tf/wfc.json diff --git a/source/tests/yaml_inputs/water_se_a_v1.json b/source/tests/tf/yaml_inputs/water_se_a_v1.json similarity index 100% rename from source/tests/yaml_inputs/water_se_a_v1.json rename to source/tests/tf/yaml_inputs/water_se_a_v1.json diff --git a/source/tests/yaml_inputs/water_se_a_v1.yaml b/source/tests/tf/yaml_inputs/water_se_a_v1.yaml similarity index 100% rename from source/tests/yaml_inputs/water_se_a_v1.yaml rename to source/tests/tf/yaml_inputs/water_se_a_v1.yaml diff --git a/source/tests/yaml_inputs/water_v1.json b/source/tests/tf/yaml_inputs/water_v1.json similarity index 100% rename from source/tests/yaml_inputs/water_v1.json rename to source/tests/tf/yaml_inputs/water_v1.json diff --git a/source/tests/yaml_inputs/water_v1.yaml b/source/tests/tf/yaml_inputs/water_v1.yaml similarity index 100% rename from source/tests/yaml_inputs/water_v1.yaml rename to source/tests/tf/yaml_inputs/water_v1.yaml diff --git a/source/tests/zinc_se_a_mask.json b/source/tests/tf/zinc_se_a_mask.json similarity index 100% rename from source/tests/zinc_se_a_mask.json rename to source/tests/tf/zinc_se_a_mask.json From 1e51a888e8501e1000c19485ab18422d2e9aecc2 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Mon, 29 Jan 2024 12:19:59 +0800 Subject: [PATCH 025/686] breaking: pt: unify the output of descriptors. (#3190) Co-authored-by: Han Wang --- deepmd/model_format/se_e2_a.py | 18 ++++- deepmd/pt/model/descriptor/dpa1.py | 35 +++++++++- deepmd/pt/model/descriptor/dpa2.py | 32 ++++++++- deepmd/pt/model/descriptor/repformers.py | 2 +- deepmd/pt/model/descriptor/se_a.py | 46 +++++++++++-- deepmd/pt/model/descriptor/se_atten.py | 7 +- deepmd/pt/model/model/dp_atomic_model.py | 69 ++++--------------- deepmd/pt/model/task/ener.py | 3 + .../tests/common/test_model_format_utils.py | 3 +- source/tests/pt/test_permutation_denoise.py | 2 + source/tests/pt/test_rot_denoise.py | 2 + source/tests/pt/test_se_e2_a.py | 36 +++++----- source/tests/pt/test_smooth_denoise.py | 2 + source/tests/pt/test_trans_denoise.py | 2 + 14 files changed, 170 insertions(+), 89 deletions(-) diff --git a/deepmd/model_format/se_e2_a.py b/deepmd/model_format/se_e2_a.py index fe516c8620..28751cad8d 100644 --- a/deepmd/model_format/se_e2_a.py +++ b/deepmd/model_format/se_e2_a.py @@ -223,7 +223,18 @@ def call( Returns ------- descriptor - The descriptor. shape: nf x nloc x ng x axis_neuron + The descriptor. shape: nf x nloc x (ng x axis_neuron) + gr + The rotationally equivariant and permutationally invariant single particle + representation. shape: nf x nloc x ng x 3 + g2 + The rotationally invariant pair-partical representation. + this descriptor returns None + h2 + The rotationally equivariant pair-partical representation. + this descriptor returns None + sw + The smooth switch function. """ # nf x nloc x nnei x 4 rr, ww = self.env_mat.call(coord_ext, atype_ext, nlist, self.davg, self.dstd) @@ -238,15 +249,17 @@ def call( gg = self.cal_g(ss, tt) # nf x nloc x ng x 4 gr += np.einsum("flni,flnj->flij", gg, tr) + # nf x nloc x ng x 4 gr /= self.nnei gr1 = gr[:, :, : self.axis_neuron, :] # nf x nloc x ng x ng1 grrg = np.einsum("flid,fljd->flij", gr, gr1) # nf x nloc x (ng x ng1) grrg = grrg.reshape(nf, nloc, ng * self.axis_neuron) - return grrg + return grrg, gr[..., 1:], None, None, ww def serialize(self) -> dict: + """Serialize the descriptor to dict.""" return { "rcut": self.rcut, "rcut_smth": self.rcut_smth, @@ -271,6 +284,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "DescrptSeA": + """Deserialize from dict.""" data = copy.deepcopy(data) variables = data.pop("@variables") embeddings = data.pop("embeddings") diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index dd34b815c9..23f521b6d8 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -135,12 +135,42 @@ def forward( nlist: torch.Tensor, mapping: Optional[torch.Tensor] = None, ): + """Compute the descriptor. + + Parameters + ---------- + coord_ext + The extended coordinates of atoms. shape: nf x (nallx3) + atype_ext + The extended aotm types. shape: nf x nall + nlist + The neighbor list. shape: nf x nloc x nnei + mapping + The index mapping, not required by this descriptor. + + Returns + ------- + descriptor + The descriptor. shape: nf x nloc x (ng x axis_neuron) + gr + The rotationally equivariant and permutationally invariant single particle + representation. shape: nf x nloc x ng x 3 + g2 + The rotationally invariant pair-partical representation. + shape: nf x nloc x nnei x ng + h2 + The rotationally equivariant pair-partical representation. + shape: nf x nloc x nnei x 3 + sw + The smooth switch function. shape: nf x nloc x nnei + + """ del mapping nframes, nloc, nnei = nlist.shape nall = extended_coord.view(nframes, -1).shape[1] // 3 g1_ext = self.type_embedding(extended_atype) g1_inp = g1_ext[:, :nloc, :] - g1, env_mat, diff, rot_mat, sw = self.se_atten( + g1, g2, h2, rot_mat, sw = self.se_atten( nlist, extended_coord, extended_atype, @@ -149,4 +179,5 @@ def forward( ) if self.concat_output_tebd: g1 = torch.cat([g1, g1_inp], dim=-1) - return g1, env_mat, diff, rot_mat, sw + + return g1, rot_mat, g2, h2, sw diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index fbdbc91dd9..409b999262 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -329,6 +329,36 @@ def forward( nlist: torch.Tensor, mapping: Optional[torch.Tensor] = None, ): + """Compute the descriptor. + + Parameters + ---------- + coord_ext + The extended coordinates of atoms. shape: nf x (nallx3) + atype_ext + The extended aotm types. shape: nf x nall + nlist + The neighbor list. shape: nf x nloc x nnei + mapping + The index mapping, mapps extended region index to local region. + + Returns + ------- + descriptor + The descriptor. shape: nf x nloc x (ng x axis_neuron) + gr + The rotationally equivariant and permutationally invariant single particle + representation. shape: nf x nloc x ng x 3 + g2 + The rotationally invariant pair-partical representation. + shape: nf x nloc x nnei x ng + h2 + The rotationally equivariant pair-partical representation. + shape: nf x nloc x nnei x 3 + sw + The smooth switch function. shape: nf x nloc x nnei + + """ nframes, nloc, nnei = nlist.shape nall = extended_coord.view(nframes, -1).shape[1] // 3 # nlists @@ -372,4 +402,4 @@ def forward( ) if self.concat_output_tebd: g1 = torch.cat([g1, g1_inp], dim=-1) - return g1, g2, h2, rot_mat, sw + return g1, rot_mat, g2, h2, sw diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py index 26887b1b75..141b5dc745 100644 --- a/deepmd/pt/model/descriptor/repformers.py +++ b/deepmd/pt/model/descriptor/repformers.py @@ -256,7 +256,7 @@ def forward( # (nb x nloc) x ng2 x 3 rot_mat = torch.permute(h2g2, (0, 1, 3, 2)) - return g1, g2, h2, rot_mat.view(-1, self.dim_emb, 3), sw + return g1, g2, h2, rot_mat.view(-1, nloc, self.dim_emb, 3), sw def compute_input_stats(self, merged): """Update mean and stddev for descriptor elements.""" diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 10aa66311e..3f42736dca 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -115,12 +115,42 @@ def get_data_process_key(cls, config): def forward( self, - extended_coord: torch.Tensor, - extended_atype: torch.Tensor, + coord_ext: torch.Tensor, + atype_ext: torch.Tensor, nlist: torch.Tensor, mapping: Optional[torch.Tensor] = None, ): - return self.sea.forward(nlist, extended_coord, extended_atype, None, mapping) + """Compute the descriptor. + + Parameters + ---------- + coord_ext + The extended coordinates of atoms. shape: nf x (nallx3) + atype_ext + The extended aotm types. shape: nf x nall + nlist + The neighbor list. shape: nf x nloc x nnei + mapping + The index mapping, not required by this descriptor. + + Returns + ------- + descriptor + The descriptor. shape: nf x nloc x (ng x axis_neuron) + gr + The rotationally equivariant and permutationally invariant single particle + representation. shape: nf x nloc x ng x 3 + g2 + The rotationally invariant pair-partical representation. + this descriptor returns None + h2 + The rotationally equivariant pair-partical representation. + this descriptor returns None + sw + The smooth switch function. + + """ + return self.sea.forward(nlist, coord_ext, atype_ext, None, mapping) def set_stat_mean_and_stddev( self, @@ -389,7 +419,7 @@ def forward( del extended_atype_embd, mapping nloc = nlist.shape[1] atype = extended_atype[:, :nloc] - dmatrix, diff, _ = prod_env_mat_se_a( + dmatrix, diff, sw = prod_env_mat_se_a( extended_coord, nlist, atype, @@ -438,12 +468,14 @@ def forward( result = torch.matmul( xyz_scatter_1, xyz_scatter_2 ) # shape is [nframes*nall, self.filter_neuron[-1], self.axis_neuron] + result = result.view(-1, nloc, self.filter_neuron[-1] * self.axis_neuron) + rot_mat = rot_mat.view([-1, nloc] + list(rot_mat.shape[1:])) # noqa:RUF005 return ( - result.view(-1, nloc, self.filter_neuron[-1] * self.axis_neuron), - None, - None, + result, + rot_mat, None, None, + sw, ) diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index 0c932f42f2..78cba59da7 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -281,9 +281,8 @@ def forward( self.rcut, self.rcut_smth, ) - dmatrix = dmatrix.view( - -1, self.ndescrpt - ) # shape is [nframes*nall, self.ndescrpt] + # [nfxnlocxnnei, self.ndescrpt] + dmatrix = dmatrix.view(-1, self.ndescrpt) nlist_mask = nlist != -1 nlist[nlist == -1] = 0 sw = torch.squeeze(sw, -1) @@ -328,7 +327,7 @@ def forward( return ( result.view(-1, nloc, self.filter_neuron[-1] * self.axis_neuron), ret.view(-1, nloc, self.nnei, self.filter_neuron[-1]), - diff, + dmatrix.view(-1, nloc, self.nnei, 4)[..., 1:], rot_mat.view(-1, self.filter_neuron[-1], 3), sw, ) diff --git a/deepmd/pt/model/model/dp_atomic_model.py b/deepmd/pt/model/model/dp_atomic_model.py index a0f9b25765..853eacb875 100644 --- a/deepmd/pt/model/model/dp_atomic_model.py +++ b/deepmd/pt/model/model/dp_atomic_model.py @@ -14,7 +14,6 @@ Descriptor, ) from deepmd.pt.model.task import ( - DenoiseNet, Fitting, ) @@ -93,40 +92,20 @@ def __init__( sampled=sampled, ) - # Fitting - if fitting_net: - fitting_net["type"] = fitting_net.get("type", "ener") - if self.descriptor_type not in ["se_e2_a"]: - fitting_net["ntypes"] = 1 - else: - fitting_net["ntypes"] = self.descriptor.get_ntype() - fitting_net["use_tebd"] = False - fitting_net["embedding_width"] = self.descriptor.dim_out - - self.grad_force = "direct" not in fitting_net["type"] - if not self.grad_force: - fitting_net["out_dim"] = self.descriptor.dim_emb - if "ener" in fitting_net["type"]: - fitting_net["return_energy"] = True - self.fitting_net = Fitting(**fitting_net) + fitting_net["type"] = fitting_net.get("type", "ener") + if self.descriptor_type not in ["se_e2_a"]: + fitting_net["ntypes"] = 1 else: - self.fitting_net = None - self.grad_force = False - if not self.split_nlist: - self.coord_denoise_net = DenoiseNet( - self.descriptor.dim_out, self.ntypes - 1, self.descriptor.dim_emb - ) - elif self.combination: - self.coord_denoise_net = DenoiseNet( - self.descriptor.dim_out, - self.ntypes - 1, - self.descriptor.dim_emb_list, - self.prefactor, - ) - else: - self.coord_denoise_net = DenoiseNet( - self.descriptor.dim_out, self.ntypes - 1, self.descriptor.dim_emb - ) + fitting_net["ntypes"] = self.descriptor.get_ntype() + fitting_net["use_tebd"] = False + fitting_net["embedding_width"] = self.descriptor.dim_out + + self.grad_force = "direct" not in fitting_net["type"] + if not self.grad_force: + fitting_net["out_dim"] = self.descriptor.dim_emb + if "ener" in fitting_net["type"]: + fitting_net["return_energy"] = True + self.fitting_net = Fitting(**fitting_net) def get_fitting_output_def(self) -> FittingOutputDef: """Get the output def of the fitting net.""" @@ -178,7 +157,7 @@ def forward_atomic( atype = extended_atype[:, :nloc] if self.do_grad(): extended_coord.requires_grad_(True) - descriptor, env_mat, diff, rot_mat, sw = self.descriptor( + descriptor, rot_mat, g2, h2, sw = self.descriptor( extended_coord, extended_atype, nlist, @@ -186,23 +165,5 @@ def forward_atomic( ) assert descriptor is not None # energy, force - if self.fitting_net is not None: - fit_ret = self.fitting_net( - descriptor, atype, atype_tebd=None, rot_mat=rot_mat - ) - # denoise - else: - nlist_list = [nlist] - if not self.split_nlist: - nnei_mask = nlist != -1 - elif self.combination: - nnei_mask = [] - for item in nlist_list: - nnei_mask_item = item != -1 - nnei_mask.append(nnei_mask_item) - else: - env_mat = env_mat[-1] - diff = diff[-1] - nnei_mask = nlist_list[-1] != -1 - fit_ret = self.coord_denoise_net(env_mat, diff, nnei_mask, descriptor, sw) + fit_ret = self.fitting_net(descriptor, atype, atype_tebd=None, rot_mat=rot_mat) return fit_ret diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 7ddcbd5c54..03043e2fcb 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -207,8 +207,11 @@ def forward( inputs ) # Shape is [nframes, nloc, m1] assert list(vec_out.size()) == [nframes, nloc, self.out_dim] + # (nf x nloc) x 1 x od vec_out = vec_out.view(-1, 1, self.out_dim) assert rot_mat is not None + # (nf x nloc) x od x 3 + rot_mat = rot_mat.view(-1, self.out_dim, 3) vec_out = ( torch.bmm(vec_out, rot_mat).squeeze(-2).view(nframes, nloc, 3) ) # Shape is [nframes, nloc, 3] diff --git a/source/tests/common/test_model_format_utils.py b/source/tests/common/test_model_format_utils.py index 22393515ec..da76c53ed9 100644 --- a/source/tests/common/test_model_format_utils.py +++ b/source/tests/common/test_model_format_utils.py @@ -367,4 +367,5 @@ def test_self_consistency( em1 = DescrptSeA.deserialize(em0.serialize()) mm0 = em0.call(self.coord_ext, self.atype_ext, self.nlist) mm1 = em1.call(self.coord_ext, self.atype_ext, self.nlist) - np.testing.assert_allclose(mm0, mm1) + for ii in [0, 1, 4]: + np.testing.assert_allclose(mm0[ii], mm1[ii]) diff --git a/source/tests/pt/test_permutation_denoise.py b/source/tests/pt/test_permutation_denoise.py index 47bd0360f2..6dd61ab7e4 100644 --- a/source/tests/pt/test_permutation_denoise.py +++ b/source/tests/pt/test_permutation_denoise.py @@ -66,6 +66,7 @@ def test( ) +@unittest.skip("support of the denoise is temporally disabled") class TestDenoiseModelDPA1(unittest.TestCase, PermutationDenoiseTest): def setUp(self): model_params = copy.deepcopy(model_dpa1) @@ -74,6 +75,7 @@ def setUp(self): self.model = get_model(model_params, sampled).to(env.DEVICE) +@unittest.skip("support of the denoise is temporally disabled") class TestDenoiseModelDPA2(unittest.TestCase, PermutationDenoiseTest): def setUp(self): model_params_sample = copy.deepcopy(model_dpa2) diff --git a/source/tests/pt/test_rot_denoise.py b/source/tests/pt/test_rot_denoise.py index cab8de7bec..2cbfd8fd38 100644 --- a/source/tests/pt/test_rot_denoise.py +++ b/source/tests/pt/test_rot_denoise.py @@ -97,6 +97,7 @@ def test( ) +@unittest.skip("support of the denoise is temporally disabled") class TestDenoiseModelDPA1(unittest.TestCase, RotDenoiseTest): def setUp(self): model_params = copy.deepcopy(model_dpa1) @@ -105,6 +106,7 @@ def setUp(self): self.model = get_model(model_params, sampled).to(env.DEVICE) +@unittest.skip("support of the denoise is temporally disabled") class TestDenoiseModelDPA2(unittest.TestCase, RotDenoiseTest): def setUp(self): model_params_sample = copy.deepcopy(model_dpa2) diff --git a/source/tests/pt/test_se_e2_a.py b/source/tests/pt/test_se_e2_a.py index 96a17c2bad..c0a106cb16 100644 --- a/source/tests/pt/test_se_e2_a.py +++ b/source/tests/pt/test_se_e2_a.py @@ -102,7 +102,7 @@ def test_consistency( ) # serialization dd1 = DescrptSeA.deserialize(dd0.serialize()) - rd1, _, _, _, _ = dd1( + rd1, gr1, _, _, sw1 = dd1( torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), torch.tensor(self.nlist, dtype=int, device=env.DEVICE), @@ -116,18 +116,19 @@ def test_consistency( ) # dp impl dd2 = DPDescrptSeA.deserialize(dd0.serialize()) - rd2 = dd2.call( + rd2, gr2, _, _, sw2 = dd2.call( self.coord_ext, self.atype_ext, self.nlist, ) - np.testing.assert_allclose( - rd0.detach().cpu().numpy(), - rd2, - rtol=rtol, - atol=atol, - err_msg=err_msg, - ) + for aa, bb in zip([rd1, gr1, sw1], [rd2, gr2, sw2]): + np.testing.assert_allclose( + aa.detach().cpu().numpy(), + bb, + rtol=rtol, + atol=atol, + err_msg=err_msg, + ) # old impl if idt is False and prec == "float64": dd3 = DescrptSeA( @@ -154,18 +155,19 @@ def test_consistency( dd3_state_dict[i] = dd3_state_dict[i].unsqueeze(0) dd3.sea.load_state_dict(dd3_state_dict) - rd3, _, _, _, _ = dd3( + rd3, gr3, _, _, sw3 = dd3( torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), torch.tensor(self.nlist, dtype=int, device=env.DEVICE), ) - np.testing.assert_allclose( - rd0.detach().cpu().numpy(), - rd3.detach().cpu().numpy(), - rtol=rtol, - atol=atol, - err_msg=err_msg, - ) + for aa, bb in zip([rd1, gr1, sw1], [rd3, gr3, sw3]): + np.testing.assert_allclose( + aa.detach().cpu().numpy(), + bb.detach().cpu().numpy(), + rtol=rtol, + atol=atol, + err_msg=err_msg, + ) def test_jit( self, diff --git a/source/tests/pt/test_smooth_denoise.py b/source/tests/pt/test_smooth_denoise.py index a66e5df957..de89f8dccc 100644 --- a/source/tests/pt/test_smooth_denoise.py +++ b/source/tests/pt/test_smooth_denoise.py @@ -96,6 +96,7 @@ def compare(ret0, ret1): compare(ret0, ret3) +@unittest.skip("support of the denoise is temporally disabled") class TestDenoiseModelDPA2(unittest.TestCase, SmoothDenoiseTest): def setUp(self): model_params_sample = copy.deepcopy(model_dpa2) @@ -116,6 +117,7 @@ def setUp(self): self.aprec = 1e-5 +@unittest.skip("support of the denoise is temporally disabled") class TestDenoiseModelDPA2_1(unittest.TestCase, SmoothDenoiseTest): def setUp(self): model_params_sample = copy.deepcopy(model_dpa2) diff --git a/source/tests/pt/test_trans_denoise.py b/source/tests/pt/test_trans_denoise.py index 360633278c..88b926a3ae 100644 --- a/source/tests/pt/test_trans_denoise.py +++ b/source/tests/pt/test_trans_denoise.py @@ -56,6 +56,7 @@ def test( torch.testing.assert_close(ret0["logits"], ret1["logits"], rtol=prec, atol=prec) +@unittest.skip("support of the denoise is temporally disabled") class TestDenoiseModelDPA1(unittest.TestCase, TransDenoiseTest): def setUp(self): model_params = copy.deepcopy(model_dpa1) @@ -64,6 +65,7 @@ def setUp(self): self.model = get_model(model_params, sampled).to(env.DEVICE) +@unittest.skip("support of the denoise is temporally disabled") class TestDenoiseModelDPA2(unittest.TestCase, TransDenoiseTest): def setUp(self): model_params_sample = copy.deepcopy(model_dpa2) From 8eadd3e6f0ed71266c104e848c7e165c5653821f Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 29 Jan 2024 05:31:55 -0500 Subject: [PATCH 026/686] docs: document PyTorch backend (#3193) Fix #3121. There are TODOs: (1) PyTorch-backend specific features and arguments; (2) Python interface installation. Currently, the TensorFlow backend is always installed, and I am considering rewriting the logic; (3) Unsupported features - write docs when implemented. --------- Signed-off-by: Jinzhe Zeng --- CITATIONS.bib | 19 +++++ backend/dynamic_metadata.py | 1 + doc/backend.md | 28 +++++++ doc/conf.py | 1 + doc/credits.rst | 7 ++ doc/index.rst | 1 + doc/inference/python.md | 9 ++- doc/install/easy-install-dev.md | 8 +- doc/install/easy-install.md | 25 +++--- doc/install/install-from-c-library.md | 6 +- doc/install/install-from-source.md | 111 ++++++++++++++++++++------ doc/model/dpa2.md | 5 ++ doc/model/index.rst | 1 + doc/model/train-energy.md | 4 +- doc/model/train-se-atten.md | 4 +- doc/model/train-se-e2-a.md | 4 +- doc/train/finetuning.md | 4 +- doc/train/multi-task-training.md | 1 + 18 files changed, 189 insertions(+), 50 deletions(-) create mode 100644 doc/backend.md create mode 100644 doc/model/dpa2.md diff --git a/CITATIONS.bib b/CITATIONS.bib index ac682b28f7..425c00ac42 100644 --- a/CITATIONS.bib +++ b/CITATIONS.bib @@ -105,6 +105,25 @@ @misc{Zhang_2022_DPA1 doi = {10.48550/arXiv.2208.08236}, } +@misc{Zhang_2023_DPA2, + annote = {DPA-2}, + author = {Duo Zhang and Xinzijian Liu and Xiangyu Zhang and Chengqian Zhang and + Chun Cai and Hangrui Bi and Yiming Du and Xuejian Qin and Jiameng Huang + and Bowen Li and Yifan Shan and Jinzhe Zeng and Yuzhi Zhang and Siyuan + Liu and Yifan Li and Junhan Chang and Xinyan Wang and Shuo Zhou and + Jianchuan Liu and Xiaoshan Luo and Zhenyu Wang and Wanrun Jiang and Jing + Wu and Yudi Yang and Jiyuan Yang and Manyi Yang and Fu-Qiang Gong and + Linshuang Zhang and Mengchao Shi and Fu-Zhi Dai and Darrin M. York and + Shi Liu and Tong Zhu and Zhicheng Zhong and Jian Lv and Jun Cheng and + Weile Jia and Mohan Chen and Guolin Ke and Weinan E and Linfeng Zhang + and Han Wang}, + title = {{DPA-2: Towards a universal large atomic model for molecular and material + simulation}}, + publisher = {arXiv}, + year = {2023}, + doi = {10.48550/arXiv.2312.15492}, +} + @article{Zhang_PhysPlasmas_2020_v27_p122704, annote = {frame-specific parameters (e.g. electronic temperature)}, author = {Zhang, Yuzhi and Gao, Chang and Liu, Qianrui and Zhang, Linfeng and Wang, Han and Chen, Mohan}, diff --git a/backend/dynamic_metadata.py b/backend/dynamic_metadata.py index a5817727f5..5646169907 100644 --- a/backend/dynamic_metadata.py +++ b/backend/dynamic_metadata.py @@ -46,6 +46,7 @@ def dynamic_metadata( "sphinx_markdown_tables", "myst-nb>=1.0.0rc0", "myst-parser>=0.19.2", + "sphinx-design", "breathe", "exhale", "numpydoc", diff --git a/doc/backend.md b/doc/backend.md new file mode 100644 index 0000000000..41a0b4d2c8 --- /dev/null +++ b/doc/backend.md @@ -0,0 +1,28 @@ +# Backend + +## Supported backends + +DeePMD-kit supports multiple backends: TensorFlow and PyTorch. +To use DeePMD-kit, you must install at least one backend. +Each backend does not support all features. +In the documentation, TensorFlow {{ tensorflow_icon }} and PyTorch {{ pytorch_icon }} icons are used to mark whether a backend supports a feature. + +### TensorFlow {{ tensorflow_icon }} + +TensorFlow 2.2 or above is required. +DeePMD-kit does not use the TensorFlow v2 API but uses the TensorFlow v1 API (`tf.compat.v1`) in the graph mode. + +### PyTorch {{ pytorch_icon }} + +PyTorch 2.0 or above is required. + +## Switch the backend + +### Training + +When training and freezing a model, you can use `dp --tf` or `dp --pt` in the command line to switch the backend. + +### Inference + +When doing inference, DeePMD-kit detects the backend from the model filename. +For example, when the model filename ends with `.pb` (the ProtoBuf file), DeePMD-kit will consider it using the TensorFlow backend. diff --git a/doc/conf.py b/doc/conf.py index 11803a9e2d..3687695b36 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -94,6 +94,7 @@ def setup(app): "breathe", "exhale", "sphinxcontrib.bibtex", + "sphinx_design", ] # breathe_domain_by_extension = { diff --git a/doc/credits.rst b/doc/credits.rst index 3fbe1d56d8..64880d9035 100644 --- a/doc/credits.rst +++ b/doc/credits.rst @@ -49,6 +49,13 @@ Cite DeePMD-kit and methods Zhang_2022_DPA1 +- If DPA-2 descriptor (`dpa2`) is used, + +.. bibliography:: + :filter: False + + Zhang_2023_DPA2 + - If frame-specific parameters (`fparam`, e.g. electronic temperature) is used, .. bibliography:: diff --git a/doc/index.rst b/doc/index.rst index b60430b566..d089507886 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -34,6 +34,7 @@ DeePMD-kit is a package written in Python/C++, designed to minimize the effort r :numbered: :caption: Advanced + backend install/index data/index model/index diff --git a/doc/inference/python.md b/doc/inference/python.md index b5d3ca1efc..db61cd7843 100644 --- a/doc/inference/python.md +++ b/doc/inference/python.md @@ -26,9 +26,14 @@ graphs = [DP("graph.000.pb"), DP("graph.001.pb")] model_devi = calc_model_devi(coord, cell, atype, graphs) ``` -Note that if the model inference or model deviation is performed cyclically, one should avoid calling the same model multiple times. Otherwise, tensorFlow will never release the memory and this may lead to an out-of-memory (OOM) error. +Note that if the model inference or model deviation is performed cyclically, one should avoid calling the same model multiple times. +Otherwise, TensorFlow or PyTorch will never release the memory, and this may lead to an out-of-memory (OOM) error. -## External neighbor list algorithm +## External neighbor list algorithm {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: The native neighbor list algorithm of the DeePMD-kit is in $O(N^2)$ complexity ($N$ is the number of atoms). While this is not a problem for small systems that quantum methods can afford, the large systems for molecular dynamics have slow performance. diff --git a/doc/install/easy-install-dev.md b/doc/install/easy-install-dev.md index f3cf52c1f5..43ff1c80a5 100644 --- a/doc/install/easy-install-dev.md +++ b/doc/install/easy-install-dev.md @@ -19,12 +19,16 @@ For CUDA 11.8 support, use the `devel_cu11` tag. Below is an one-line shell command to download the [artifact](https://nightly.link/deepmodeling/deepmd-kit/workflows/build_wheel/devel/artifact.zip) containing wheels and install it with `pip`: ```sh -pip install -U --pre deepmd-kit[gpu,cu12,lmp] --extra-index-url https://deepmodeling.github.io/deepmd-kit/simple +pip install -U --pre deepmd-kit[gpu,cu12,lmp,torch] --extra-index-url https://deepmodeling.github.io/deepmd-kit/simple ``` `cu12` and `lmp` are optional, which is the same as the stable version. -## Download pre-compiled C Library +## Download pre-compiled C Library {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: The [pre-comiled C library](./install-from-c-library.md) can be downloaded from [here](https://nightly.link/deepmodeling/deepmd-kit/workflows/package_c/devel/libdeepmd_c-0-libdeepmd_c.tar.gz.zip), or via a shell command: diff --git a/doc/install/easy-install.md b/doc/install/easy-install.md index 2d0972c8be..e1861a6096 100644 --- a/doc/install/easy-install.md +++ b/doc/install/easy-install.md @@ -19,9 +19,9 @@ Python 3.8 or above is required for Python interface. ## Install off-line packages -Both CPU and GPU version offline packages are available in [the Releases page](https://github.com/deepmodeling/deepmd-kit/releases). +Both CPU and GPU version offline packages are available on [the Releases page](https://github.com/deepmodeling/deepmd-kit/releases). -Some packages are splited into two files due to size limit of GitHub. One may merge them into one after downloading: +Some packages are split into two files due to the size limit of GitHub. One may merge them into one after downloading: ```bash cat deepmd-kit-2.1.1-cuda11.6_gpu-Linux-x86_64.sh.0 deepmd-kit-2.1.1-cuda11.6_gpu-Linux-x86_64.sh.1 > deepmd-kit-2.1.1-cuda11.6_gpu-Linux-x86_64.sh ``` @@ -73,17 +73,12 @@ A docker for installing the DeePMD-kit is available [here](https://github.com/or To pull the CPU version: ```bash -docker pull ghcr.io/deepmodeling/deepmd-kit:2.1.1_cpu +docker pull ghcr.io/deepmodeling/deepmd-kit:2.2.8_cpu ``` To pull the GPU version: ```bash -docker pull ghcr.io/deepmodeling/deepmd-kit:2.1.1_cuda11.6_gpu -``` - -To pull the ROCm version: -```bash -docker pull deepmodeling/dpmdkit-rocm:dp2.0.3-rocm4.5.2-tf2.6-lmp29Sep2021 +docker pull ghcr.io/deepmodeling/deepmd-kit:2.2.8_cuda12.0_gpu ``` ## Install Python interface with pip @@ -91,7 +86,7 @@ docker pull deepmodeling/dpmdkit-rocm:dp2.0.3-rocm4.5.2-tf2.6-lmp29Sep2021 If you have no existing TensorFlow installed, you can use `pip` to install the pre-built package of the Python interface with CUDA 12 supported: ```bash -pip install deepmd-kit[gpu,cu12] +pip install deepmd-kit[gpu,cu12,torch] ``` `cu12` is required only when CUDA Toolkit and cuDNN were not installed. @@ -99,24 +94,26 @@ pip install deepmd-kit[gpu,cu12] To install the package built against CUDA 11.8, use ```bash +pip install torch --index-url https://download.pytorch.org/whl/cu118 pip install deepmd-kit-cu11[gpu,cu11] ``` Or install the CPU version without CUDA supported: ```bash +pip install torch --index-url https://download.pytorch.org/whl/cpu pip install deepmd-kit[cpu] ``` -[The LAMMPS module](../third-party/lammps-command.md) and [the i-Pi driver](../third-party/ipi.md) are only provided on Linux and macOS. To install LAMMPS and/or i-Pi, add `lmp` and/or `ipi` to extras: +[The LAMMPS module](../third-party/lammps-command.md) and [the i-Pi driver](../third-party/ipi.md) are only provided on Linux and macOS for the TensorFlow backend. To install LAMMPS and/or i-Pi, add `lmp` and/or `ipi` to extras: ```bash -pip install deepmd-kit[gpu,cu12,lmp,ipi] +pip install deepmd-kit[gpu,cu12,torch,lmp,ipi] ``` MPICH is required for parallel running. (The macOS arm64 package doesn't support MPI yet.) It is suggested to install the package into an isolated environment. The supported platform includes Linux x86-64 and aarch64 with GNU C Library 2.28 or above, macOS x86-64 and arm64, and Windows x86-64. -A specific version of TensorFlow which is compatible with DeePMD-kit will be also installed. +A specific version of TensorFlow and PyTorch which is compatible with DeePMD-kit will be also installed. :::{Warning} -If your platform is not supported, or want to build against the installed TensorFlow, or want to enable ROCM support, please [build from source](install-from-source.md). +If your platform is not supported, or you want to build against the installed TensorFlow, or you want to enable ROCM support, please [build from source](install-from-source.md). ::: diff --git a/doc/install/install-from-c-library.md b/doc/install/install-from-c-library.md index 7613fdb772..36944f03e6 100644 --- a/doc/install/install-from-c-library.md +++ b/doc/install/install-from-c-library.md @@ -1,4 +1,8 @@ -# Install from pre-compiled C library +# Install from pre-compiled C library {{ tensorflow_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: DeePMD-kit provides pre-compiled C library package (`libdeepmd_c.tar.gz`) in each [release](https://github.com/deepmodeling/deepmd-kit/releases). It can be used to build the [LAMMPS plugin](./install-lammps.md) and [GROMACS patch](./install-gromacs.md), as well as many [third-party software packages](../third-party/out-of-deepmd-kit.md), without building TensorFlow and DeePMD-kit on one's own. It can be downloaded via the shell command: diff --git a/doc/install/install-from-source.md b/doc/install/install-from-source.md index 51d1f4c1e5..ae1509f2ca 100644 --- a/doc/install/install-from-source.md +++ b/doc/install/install-from-source.md @@ -14,45 +14,74 @@ cd deepmd-kit deepmd_source_dir=`pwd` ``` -## Install the python interface -### Install Tensorflow's python interface -First, check the python version on your machine. +## Install the Python interface +### Install Backend's Python interface +First, check the Python version on your machine. Python 3.8 or above is required. ```bash python --version ``` -We follow the virtual environment approach to install TensorFlow's Python interface. The full instruction can be found on the official [TensorFlow website](https://www.tensorflow.org/install/pip). TensorFlow 2.2 or later is supported. Now we assume that the Python interface will be installed to the virtual environment directory `$tensorflow_venv` +We follow the virtual environment approach to install the backend's Python interface. +Now we assume that the Python interface will be installed in the virtual environment directory `$deepmd_venv`: + ```bash -virtualenv -p python3 $tensorflow_venv -source $tensorflow_venv/bin/activate +virtualenv -p python3 $deepmd_venv +source $deepmd_venv/bin/activate pip install --upgrade pip +``` + +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} + +The full instruction to install TensorFlow can be found on the official [TensorFlow website](https://www.tensorflow.org/install/pip). TensorFlow 2.2 or later is supported. +```bash pip install --upgrade tensorflow ``` -It is important that every time a new shell is started and one wants to use `DeePMD-kit`, the virtual environment should be activated by + +If one does not need the GPU support of DeePMD-kit and is concerned about package size, the CPU-only version of TensorFlow should be installed by ```bash -source $tensorflow_venv/bin/activate +pip install --upgrade tensorflow-cpu ``` -if one wants to skip out of the virtual environment, he/she can do + +To verify the installation, run ```bash -deactivate +python -c "import tensorflow as tf;print(tf.reduce_sum(tf.random.normal([1000, 1000])))" ``` -If one has multiple python interpreters named something like python3.x, it can be specified by, for example + +One can also [build the TensorFlow Python interface from source](https://www.tensorflow.org/install/source) for customized hardware optimization, such as CUDA, ROCM, or OneDNN support. + +::: + +:::{tab-item} PyTorch {{ pytorch_icon }} + +To install PyTorch, run + +```sh +pip install torch +``` + +Follow [PyTorch documentation](https://pytorch.org/get-started/locally/) to install PyTorch built against different CUDA versions or without CUDA. + +::: + +:::: + +It is important that every time a new shell is started and one wants to use `DeePMD-kit`, the virtual environment should be activated by ```bash -virtualenv -p python3.8 $tensorflow_venv +source $deepmd_venv/bin/activate ``` -If one does not need the GPU support of DeePMD-kit and is concerned about package size, the CPU-only version of TensorFlow should be installed by +if one wants to skip out of the virtual environment, he/she can do ```bash -pip install --upgrade tensorflow-cpu +deactivate ``` -To verify the installation, run +If one has multiple python interpreters named something like python3.x, it can be specified by, for example ```bash -python -c "import tensorflow as tf;print(tf.reduce_sum(tf.random.normal([1000, 1000])))" +virtualenv -p python3.8 $deepmd_venv ``` One should remember to activate the virtual environment every time he/she uses DeePMD-kit. -One can also [build the TensorFlow Python interface from source](https://www.tensorflow.org/install/source) for custom hardware optimization, such as CUDA, ROCM, or OneDNN support. - ### Install the DeePMD-kit's python interface Check the compiler version on your machine @@ -106,7 +135,7 @@ Valid subcommands: test test the model ``` -### Install horovod and mpi4py +### Install horovod and mpi4py {{ tensorflow_icon }} [Horovod](https://github.com/horovod/horovod) and [mpi4py](https://github.com/mpi4py/mpi4py) are used for parallel training. For better performance on GPU, please follow the tuning steps in [Horovod on GPU](https://github.com/horovod/horovod/blob/master/docs/gpus.rst). ```bash @@ -152,7 +181,11 @@ If you don't install Horovod, DeePMD-kit will fall back to serial mode. If one does not need to use DeePMD-kit with Lammps or I-Pi, then the python interface installed in the previous section does everything and he/she can safely skip this section. -### Install Tensorflow's C++ interface (optional) +### Install Backends' C++ interface (optional) + +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} Since TensorFlow 2.12, TensorFlow C++ library (`libtensorflow_cc`) is packaged inside the Python library. Thus, you can skip building TensorFlow C++ library manually. If that does not work for you, you can still build it manually. @@ -160,6 +193,17 @@ The C++ interface of DeePMD-kit was tested with compiler GCC >= 4.8. It is notic First, the C++ interface of Tensorflow should be installed. It is noted that the version of Tensorflow should be consistent with the python interface. You may follow [the instruction](install-tf.2.12.md) or run the script `$deepmd_source_dir/source/install/build_tf.py` to install the corresponding C++ interface. +::: + +:::{tab-item} PyTorch {{ pytorch_icon }} + +If you have installed PyTorch using pip, you can use libtorch inside the PyTorch Python package. +You can also download libtorch prebuilt library from the [PyTorch website](https://pytorch.org/get-started/locally/). + +::: + +:::: + ### Install DeePMD-kit's C++ interface Now go to the source code directory of DeePMD-kit and make a building place. @@ -175,25 +219,46 @@ The installation requires CMake 3.16 or later for the CPU version, CMake 3.23 or pip install -U cmake ``` +You must enable at least one backend. +If you enable two or more backends, these backend libraries must be built in a compatible way, e.g. using the same `_GLIBCXX_USE_CXX11_ABI` flag. + +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} + I assume you have activated the TensorFlow Python environment and want to install DeePMD-kit into path `$deepmd_root`, then execute CMake ```bash -cmake -DUSE_TF_PYTHON_LIBS=TRUE -DCMAKE_INSTALL_PREFIX=$deepmd_root .. +cmake -DENABLE_TENSORFLOW=TRUE -DUSE_TF_PYTHON_LIBS=TRUE -DCMAKE_INSTALL_PREFIX=$deepmd_root .. ``` If you specify `-DUSE_TF_PYTHON_LIBS=FALSE`, you need to give the location where TensorFlow's C++ interface is installed to `-DTENSORFLOW_ROOT=${tensorflow_root}`. +::: + +:::{tab-item} PyTorch {{ pytorch_icon }} + +I assume you have installed the PyTorch (either Python or C++ interface) to `$torch_root`, then execute CMake +```bash +cmake -DENABLE_PYTORCH=TRUE -DCMAKE_PREFIX_PATH=$torch_root -DCMAKE_INSTALL_PREFIX=$deepmd_root .. +``` +::: + +:::: + One may add the following arguments to `cmake`: | CMake Aurgements | Allowed value | Default value | Usage | | ------------------------ | ------------------- | ------------- | ------------------------| -| -DTENSORFLOW_ROOT=<value> | Path | - | The Path to TensorFlow's C++ interface. | +| -DENABLE_TENSORFLOW=<value> | `TRUE` or `FALSE` | `FALSE` | {{ tensorflow_icon }} Whether building the TensorFlow backend. | +| -DENABLE_PYTORCH=<value> | `TRUE` or `FALSE` | `FALSE` | {{ pytorch_icon }} Whether building the PyTorch backend. | +| -DTENSORFLOW_ROOT=<value> | Path | - | {{ tensorflow_icon }} The Path to TensorFlow's C++ interface. | | -DCMAKE_INSTALL_PREFIX=<value> | Path | - | The Path where DeePMD-kit will be installed. | | -DUSE_CUDA_TOOLKIT=<value> | `TRUE` or `FALSE` | `FALSE` | If `TRUE`, Build GPU support with CUDA toolkit. | | -DCUDAToolkit_ROOT=<value> | Path | Detected automatically | The path to the CUDA toolkit directory. CUDA 9.0 or later is supported. NVCC is required. | | -DUSE_ROCM_TOOLKIT=<value> | `TRUE` or `FALSE` | `FALSE` | If `TRUE`, Build GPU support with ROCM toolkit. | | -DCMAKE_HIP_COMPILER_ROCM_ROOT=<value> | Path | Detected automatically | The path to the ROCM toolkit directory. | | -DLAMMPS_SOURCE_ROOT=<value> | Path | - | Only neccessary for LAMMPS plugin mode. The path to the [LAMMPS source code](install-lammps.md). LAMMPS 8Apr2021 or later is supported. If not assigned, the plugin mode will not be enabled. | -| -DUSE_TF_PYTHON_LIBS=<value> | `TRUE` or `FALSE` | `FALSE` | If `TRUE`, Build C++ interface with TensorFlow's Python libraries(TensorFlow's Python Interface is required). And there's no need for building TensorFlow's C++ interface.| +| -DUSE_TF_PYTHON_LIBS=<value> | `TRUE` or `FALSE` | `FALSE` | {{ tensorflow_icon }} If `TRUE`, Build C++ interface with TensorFlow's Python libraries (TensorFlow's Python Interface is required). And there's no need for building TensorFlow's C++ interface.| | -DENABLE_NATIVE_OPTIMIZATION=<value> | `TRUE` or `FALSE` | `FALSE` | Enable compilation optimization for the native machine's CPU type. Do not enable it if generated code will run on different CPUs. | | -DCMAKE_<LANG>_FLAGS=<value> (``=`CXX`, `CUDA` or `HIP`) | str | - | Default compilation flags to be used when compiling `` files. See [CMake documentation](https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_FLAGS.html). | diff --git a/doc/model/dpa2.md b/doc/model/dpa2.md new file mode 100644 index 0000000000..e295f6b6bb --- /dev/null +++ b/doc/model/dpa2.md @@ -0,0 +1,5 @@ +# Descriptor DPA-2 {{ pytorch_icon }} + +:::{note} +**Supported backends**: PyTorch {{ pytorch_icon }} +::: diff --git a/doc/model/index.rst b/doc/model/index.rst index 1e850cac67..7b7fb082f1 100644 --- a/doc/model/index.rst +++ b/doc/model/index.rst @@ -9,6 +9,7 @@ Model train-se-e2-r train-se-e3 train-se-atten + dpa2 train-hybrid sel train-energy diff --git a/doc/model/train-energy.md b/doc/model/train-energy.md index 74a933c79c..a4760b8375 100644 --- a/doc/model/train-energy.md +++ b/doc/model/train-energy.md @@ -1,7 +1,7 @@ -# Fit energy {{ tensorflow_icon }} +# Fit energy {{ tensorflow_icon }} {{ pytorch_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }} ::: In this section, we will take `$deepmd_source_dir/examples/water/se_e2_a/input.json` as an example of the input file. diff --git a/doc/model/train-se-atten.md b/doc/model/train-se-atten.md index b4e346327d..1ac1b33519 100644 --- a/doc/model/train-se-atten.md +++ b/doc/model/train-se-atten.md @@ -1,7 +1,7 @@ -# Descriptor `"se_atten"` {{ tensorflow_icon }} +# Descriptor `"se_atten"` {{ tensorflow_icon }} {{ pytorch_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }} ::: ## DPA-1: Pretraining of Attention-based Deep Potential Model for Molecular Simulation diff --git a/doc/model/train-se-e2-a.md b/doc/model/train-se-e2-a.md index d40bb513ea..22e5c20cb9 100644 --- a/doc/model/train-se-e2-a.md +++ b/doc/model/train-se-e2-a.md @@ -1,7 +1,7 @@ -# Descriptor `"se_e2_a"` {{ tensorflow_icon }} +# Descriptor `"se_e2_a"` {{ tensorflow_icon }} {{ pytorch_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }} ::: The notation of `se_e2_a` is short for the Deep Potential Smooth Edition (DeepPot-SE) constructed from all information (both angular and radial) of atomic configurations. The `e2` stands for the embedding with two-atoms information. This descriptor was described in detail in [the DeepPot-SE paper](https://arxiv.org/abs/1805.09003). diff --git a/doc/train/finetuning.md b/doc/train/finetuning.md index bbab74f41e..e4fa00e23d 100644 --- a/doc/train/finetuning.md +++ b/doc/train/finetuning.md @@ -1,7 +1,7 @@ -# Finetune the pretrained model {{ tensorflow_icon }} +# Finetune the pretrained model {{ tensorflow_icon }} {{ pytorch_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }} ::: Pretraining-and-finetuning is a widely used approach in other fields such as Computer Vision (CV) or Natural Language Processing (NLP) diff --git a/doc/train/multi-task-training.md b/doc/train/multi-task-training.md index 76f404ab88..974606190e 100644 --- a/doc/train/multi-task-training.md +++ b/doc/train/multi-task-training.md @@ -3,6 +3,7 @@ :::{note} **Supported backends**: TensorFlow {{ tensorflow_icon }} ::: + ## Theory From 4a29c8c77771917bb67a6151f8c7fd82a1764b56 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 29 Jan 2024 05:32:23 -0500 Subject: [PATCH 027/686] fix: install CU11 PyTorch in the CU11 docker image (#3198) The default one from PyPI is for CU12. --------- Signed-off-by: Jinzhe Zeng --- source/install/docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/install/docker/Dockerfile b/source/install/docker/Dockerfile index 793272ae6a..1e25fbb6d3 100644 --- a/source/install/docker/Dockerfile +++ b/source/install/docker/Dockerfile @@ -6,7 +6,8 @@ RUN python -m venv /opt/deepmd-kit ENV PATH="/opt/deepmd-kit/bin:$PATH" # Install package COPY dist /dist -RUN pip install "$(ls /dist/deepmd_kit${VARIANT}-*manylinux*_x86_64.whl)[gpu,cu${CUDA_VERSION},lmp,ipi,torch]" \ +RUN if [ "${CUDA_VERSION}" = 11 ]; then pip install torch --index-url https://download.pytorch.org/whl/cu118; fi \ + && pip install "$(ls /dist/deepmd_kit${VARIANT}-*manylinux*_x86_64.whl)[gpu,cu${CUDA_VERSION},lmp,ipi,torch]" \ && dp -h \ && lmp -h \ && dp_ipi \ From f9a7fe8a9ddc3e966712f74e43fe8dda5a040cb2 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 29 Jan 2024 22:44:30 -0500 Subject: [PATCH 028/686] throw errors when PyTorch CXX11 ABI is different from TensorFlow (#3201) If so, throw the following error: ``` -- PyTorch CXX11 ABI: 0 CMake Error at CMakeLists.txt:162 (message): PyTorch CXX11 ABI mismatch TensorFlow: 0 != 1 ``` Signed-off-by: Jinzhe Zeng --- source/CMakeLists.txt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index c273bc9263..6a95ce3633 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -154,7 +154,22 @@ if(ENABLE_TENSORFLOW AND NOT DEEPMD_C_ROOT) endif() if(ENABLE_PYTORCH AND NOT DEEPMD_C_ROOT) find_package(Torch REQUIRED) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}") + string(REGEX MATCH "_GLIBCXX_USE_CXX11_ABI=([0-9]+)" CXXABI_PT_MATCH + ${TORCH_CXX_FLAGS}) + if(CXXABI_PT_MATCH) + message(STATUS "PyTorch CXX11 ABI: ${CMAKE_MATCH_1}") + if(DEFINED OP_CXX_ABI) + if(NOT ${CMAKE_MATCH_1} EQUAL ${OP_CXX_ABI}) + message( + FATAL_ERROR + "PyTorch CXX11 ABI mismatch TensorFlow: ${CMAKE_MATCH_1} != ${OP_CXX_ABI}" + ) + endif() + else() + set(OP_CXX_ABI ${CMAKE_MATCH_1}) + add_definitions(-D_GLIBCXX_USE_CXX11_ABI=${OP_CXX_ABI}) + endif() + endif() endif() # log enabled backends if(NOT DEEPMD_C_ROOT) From de18f783ac8bea7977625bc2d978297a1f0d7872 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 29 Jan 2024 22:47:10 -0500 Subject: [PATCH 029/686] allow disabling TensorFlow backend during Python installation (#3200) Fix #3120. One can disable building the TensorFlow backend during `pip install` by setting `DP_ENABLE_TENSORFLOW=0`. --------- Signed-off-by: Jinzhe Zeng --- backend/find_tensorflow.py | 6 ++++++ backend/read_env.py | 24 +++++++++++++++++------- deepmd/tf/env.py | 5 +++++ doc/install/install-from-source.md | 15 +++++++++++++-- source/CMakeLists.txt | 4 +++- source/config/CMakeLists.txt | 14 ++++++++++++++ source/config/run_config.ini | 2 ++ source/lib/src/gpu/CMakeLists.txt | 6 ++++-- 8 files changed, 64 insertions(+), 12 deletions(-) diff --git a/backend/find_tensorflow.py b/backend/find_tensorflow.py index 32ae62469c..083e2673f7 100644 --- a/backend/find_tensorflow.py +++ b/backend/find_tensorflow.py @@ -127,6 +127,12 @@ def get_tf_requirement(tf_version: str = "") -> dict: dict TensorFlow requirement, including cpu and gpu. """ + if tf_version is None: + return { + "cpu": [], + "gpu": [], + "mpi": [], + } if tf_version == "": tf_version = os.environ.get("TENSORFLOW_VERSION", "") diff --git a/backend/read_env.py b/backend/read_env.py index 2cf433181a..bee5d607e3 100644 --- a/backend/read_env.py +++ b/backend/read_env.py @@ -80,16 +80,26 @@ def get_argument_from_env() -> Tuple[str, list, list, dict, str]: cmake_args.append("-DENABLE_IPI:BOOL=TRUE") extra_scripts["dp_ipi"] = "deepmd.tf.entrypoints.ipi:dp_ipi" - tf_install_dir, _ = find_tensorflow() - tf_version = get_tf_version(tf_install_dir) - if tf_version == "" or Version(tf_version) >= Version("2.12"): - find_libpython_requires = [] + if os.environ.get("DP_ENABLE_TENSORFLOW", "1") == "1": + tf_install_dir, _ = find_tensorflow() + tf_version = get_tf_version(tf_install_dir) + if tf_version == "" or Version(tf_version) >= Version("2.12"): + find_libpython_requires = [] + else: + find_libpython_requires = ["find_libpython"] + cmake_args.extend( + [ + "-DENABLE_TENSORFLOW=ON", + f"-DTENSORFLOW_VERSION={tf_version}", + f"-DTENSORFLOW_ROOT:PATH={tf_install_dir}", + ] + ) else: - find_libpython_requires = ["find_libpython"] - cmake_args.append(f"-DTENSORFLOW_VERSION={tf_version}") + find_libpython_requires = [] + cmake_args.append("-DENABLE_TENSORFLOW=OFF") + tf_version = None cmake_args = [ - f"-DTENSORFLOW_ROOT:PATH={tf_install_dir}", "-DBUILD_PY_IF:BOOL=TRUE", *cmake_args, ] diff --git a/deepmd/tf/env.py b/deepmd/tf/env.py index da03631689..eada2774d3 100644 --- a/deepmd/tf/env.py +++ b/deepmd/tf/env.py @@ -472,6 +472,11 @@ def _get_package_constants( GLOBAL_CONFIG = _get_package_constants() +if GLOBAL_CONFIG["enable_tensorflow"] == "0": + raise RuntimeError( + "TensorFlow backend is not built. To enable it, " + "set the environmental variable DP_ENABLE_TENSORFLOW=1." + ) MODEL_VERSION = GLOBAL_CONFIG["model_version"] TF_VERSION = GLOBAL_CONFIG["tf_version"] TF_CXX11_ABI_FLAG = int(GLOBAL_CONFIG["tf_cxx11_abi_flag"]) diff --git a/doc/install/install-from-source.md b/doc/install/install-from-source.md index ae1509f2ca..389cc78c9f 100644 --- a/doc/install/install-from-source.md +++ b/doc/install/install-from-source.md @@ -90,7 +90,17 @@ Check the compiler version on your machine gcc --version ``` -The compiler GCC 4.8 or later is supported in the DeePMD-kit. Note that TensorFlow may have specific requirements for the compiler version to support the C++ standard version and [`_GLIBCXX_USE_CXX11_ABI`](https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_dual_abi.html) used by TensorFlow. It is recommended to use [the same compiler version as TensorFlow](https://www.tensorflow.org/install/source#tested_build_configurations), which can be printed by `python -c "import tensorflow;print(tensorflow.version.COMPILER_VERSION)"`. +The compiler GCC 4.8 or later is supported in the DeePMD-kit. + +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} + +Note that TensorFlow may have specific requirements for the compiler version to support the C++ standard version and [`_GLIBCXX_USE_CXX11_ABI`](https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_dual_abi.html) used by TensorFlow. It is recommended to use [the same compiler version as TensorFlow](https://www.tensorflow.org/install/source#tested_build_configurations), which can be printed by `python -c "import tensorflow;print(tensorflow.version.COMPILER_VERSION)"`. + +::: + +:::: Execute ```bash @@ -105,7 +115,8 @@ One may set the following environment variables before executing `pip`: | DP_VARIANT | `cpu`, `cuda`, `rocm` | `cpu` | Build CPU variant or GPU variant with CUDA or ROCM support. | | CUDAToolkit_ROOT | Path | Detected automatically | The path to the CUDA toolkit directory. CUDA 9.0 or later is supported. NVCC is required. | | ROCM_ROOT | Path | Detected automatically | The path to the ROCM toolkit directory. | -| TENSORFLOW_ROOT | Path | Detected automatically | The path to TensorFlow Python library. By default the installer only finds TensorFlow under user site-package directory (`site.getusersitepackages()`) or system site-package directory (`sysconfig.get_path("purelib")`) due to limitation of [PEP-517](https://peps.python.org/pep-0517/). If not found, the latest TensorFlow (or the environment variable `TENSORFLOW_VERSION` if given) from PyPI will be built against.| +| DP_ENABLE_TENSORFLOW | 0, 1 | 1 | {{ tensorflow_icon }} Enable the TensorFlow backend. +| TENSORFLOW_ROOT | Path | Detected automatically | {{ tensorflow_icon }} The path to TensorFlow Python library. By default the installer only finds TensorFlow under user site-package directory (`site.getusersitepackages()`) or system site-package directory (`sysconfig.get_path("purelib")`) due to limitation of [PEP-517](https://peps.python.org/pep-0517/). If not found, the latest TensorFlow (or the environment variable `TENSORFLOW_VERSION` if given) from PyPI will be built against.| | DP_ENABLE_NATIVE_OPTIMIZATION | 0, 1 | 0 | Enable compilation optimization for the native machine's CPU type. Do not enable it if generated code will run on different CPUs. | | CMAKE_ARGS | str | - | Additional CMake arguments | | <LANG>FLAGS (``=`CXX`, `CUDA` or `HIP`) | str | - | Default compilation flags to be used when compiling `` files. See [CMake documentation](https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_FLAGS.html). | diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 6a95ce3633..d6ee3d0958 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -180,7 +180,9 @@ if(NOT DEEPMD_C_ROOT) if(ENABLE_PYTORCH) message(STATUS "- PyTorch") endif() - if(NOT ENABLE_TENSORFLOW AND NOT ENABLE_PYTORCH) + if(NOT ENABLE_TENSORFLOW + AND NOT ENABLE_PYTORCH + AND NOT BUILD_PY_IF) message(FATAL_ERROR "No backend is enabled.") endif() endif() diff --git a/source/config/CMakeLists.txt b/source/config/CMakeLists.txt index 5473b91f29..b1ce17566f 100644 --- a/source/config/CMakeLists.txt +++ b/source/config/CMakeLists.txt @@ -1,5 +1,19 @@ # config +# cmake will treat true, false, on, off, 1, 0 as booleans we hope an easy way to +# check it +if(ENABLE_TENSORFLOW) + set(ENABLE_TENSORFLOW 1) +else() + set(ENABLE_TENSORFLOW 0) +endif() + +if(ENABLE_PYTORCH) + set(ENABLE_PYTORCH 1) +else() + set(ENABLE_PYTORCH 0) +endif() + configure_file("run_config.ini" "${CMAKE_CURRENT_BINARY_DIR}/run_config.ini" @ONLY) diff --git a/source/config/run_config.ini b/source/config/run_config.ini index 3f0a7a33a8..11f4100e61 100644 --- a/source/config/run_config.ini +++ b/source/config/run_config.ini @@ -4,6 +4,8 @@ GIT_SUMM = @GIT_SUMM@ GIT_HASH = @GIT_HASH@ GIT_DATE = @GIT_DATE@ GIT_BRANCH = @GIT_BRANCH@ +ENABLE_TENSORFLOW = @ENABLE_TENSORFLOW@ +ENABLE_PYTORCH = @ENABLE_PYTORCH@ TF_INCLUDE_DIR = @TensorFlow_INCLUDE_DIRS@ TF_LIBS = @TensorFlow_LIBRARY@ TF_VERSION = @TENSORFLOW_VERSION@ diff --git a/source/lib/src/gpu/CMakeLists.txt b/source/lib/src/gpu/CMakeLists.txt index 3bd24cc620..804e1c0506 100644 --- a/source/lib/src/gpu/CMakeLists.txt +++ b/source/lib/src/gpu/CMakeLists.txt @@ -10,8 +10,10 @@ if(USE_CUDA_TOOLKIT) endif() enable_language(CUDA) set(CMAKE_CUDA_STANDARD 11) - add_compile_definitions( - "$<$:_GLIBCXX_USE_CXX11_ABI=${OP_CXX_ABI}>") + if(DEFINED OP_CXX_ABI) + add_compile_definitions( + "$<$:_GLIBCXX_USE_CXX11_ABI=${OP_CXX_ABI}>") + endif() find_package(CUDAToolkit REQUIRED) From c6a6b59cef5cd28c80f9ba5d9dc952ac1c2e692c Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Tue, 30 Jan 2024 13:56:24 +0800 Subject: [PATCH 030/686] breaking: pt: add dp model format and refactor pt impl for the fitting net. (#3199) - add dp model format (backend independent definition) for the fitting - refactor torch support, compatible with dp model format - fix mlp issue: the idt should only be used when a skip connection is available. - add tools `to_numpy_array` and `to_torch_tensor`. --------- Co-authored-by: Han Wang --- deepmd/model_format/__init__.py | 4 + deepmd/model_format/fitting.py | 355 +++++++++++++++++ deepmd/model_format/network.py | 2 + deepmd/model_format/se_e2_a.py | 10 +- deepmd/pt/model/model/dp_atomic_model.py | 10 +- deepmd/pt/model/network/mlp.py | 7 +- deepmd/pt/model/task/ener.py | 374 +++++++++++++++--- deepmd/pt/model/task/fitting.py | 13 +- deepmd/pt/model/task/task.py | 18 +- deepmd/pt/utils/utils.py | 40 ++ .../tests/common/test_model_format_utils.py | 121 ++++++ source/tests/pt/test_ener_fitting.py | 181 +++++++++ source/tests/pt/test_fitting_net.py | 24 +- source/tests/pt/test_model.py | 25 +- source/tests/pt/test_se_e2_a.py | 33 +- source/tests/pt/test_utils.py | 31 ++ 16 files changed, 1118 insertions(+), 130 deletions(-) create mode 100644 deepmd/model_format/fitting.py create mode 100644 source/tests/pt/test_ener_fitting.py create mode 100644 source/tests/pt/test_utils.py diff --git a/deepmd/model_format/__init__.py b/deepmd/model_format/__init__.py index 253bca3507..e15f73758e 100644 --- a/deepmd/model_format/__init__.py +++ b/deepmd/model_format/__init__.py @@ -7,6 +7,9 @@ from .env_mat import ( EnvMat, ) +from .fitting import ( + InvarFitting, +) from .network import ( EmbeddingNet, FittingNet, @@ -34,6 +37,7 @@ ) __all__ = [ + "InvarFitting", "DescrptSeA", "EnvMat", "make_multilayer_network", diff --git a/deepmd/model_format/fitting.py b/deepmd/model_format/fitting.py new file mode 100644 index 0000000000..904fb42b76 --- /dev/null +++ b/deepmd/model_format/fitting.py @@ -0,0 +1,355 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +from typing import ( + Any, + List, + Optional, +) + +import numpy as np + +from .common import ( + DEFAULT_PRECISION, + NativeOP, +) +from .network import ( + FittingNet, + NetworkCollection, +) +from .output_def import ( + FittingOutputDef, + OutputVariableDef, + fitting_check_output, +) + + +@fitting_check_output +class InvarFitting(NativeOP): + r"""Fitting the energy (or a porperty of `dim_out`) of the system. The force and the virial can also be trained. + + Lets take the energy fitting task as an example. + The potential energy :math:`E` is a fitting network function of the descriptor :math:`\mathcal{D}`: + + .. math:: + E(\mathcal{D}) = \mathcal{L}^{(n)} \circ \mathcal{L}^{(n-1)} + \circ \cdots \circ \mathcal{L}^{(1)} \circ \mathcal{L}^{(0)} + + The first :math:`n` hidden layers :math:`\mathcal{L}^{(0)}, \cdots, \mathcal{L}^{(n-1)}` are given by + + .. math:: + \mathbf{y}=\mathcal{L}(\mathbf{x};\mathbf{w},\mathbf{b})= + \boldsymbol{\phi}(\mathbf{x}^T\mathbf{w}+\mathbf{b}) + + where :math:`\mathbf{x} \in \mathbb{R}^{N_1}` is the input vector and :math:`\mathbf{y} \in \mathbb{R}^{N_2}` + is the output vector. :math:`\mathbf{w} \in \mathbb{R}^{N_1 \times N_2}` and + :math:`\mathbf{b} \in \mathbb{R}^{N_2}` are weights and biases, respectively, + both of which are trainable if `trainable[i]` is `True`. :math:`\boldsymbol{\phi}` + is the activation function. + + The output layer :math:`\mathcal{L}^{(n)}` is given by + + .. math:: + \mathbf{y}=\mathcal{L}^{(n)}(\mathbf{x};\mathbf{w},\mathbf{b})= + \mathbf{x}^T\mathbf{w}+\mathbf{b} + + where :math:`\mathbf{x} \in \mathbb{R}^{N_{n-1}}` is the input vector and :math:`\mathbf{y} \in \mathbb{R}` + is the output scalar. :math:`\mathbf{w} \in \mathbb{R}^{N_{n-1}}` and + :math:`\mathbf{b} \in \mathbb{R}` are weights and bias, respectively, + both of which are trainable if `trainable[n]` is `True`. + + Parameters + ---------- + var_name + The name of the output variable. + ntypes + The number of atom types. + dim_descrpt + The dimension of the input descriptor. + dim_out + The dimension of the output fit property. + neuron + Number of neurons :math:`N` in each hidden layer of the fitting net + resnet_dt + Time-step `dt` in the resnet construction: + :math:`y = x + dt * \phi (Wx + b)` + numb_fparam + Number of frame parameter + numb_aparam + Number of atomic parameter + rcond + The condition number for the regression of atomic energy. + tot_ener_zero + Force the total energy to zero. Useful for the charge fitting. + trainable + If the weights of fitting net are trainable. + Suppose that we have :math:`N_l` hidden layers in the fitting net, + this list is of length :math:`N_l + 1`, specifying if the hidden layers and the output layer are trainable. + atom_ener + Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. + activation_function + The activation function :math:`\boldsymbol{\phi}` in the embedding net. Supported options are |ACTIVATION_FN| + precision + The precision of the embedding net parameters. Supported options are |PRECISION| + layer_name : list[Optional[str]], optional + The name of the each layer. If two layers, either in the same fitting or different fittings, + have the same name, they will share the same neural network parameters. + use_aparam_as_mask: bool, optional + If True, the atomic parameters will be used as a mask that determines the atom is real/virtual. + And the aparam will not be used as the atomic parameters for embedding. + distinguish_types + Different atomic types uses different fitting net. + + """ + + def __init__( + self, + var_name: str, + ntypes: int, + dim_descrpt: int, + dim_out: int, + neuron: List[int] = [120, 120, 120], + resnet_dt: bool = True, + numb_fparam: int = 0, + numb_aparam: int = 0, + rcond: Optional[float] = None, + tot_ener_zero: bool = False, + trainable: Optional[List[bool]] = None, + atom_ener: Optional[List[float]] = None, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + layer_name: Optional[List[Optional[str]]] = None, + use_aparam_as_mask: bool = False, + spin: Any = None, + distinguish_types: bool = False, + ): + # seed, uniform_seed are not included + if tot_ener_zero: + raise NotImplementedError("tot_ener_zero is not implemented") + if spin is not None: + raise NotImplementedError("spin is not implemented") + if use_aparam_as_mask: + raise NotImplementedError("use_aparam_as_mask is not implemented") + if use_aparam_as_mask: + raise NotImplementedError("use_aparam_as_mask is not implemented") + if layer_name is not None: + raise NotImplementedError("layer_name is not implemented") + if atom_ener is not None: + raise NotImplementedError("atom_ener is not implemented") + + self.var_name = var_name + self.ntypes = ntypes + self.dim_descrpt = dim_descrpt + self.dim_out = dim_out + self.neuron = neuron + self.resnet_dt = resnet_dt + self.numb_fparam = numb_fparam + self.numb_aparam = numb_aparam + self.rcond = rcond + self.tot_ener_zero = tot_ener_zero + self.trainable = trainable + self.atom_ener = atom_ener + self.activation_function = activation_function + self.precision = precision + self.layer_name = layer_name + self.use_aparam_as_mask = use_aparam_as_mask + self.spin = spin + self.distinguish_types = distinguish_types + if self.spin is not None: + raise NotImplementedError("spin is not supported") + + # init constants + self.bias_atom_e = np.zeros([self.ntypes, self.dim_out]) + if self.numb_fparam > 0: + self.fparam_avg = np.zeros(self.numb_fparam) + self.fparam_inv_std = np.ones(self.numb_fparam) + else: + self.fparam_avg, self.fparam_inv_std = None, None + if self.numb_aparam > 0: + self.aparam_avg = np.zeros(self.numb_aparam) + self.aparam_inv_std = np.ones(self.numb_aparam) + else: + self.aparam_avg, self.aparam_inv_std = None, None + # init networks + in_dim = self.dim_descrpt + self.numb_fparam + self.numb_aparam + out_dim = self.dim_out + self.nets = NetworkCollection( + 1 if self.distinguish_types else 0, + self.ntypes, + network_type="fitting_network", + networks=[ + FittingNet( + in_dim, + out_dim, + self.neuron, + self.activation_function, + self.resnet_dt, + self.precision, + bias_out=True, + ) + for ii in range(self.ntypes if self.distinguish_types else 1) + ], + ) + + def output_def(self): + return FittingOutputDef( + [ + OutputVariableDef( + self.var_name, [self.dim_out], reduciable=True, differentiable=True + ), + ] + ) + + def __setitem__(self, key, value): + if key in ["bias_atom_e"]: + self.bias_atom_e = value + elif key in ["fparam_avg"]: + self.fparam_avg = value + elif key in ["fparam_inv_std"]: + self.fparam_inv_std = value + elif key in ["aparam_avg"]: + self.aparam_avg = value + elif key in ["aparam_inv_std"]: + self.aparam_inv_std = value + else: + raise KeyError(key) + + def __getitem__(self, key): + if key in ["bias_atom_e"]: + return self.bias_atom_e + elif key in ["fparam_avg"]: + return self.fparam_avg + elif key in ["fparam_inv_std"]: + return self.fparam_inv_std + elif key in ["aparam_avg"]: + return self.aparam_avg + elif key in ["aparam_inv_std"]: + return self.aparam_inv_std + else: + raise KeyError(key) + + def serialize(self) -> dict: + """Serialize the fitting to dict.""" + return { + "var_name": self.var_name, + "ntypes": self.ntypes, + "dim_descrpt": self.dim_descrpt, + "dim_out": self.dim_out, + "neuron": self.neuron, + "resnet_dt": self.resnet_dt, + "numb_fparam": self.numb_fparam, + "numb_aparam": self.numb_aparam, + "rcond": self.rcond, + "activation_function": self.activation_function, + "precision": self.precision, + "distinguish_types": self.distinguish_types, + "nets": self.nets.serialize(), + "@variables": { + "bias_atom_e": self.bias_atom_e, + "fparam_avg": self.fparam_avg, + "fparam_inv_std": self.fparam_inv_std, + "aparam_avg": self.aparam_avg, + "aparam_inv_std": self.aparam_inv_std, + }, + # not supported + "tot_ener_zero": self.tot_ener_zero, + "trainable": self.trainable, + "atom_ener": self.atom_ener, + "layer_name": self.layer_name, + "use_aparam_as_mask": self.use_aparam_as_mask, + "spin": self.spin, + } + + @classmethod + def deserialize(cls, data: dict) -> "InvarFitting": + data = copy.deepcopy(data) + variables = data.pop("@variables") + nets = data.pop("nets") + obj = cls(**data) + for kk in variables.keys(): + obj[kk] = variables[kk] + obj.nets = NetworkCollection.deserialize(nets) + return obj + + def call( + self, + descriptor: np.array, + atype: np.array, + gr: Optional[np.array] = None, + g2: Optional[np.array] = None, + h2: Optional[np.array] = None, + fparam: Optional[np.array] = None, + aparam: Optional[np.array] = None, + ): + """Calculate the fitting. + + Parameters + ---------- + descriptor + input descriptor. shape: nf x nloc x nd + atype + the atom type. shape: nf x nloc + gr + The rotationally equivariant and permutationally invariant single particle + representation. shape: nf x nloc x ng x 3 + g2 + The rotationally invariant pair-partical representation. + shape: nf x nloc x nnei x ng + h2 + The rotationally equivariant pair-partical representation. + shape: nf x nloc x nnei x 3 + fparam + The frame parameter. shape: nf x nfp. nfp being `numb_fparam` + aparam + The atomic parameter. shape: nf x nloc x nap. nap being `numb_aparam` + + """ + nf, nloc, nd = descriptor.shape + # check input dim + if nd != self.dim_descrpt: + raise ValueError( + "get an input descriptor of dim {nd}," + "which is not consistent with {self.dim_descrpt}." + ) + xx = descriptor + # check fparam dim, concate to input descriptor + if self.numb_fparam > 0: + assert fparam is not None, "fparam should not be None" + if fparam.shape[-1] != self.numb_fparam: + raise ValueError( + "get an input fparam of dim {fparam.shape[-1]}, ", + "which is not consistent with {self.numb_fparam}.", + ) + fparam = (fparam - self.fparam_avg) * self.fparam_inv_std + fparam = np.tile(fparam.reshape([nf, 1, -1]), [1, nloc, 1]) + xx = np.concatenate( + [xx, fparam], + axis=-1, + ) + # check aparam dim, concate to input descriptor + if self.numb_aparam > 0: + assert aparam is not None, "aparam should not be None" + if aparam.shape[-1] != self.numb_aparam: + raise ValueError( + "get an input aparam of dim {aparam.shape[-1]}, ", + "which is not consistent with {self.numb_aparam}.", + ) + aparam = (aparam - self.aparam_avg) * self.aparam_inv_std + xx = np.concatenate( + [xx, aparam], + axis=-1, + ) + + # calcualte the prediction + if self.distinguish_types: + outs = np.zeros([nf, nloc, self.dim_out]) + for type_i in range(self.ntypes): + mask = np.tile( + (atype == type_i).reshape([nf, nloc, 1]), [1, 1, self.dim_out] + ) + atom_energy = self.nets[(type_i,)](xx) + atom_energy = atom_energy + self.bias_atom_e[type_i] + atom_energy = atom_energy * mask + outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] + else: + outs = self.nets[()](xx) + self.bias_atom_e[atype] + return {self.var_name: outs} diff --git a/deepmd/model_format/network.py b/deepmd/model_format/network.py index a327d990c9..f2056c0b95 100644 --- a/deepmd/model_format/network.py +++ b/deepmd/model_format/network.py @@ -161,6 +161,8 @@ def __init__( ) -> None: prec = PRECISION_DICT[precision.lower()] self.precision = precision + # only use_timestep when skip connection is established. + use_timestep = use_timestep and (num_out == num_in or num_out == num_in * 2) rng = np.random.default_rng() self.w = rng.normal(size=(num_in, num_out)).astype(prec) self.b = rng.normal(size=(num_out,)).astype(prec) if bias else None diff --git a/deepmd/model_format/se_e2_a.py b/deepmd/model_format/se_e2_a.py index 28751cad8d..f179b10ac3 100644 --- a/deepmd/model_format/se_e2_a.py +++ b/deepmd/model_format/se_e2_a.py @@ -171,9 +171,8 @@ def __init__( ) self.env_mat = EnvMat(self.rcut, self.rcut_smth) self.nnei = np.sum(self.sel) - self.nneix4 = self.nnei * 4 - self.davg = np.zeros([self.ntypes, self.nneix4]) - self.dstd = np.ones([self.ntypes, self.nneix4]) + self.davg = np.zeros([self.ntypes, self.nnei, 4]) + self.dstd = np.ones([self.ntypes, self.nnei, 4]) self.orig_sel = self.sel def __setitem__(self, key, value): @@ -192,6 +191,11 @@ def __getitem__(self, key): else: raise KeyError(key) + @property + def dim_out(self): + """Returns the output dimension of this descriptor.""" + return self.neuron[-1] * self.axis_neuron + def cal_g( self, ss, diff --git a/deepmd/pt/model/model/dp_atomic_model.py b/deepmd/pt/model/model/dp_atomic_model.py index 853eacb875..a222c8e6f6 100644 --- a/deepmd/pt/model/model/dp_atomic_model.py +++ b/deepmd/pt/model/model/dp_atomic_model.py @@ -93,11 +93,11 @@ def __init__( ) fitting_net["type"] = fitting_net.get("type", "ener") - if self.descriptor_type not in ["se_e2_a"]: - fitting_net["ntypes"] = 1 + fitting_net["ntypes"] = self.descriptor.get_ntype() + if self.descriptor_type in ["se_e2_a"]: + fitting_net["distinguish_types"] = True else: - fitting_net["ntypes"] = self.descriptor.get_ntype() - fitting_net["use_tebd"] = False + fitting_net["distinguish_types"] = False fitting_net["embedding_width"] = self.descriptor.dim_out self.grad_force = "direct" not in fitting_net["type"] @@ -165,5 +165,5 @@ def forward_atomic( ) assert descriptor is not None # energy, force - fit_ret = self.fitting_net(descriptor, atype, atype_tebd=None, rot_mat=rot_mat) + fit_ret = self.fitting_net(descriptor, atype, gr=rot_mat) return fit_ret diff --git a/deepmd/pt/model/network/mlp.py b/deepmd/pt/model/network/mlp.py index e3ac0e7bc2..d76abd82f9 100644 --- a/deepmd/pt/model/network/mlp.py +++ b/deepmd/pt/model/network/mlp.py @@ -56,7 +56,10 @@ def __init__( precision: str = DEFAULT_PRECISION, ): super().__init__() - self.use_timestep = use_timestep + # only use_timestep when skip connection is established. + self.use_timestep = use_timestep and ( + num_out == num_in or num_out == num_in * 2 + ) self.activate_name = activation_function self.activate = ActivationFn(self.activate_name) self.precision = precision @@ -207,7 +210,7 @@ class NetworkCollection(DPNetworkCollection, nn.Module): NETWORK_TYPE_MAP: ClassVar[Dict[str, type]] = { "network": MLP, "embedding_network": EmbeddingNet, - # "fitting_network": FittingNet, + "fitting_network": FittingNet, } def __init__(self, *args, **kwargs): diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 03043e2fcb..91ccd03d9a 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -1,10 +1,13 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy import logging from typing import ( + List, Optional, Tuple, ) +import numpy as np import torch from deepmd.model_format import ( @@ -12,6 +15,10 @@ OutputVariableDef, fitting_check_output, ) +from deepmd.pt.model.network.mlp import ( + FittingNet, + NetworkCollection, +) from deepmd.pt.model.network.network import ( ResidualDeep, ) @@ -21,19 +28,35 @@ from deepmd.pt.utils import ( env, ) +from deepmd.pt.utils.env import ( + DEFAULT_PRECISION, + PRECISION_DICT, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, + to_torch_tensor, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION +device = env.DEVICE -@Fitting.register("ener") @fitting_check_output -class EnergyFittingNet(Fitting): +class InvarFitting(Fitting): def __init__( self, - ntypes, - embedding_width, - neuron, - bias_atom_e, - resnet_dt=True, - use_tebd=True, + var_name: str, + ntypes: int, + dim_descrpt: int, + dim_out: int, + neuron: List[int] = [128, 128, 128], + bias_atom_e: Optional[torch.Tensor] = None, + resnet_dt: bool = True, + numb_fparam: int = 0, + numb_aparam: int = 0, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + distinguish_types: bool = False, **kwargs, ): """Construct a fitting net for energy. @@ -46,67 +69,323 @@ def __init__( - resnet_dt: Using time-step in the ResNet construction. """ super().__init__() + self.var_name = var_name self.ntypes = ntypes - self.embedding_width = embedding_width - self.use_tebd = use_tebd - if not use_tebd: - assert self.ntypes == len(bias_atom_e), "Element count mismatches!" - bias_atom_e = torch.tensor(bias_atom_e) + self.dim_descrpt = dim_descrpt + self.dim_out = dim_out + self.neuron = neuron + self.distinguish_types = distinguish_types + self.use_tebd = not self.distinguish_types + self.resnet_dt = resnet_dt + self.numb_fparam = numb_fparam + self.numb_aparam = numb_aparam + self.activation_function = activation_function + self.precision = precision + self.prec = PRECISION_DICT[self.precision] + if bias_atom_e is None: + bias_atom_e = np.zeros([self.ntypes, self.dim_out]) + bias_atom_e = torch.tensor(bias_atom_e, dtype=self.prec, device=device) + bias_atom_e = bias_atom_e.view([self.ntypes, self.dim_out]) + if not self.use_tebd: + assert self.ntypes == bias_atom_e.shape[0], "Element count mismatches!" self.register_buffer("bias_atom_e", bias_atom_e) + # init constants + if self.numb_fparam > 0: + self.register_buffer( + "fparam_avg", + torch.zeros(self.numb_fparam, dtype=self.prec, device=device), + ) + self.register_buffer( + "fparam_inv_std", + torch.ones(self.numb_fparam, dtype=self.prec, device=device), + ) + else: + self.fparam_avg, self.fparam_inv_std = None, None + if self.numb_aparam > 0: + self.register_buffer( + "aparam_avg", + torch.zeros(self.numb_aparam, dtype=self.prec, device=device), + ) + self.register_buffer( + "aparam_inv_std", + torch.ones(self.numb_aparam, dtype=self.prec, device=device), + ) + else: + self.aparam_avg, self.aparam_inv_std = None, None - filter_layers = [] - for type_i in range(self.ntypes): - bias_type = 0.0 - one = ResidualDeep( - type_i, embedding_width, neuron, bias_type, resnet_dt=resnet_dt + in_dim = self.dim_descrpt + self.numb_fparam + self.numb_aparam + out_dim = 1 + + self.old_impl = kwargs.get("old_impl", False) + if self.old_impl: + filter_layers = [] + for type_i in range(self.ntypes): + bias_type = 0.0 + one = ResidualDeep( + type_i, + self.dim_descrpt, + self.neuron, + bias_type, + resnet_dt=self.resnet_dt, + ) + filter_layers.append(one) + self.filter_layers_old = torch.nn.ModuleList(filter_layers) + self.filter_layers = None + else: + self.filter_layers = NetworkCollection( + 1 if self.distinguish_types else 0, + self.ntypes, + network_type="fitting_network", + networks=[ + FittingNet( + in_dim, + out_dim, + self.neuron, + self.activation_function, + self.resnet_dt, + self.precision, + bias_out=True, + ) + for ii in range(self.ntypes if self.distinguish_types else 1) + ], ) - filter_layers.append(one) - self.filter_layers = torch.nn.ModuleList(filter_layers) + self.filter_layers_old = None + # very bad design... if "seed" in kwargs: logging.info("Set seed to %d in fitting net.", kwargs["seed"]) torch.manual_seed(kwargs["seed"]) - def output_def(self): + def output_def(self) -> FittingOutputDef: return FittingOutputDef( [ - OutputVariableDef("energy", [1], reduciable=True, differentiable=True), + OutputVariableDef( + self.var_name, [self.dim_out], reduciable=True, differentiable=True + ), ] ) + def __setitem__(self, key, value): + if key in ["bias_atom_e"]: + # correct bias_atom_e shape. user may provide stupid shape + self.bias_atom_e = value + elif key in ["fparam_avg"]: + self.fparam_avg = value + elif key in ["fparam_inv_std"]: + self.fparam_inv_std = value + elif key in ["aparam_avg"]: + self.aparam_avg = value + elif key in ["aparam_inv_std"]: + self.aparam_inv_std = value + else: + raise KeyError(key) + + def __getitem__(self, key): + if key in ["bias_atom_e"]: + return self.bias_atom_e + elif key in ["fparam_avg"]: + return self.fparam_avg + elif key in ["fparam_inv_std"]: + return self.fparam_inv_std + elif key in ["aparam_avg"]: + return self.aparam_avg + elif key in ["aparam_inv_std"]: + return self.aparam_inv_std + else: + raise KeyError(key) + + def serialize(self) -> dict: + """Serialize the fitting to dict.""" + return { + "var_name": self.var_name, + "ntypes": self.ntypes, + "dim_descrpt": self.dim_descrpt, + "dim_out": self.dim_out, + "neuron": self.neuron, + "resnet_dt": self.resnet_dt, + "numb_fparam": self.numb_fparam, + "numb_aparam": self.numb_aparam, + "activation_function": self.activation_function, + "precision": self.precision, + "distinguish_types": self.distinguish_types, + "nets": self.filter_layers.serialize(), + "@variables": { + "bias_atom_e": to_numpy_array(self.bias_atom_e), + "fparam_avg": to_numpy_array(self.fparam_avg), + "fparam_inv_std": to_numpy_array(self.fparam_inv_std), + "aparam_avg": to_numpy_array(self.aparam_avg), + "aparam_inv_std": to_numpy_array(self.aparam_inv_std), + }, + # "rcond": self.rcond , + # "tot_ener_zero": self.tot_ener_zero , + # "trainable": self.trainable , + # "atom_ener": self.atom_ener , + # "layer_name": self.layer_name , + # "use_aparam_as_mask": self.use_aparam_as_mask , + # "spin": self.spin , + ## NOTICE: not supported by far + "rcond": None, + "tot_ener_zero": False, + "trainable": True, + "atom_ener": None, + "layer_name": None, + "use_aparam_as_mask": False, + "spin": None, + } + + @classmethod + def deserialize(cls, data: dict) -> "InvarFitting": + data = copy.deepcopy(data) + variables = data.pop("@variables") + nets = data.pop("nets") + obj = cls(**data) + for kk in variables.keys(): + obj[kk] = to_torch_tensor(variables[kk]) + obj.filter_layers = NetworkCollection.deserialize(nets) + return obj + + def _extend_f_avg_std(self, xx: torch.Tensor, nb: int) -> torch.Tensor: + return torch.tile(xx.view([1, self.numb_fparam]), [nb, 1]) + + def _extend_a_avg_std(self, xx: torch.Tensor, nb: int, nloc: int) -> torch.Tensor: + return torch.tile(xx.view([1, 1, self.numb_aparam]), [nb, nloc, 1]) + def forward( self, - inputs: torch.Tensor, + descriptor: torch.Tensor, atype: torch.Tensor, - atype_tebd: Optional[torch.Tensor] = None, - rot_mat: Optional[torch.Tensor] = None, + gr: Optional[torch.Tensor] = None, + g2: Optional[torch.Tensor] = None, + h2: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, ): """Based on embedding net output, alculate total energy. Args: - - inputs: Embedding matrix. Its shape is [nframes, natoms[0], self.embedding_width]. + - inputs: Embedding matrix. Its shape is [nframes, natoms[0], self.dim_descrpt]. - natoms: Tell atom count and element count. Its shape is [2+self.ntypes]. Returns ------- - `torch.Tensor`: Total energy with shape [nframes, natoms[0]]. """ + xx = descriptor + nf, nloc, nd = xx.shape + # NOTICE in tests/pt/test_model.py + # it happens that the user directly access the data memeber self.bias_atom_e + # and set it to a wrong shape! + self.bias_atom_e = self.bias_atom_e.view([self.ntypes, self.dim_out]) + # check input dim + if nd != self.dim_descrpt: + raise ValueError( + "get an input descriptor of dim {nd}," + "which is not consistent with {self.dim_descrpt}." + ) + # check fparam dim, concate to input descriptor + if self.numb_fparam > 0: + assert fparam is not None, "fparam should not be None" + assert self.fparam_avg is not None + assert self.fparam_inv_std is not None + if fparam.shape[-1] != self.numb_fparam: + raise ValueError( + "get an input fparam of dim {fparam.shape[-1]}, ", + "which is not consistent with {self.numb_fparam}.", + ) + nb, _ = fparam.shape + t_fparam_avg = self._extend_f_avg_std(self.fparam_avg, nb) + t_fparam_inv_std = self._extend_f_avg_std(self.fparam_inv_std, nb) + fparam = (fparam - t_fparam_avg) * t_fparam_inv_std + fparam = torch.tile(fparam.reshape([nf, 1, -1]), [1, nloc, 1]) + xx = torch.cat( + [xx, fparam], + dim=-1, + ) + # check aparam dim, concate to input descriptor + if self.numb_aparam > 0: + assert aparam is not None, "aparam should not be None" + assert self.aparam_avg is not None + assert self.aparam_inv_std is not None + if aparam.shape[-1] != self.numb_aparam: + raise ValueError( + "get an input aparam of dim {aparam.shape[-1]}, ", + "which is not consistent with {self.numb_aparam}.", + ) + nb, nloc, _ = aparam.shape + t_aparam_avg = self._extend_a_avg_std(self.aparam_avg, nb, nloc) + t_aparam_inv_std = self._extend_a_avg_std(self.aparam_inv_std, nb, nloc) + aparam = (aparam - t_aparam_avg) * t_aparam_inv_std + xx = torch.cat( + [xx, aparam], + dim=-1, + ) + outs = torch.zeros_like(atype).unsqueeze(-1) # jit assertion - if self.use_tebd: - if atype_tebd is not None: - inputs = torch.concat([inputs, atype_tebd], dim=-1) - atom_energy = self.filter_layers[0](inputs) + self.bias_atom_e[ - atype - ].unsqueeze(-1) - outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] + if self.old_impl: + outs = torch.zeros_like(atype).unsqueeze(-1) # jit assertion + assert self.filter_layers_old is not None + if self.use_tebd: + atom_energy = self.filter_layers_old[0](xx) + self.bias_atom_e[ + atype + ].unsqueeze(-1) + outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] + else: + for type_i, filter_layer in enumerate(self.filter_layers_old): + mask = atype == type_i + atom_energy = filter_layer(xx) + atom_energy = atom_energy + self.bias_atom_e[type_i] + atom_energy = atom_energy * mask.unsqueeze(-1) + outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] + return {"energy": outs.to(env.GLOBAL_PT_FLOAT_PRECISION)} else: - for type_i, filter_layer in enumerate(self.filter_layers): - mask = atype == type_i - atom_energy = filter_layer(inputs) - atom_energy = atom_energy + self.bias_atom_e[type_i] - atom_energy = atom_energy * mask.unsqueeze(-1) + if self.use_tebd: + atom_energy = ( + self.filter_layers.networks[0](xx) + self.bias_atom_e[atype] + ) outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] - return {"energy": outs.to(env.GLOBAL_PT_FLOAT_PRECISION)} + else: + for type_i, ll in enumerate(self.filter_layers.networks): + mask = (atype == type_i).unsqueeze(-1) + mask = torch.tile(mask, (1, 1, self.dim_out)) + atom_energy = ll(xx) + atom_energy = atom_energy + self.bias_atom_e[type_i] + atom_energy = atom_energy * mask + outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] + return {self.var_name: outs.to(env.GLOBAL_PT_FLOAT_PRECISION)} + + +@Fitting.register("ener") +@fitting_check_output +class EnergyFittingNet(InvarFitting): + def __init__( + self, + ntypes: int, + embedding_width: int, + neuron: List[int] = [128, 128, 128], + bias_atom_e: Optional[torch.Tensor] = None, + resnet_dt: bool = True, + numb_fparam: int = 0, + numb_aparam: int = 0, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + use_tebd: bool = True, + **kwargs, + ): + super().__init__( + "energy", + ntypes, + embedding_width, + 1, + neuron=neuron, + bias_atom_e=bias_atom_e, + resnet_dt=resnet_dt, + numb_fparam=numb_fparam, + numb_aparam=numb_aparam, + activation_function=activation_function, + precision=precision, + use_tebd=use_tebd, + **kwargs, + ) @Fitting.register("direct_force") @@ -136,7 +415,7 @@ def __init__( """ super().__init__() self.ntypes = ntypes - self.embedding_width = embedding_width + self.dim_descrpt = embedding_width self.use_tebd = use_tebd self.out_dim = out_dim if not use_tebd: @@ -186,13 +465,12 @@ def forward( self, inputs: torch.Tensor, atype: torch.Tensor, - atype_tebd: Optional[torch.Tensor] = None, - rot_mat: Optional[torch.Tensor] = None, + gr: Optional[torch.Tensor] = None, ) -> Tuple[torch.Tensor, None]: """Based on embedding net output, alculate total energy. Args: - - inputs: Embedding matrix. Its shape is [nframes, natoms[0], self.embedding_width]. + - inputs: Embedding matrix. Its shape is [nframes, natoms[0], self.dim_descrpt]. - natoms: Tell atom count and element count. Its shape is [2+self.ntypes]. Returns @@ -201,19 +479,19 @@ def forward( """ nframes, nloc, _ = inputs.size() if self.use_tebd: - if atype_tebd is not None: - inputs = torch.concat([inputs, atype_tebd], dim=-1) + # if atype_tebd is not None: + # inputs = torch.concat([inputs, atype_tebd], dim=-1) vec_out = self.filter_layers_dipole[0]( inputs ) # Shape is [nframes, nloc, m1] assert list(vec_out.size()) == [nframes, nloc, self.out_dim] # (nf x nloc) x 1 x od vec_out = vec_out.view(-1, 1, self.out_dim) - assert rot_mat is not None + assert gr is not None # (nf x nloc) x od x 3 - rot_mat = rot_mat.view(-1, self.out_dim, 3) + gr = gr.view(-1, self.out_dim, 3) vec_out = ( - torch.bmm(vec_out, rot_mat).squeeze(-2).view(nframes, nloc, 3) + torch.bmm(vec_out, gr).squeeze(-2).view(nframes, nloc, 3) ) # Shape is [nframes, nloc, 3] else: vec_out = torch.zeros_like(atype).unsqueeze(-1) # jit assertion diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 16e80f9c20..c6fb6b27e1 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -7,9 +7,6 @@ import numpy as np import torch -from deepmd.model_format import ( - FittingOutputDef, -) from deepmd.pt.model.task.task import ( TaskBaseMethod, ) @@ -61,17 +58,9 @@ def __new__(cls, *args, **kwargs): if fitting_type in Fitting.__plugins.plugins: cls = Fitting.__plugins.plugins[fitting_type] else: - raise RuntimeError("Unknown descriptor type: " + fitting_type) + raise RuntimeError("Unknown fitting type: " + fitting_type) return super().__new__(cls) - def output_def(self) -> FittingOutputDef: - """Definition for the task Output.""" - raise NotImplementedError - - def forward(self, **kwargs): - """Task Output.""" - raise NotImplementedError - def share_params(self, base_class, shared_level, resume=False): assert ( self.__class__ == base_class.__class__ diff --git a/deepmd/pt/model/task/task.py b/deepmd/pt/model/task/task.py index a9b2efeb9a..b2dc03e4bd 100644 --- a/deepmd/pt/model/task/task.py +++ b/deepmd/pt/model/task/task.py @@ -1,12 +1,18 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from abc import ( + ABC, + abstractmethod, +) + import torch +from deepmd.model_format import ( + FittingOutputDef, +) -class TaskBaseMethod(torch.nn.Module): - def __init__(self, **kwargs): - """Construct a basic head for different tasks.""" - super().__init__() - def forward(self, **kwargs): - """Task Output.""" +class TaskBaseMethod(torch.nn.Module, ABC): + @abstractmethod + def output_def(self) -> FittingOutputDef: + """Definition for the task Output.""" raise NotImplementedError diff --git a/deepmd/pt/utils/utils.py b/deepmd/pt/utils/utils.py index 780dbf7e62..e83e12f608 100644 --- a/deepmd/pt/utils/utils.py +++ b/deepmd/pt/utils/utils.py @@ -4,9 +4,17 @@ Optional, ) +import numpy as np import torch import torch.nn.functional as F +from deepmd.model_format.common import PRECISION_DICT as NP_PRECISION_DICT + +from .env import ( + DEVICE, +) +from .env import PRECISION_DICT as PT_PRECISION_DICT + def get_activation_fn(activation: str) -> Callable: """Returns the activation function corresponding to `activation`.""" @@ -41,3 +49,35 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return x else: raise RuntimeError(f"activation function {self.activation} not supported") + + +def to_numpy_array( + xx: torch.Tensor, +) -> np.ndarray: + if xx is None: + return None + assert xx is not None + # Create a reverse mapping of PT_PRECISION_DICT + reverse_precision_dict = {v: k for k, v in PT_PRECISION_DICT.items()} + # Use the reverse mapping to find keys with the desired value + prec = reverse_precision_dict.get(xx.dtype, None) + prec = NP_PRECISION_DICT.get(prec, None) + if prec is None: + raise ValueError(f"unknown precision {xx.dtype}") + return xx.detach().cpu().numpy().astype(prec) + + +def to_torch_tensor( + xx: np.ndarray, +) -> torch.Tensor: + if xx is None: + return None + assert xx is not None + # Create a reverse mapping of NP_PRECISION_DICT + reverse_precision_dict = {v: k for k, v in NP_PRECISION_DICT.items()} + # Use the reverse mapping to find keys with the desired value + prec = reverse_precision_dict.get(type(xx.flat[0]), None) + prec = PT_PRECISION_DICT.get(prec, None) + if prec is None: + raise ValueError(f"unknown precision {xx.dtype}") + return torch.tensor(xx, dtype=prec, device=DEVICE) diff --git a/source/tests/common/test_model_format_utils.py b/source/tests/common/test_model_format_utils.py index da76c53ed9..cb85fd2bb2 100644 --- a/source/tests/common/test_model_format_utils.py +++ b/source/tests/common/test_model_format_utils.py @@ -13,6 +13,7 @@ EmbeddingNet, EnvMat, FittingNet, + InvarFitting, NativeLayer, NativeNet, NetworkCollection, @@ -369,3 +370,123 @@ def test_self_consistency( mm1 = em1.call(self.coord_ext, self.atype_ext, self.nlist) for ii in [0, 1, 4]: np.testing.assert_allclose(mm0[ii], mm1[ii]) + + +class TestInvarFitting(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_self_consistency( + self, + ): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA(self.rcut, self.rcut_smth, self.sel) + dd = ds.call(self.coord_ext, self.atype_ext, self.nlist) + atype = self.atype_ext[:, :nloc] + + for ( + distinguish_types, + od, + nfp, + nap, + ) in itertools.product( + [True, False], + [1, 2], + [0, 3], + [0, 4], + ): + ifn0 = InvarFitting( + "energy", + self.nt, + ds.dim_out, + od, + numb_fparam=nfp, + numb_aparam=nap, + distinguish_types=distinguish_types, + ) + ifn1 = InvarFitting.deserialize(ifn0.serialize()) + if nfp > 0: + ifp = rng.normal(size=(self.nf, nfp)) + else: + ifp = None + if nap > 0: + iap = rng.normal(size=(self.nf, self.nloc, nap)) + else: + iap = None + ret0 = ifn0(dd[0], atype, fparam=ifp, aparam=iap) + ret1 = ifn1(dd[0], atype, fparam=ifp, aparam=iap) + np.testing.assert_allclose(ret0["energy"], ret1["energy"]) + + def test_self_exception( + self, + ): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA(self.rcut, self.rcut_smth, self.sel) + dd = ds.call(self.coord_ext, self.atype_ext, self.nlist) + atype = self.atype_ext[:, :nloc] + + for ( + distinguish_types, + od, + nfp, + nap, + ) in itertools.product( + [True, False], + [1, 2], + [0, 3], + [0, 4], + ): + ifn0 = InvarFitting( + "energy", + self.nt, + ds.dim_out, + od, + numb_fparam=nfp, + numb_aparam=nap, + distinguish_types=distinguish_types, + ) + + if nfp > 0: + ifp = rng.normal(size=(self.nf, nfp)) + else: + ifp = None + if nap > 0: + iap = rng.normal(size=(self.nf, self.nloc, nap)) + else: + iap = None + with self.assertRaises(ValueError) as context: + ret0 = ifn0(dd[0][:, :, :-2], atype, fparam=ifp, aparam=iap) + self.assertIn("input descriptor", context.exception) + + if nfp > 0: + ifp = rng.normal(size=(self.nf, nfp - 1)) + with self.assertRaises(ValueError) as context: + ret0 = ifn0(dd[0], atype, fparam=ifp, aparam=iap) + self.assertIn("input fparam", context.exception) + + if nap > 0: + iap = rng.normal(size=(self.nf, self.nloc, nap - 1)) + with self.assertRaises(ValueError) as context: + ret0 = ifn0(dd[0], atype, fparam=ifp, aparam=iap) + self.assertIn("input aparam", context.exception) + + def test_get_set(self): + ifn0 = InvarFitting( + "energy", + self.nt, + 3, + 1, + ) + rng = np.random.default_rng() + foo = rng.normal([3, 4]) + for ii in [ + "bias_atom_e", + "fparam_avg", + "fparam_inv_std", + "aparam_avg", + "aparam_inv_std", + ]: + ifn0[ii] = foo + np.testing.assert_allclose(foo, ifn0[ii]) diff --git a/source/tests/pt/test_ener_fitting.py b/source/tests/pt/test_ener_fitting.py new file mode 100644 index 0000000000..eece8447df --- /dev/null +++ b/source/tests/pt/test_ener_fitting.py @@ -0,0 +1,181 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import itertools +import unittest + +import numpy as np +import torch + +from deepmd.model_format import InvarFitting as DPInvarFitting +from deepmd.pt.model.descriptor.se_a import ( + DescrptSeA, +) +from deepmd.pt.model.task.ener import ( + EnergyFittingNet, + InvarFitting, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, +) + +from .test_env_mat import ( + TestCaseSingleFrameWithNlist, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION + + +class TestInvarFitting(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_consistency( + self, + ): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + dd0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) + rd0, _, _, _, _ = dd0( + torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), + torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), + torch.tensor(self.nlist, dtype=int, device=env.DEVICE), + ) + atype = torch.tensor(self.atype_ext[:, :nloc], dtype=int, device=env.DEVICE) + + for od, distinguish_types, nfp, nap in itertools.product( + [1, 3], + [True, False], + [0, 3], + [0, 4], + ): + ft0 = InvarFitting( + "foo", + self.nt, + dd0.dim_out, + od, + numb_fparam=nfp, + numb_aparam=nap, + use_tebd=(not distinguish_types), + ).to(env.DEVICE) + ft1 = DPInvarFitting.deserialize(ft0.serialize()) + ft2 = InvarFitting.deserialize(ft0.serialize()) + + if nfp > 0: + ifp = torch.tensor( + rng.normal(size=(self.nf, nfp)), dtype=dtype, device=env.DEVICE + ) + else: + ifp = None + if nap > 0: + iap = torch.tensor( + rng.normal(size=(self.nf, self.nloc, nap)), + dtype=dtype, + device=env.DEVICE, + ) + else: + iap = None + + ret0 = ft0(rd0, atype, fparam=ifp, aparam=iap) + ret1 = ft1( + rd0.detach().cpu().numpy(), + atype.detach().cpu().numpy(), + fparam=to_numpy_array(ifp), + aparam=to_numpy_array(iap), + ) + ret2 = ft2(rd0, atype, fparam=ifp, aparam=iap) + np.testing.assert_allclose( + to_numpy_array(ret0["foo"]), + ret1["foo"], + ) + np.testing.assert_allclose( + to_numpy_array(ret0["foo"]), + to_numpy_array(ret2["foo"]), + ) + + def test_new_old( + self, + ): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + dd = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) + rd0, _, _, _, _ = dd( + torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), + torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), + torch.tensor(self.nlist, dtype=int, device=env.DEVICE), + ) + atype = torch.tensor(self.atype_ext[:, :nloc], dtype=int, device=env.DEVICE) + + od = 1 + for distinguish_types in itertools.product( + [True, False], + ): + ft0 = EnergyFittingNet( + self.nt, + dd.dim_out, + distinguish_types=distinguish_types, + ).to(env.DEVICE) + ft1 = EnergyFittingNet( + self.nt, + dd.dim_out, + distinguish_types=distinguish_types, + old_impl=True, + ).to(env.DEVICE) + dd0 = ft0.state_dict() + dd1 = ft1.state_dict() + for kk, vv in dd1.items(): + new_kk = kk + new_kk = new_kk.replace("filter_layers_old", "filter_layers.networks") + new_kk = new_kk.replace("deep_layers", "layers") + new_kk = new_kk.replace("final_layer", "layers.3") + dd1[kk] = dd0[new_kk] + if kk.split(".")[-1] in ["idt", "bias"]: + dd1[kk] = dd1[kk].unsqueeze(0) + dd1["bias_atom_e"] = dd0["bias_atom_e"] + ft1.load_state_dict(dd1) + ret0 = ft0(rd0, atype) + ret1 = ft1(rd0, atype) + np.testing.assert_allclose( + to_numpy_array(ret0["energy"]), + to_numpy_array(ret1["energy"]), + ) + + def test_jit( + self, + ): + for od, distinguish_types, nfp, nap in itertools.product( + [1, 3], + [True, False], + [0, 3], + [0, 4], + ): + ft0 = InvarFitting( + "foo", + self.nt, + 9, + od, + numb_fparam=nfp, + numb_aparam=nap, + use_tebd=(not distinguish_types), + ).to(env.DEVICE) + torch.jit.script(ft0) + + def test_get_set(self): + ifn0 = InvarFitting( + "energy", + self.nt, + 3, + 1, + ) + rng = np.random.default_rng() + foo = rng.normal([3, 4]) + for ii in [ + "bias_atom_e", + "fparam_avg", + "fparam_inv_std", + "aparam_avg", + "aparam_inv_std", + ]: + ifn0[ii] = torch.tensor(foo, dtype=dtype, device=env.DEVICE) + np.testing.assert_allclose(foo, ifn0[ii].detach().cpu().numpy()) diff --git a/source/tests/pt/test_fitting_net.py b/source/tests/pt/test_fitting_net.py index 3feb4f4739..ed2c428de5 100644 --- a/source/tests/pt/test_fitting_net.py +++ b/source/tests/pt/test_fitting_net.py @@ -102,25 +102,25 @@ def test_consistency(self): my_fn = EnergyFittingNet( self.ntypes, self.embedding_width, - self.n_neuron, - self.dp_fn.bias_atom_e, - use_tebd=False, + neuron=self.n_neuron, + bias_atom_e=self.dp_fn.bias_atom_e, + distinguish_types=True, ) for name, param in my_fn.named_parameters(): - matched = re.match("filter_layers\.(\d).deep_layers\.(\d)\.([a-z]+)", name) + matched = re.match( + "filter_layers\.networks\.(\d).layers\.(\d)\.([a-z]+)", name + ) key = None if matched: + if int(matched.group(2)) == len(self.n_neuron): + layer_id = -1 + else: + layer_id = matched.group(2) key = gen_key( type_id=matched.group(1), - layer_id=matched.group(2), + layer_id=layer_id, w_or_b=matched.group(3), ) - else: - matched = re.match("filter_layers\.(\d).final_layer\.([a-z]+)", name) - if matched: - key = gen_key( - type_id=matched.group(1), layer_id=-1, w_or_b=matched.group(2) - ) assert key is not None var = values[key] with torch.no_grad(): @@ -132,7 +132,7 @@ def test_consistency(self): ret = my_fn(embedding, atype) my_energy = ret["energy"] my_energy = my_energy.detach() - self.assertTrue(np.allclose(dp_energy, my_energy.numpy().reshape([-1]))) + np.testing.assert_allclose(dp_energy, my_energy.numpy().reshape([-1])) if __name__ == "__main__": diff --git a/source/tests/pt/test_model.py b/source/tests/pt/test_model.py index 5bbbc9e352..c6595e6471 100644 --- a/source/tests/pt/test_model.py +++ b/source/tests/pt/test_model.py @@ -53,23 +53,24 @@ VariableState = collections.namedtuple("VariableState", ["value", "gradient"]) -def torch2tf(torch_name): +def torch2tf(torch_name, last_layer_id=None): fields = torch_name.split(".") offset = int(fields[2] == "networks") element_id = int(fields[2 + offset]) if fields[0] == "descriptor": layer_id = int(fields[4 + offset]) + 1 weight_type = fields[5 + offset] - return "filter_type_all/%s_%d_%d:0" % (weight_type, layer_id, element_id) - elif fields[3] == "deep_layers": - layer_id = int(fields[4]) - weight_type = fields[5] - return "layer_%d_type_%d/%s:0" % (layer_id, element_id, weight_type) - elif fields[3] == "final_layer": - weight_type = fields[4] - return "final_layer_type_%d/%s:0" % (element_id, weight_type) + ret = "filter_type_all/%s_%d_%d:0" % (weight_type, layer_id, element_id) + elif fields[0] == "fitting_net": + layer_id = int(fields[4 + offset]) + weight_type = fields[5 + offset] + if layer_id != last_layer_id: + ret = "layer_%d_type_%d/%s:0" % (layer_id, element_id, weight_type) + else: + ret = "final_layer_type_%d/%s:0" % (element_id, weight_type) else: raise RuntimeError("Unexpected parameter name: %s" % torch_name) + return ret class DpTrainer: @@ -290,7 +291,7 @@ def test_consistency(self): "neuron": self.filter_neuron, "axis_neuron": self.axis_neuron, }, - "fitting_net": {"neuron": self.n_neuron}, + "fitting_net": {"neuron": self.n_neuron, "distinguish_types": True}, "data_stat_nbatch": self.data_stat_nbatch, "type_map": self.type_map, }, @@ -323,7 +324,7 @@ def test_consistency(self): # Keep parameter value consistency between 2 implentations for name, param in my_model.named_parameters(): name = name.replace("sea.", "") - var_name = torch2tf(name) + var_name = torch2tf(name, last_layer_id=len(self.n_neuron)) var = vs_dict[var_name].value with torch.no_grad(): src = torch.from_numpy(var) @@ -404,7 +405,7 @@ def step(step_id): for name, param in my_model.named_parameters(): name = name.replace("sea.", "") - var_name = torch2tf(name) + var_name = torch2tf(name, last_layer_id=len(self.n_neuron)) var_grad = vs_dict[var_name].gradient param_grad = param.grad.cpu() var_grad = torch.tensor(var_grad) diff --git a/source/tests/pt/test_se_e2_a.py b/source/tests/pt/test_se_e2_a.py index c0a106cb16..0da80ea1ea 100644 --- a/source/tests/pt/test_se_e2_a.py +++ b/source/tests/pt/test_se_e2_a.py @@ -25,6 +25,9 @@ PRECISION_DICT, ) +from .test_env_mat import ( + TestCaseSingleFrameWithNlist, +) from .test_mlp import ( get_tols, ) @@ -32,36 +35,6 @@ dtype = env.GLOBAL_PT_FLOAT_PRECISION -class TestCaseSingleFrameWithNlist: - def setUp(self): - # nloc == 3, nall == 4 - self.nloc = 3 - self.nall = 4 - self.nf, self.nt = 1, 2 - self.coord_ext = np.array( - [ - [0, 0, 0], - [0, 1, 0], - [0, 0, 1], - [0, -2, 0], - ], - dtype=np.float64, - ).reshape([1, self.nall * 3]) - self.atype_ext = np.array([0, 0, 1, 0], dtype=int).reshape([1, self.nall]) - # sel = [5, 2] - self.sel = [5, 2] - self.nlist = np.array( - [ - [1, 3, -1, -1, -1, 2, -1], - [0, -1, -1, -1, -1, 2, -1], - [0, 1, -1, -1, -1, 0, -1], - ], - dtype=int, - ).reshape([1, self.nloc, sum(self.sel)]) - self.rcut = 0.4 - self.rcut_smth = 2.2 - - # to be merged with the tf test case @unittest.skipIf(not support_se_e2_a, "EnvMat not supported") class TestDescrptSeA(unittest.TestCase, TestCaseSingleFrameWithNlist): diff --git a/source/tests/pt/test_utils.py b/source/tests/pt/test_utils.py new file mode 100644 index 0000000000..9c9a9479ad --- /dev/null +++ b/source/tests/pt/test_utils.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np +import torch + +from deepmd.pt.utils.utils import ( + to_numpy_array, + to_torch_tensor, +) + + +class TestCvt(unittest.TestCase): + def test_to_numpy(self): + rng = np.random.default_rng() + foo = rng.normal([3, 4]) + for ptp, npp in zip( + [torch.float16, torch.float32, torch.float64], + [np.float16, np.float32, np.float64], + ): + foo = foo.astype(npp) + bar = to_torch_tensor(foo) + self.assertEqual(bar.dtype, ptp) + onk = to_numpy_array(bar) + self.assertEqual(onk.dtype, npp) + with self.assertRaises(ValueError) as ee: + foo = foo.astype(np.int32) + bar = to_torch_tensor(foo) + with self.assertRaises(ValueError) as ee: + bar = to_torch_tensor(foo) + bar = to_numpy_array(bar.int()) From b8000438bec271b97254c5fdee1013f277682aaf Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Tue, 30 Jan 2024 16:51:59 +0800 Subject: [PATCH 031/686] remove duplicated fitting output check. fix codeql (#3202) Co-authored-by: Han Wang --- deepmd/pt/model/task/ener.py | 1 - 1 file changed, 1 deletion(-) diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 91ccd03d9a..e40a6bda44 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -355,7 +355,6 @@ def forward( @Fitting.register("ener") -@fitting_check_output class EnergyFittingNet(InvarFitting): def __init__( self, From 7f069ccaef773c22ad4441f3e93c982809a52d7d Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Wed, 31 Jan 2024 09:06:30 +0800 Subject: [PATCH 032/686] Fix GPU UTs (#3203) This PR fixes GPU UTs; Delete the PREPROCESS_DEVICE in torch data preprocess and use training DEVICE instead, which will be removed after the dataset is refomated. --------- Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jinzhe Zeng Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Co-authored-by: Han Wang --- .github/workflows/test_cuda.yml | 1 + deepmd/pt/utils/dataloader.py | 2 - deepmd/pt/utils/dataset.py | 57 +++++------------- deepmd/pt/utils/env.py | 5 -- deepmd/pt/utils/preprocess.py | 47 +++++---------- deepmd/pt/utils/stat.py | 7 +-- source/tests/pt/test_descriptor.py | 23 +++++--- source/tests/pt/test_descriptor_dpa1.py | 8 +-- source/tests/pt/test_descriptor_dpa2.py | 8 +-- source/tests/pt/test_embedding_net.py | 18 ++++-- source/tests/pt/test_fitting_net.py | 9 ++- source/tests/pt/test_mlp.py | 73 +++++++++++++----------- source/tests/pt/test_model.py | 18 ++++-- source/tests/pt/test_saveload_dpa1.py | 4 +- source/tests/pt/test_saveload_se_e2_a.py | 4 +- 15 files changed, 131 insertions(+), 153 deletions(-) diff --git a/.github/workflows/test_cuda.yml b/.github/workflows/test_cuda.yml index f164758304..45b689cb3e 100644 --- a/.github/workflows/test_cuda.yml +++ b/.github/workflows/test_cuda.yml @@ -42,6 +42,7 @@ jobs: DP_BUILD_TESTING: 1 DP_VARIANT: cuda CUDA_PATH: /usr/local/cuda-12.2 + NUM_WORKERS: 0 - run: dp --version - run: python -m pytest -s --cov=deepmd source/tests --durations=0 - run: source/install/test_cc_local.sh diff --git a/deepmd/pt/utils/dataloader.py b/deepmd/pt/utils/dataloader.py index 7c95f66c9c..7a6684e82e 100644 --- a/deepmd/pt/utils/dataloader.py +++ b/deepmd/pt/utils/dataloader.py @@ -276,13 +276,11 @@ def collate_batch(batch): result[key] = torch.zeros( (n_frames, natoms_extended, 3), dtype=env.GLOBAL_PT_FLOAT_PRECISION, - device=env.PREPROCESS_DEVICE, ) else: result[key] = torch.zeros( (n_frames, natoms_extended), dtype=torch.long, - device=env.PREPROCESS_DEVICE, ) for i in range(len(batch)): natoms_tmp = list[i].shape[0] diff --git a/deepmd/pt/utils/dataset.py b/deepmd/pt/utils/dataset.py index c104e64491..68d4a09ce4 100644 --- a/deepmd/pt/utils/dataset.py +++ b/deepmd/pt/utils/dataset.py @@ -477,11 +477,7 @@ def preprocess(self, batch): if "find_" in kk: pass else: - batch[kk] = torch.tensor( - batch[kk], - dtype=env.GLOBAL_PT_FLOAT_PRECISION, - device=env.PREPROCESS_DEVICE, - ) + batch[kk] = torch.tensor(batch[kk], dtype=env.GLOBAL_PT_FLOAT_PRECISION) if self._data_dict[kk]["atomic"]: batch[kk] = batch[kk].view( n_frames, -1, self._data_dict[kk]["ndof"] @@ -489,9 +485,7 @@ def preprocess(self, batch): for kk in ["type", "real_natoms_vec"]: if kk in batch.keys(): - batch[kk] = torch.tensor( - batch[kk], dtype=torch.long, device=env.PREPROCESS_DEVICE - ) + batch[kk] = torch.tensor(batch[kk], dtype=torch.long) batch["atype"] = batch.pop("type") keys = ["nlist", "nlist_loc", "nlist_type", "shift", "mapping"] @@ -524,13 +518,9 @@ def preprocess(self, batch): batch["nlist_type"] = nlist_type natoms_extended = max([item.shape[0] for item in shift]) batch["shift"] = torch.zeros( - (n_frames, natoms_extended, 3), - dtype=env.GLOBAL_PT_FLOAT_PRECISION, - device=env.PREPROCESS_DEVICE, - ) - batch["mapping"] = torch.zeros( - (n_frames, natoms_extended), dtype=torch.long, device=env.PREPROCESS_DEVICE + (n_frames, natoms_extended, 3), dtype=env.GLOBAL_PT_FLOAT_PRECISION ) + batch["mapping"] = torch.zeros((n_frames, natoms_extended), dtype=torch.long) for i in range(len(shift)): natoms_tmp = shift[i].shape[0] batch["shift"][i, :natoms_tmp] = shift[i] @@ -566,17 +556,13 @@ def single_preprocess(self, batch, sid): pass else: batch[kk] = torch.tensor( - batch[kk][sid], - dtype=env.GLOBAL_PT_FLOAT_PRECISION, - device=env.PREPROCESS_DEVICE, + batch[kk][sid], dtype=env.GLOBAL_PT_FLOAT_PRECISION ) if self._data_dict[kk]["atomic"]: batch[kk] = batch[kk].view(-1, self._data_dict[kk]["ndof"]) for kk in ["type", "real_natoms_vec"]: if kk in batch.keys(): - batch[kk] = torch.tensor( - batch[kk][sid], dtype=torch.long, device=env.PREPROCESS_DEVICE - ) + batch[kk] = torch.tensor(batch[kk][sid], dtype=torch.long) clean_coord = batch.pop("coord") clean_type = batch.pop("type") nloc = clean_type.shape[0] @@ -670,30 +656,22 @@ def single_preprocess(self, batch, sid): NotImplementedError(f"Unknown noise type {self.noise_type}!") noised_coord = _clean_coord.clone().detach() noised_coord[coord_mask] += noise_on_coord - batch["coord_mask"] = torch.tensor( - coord_mask, dtype=torch.bool, device=env.PREPROCESS_DEVICE - ) + batch["coord_mask"] = torch.tensor(coord_mask, dtype=torch.bool) else: noised_coord = _clean_coord batch["coord_mask"] = torch.tensor( - np.zeros_like(coord_mask, dtype=bool), - dtype=torch.bool, - device=env.PREPROCESS_DEVICE, + np.zeros_like(coord_mask, dtype=bool), dtype=torch.bool ) # add mask for type if self.mask_type: masked_type = clean_type.clone().detach() masked_type[type_mask] = self.mask_type_idx - batch["type_mask"] = torch.tensor( - type_mask, dtype=torch.bool, device=env.PREPROCESS_DEVICE - ) + batch["type_mask"] = torch.tensor(type_mask, dtype=torch.bool) else: masked_type = clean_type batch["type_mask"] = torch.tensor( - np.zeros_like(type_mask, dtype=bool), - dtype=torch.bool, - device=env.PREPROCESS_DEVICE, + np.zeros_like(type_mask, dtype=bool), dtype=torch.bool ) if self.pbc: _coord = normalize_coord(noised_coord, region, nloc) @@ -803,7 +781,7 @@ def __len__(self): def __getitem__(self, index): """Get a frame from the selected system.""" b_data = self._data_system._get_item(index) - b_data["natoms"] = torch.tensor(self._natoms_vec, device=env.PREPROCESS_DEVICE) + b_data["natoms"] = torch.tensor(self._natoms_vec) return b_data @@ -878,9 +856,7 @@ def __getitem__(self, index=None): if index is None: index = dp_random.choice(np.arange(self.nsystems), p=self.probs) b_data = self._data_systems[index].get_batch(self._batch_size) - b_data["natoms"] = torch.tensor( - self._natoms_vec[index], device=env.PREPROCESS_DEVICE - ) + b_data["natoms"] = torch.tensor(self._natoms_vec[index]) batch_size = b_data["coord"].shape[0] b_data["natoms"] = b_data["natoms"].unsqueeze(0).expand(batch_size, -1) return b_data @@ -891,9 +867,7 @@ def get_training_batch(self, index=None): if index is None: index = dp_random.choice(np.arange(self.nsystems), p=self.probs) b_data = self._data_systems[index].get_batch_for_train(self._batch_size) - b_data["natoms"] = torch.tensor( - self._natoms_vec[index], device=env.PREPROCESS_DEVICE - ) + b_data["natoms"] = torch.tensor(self._natoms_vec[index]) batch_size = b_data["coord"].shape[0] b_data["natoms"] = b_data["natoms"].unsqueeze(0).expand(batch_size, -1) return b_data @@ -902,10 +876,7 @@ def get_batch(self, sys_idx=None): """TF-compatible batch for testing.""" pt_batch = self[sys_idx] np_batch = {} - for key in ["coord", "box", "force", "energy", "virial"]: - if key in pt_batch.keys(): - np_batch[key] = pt_batch[key].cpu().numpy() - for key in ["atype", "natoms"]: + for key in ["coord", "box", "force", "energy", "virial", "atype", "natoms"]: if key in pt_batch.keys(): np_batch[key] = pt_batch[key].cpu().numpy() batch_size = pt_batch["coord"].shape[0] diff --git a/deepmd/pt/utils/env.py b/deepmd/pt/utils/env.py index 6fa72943c7..559dba0167 100644 --- a/deepmd/pt/utils/env.py +++ b/deepmd/pt/utils/env.py @@ -24,11 +24,6 @@ else: DEVICE = torch.device(f"cuda:{LOCAL_RANK}") -if os.environ.get("PREPROCESS_DEVICE") == "gpu": - PREPROCESS_DEVICE = torch.device(f"cuda:{LOCAL_RANK}") -else: - PREPROCESS_DEVICE = torch.device("cpu") - JIT = False CACHE_PER_SYS = 5 # keep at most so many sets per sys in memory ENERGY_BIAS_TRAINABLE = True diff --git a/deepmd/pt/utils/preprocess.py b/deepmd/pt/utils/preprocess.py index 463ac112ad..18c798138e 100644 --- a/deepmd/pt/utils/preprocess.py +++ b/deepmd/pt/utils/preprocess.py @@ -99,7 +99,7 @@ def build_inside_clist(coord, region: Region3D, ncell): cell_offset[cell_offset < 0] = 0 delta = cell_offset - ncell a2c = compute_serial_cid(cell_offset, ncell) # cell id of atoms - arange = torch.arange(0, loc_ncell, 1, device=env.PREPROCESS_DEVICE) + arange = torch.arange(0, loc_ncell, 1) cellid = a2c == arange.unsqueeze(-1) # one hot cellid c2a = cellid.nonzero() lst = [] @@ -131,18 +131,12 @@ def append_neighbors(coord, region: Region3D, atype, rcut: float): # add ghost atoms a2c, c2a = build_inside_clist(coord, region, ncell) - xi = torch.arange(-ngcell[0], ncell[0] + ngcell[0], 1, device=env.PREPROCESS_DEVICE) - yi = torch.arange(-ngcell[1], ncell[1] + ngcell[1], 1, device=env.PREPROCESS_DEVICE) - zi = torch.arange(-ngcell[2], ncell[2] + ngcell[2], 1, device=env.PREPROCESS_DEVICE) - xyz = xi.view(-1, 1, 1, 1) * torch.tensor( - [1, 0, 0], dtype=torch.long, device=env.PREPROCESS_DEVICE - ) - xyz = xyz + yi.view(1, -1, 1, 1) * torch.tensor( - [0, 1, 0], dtype=torch.long, device=env.PREPROCESS_DEVICE - ) - xyz = xyz + zi.view(1, 1, -1, 1) * torch.tensor( - [0, 0, 1], dtype=torch.long, device=env.PREPROCESS_DEVICE - ) + xi = torch.arange(-ngcell[0], ncell[0] + ngcell[0], 1) + yi = torch.arange(-ngcell[1], ncell[1] + ngcell[1], 1) + zi = torch.arange(-ngcell[2], ncell[2] + ngcell[2], 1) + xyz = xi.view(-1, 1, 1, 1) * torch.tensor([1, 0, 0], dtype=torch.long) + xyz = xyz + yi.view(1, -1, 1, 1) * torch.tensor([0, 1, 0], dtype=torch.long) + xyz = xyz + zi.view(1, 1, -1, 1) * torch.tensor([0, 0, 1], dtype=torch.long) xyz = xyz.view(-1, 3) mask_a = (xyz >= 0).all(dim=-1) mask_b = (xyz < ncell).all(dim=-1) @@ -165,9 +159,7 @@ def append_neighbors(coord, region: Region3D, atype, rcut: float): merged_coord = torch.cat([coord, tmp_coord]) merged_coord_shift = torch.cat([torch.zeros_like(coord), coord_shift[tmp]]) merged_atype = torch.cat([atype, tmp_atype]) - merged_mapping = torch.cat( - [torch.arange(atype.numel(), device=env.PREPROCESS_DEVICE), aid] - ) + merged_mapping = torch.cat([torch.arange(atype.numel()), aid]) return merged_coord_shift, merged_atype, merged_mapping @@ -188,22 +180,16 @@ def build_neighbor_list( distance = coord_l - coord_r distance = torch.linalg.norm(distance, dim=-1) DISTANCE_INF = distance.max().detach() + rcut - distance[:nloc, :nloc] += ( - torch.eye(nloc, dtype=torch.bool, device=env.PREPROCESS_DEVICE) * DISTANCE_INF - ) + distance[:nloc, :nloc] += torch.eye(nloc, dtype=torch.bool) * DISTANCE_INF if min_check: if distance.min().abs() < 1e-6: RuntimeError("Atom dist too close!") if not type_split: sec = sec[-1:] lst = [] - nlist = torch.zeros((nloc, sec[-1].item()), device=env.PREPROCESS_DEVICE).long() - 1 - nlist_loc = ( - torch.zeros((nloc, sec[-1].item()), device=env.PREPROCESS_DEVICE).long() - 1 - ) - nlist_type = ( - torch.zeros((nloc, sec[-1].item()), device=env.PREPROCESS_DEVICE).long() - 1 - ) + nlist = torch.zeros((nloc, sec[-1].item())).long() - 1 + nlist_loc = torch.zeros((nloc, sec[-1].item())).long() - 1 + nlist_type = torch.zeros((nloc, sec[-1].item())).long() - 1 for i, nnei in enumerate(sec): if i > 0: nnei = nnei - sec[i - 1] @@ -216,11 +202,8 @@ def build_neighbor_list( _sorted, indices = torch.topk(tmp, nnei, dim=1, largest=False) else: # when nnei > nall - indices = torch.zeros((nloc, nnei), device=env.PREPROCESS_DEVICE).long() - 1 - _sorted = ( - torch.ones((nloc, nnei), device=env.PREPROCESS_DEVICE).long() - * DISTANCE_INF - ) + indices = torch.zeros((nloc, nnei)).long() - 1 + _sorted = torch.ones((nloc, nnei)).long() * DISTANCE_INF _sorted_nnei, indices_nnei = torch.topk( tmp, tmp.shape[1], dim=1, largest=False ) @@ -284,7 +267,7 @@ def make_env_mat( else: merged_coord_shift = torch.zeros_like(coord) merged_atype = atype.clone() - merged_mapping = torch.arange(atype.numel(), device=env.PREPROCESS_DEVICE) + merged_mapping = torch.arange(atype.numel()) merged_coord = coord.clone() # build nlist diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index 18ee4d9abe..eec7179bcd 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -62,14 +62,9 @@ def make_stat_input(datasets, dataloaders, nbatches): shape = torch.zeros( (n_frames, extend, 3), dtype=env.GLOBAL_PT_FLOAT_PRECISION, - device=env.PREPROCESS_DEVICE, ) else: - shape = torch.zeros( - (n_frames, extend), - dtype=torch.long, - device=env.PREPROCESS_DEVICE, - ) + shape = torch.zeros((n_frames, extend), dtype=torch.long) for i in range(len(item)): natoms_tmp = l[i].shape[0] shape[i, :natoms_tmp] = l[i] diff --git a/source/tests/pt/test_descriptor.py b/source/tests/pt/test_descriptor.py index da38cf007f..2dd996349b 100644 --- a/source/tests/pt/test_descriptor.py +++ b/source/tests/pt/test_descriptor.py @@ -18,6 +18,7 @@ ) from deepmd.pt.utils import ( dp_random, + env, ) from deepmd.pt.utils.dataset import ( DeepmdDataSet, @@ -112,29 +113,33 @@ def setUp(self): def test_consistency(self): avg_zero = torch.zeros( - [self.ntypes, self.nnei * 4], dtype=GLOBAL_PT_FLOAT_PRECISION + [self.ntypes, self.nnei * 4], + dtype=GLOBAL_PT_FLOAT_PRECISION, + device=env.DEVICE, ) std_ones = torch.ones( - [self.ntypes, self.nnei * 4], dtype=GLOBAL_PT_FLOAT_PRECISION + [self.ntypes, self.nnei * 4], + dtype=GLOBAL_PT_FLOAT_PRECISION, + device=env.DEVICE, ) base_d, base_force, nlist = base_se_a( rcut=self.rcut, rcut_smth=self.rcut_smth, sel=self.sel, batch=self.np_batch, - mean=avg_zero, - stddev=std_ones, + mean=avg_zero.detach().cpu(), + stddev=std_ones.detach().cpu(), ) - pt_coord = self.pt_batch["coord"] + pt_coord = self.pt_batch["coord"].to(env.DEVICE) pt_coord.requires_grad_(True) - index = self.pt_batch["mapping"].unsqueeze(-1).expand(-1, -1, 3) + index = self.pt_batch["mapping"].unsqueeze(-1).expand(-1, -1, 3).to(env.DEVICE) extended_coord = torch.gather(pt_coord, dim=1, index=index) - extended_coord = extended_coord - self.pt_batch["shift"] + extended_coord = extended_coord - self.pt_batch["shift"].to(env.DEVICE) my_d, _, _ = prod_env_mat_se_a( extended_coord.to(DEVICE), - self.pt_batch["nlist"], - self.pt_batch["atype"], + self.pt_batch["nlist"].to(env.DEVICE), + self.pt_batch["atype"].to(env.DEVICE), avg_zero.reshape([-1, self.nnei, 4]).to(DEVICE), std_ones.reshape([-1, self.nnei, 4]).to(DEVICE), self.rcut, diff --git a/source/tests/pt/test_descriptor_dpa1.py b/source/tests/pt/test_descriptor_dpa1.py index 689fa7e49c..725369d68d 100644 --- a/source/tests/pt/test_descriptor_dpa1.py +++ b/source/tests/pt/test_descriptor_dpa1.py @@ -243,7 +243,7 @@ def test_descriptor_block(self): dparams["ntypes"] = ntypes des = DescrptBlockSeAtten( **dparams, - ) + ).to(env.DEVICE) des.load_state_dict(torch.load(self.file_model_param)) rcut = dparams["rcut"] nsel = dparams["sel"] @@ -260,7 +260,7 @@ def test_descriptor_block(self): extended_coord, extended_atype, nloc, rcut, nsel, distinguish_types=False ) # handel type_embedding - type_embedding = TypeEmbedNet(ntypes, 8) + type_embedding = TypeEmbedNet(ntypes, 8).to(env.DEVICE) type_embedding.load_state_dict(torch.load(self.file_type_embed)) ## to save model parameters @@ -293,7 +293,7 @@ def test_descriptor(self): dparams["concat_output_tebd"] = False des = DescrptDPA1( **dparams, - ) + ).to(env.DEVICE) target_dict = des.state_dict() source_dict = torch.load(self.file_model_param) type_embd_dict = torch.load(self.file_type_embed) @@ -337,7 +337,7 @@ def test_descriptor(self): dparams["concat_output_tebd"] = True des = DescrptDPA1( **dparams, - ) + ).to(env.DEVICE) descriptor, env_mat, diff, rot_mat, sw = des( extended_coord, extended_atype, diff --git a/source/tests/pt/test_descriptor_dpa2.py b/source/tests/pt/test_descriptor_dpa2.py index 45c95961fe..aa6b16964e 100644 --- a/source/tests/pt/test_descriptor_dpa2.py +++ b/source/tests/pt/test_descriptor_dpa2.py @@ -124,7 +124,7 @@ def test_descriptor_hyb(self): dlist, ntypes, hybrid_mode=dparams["hybrid_mode"], - ) + ).to(env.DEVICE) model_dict = torch.load(self.file_model_param) # type_embd of repformer is removed model_dict.pop("descriptor_list.1.type_embd.embedding.weight") @@ -158,7 +158,7 @@ def test_descriptor_hyb(self): ) nlist = torch.cat(nlist_list, -1) # handel type_embedding - type_embedding = TypeEmbedNet(ntypes, 8) + type_embedding = TypeEmbedNet(ntypes, 8).to(env.DEVICE) type_embedding.load_state_dict(torch.load(self.file_type_embed)) ## to save model parameters @@ -186,7 +186,7 @@ def test_descriptor(self): dparams["concat_output_tebd"] = False des = DescrptDPA2( **dparams, - ) + ).to(env.DEVICE) target_dict = des.state_dict() source_dict = torch.load(self.file_model_param) # type_embd of repformer is removed @@ -232,7 +232,7 @@ def test_descriptor(self): dparams["concat_output_tebd"] = True des = DescrptDPA2( **dparams, - ) + ).to(env.DEVICE) descriptor, env_mat, diff, rot_mat, sw = des( extended_coord, extended_atype, diff --git a/source/tests/pt/test_embedding_net.py b/source/tests/pt/test_embedding_net.py index fc98ddc9f9..407f4949b5 100644 --- a/source/tests/pt/test_embedding_net.py +++ b/source/tests/pt/test_embedding_net.py @@ -8,6 +8,10 @@ import tensorflow.compat.v1 as tf import torch +from deepmd.pt.utils import ( + env, +) + tf.disable_eager_execution() from pathlib import ( @@ -148,18 +152,22 @@ def test_consistency(self): # Keep parameter value consistency between 2 implentations param.data.copy_(torch.from_numpy(var)) - pt_coord = self.torch_batch["coord"] + pt_coord = self.torch_batch["coord"].to(env.DEVICE) pt_coord.requires_grad_(True) - index = self.torch_batch["mapping"].unsqueeze(-1).expand(-1, -1, 3) + index = ( + self.torch_batch["mapping"].unsqueeze(-1).expand(-1, -1, 3).to(env.DEVICE) + ) extended_coord = torch.gather(pt_coord, dim=1, index=index) - extended_coord = extended_coord - self.torch_batch["shift"] + extended_coord = extended_coord - self.torch_batch["shift"].to(env.DEVICE) extended_atype = torch.gather( - self.torch_batch["atype"], dim=1, index=self.torch_batch["mapping"] + self.torch_batch["atype"].to(env.DEVICE), + dim=1, + index=self.torch_batch["mapping"].to(env.DEVICE), ) descriptor_out, _, _, _, _ = descriptor( extended_coord, extended_atype, - self.torch_batch["nlist"], + self.torch_batch["nlist"].to(env.DEVICE), ) my_embedding = descriptor_out.cpu().detach().numpy() fake_energy = torch.sum(descriptor_out) diff --git a/source/tests/pt/test_fitting_net.py b/source/tests/pt/test_fitting_net.py index ed2c428de5..e12a397347 100644 --- a/source/tests/pt/test_fitting_net.py +++ b/source/tests/pt/test_fitting_net.py @@ -11,6 +11,9 @@ from deepmd.pt.model.task import ( EnergyFittingNet, ) +from deepmd.pt.utils import ( + env, +) from deepmd.pt.utils.env import ( GLOBAL_NP_FLOAT_PRECISION, ) @@ -105,7 +108,7 @@ def test_consistency(self): neuron=self.n_neuron, bias_atom_e=self.dp_fn.bias_atom_e, distinguish_types=True, - ) + ).to(env.DEVICE) for name, param in my_fn.named_parameters(): matched = re.match( "filter_layers\.networks\.(\d).layers\.(\d)\.([a-z]+)", name @@ -129,9 +132,9 @@ def test_consistency(self): embedding = torch.from_numpy(self.embedding) embedding = embedding.view(4, -1, self.embedding_width) atype = torch.from_numpy(self.atype) - ret = my_fn(embedding, atype) + ret = my_fn(embedding.to(env.DEVICE), atype.to(env.DEVICE)) my_energy = ret["energy"] - my_energy = my_energy.detach() + my_energy = my_energy.detach().cpu() np.testing.assert_allclose(dp_energy, my_energy.numpy().reshape([-1])) diff --git a/source/tests/pt/test_mlp.py b/source/tests/pt/test_mlp.py index c06047b2a5..26f0041bf9 100644 --- a/source/tests/pt/test_mlp.py +++ b/source/tests/pt/test_mlp.py @@ -5,6 +5,9 @@ import numpy as np import torch +from deepmd.pt.utils import ( + env, +) from deepmd.pt.utils.env import ( PRECISION_DICT, ) @@ -104,23 +107,27 @@ def test_match_native_layer( inp_shap = ashp + inp_shap rtol, atol = get_tols(prec) dtype = PRECISION_DICT[prec] - xx = torch.arange(np.prod(inp_shap), dtype=dtype).view(inp_shap) + xx = torch.arange(np.prod(inp_shap), dtype=dtype, device=env.DEVICE).view( + inp_shap + ) # def mlp layer - ml = MLPLayer(ninp, nout, bias, ut, ac, resnet, precision=prec) + ml = MLPLayer(ninp, nout, bias, ut, ac, resnet, precision=prec).to( + env.DEVICE + ) # check consistency nl = NativeLayer.deserialize(ml.serialize()) np.testing.assert_allclose( - ml.forward(xx).detach().numpy(), - nl.call(xx.detach().numpy()), + ml.forward(xx).detach().cpu().numpy(), + nl.call(xx.detach().cpu().numpy()), rtol=rtol, atol=atol, err_msg=f"(i={ninp}, o={nout}) bias={bias} use_dt={ut} act={ac} resnet={resnet} prec={prec}", ) # check self-consistency - ml1 = MLPLayer.deserialize(ml.serialize()) + ml1 = MLPLayer.deserialize(ml.serialize()).to(env.DEVICE) np.testing.assert_allclose( - ml.forward(xx).detach().numpy(), - ml1.forward(xx).detach().numpy(), + ml.forward(xx).detach().cpu().numpy(), + ml1.forward(xx).detach().cpu().numpy(), rtol=rtol, atol=atol, err_msg=f"(i={ninp}, o={nout}) bias={bias} use_dt={ut} act={ac} resnet={resnet} prec={prec}", @@ -157,7 +164,9 @@ def test_match_native_net( inp_shap = ashp + inp_shap rtol, atol = get_tols(prec) dtype = PRECISION_DICT[prec] - xx = torch.arange(np.prod(inp_shap), dtype=dtype).view(inp_shap) + xx = torch.arange(np.prod(inp_shap), dtype=dtype, device=env.DEVICE).view( + inp_shap + ) # def MLP layers = [] for ii in range(1, len(ndims)): @@ -166,21 +175,21 @@ def test_match_native_net( ndims[ii - 1], ndims[ii], bias, ut, ac, resnet, precision=prec ).serialize() ) - ml = MLP(layers) + ml = MLP(layers).to(env.DEVICE) # check consistency nl = NativeNet.deserialize(ml.serialize()) np.testing.assert_allclose( - ml.forward(xx).detach().numpy(), - nl.call(xx.detach().numpy()), + ml.forward(xx).detach().cpu().numpy(), + nl.call(xx.detach().cpu().numpy()), rtol=rtol, atol=atol, err_msg=f"net={ndims} bias={bias} use_dt={ut} act={ac} resnet={resnet} prec={prec}", ) # check self-consistency - ml1 = MLP.deserialize(ml.serialize()) + ml1 = MLP.deserialize(ml.serialize()).to(env.DEVICE) np.testing.assert_allclose( - ml.forward(xx).detach().numpy(), - ml1.forward(xx).detach().numpy(), + ml.forward(xx).detach().cpu().numpy(), + ml1.forward(xx).detach().cpu().numpy(), rtol=rtol, atol=atol, err_msg=f"net={ndims} bias={bias} use_dt={ut} act={ac} resnet={resnet} prec={prec}", @@ -219,23 +228,23 @@ def test_match_embedding_net( # input rtol, atol = get_tols(prec) dtype = PRECISION_DICT[prec] - xx = torch.arange(idim, dtype=dtype) + xx = torch.arange(idim, dtype=dtype, device=env.DEVICE) # def MLP - ml = EmbeddingNet(idim, nn, act, idt, prec) + ml = EmbeddingNet(idim, nn, act, idt, prec).to(env.DEVICE) # check consistency nl = DPEmbeddingNet.deserialize(ml.serialize()) np.testing.assert_allclose( - ml.forward(xx).detach().numpy(), - nl.call(xx.detach().numpy()), + ml.forward(xx).detach().cpu().numpy(), + nl.call(xx.detach().cpu().numpy()), rtol=rtol, atol=atol, err_msg=f"idim={idim} nn={nn} use_dt={idt} act={act} prec={prec}", ) # check self-consistency - ml1 = EmbeddingNet.deserialize(ml.serialize()) + ml1 = EmbeddingNet.deserialize(ml.serialize()).to(env.DEVICE) np.testing.assert_allclose( - ml.forward(xx).detach().numpy(), - ml1.forward(xx).detach().numpy(), + ml.forward(xx).detach().cpu().numpy(), + ml1.forward(xx).detach().cpu().numpy(), rtol=rtol, atol=atol, err_msg=f"idim={idim} nn={nn} use_dt={idt} act={act} prec={prec}", @@ -246,8 +255,8 @@ def test_jit( ): for idim, nn, act, idt, prec in self.test_cases: # def MLP - ml = EmbeddingNet(idim, nn, act, idt, prec) - ml1 = EmbeddingNet.deserialize(ml.serialize()) + ml = EmbeddingNet(idim, nn, act, idt, prec).to(env.DEVICE) + ml1 = EmbeddingNet.deserialize(ml.serialize()).to(env.DEVICE) model = torch.jit.script(ml) model = torch.jit.script(ml1) @@ -272,7 +281,7 @@ def test_match_fitting_net( # input rtol, atol = get_tols(prec) dtype = PRECISION_DICT[prec] - xx = torch.arange(idim, dtype=dtype) + xx = torch.arange(idim, dtype=dtype, device=env.DEVICE) # def MLP ml = FittingNet( idim, @@ -282,21 +291,21 @@ def test_match_fitting_net( resnet_dt=idt, precision=prec, bias_out=ob, - ) + ).to(env.DEVICE) # check consistency nl = DPFittingNet.deserialize(ml.serialize()) np.testing.assert_allclose( - ml.forward(xx).detach().numpy(), - nl.call(xx.detach().numpy()), + ml.forward(xx).detach().cpu().numpy(), + nl.call(xx.detach().cpu().numpy()), rtol=rtol, atol=atol, err_msg=f"idim={idim} nn={nn} use_dt={idt} act={act} prec={prec}", ) # check self-consistency - ml1 = FittingNet.deserialize(ml.serialize()) + ml1 = FittingNet.deserialize(ml.serialize()).to(env.DEVICE) np.testing.assert_allclose( - ml.forward(xx).detach().numpy(), - ml1.forward(xx).detach().numpy(), + ml.forward(xx).detach().cpu().numpy(), + ml1.forward(xx).detach().cpu().numpy(), rtol=rtol, atol=atol, err_msg=f"idim={idim} nn={nn} use_dt={idt} act={act} prec={prec}", @@ -315,7 +324,7 @@ def test_jit( resnet_dt=idt, precision=prec, bias_out=ob, - ) - ml1 = FittingNet.deserialize(ml.serialize()) + ).to(env.DEVICE) + ml1 = FittingNet.deserialize(ml.serialize()).to(env.DEVICE) model = torch.jit.script(ml) model = torch.jit.script(ml1) diff --git a/source/tests/pt/test_model.py b/source/tests/pt/test_model.py index c6595e6471..e87a53969c 100644 --- a/source/tests/pt/test_model.py +++ b/source/tests/pt/test_model.py @@ -7,6 +7,10 @@ import tensorflow.compat.v1 as tf import torch +from deepmd.pt.utils import ( + env, +) + tf.disable_eager_execution() from pathlib import ( @@ -340,10 +344,16 @@ def test_consistency(self): batch["natoms_vec"], device=batch["coord"].device ).unsqueeze(0) model_predict = my_model( - batch["coord"], batch["atype"], batch["box"], do_atomic_virial=True + batch["coord"].to(env.DEVICE), + batch["atype"].to(env.DEVICE), + batch["box"].to(env.DEVICE), + do_atomic_virial=True, ) model_predict_1 = my_model( - batch["coord"], batch["atype"], batch["box"], do_atomic_virial=False + batch["coord"].to(env.DEVICE), + batch["atype"].to(env.DEVICE), + batch["box"].to(env.DEVICE), + do_atomic_virial=False, ) p_energy, p_force, p_virial, p_atomic_virial = ( model_predict["energy"], @@ -357,8 +367,8 @@ def test_consistency(self): "force": p_force, } label = { - "energy": batch["energy"], - "force": batch["force"], + "energy": batch["energy"].to(env.DEVICE), + "force": batch["force"].to(env.DEVICE), } loss, _ = my_loss(model_pred, label, int(batch["natoms"][0, 0]), cur_lr) np.testing.assert_allclose( diff --git a/source/tests/pt/test_saveload_dpa1.py b/source/tests/pt/test_saveload_dpa1.py index d1043f7029..1b4c41a204 100644 --- a/source/tests/pt/test_saveload_dpa1.py +++ b/source/tests/pt/test_saveload_dpa1.py @@ -129,13 +129,13 @@ def get_data(self): input_dict = {} for item in ["coord", "atype", "box"]: if item in batch_data: - input_dict[item] = batch_data[item] + input_dict[item] = batch_data[item].to(env.DEVICE) else: input_dict[item] = None label_dict = {} for item in ["energy", "force", "virial"]: if item in batch_data: - label_dict[item] = batch_data[item] + label_dict[item] = batch_data[item].to(env.DEVICE) return input_dict, label_dict def test_saveload(self): diff --git a/source/tests/pt/test_saveload_se_e2_a.py b/source/tests/pt/test_saveload_se_e2_a.py index 95d7f97a88..7f8364a16f 100644 --- a/source/tests/pt/test_saveload_se_e2_a.py +++ b/source/tests/pt/test_saveload_se_e2_a.py @@ -123,13 +123,13 @@ def get_data(self): input_dict = {} for item in ["coord", "atype", "box"]: if item in batch_data: - input_dict[item] = batch_data[item] + input_dict[item] = batch_data[item].to(env.DEVICE) else: input_dict[item] = None label_dict = {} for item in ["energy", "force", "virial"]: if item in batch_data: - label_dict[item] = batch_data[item] + label_dict[item] = batch_data[item].to(env.DEVICE) return input_dict, label_dict def test_saveload(self): From 664c70b395dd8caf73f4aab19e613bd469b1eefc Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 30 Jan 2024 20:23:54 -0500 Subject: [PATCH 033/686] build macos-arm64 wheel on M1 runners (#3206) Today [GitHub introduced the new M1 runners](https://github.blog/changelog/2024-01-30-github-actions-introducing-the-new-m1-macos-runner-available-to-open-source/), making it possible to build macos-arm64 wheels without cross-building. Remove old hacked codes for cross-building. --- .github/workflows/build_wheel.yml | 4 ++-- backend/find_tensorflow.py | 6 ------ pyproject.toml | 6 ++---- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index fa109cac5e..392ce7ac5b 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -41,12 +41,12 @@ jobs: cuda_version: 11.8 dp_pkg_name: deepmd-kit-cu11 # macos-x86-64 - - os: macos-latest + - os: macos-13 python: 311 platform_id: macosx_x86_64 dp_variant: cpu # macos-arm64 - - os: macos-latest + - os: macos-14 python: 311 platform_id: macosx_arm64 dp_variant: cpu diff --git a/backend/find_tensorflow.py b/backend/find_tensorflow.py index 083e2673f7..b43b32f954 100644 --- a/backend/find_tensorflow.py +++ b/backend/find_tensorflow.py @@ -50,12 +50,6 @@ def find_tensorflow() -> Tuple[Optional[str], List[str]]: requires = [] tf_spec = None - if os.environ.get("CIBUILDWHEEL", "0") == "1" and os.environ.get( - "CIBW_BUILD", "" - ).endswith("macosx_arm64"): - # cibuildwheel cross build - site_packages = Path(os.environ.get("RUNNER_TEMP")) / "tensorflow" - tf_spec = FileFinder(str(site_packages)).find_spec("tensorflow") if (tf_spec is None or not tf_spec) and os.environ.get( "TENSORFLOW_ROOT" diff --git a/pyproject.toml b/pyproject.toml index 1d37246a88..aa0da4725d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -143,12 +143,10 @@ manylinux-aarch64-image = "manylinux_2_28" [tool.cibuildwheel.macos] environment = { PIP_PREFER_BINARY="1", DP_LAMMPS_VERSION="stable_2Aug2023_update2", DP_ENABLE_IPI="1" } before-all = [ + # enable MPI for macos-arm64 in the next lammps release for compatibility """if [[ "$CIBW_BUILD" != *macosx_arm64* ]]; then brew install mpich; fi""", ] -before-build = [ - """if [[ "$CIBW_BUILD" == *macosx_arm64* ]]; then python -m pip install "tensorflow-macos>=2.13.0rc0" --platform macosx_12_0_arm64 --no-deps --target=$RUNNER_TEMP/tensorflow; fi""", -] -repair-wheel-command = """if [[ "$CIBW_BUILD" == *macosx_arm64* ]]; then rm -rf $RUNNER_TEMP/tensorflow; fi && delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} --ignore-missing-dependencies""" +repair-wheel-command = """delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} --ignore-missing-dependencies""" [tool.cibuildwheel.linux] repair-wheel-command = "auditwheel repair --exclude libtensorflow_framework.so.2 --exclude libtensorflow_framework.so.1 --exclude libtensorflow_framework.so --exclude _pywrap_tensorflow_internal.so --exclude libtensorflow_cc.so.2 -w {dest_dir} {wheel}" From d2edb775bde315213fcb24e4b82f8441210ab996 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 31 Jan 2024 00:15:52 -0500 Subject: [PATCH 034/686] fix GPU test OOM problem (#3207) Signed-off-by: Jinzhe Zeng --- .github/workflows/test_cuda.yml | 10 +++++----- deepmd/tf/env.py | 3 +++ source/tests/pt/conftest.py | 9 +++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 source/tests/pt/conftest.py diff --git a/.github/workflows/test_cuda.yml b/.github/workflows/test_cuda.yml index 45b689cb3e..4e9725103a 100644 --- a/.github/workflows/test_cuda.yml +++ b/.github/workflows/test_cuda.yml @@ -34,9 +34,9 @@ jobs: && sudo apt-get -y install cuda-12-2 libcudnn8=8.9.5.*-1+cuda12.2 if: false # skip as we use nvidia image - name: Set PyPI mirror for Aliyun cloud machine - run: python -m pip config --user set global.index-url https://mirrors.aliyun.com/pypi/simple/ + run: python -m pip config --user set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/ - run: python -m pip install -U "pip>=21.3.1,!=23.0.0" - - run: python -m pip install "tensorflow>=2.15.0rc0" + - run: python -m pip install "tensorflow>=2.15.0rc0" "torch>=2.2.0" - run: python -m pip install -v -e .[gpu,test,lmp,cu12,torch] "ase @ https://gitlab.com/ase/ase/-/archive/8c5aa5fd6448c5cfb517a014dccf2b214a9dfa8f/ase-8c5aa5fd6448c5cfb517a014dccf2b214a9dfa8f.tar.gz" env: DP_BUILD_TESTING: 1 @@ -44,7 +44,7 @@ jobs: CUDA_PATH: /usr/local/cuda-12.2 NUM_WORKERS: 0 - run: dp --version - - run: python -m pytest -s --cov=deepmd source/tests --durations=0 + - run: python -m pytest --cov=deepmd source/tests --durations=0 - run: source/install/test_cc_local.sh env: OMP_NUM_THREADS: 1 @@ -58,8 +58,8 @@ jobs: - run: | export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/dp_test/lib:$CUDA_PATH/lib64:$LD_LIBRARY_PATH export PATH=$GITHUB_WORKSPACE/dp_test/bin:$PATH - python -m pytest -s --cov=deepmd source/lmp/tests - python -m pytest -s --cov=deepmd source/ipi/tests + python -m pytest --cov=deepmd source/lmp/tests + python -m pytest --cov=deepmd source/ipi/tests env: OMP_NUM_THREADS: 1 TF_INTRA_OP_PARALLELISM_THREADS: 1 diff --git a/deepmd/tf/env.py b/deepmd/tf/env.py index eada2774d3..993768c4a4 100644 --- a/deepmd/tf/env.py +++ b/deepmd/tf/env.py @@ -483,6 +483,9 @@ def _get_package_constants( op_module = get_module("deepmd_op") op_grads_module = get_module("op_grads") +# prevent OOM when using with other backends +# tf.config doesn't work for unclear reason +set_env_if_empty("TF_FORCE_GPU_ALLOW_GROWTH", "true", verbose=False) # FLOAT_PREC GLOBAL_TF_FLOAT_PRECISION = tf.dtypes.as_dtype(GLOBAL_NP_FLOAT_PRECISION) diff --git a/source/tests/pt/conftest.py b/source/tests/pt/conftest.py new file mode 100644 index 0000000000..a1dea6da5a --- /dev/null +++ b/source/tests/pt/conftest.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import pytest +import torch + + +@pytest.fixture(scope="package", autouse=True) +def clear_cuda_memory(request): + yield + torch.cuda.empty_cache() From afb440a5057ad27de594595b7d8b32d1d93ac89e Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Wed, 31 Jan 2024 17:20:22 +0800 Subject: [PATCH 035/686] Feat: add pair table model to pytorch (#3192) Migrated from this [PR](https://github.com/dptech-corp/deepmd-pytorch/pull/174). This is to reimplement the PairTab Model in Pytorch. Notes: 1. Different from the tensorflow version, the pytorch version abstracts away all the post energy conversion operations (force, virial). 2. Added extrapolation when `rcut` > `rmax`. The pytorch version overwrite energy beyond extrapolation endpoint to `0`. These features are not available in the tensorflow version. The extrapolation uses a cubic spline form, the 1st order derivation for the starting point is estimated using the last two rows in the user defined table. See example below: ![img_v3_027k_b50c690d-dc2d-4803-bd2c-2e73aa3c73fg](https://github.com/deepmodeling/deepmd-kit/assets/137014849/f3efa4d3-795e-4ff8-acdc-642227f0e19c) ![img_v3_027k_8de38597-ef4e-4e5b-989e-dbd13cc93fag](https://github.com/deepmodeling/deepmd-kit/assets/137014849/493da26d-f01d-4dd0-8520-ea2d84e7b548) ![img_v3_027k_f8268564-3f5d-49e6-91d6-169a61d9347g](https://github.com/deepmodeling/deepmd-kit/assets/137014849/b8ad4d4d-a4a4-40f0-94d1-810006e7175b) ![img_v3_027k_3966ef67-dd5e-4f48-992e-c2763311451g](https://github.com/deepmodeling/deepmd-kit/assets/137014849/27f31e79-13c8-4ce8-9911-b4cc0ac8188c) --------- Co-authored-by: Anyang Peng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/pt/model/model/pair_tab.py | 312 ++++++++++++++++++ deepmd/utils/pair_tab.py | 154 ++++++++- .../tests/common/test_pairtab_preprocess.py | 263 +++++++++++++++ source/tests/pt/test_pairtab.py | 190 +++++++++++ 4 files changed, 914 insertions(+), 5 deletions(-) create mode 100644 deepmd/pt/model/model/pair_tab.py create mode 100644 source/tests/common/test_pairtab_preprocess.py create mode 100644 source/tests/pt/test_pairtab.py diff --git a/deepmd/pt/model/model/pair_tab.py b/deepmd/pt/model/model/pair_tab.py new file mode 100644 index 0000000000..6f0782289a --- /dev/null +++ b/deepmd/pt/model/model/pair_tab.py @@ -0,0 +1,312 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + List, + Optional, + Union, +) + +import torch +from torch import ( + nn, +) + +from deepmd.model_format import ( + FittingOutputDef, + OutputVariableDef, +) +from deepmd.utils.pair_tab import ( + PairTab, +) + +from .atomic_model import ( + AtomicModel, +) + + +class PairTabModel(nn.Module, AtomicModel): + """Pairwise tabulation energy model. + + This model can be used to tabulate the pairwise energy between atoms for either + short-range or long-range interactions, such as D3, LJ, ZBL, etc. It should not + be used alone, but rather as one submodel of a linear (sum) model, such as + DP+D3. + + Do not put the model on the first model of a linear model, since the linear + model fetches the type map from the first model. + + At this moment, the model does not smooth the energy at the cutoff radius, so + one needs to make sure the energy has been smoothed to zero. + + Parameters + ---------- + tab_file : str + The path to the tabulation file. + rcut : float + The cutoff radius. + sel : int or list[int] + The maxmum number of atoms in the cut-off radius. + """ + + def __init__( + self, tab_file: str, rcut: float, sel: Union[int, List[int]], **kwargs + ): + super().__init__() + self.tab_file = tab_file + self.rcut = rcut + + self.tab = PairTab(self.tab_file, rcut=rcut) + self.ntypes = self.tab.ntypes + + tab_info, tab_data = self.tab.get() # this returns -> Tuple[np.array, np.array] + self.tab_info = torch.from_numpy(tab_info) + self.tab_data = torch.from_numpy(tab_data) + + # self.model_type = "ener" + # self.model_version = MODEL_VERSION ## this shoud be in the parent class + + if isinstance(sel, int): + self.sel = sel + elif isinstance(sel, list): + self.sel = sum(sel) + else: + raise TypeError("sel must be int or list[int]") + + def get_fitting_output_def(self) -> FittingOutputDef: + return FittingOutputDef( + [ + OutputVariableDef( + name="energy", shape=[1], reduciable=True, differentiable=True + ) + ] + ) + + def get_rcut(self) -> float: + return self.rcut + + def get_sel(self) -> int: + return self.sel + + def distinguish_types(self) -> bool: + # to match DPA1 and DPA2. + return False + + def forward_atomic( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + self.nframes, self.nloc, self.nnei = nlist.shape + + # this will mask all -1 in the nlist + masked_nlist = torch.clamp(nlist, 0) + + atype = extended_atype[:, : self.nloc] # (nframes, nloc) + pairwise_dr = self._get_pairwise_dist( + extended_coord + ) # (nframes, nall, nall, 3) + pairwise_rr = pairwise_dr.pow(2).sum(-1).sqrt() # (nframes, nall, nall) + + self.tab_data = self.tab_data.reshape( + self.tab.ntypes, self.tab.ntypes, self.tab.nspline, 4 + ) + + # to calculate the atomic_energy, we need 3 tensors, i_type, j_type, rr + # i_type : (nframes, nloc), this is atype. + # j_type : (nframes, nloc, nnei) + j_type = extended_atype[ + torch.arange(extended_atype.size(0))[:, None, None], masked_nlist + ] + + # slice rr to get (nframes, nloc, nnei) + rr = torch.gather(pairwise_rr[:, : self.nloc, :], 2, masked_nlist) + + raw_atomic_energy = self._pair_tabulated_inter(nlist, atype, j_type, rr) + + atomic_energy = 0.5 * torch.sum( + torch.where( + nlist != -1, raw_atomic_energy, torch.zeros_like(raw_atomic_energy) + ), + dim=-1, + ) + + return {"energy": atomic_energy} + + def _pair_tabulated_inter( + self, + nlist: torch.Tensor, + i_type: torch.Tensor, + j_type: torch.Tensor, + rr: torch.Tensor, + ) -> torch.Tensor: + """Pairwise tabulated energy. + + Parameters + ---------- + nlist : torch.Tensor + The unmasked neighbour list. (nframes, nloc) + i_type : torch.Tensor + The integer representation of atom type for all local atoms for all frames. (nframes, nloc) + j_type : torch.Tensor + The integer representation of atom type for all neighbour atoms of all local atoms for all frames. (nframes, nloc, nnei) + rr : torch.Tensor + The salar distance vector between two atoms. (nframes, nloc, nnei) + + Returns + ------- + torch.Tensor + The masked atomic energy for all local atoms for all frames. (nframes, nloc, nnei) + + Raises + ------ + Exception + If the distance is beyond the table. + + Notes + ----- + This function is used to calculate the pairwise energy between two atoms. + It uses a table containing cubic spline coefficients calculated in PairTab. + """ + rmin = self.tab_info[0] + hh = self.tab_info[1] + hi = 1.0 / hh + + self.nspline = int(self.tab_info[2] + 0.1) + + uu = (rr - rmin) * hi # this is broadcasted to (nframes,nloc,nnei) + + # if nnei of atom 0 has -1 in the nlist, uu would be 0. + # this is to handle the nlist where the mask is set to 0, so that we don't raise exception for those atoms. + uu = torch.where(nlist != -1, uu, self.nspline + 1) + + if torch.any(uu < 0): + raise Exception("coord go beyond table lower boundary") + + idx = uu.to(torch.int) + + uu -= idx + + table_coef = self._extract_spline_coefficient( + i_type, j_type, idx, self.tab_data, self.nspline + ) + table_coef = table_coef.reshape(self.nframes, self.nloc, self.nnei, 4) + ener = self._calcualte_ener(table_coef, uu) + + # here we need to overwrite energy to zero at rcut and beyond. + mask_beyond_rcut = rr >= self.rcut + # also overwrite values beyond extrapolation to zero + extrapolation_mask = rr >= self.tab.rmin + self.nspline * self.tab.hh + ener[mask_beyond_rcut] = 0 + ener[extrapolation_mask] = 0 + + return ener + + @staticmethod + def _get_pairwise_dist(coords: torch.Tensor) -> torch.Tensor: + """Get pairwise distance `dr`. + + Parameters + ---------- + coords : torch.Tensor + The coordinate of the atoms shape of (nframes * nall * 3). + + Returns + ------- + torch.Tensor + The pairwise distance between the atoms (nframes * nall * nall * 3). + + Examples + -------- + coords = torch.tensor([[ + [0,0,0], + [1,3,5], + [2,4,6] + ]]) + + dist = tensor([[ + [[ 0, 0, 0], + [-1, -3, -5], + [-2, -4, -6]], + + [[ 1, 3, 5], + [ 0, 0, 0], + [-1, -1, -1]], + + [[ 2, 4, 6], + [ 1, 1, 1], + [ 0, 0, 0]] + ]]) + """ + return coords.unsqueeze(2) - coords.unsqueeze(1) + + @staticmethod + def _extract_spline_coefficient( + i_type: torch.Tensor, + j_type: torch.Tensor, + idx: torch.Tensor, + tab_data: torch.Tensor, + nspline: int, + ) -> torch.Tensor: + """Extract the spline coefficient from the table. + + Parameters + ---------- + i_type : torch.Tensor + The integer representation of atom type for all local atoms for all frames. (nframes, nloc) + j_type : torch.Tensor + The integer representation of atom type for all neighbour atoms of all local atoms for all frames. (nframes, nloc, nnei) + idx : torch.Tensor + The index of the spline coefficient. (nframes, nloc, nnei) + tab_data : torch.Tensor + The table storing all the spline coefficient. (ntype, ntype, nspline, 4) + nspline : int + The number of splines in the table. + + Returns + ------- + torch.Tensor + The spline coefficient. (nframes, nloc, nnei, 4), shape may be squeezed. + + """ + # (nframes, nloc, nnei) + expanded_i_type = i_type.unsqueeze(-1).expand(-1, -1, j_type.shape[-1]) + + # (nframes, nloc, nnei, nspline, 4) + expanded_tab_data = tab_data[expanded_i_type, j_type] + + # (nframes, nloc, nnei, 1, 4) + expanded_idx = idx.unsqueeze(-1).unsqueeze(-1).expand(-1, -1, -1, -1, 4) + + # handle the case where idx is beyond the number of splines + clipped_indices = torch.clamp(expanded_idx, 0, nspline - 1).to(torch.int64) + + # (nframes, nloc, nnei, 4) + final_coef = torch.gather(expanded_tab_data, 3, clipped_indices).squeeze() + + # when the spline idx is beyond the table, all spline coefficients are set to `0`, and the resulting ener corresponding to the idx is also `0`. + final_coef[expanded_idx.squeeze() > nspline] = 0 + return final_coef + + @staticmethod + def _calcualte_ener(coef: torch.Tensor, uu: torch.Tensor) -> torch.Tensor: + """Calculate energy using spline coeeficients. + + Parameters + ---------- + coef : torch.Tensor + The spline coefficients. (nframes, nloc, nnei, 4) + uu : torch.Tensor + The atom displancemnt used in interpolation and extrapolation (nframes, nloc, nnei) + + Returns + ------- + torch.Tensor + The atomic energy for all local atoms for all frames. (nframes, nloc, nnei) + """ + a3, a2, a1, a0 = torch.unbind(coef, dim=-1) + etmp = (a3 * uu + a2) * uu + a1 # this should be elementwise operations. + ener = etmp * uu + a0 # this energy has the extrapolated value when rcut > rmax + return ener diff --git a/deepmd/utils/pair_tab.py b/deepmd/utils/pair_tab.py index 4451f53379..56f8e618df 100644 --- a/deepmd/utils/pair_tab.py +++ b/deepmd/utils/pair_tab.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: LGPL-3.0-or-later +import logging from typing import ( + Optional, Tuple, ) @@ -25,11 +27,11 @@ class PairTab: The columes from 2nd to 4th are for 0-0, 0-1 and 1-1 correspondingly. """ - def __init__(self, filename: str) -> None: + def __init__(self, filename: str, rcut: Optional[float] = None) -> None: """Constructor.""" - self.reinit(filename) + self.reinit(filename, rcut) - def reinit(self, filename: str) -> None: + def reinit(self, filename: str, rcut: Optional[float] = None) -> None: """Initialize the tabulated interaction. Parameters @@ -44,8 +46,8 @@ def reinit(self, filename: str) -> None: """ self.vdata = np.loadtxt(filename) self.rmin = self.vdata[0][0] + self.rmax = self.vdata[-1][0] self.hh = self.vdata[1][0] - self.vdata[0][0] - self.nspline = self.vdata.shape[0] - 1 ncol = self.vdata.shape[1] - 1 n0 = (-1 + np.sqrt(1 + 8 * ncol)) * 0.5 self.ntypes = int(n0 + 0.1) @@ -53,13 +55,155 @@ def reinit(self, filename: str) -> None: "number of volumes provided in %s does not match guessed number of types %d" % (filename, self.ntypes) ) + + # check table data against rcut and update tab_file if needed, table upper boundary is used as rcut if not provided. + self.rcut = rcut if rcut is not None else self.rmax + self._check_table_upper_boundary() + self.nspline = ( + self.vdata.shape[0] - 1 + ) # this nspline is updated based on the expanded table. self.tab_info = np.array([self.rmin, self.hh, self.nspline, self.ntypes]) self.tab_data = self._make_data() + def _check_table_upper_boundary(self) -> None: + """Update User Provided Table Based on `rcut`. + + This function checks the upper boundary provided in the table against rcut. + If the table upper boundary values decay to zero before rcut, padding zeros will + be added to the table to cover rcut; if the table upper boundary values do not decay to zero + before ruct, extrapolation will be performed till rcut. + + Examples + -------- + table = [[0.005 1. 2. 3. ] + [0.01 0.8 1.6 2.4 ] + [0.015 0. 1. 1.5 ]] + + rcut = 0.022 + + new_table = [[0.005 1. 2. 3. ] + [0.01 0.8 1.6 2.4 ] + [0.015 0. 1. 1.5 ] + [0.02 0. 0. 0. ] + + ---------------------------------------------- + + table = [[0.005 1. 2. 3. ] + [0.01 0.8 1.6 2.4 ] + [0.015 0.5 1. 1.5 ] + [0.02 0.25 0.4 0.75 ] + [0.025 0. 0.1 0. ] + [0.03 0. 0. 0. ]] + + rcut = 0.031 + + new_table = [[0.005 1. 2. 3. ] + [0.01 0.8 1.6 2.4 ] + [0.015 0.5 1. 1.5 ] + [0.02 0.25 0.4 0.75 ] + [0.025 0. 0.1 0. ] + [0.03 0. 0. 0. ] + [0.035 0. 0. 0. ]] + """ + upper_val = self.vdata[-1][1:] + upper_idx = self.vdata.shape[0] - 1 + self.ncol = self.vdata.shape[1] + + # the index in table for the grid point of rcut, always give the point after rcut. + rcut_idx = int(np.ceil(self.rcut / self.hh - self.rmin / self.hh)) + if np.all(upper_val == 0): + # if table values decay to `0` after rcut + if self.rcut < self.rmax and np.any(self.vdata[rcut_idx - 1][1:] != 0): + logging.warning( + "The energy provided in the table does not decay to 0 at rcut." + ) + # if table values decay to `0` at rcut, do nothing + + # if table values decay to `0` before rcut, pad table with `0`s. + elif self.rcut > self.rmax: + pad_zero = np.zeros((rcut_idx - upper_idx, self.ncol)) + pad_zero[:, 0] = np.linspace( + self.rmax + self.hh, + self.rmax + self.hh * (rcut_idx - upper_idx), + rcut_idx - upper_idx, + ) + self.vdata = np.concatenate((self.vdata, pad_zero), axis=0) + else: + # if table values do not decay to `0` at rcut + if self.rcut <= self.rmax: + logging.warning( + "The energy provided in the table does not decay to 0 at rcut." + ) + # if rcut goes beyond table upper bond, need extrapolation, ensure values decay to `0` before rcut. + else: + logging.warning( + "The rcut goes beyond table upper boundary, performing extrapolation." + ) + pad_extrapolation = np.zeros((rcut_idx - upper_idx, self.ncol)) + + pad_extrapolation[:, 0] = np.linspace( + self.rmax + self.hh, + self.rmax + self.hh * (rcut_idx - upper_idx), + rcut_idx - upper_idx, + ) + # need to calculate table values to fill in with cubic spline + pad_extrapolation = self._extrapolate_table(pad_extrapolation) + + self.vdata = np.concatenate((self.vdata, pad_extrapolation), axis=0) + def get(self) -> Tuple[np.array, np.array]: """Get the serialized table.""" return self.tab_info, self.tab_data + def _extrapolate_table(self, pad_extrapolation: np.array) -> np.array: + """Soomth extrapolation between table upper boundary and rcut. + + This method should only be used when the table upper boundary `rmax` is smaller than `rcut`, and + the table upper boundary values are not zeros. To simplify the problem, we use a single + cubic spline between `rmax` and `rcut` for each pair of atom types. One can substitute this extrapolation + to higher order polynomials if needed. + + There are two scenarios: + 1. `ruct` - `rmax` >= hh: + Set values at the grid point right before `rcut` to 0, and perform exterapolation between + the grid point and `rmax`, this allows smooth decay to 0 at `rcut`. + 2. `rcut` - `rmax` < hh: + Set values at `rmax + hh` to 0, and perform extrapolation between `rmax` and `rmax + hh`. + + Parameters + ---------- + pad_extrapolation : np.array + The emepty grid that holds the extrapolation values. + + Returns + ------- + np.array + The cubic spline extrapolation. + """ + # in theory we should check if the table has at least two rows. + slope = self.vdata[-1, 1:] - self.vdata[-2, 1:] # shape of (ncol-1, ) + + # for extrapolation, we want values decay to `0` prior to `ruct` if possible + # here we try to find the grid point prior to `rcut` + grid_point = ( + -2 if pad_extrapolation[-1, 0] / self.hh - self.rmax / self.hh >= 2 else -1 + ) + temp_grid = np.stack((self.vdata[-1, :], pad_extrapolation[grid_point, :])) + vv = temp_grid[:, 1:] + xx = temp_grid[:, 0] + cs = CubicSpline(xx, vv, bc_type=((1, slope), (1, np.zeros_like(slope)))) + xx_grid = pad_extrapolation[:, 0] + res = cs(xx_grid) + + pad_extrapolation[:, 1:] = res + + # Note: when doing cubic spline, if we want to ensure values decay to zero prior to `rcut` + # this may cause values be positive post `rcut`, we need to overwrite those values to zero + pad_extrapolation = ( + pad_extrapolation if grid_point == -1 else pad_extrapolation[:-1, :] + ) + return pad_extrapolation + def _make_data(self): data = np.zeros([self.ntypes * self.ntypes * 4 * self.nspline]) stride = 4 * self.nspline @@ -68,7 +212,7 @@ def _make_data(self): for t0 in range(self.ntypes): for t1 in range(t0, self.ntypes): vv = self.vdata[:, 1 + idx_iter] - cs = CubicSpline(xx, vv) + cs = CubicSpline(xx, vv, bc_type="clamped") dd = cs(xx, 1) dd *= self.hh dtmp = np.zeros(stride) diff --git a/source/tests/common/test_pairtab_preprocess.py b/source/tests/common/test_pairtab_preprocess.py new file mode 100644 index 0000000000..a866c42236 --- /dev/null +++ b/source/tests/common/test_pairtab_preprocess.py @@ -0,0 +1,263 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest +from unittest.mock import ( + patch, +) + +import numpy as np + +from deepmd.utils.pair_tab import ( + PairTab, +) + + +class TestPairTabPreprocessExtrapolate(unittest.TestCase): + @patch("numpy.loadtxt") + def setUp(self, mock_loadtxt) -> None: + file_path = "dummy_path" + mock_loadtxt.return_value = np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + ] + ) + + self.tab1 = PairTab(filename=file_path, rcut=0.028) + self.tab2 = PairTab(filename=file_path, rcut=0.02) + self.tab3 = PairTab(filename=file_path, rcut=0.022) + self.tab4 = PairTab(filename=file_path, rcut=0.03) + self.tab5 = PairTab(filename=file_path, rcut=0.032) + + def test_preprocess(self): + np.testing.assert_allclose( + self.tab1.vdata, + np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.0, 0.0, 0.0], + ] + ), + rtol=1e-04, + atol=1e-04, + ) + np.testing.assert_allclose( + self.tab2.vdata, + np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + ] + ), + rtol=1e-04, + atol=1e-04, + ) + + # for this test case, the table does not decay to zero at rcut = 0.22, + # in the cubic spline code, we use a fixed size grid, if will be a problem if we introduce variable gird size. + # we will do post process to overwrite spline coefficient `a3`,`a2`,`a1`,`a0`, to ensure energy decays to `0`. + np.testing.assert_allclose( + self.tab3.vdata, + np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.0, 0.0, 0.0], + ] + ), + rtol=1e-04, + atol=1e-04, + ) + + np.testing.assert_allclose( + self.tab4.vdata, + np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.0, 0.0, 0.0], + ] + ), + rtol=1e-04, + atol=1e-04, + ) + + np.testing.assert_allclose( + self.tab5.vdata, + np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.12468, 0.1992, 0.3741], + [0.03, 0.0, 0.0, 0.0], + ] + ), + rtol=1e-04, + atol=1e-04, + ) + + +class TestPairTabPreprocessZero(unittest.TestCase): + @patch("numpy.loadtxt") + def setUp(self, mock_loadtxt) -> None: + file_path = "dummy_path" + mock_loadtxt.return_value = np.array( + [ + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.0, 0.0, 0.0], + ] + ) + + self.tab1 = PairTab(filename=file_path, rcut=0.023) + self.tab2 = PairTab(filename=file_path, rcut=0.025) + self.tab3 = PairTab(filename=file_path, rcut=0.028) + self.tab4 = PairTab(filename=file_path, rcut=0.033) + + def test_preprocess(self): + np.testing.assert_allclose( + self.tab1.vdata, + np.array( + [ + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.0, 0.0, 0.0], + ] + ), + ) + np.testing.assert_allclose( + self.tab2.vdata, + np.array( + [ + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.0, 0.0, 0.0], + ] + ), + ) + + np.testing.assert_allclose( + self.tab3.vdata, + np.array( + [ + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.0, 0.0, 0.0], + [0.03, 0.0, 0.0, 0.0], + ] + ), + ) + + np.testing.assert_allclose( + self.tab4.vdata, + np.array( + [ + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.0, 0.0, 0.0], + [0.03, 0.0, 0.0, 0.0], + [0.035, 0.0, 0.0, 0.0], + ] + ), + ) + + +class TestPairTabPreprocessUneven(unittest.TestCase): + @patch("numpy.loadtxt") + def setUp(self, mock_loadtxt) -> None: + file_path = "dummy_path" + mock_loadtxt.return_value = np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.0, 0.1, 0.0], + ] + ) + + self.tab1 = PairTab(filename=file_path, rcut=0.025) + self.tab2 = PairTab(filename=file_path, rcut=0.028) + self.tab3 = PairTab(filename=file_path, rcut=0.03) + self.tab4 = PairTab(filename=file_path, rcut=0.037) + + def test_preprocess(self): + np.testing.assert_allclose( + self.tab1.vdata, + np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.0, 0.1, 0.0], + ] + ), + rtol=1e-04, + atol=1e-04, + ) + np.testing.assert_allclose( + self.tab2.vdata, + np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.0, 0.1, 0.0], + [0.03, 0.0, 0.0, 0.0], + ] + ), + rtol=1e-04, + atol=1e-04, + ) + + np.testing.assert_allclose( + self.tab3.vdata, + np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.0, 0.1, 0.0], + [0.03, 0.0, 0.0, 0.0], + ] + ), + rtol=1e-04, + atol=1e-04, + ) + + np.testing.assert_allclose( + self.tab4.vdata, + np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + [0.025, 0.0, 0.1, 0.0], + [0.03, 0.0, 0.04963, 0.0], + [0.035, 0.0, 0.0, 0.0], + ] + ), + rtol=1e-03, + atol=1e-03, + ) diff --git a/source/tests/pt/test_pairtab.py b/source/tests/pt/test_pairtab.py new file mode 100644 index 0000000000..b4dbda6702 --- /dev/null +++ b/source/tests/pt/test_pairtab.py @@ -0,0 +1,190 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest +from unittest.mock import ( + patch, +) + +import numpy as np +import torch + +from deepmd.pt.model.model.pair_tab import ( + PairTabModel, +) + + +class TestPairTab(unittest.TestCase): + @patch("numpy.loadtxt") + def setUp(self, mock_loadtxt) -> None: + file_path = "dummy_path" + mock_loadtxt.return_value = np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + ] + ) + + self.model = PairTabModel(tab_file=file_path, rcut=0.02, sel=2) + + self.extended_coord = torch.tensor( + [ + [ + [0.01, 0.01, 0.01], + [0.01, 0.02, 0.01], + [0.01, 0.01, 0.02], + [0.02, 0.01, 0.01], + ], + [ + [0.01, 0.01, 0.01], + [0.01, 0.02, 0.01], + [0.01, 0.01, 0.02], + [0.05, 0.01, 0.01], + ], + ] + ) + + # nframes=2, nall=4 + self.extended_atype = torch.tensor([[0, 1, 0, 1], [0, 0, 1, 1]]) + + # nframes=2, nloc=2, nnei=2 + self.nlist = torch.tensor([[[1, 2], [0, 2]], [[1, 2], [0, 3]]]) + + def test_without_mask(self): + result = self.model.forward_atomic( + self.extended_coord, self.extended_atype, self.nlist + ) + expected_result = torch.tensor([[1.2000, 1.3614], [1.2000, 0.4000]]) + + torch.testing.assert_allclose(result["energy"], expected_result, 0.0001, 0.0001) + + def test_with_mask(self): + self.nlist = torch.tensor([[[1, -1], [0, 2]], [[1, 2], [0, 3]]]) + + result = self.model.forward_atomic( + self.extended_coord, self.extended_atype, self.nlist + ) + expected_result = torch.tensor([[0.8000, 1.3614], [1.2000, 0.4000]]) + + torch.testing.assert_allclose(result["energy"], expected_result, 0.0001, 0.0001) + + def test_jit(self): + model = torch.jit.script(self.model) + + +class TestPairTabTwoAtoms(unittest.TestCase): + @patch("numpy.loadtxt") + def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: + """Scenarios to test. + + rcut < rmax: + rr < rcut: use table values, or interpolate. + rr == rcut: use table values, or interpolate. + rr > rcut: should be 0 + rcut == rmax: + rr < rcut: use table values, or interpolate. + rr == rcut: use table values, or interpolate. + rr > rcut: should be 0 + rcut > rmax: + rr < rmax: use table values, or interpolate. + rr == rmax: use table values, or interpolate. + rmax < rr < rcut: extrapolate + rr >= rcut: should be 0 + + """ + file_path = "dummy_path" + mock_loadtxt.return_value = np.array( + [ + [0.005, 1.0], + [0.01, 0.8], + [0.015, 0.5], + [0.02, 0.25], + ] + ) + + # nframes=1, nall=2 + extended_atype = torch.tensor([[0, 0]]) + + # nframes=1, nloc=2, nnei=1 + nlist = torch.tensor([[[1], [-1]]]) + + results = [] + + for dist, rcut in zip( + [ + 0.01, + 0.015, + 0.020, + 0.015, + 0.02, + 0.021, + 0.015, + 0.02, + 0.021, + 0.025, + 0.026, + 0.025, + 0.025, + 0.0216161, + ], + [ + 0.015, + 0.015, + 0.015, + 0.02, + 0.02, + 0.02, + 0.022, + 0.022, + 0.022, + 0.025, + 0.025, + 0.03, + 0.035, + 0.025, + ], + ): + extended_coord = torch.tensor( + [ + [ + [0.0, 0.0, 0.0], + [0.0, dist, 0.0], + ], + ] + ) + + model = PairTabModel(tab_file=file_path, rcut=rcut, sel=2) + results.append( + model.forward_atomic(extended_coord, extended_atype, nlist)["energy"] + ) + + expected_result = torch.stack( + [ + torch.tensor( + [ + [ + [0.4, 0], + [0.0, 0], + [0.0, 0], + [0.25, 0], + [0, 0], + [0, 0], + [0.25, 0], + [0.125, 0], + [0.0922, 0], + [0, 0], + [0, 0], + [0, 0], + [0.0923, 0], + [0.0713, 0], + ] + ] + ) + ] + ).reshape(14, 2) + results = torch.stack(results).reshape(14, 2) + + torch.testing.assert_allclose(results, expected_result, 0.0001, 0.0001) + + if __name__ == "__main__": + unittest.main() From 19a8dfbcb17d69110a414dee93be824a5af21e53 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 31 Jan 2024 09:27:32 -0500 Subject: [PATCH 036/686] pt: set nthreads from env (#3205) Signed-off-by: Jinzhe Zeng --- deepmd/env.py | 83 +++++++++++++++++++++++ deepmd/pt/utils/env.py | 13 ++++ deepmd/tf/env.py | 67 ++---------------- doc/troubleshooting/howtoset_num_nodes.md | 37 +++++++--- source/api_cc/include/common.h | 4 +- source/api_cc/src/common.cc | 19 +++++- source/tests/tf/test_env.py | 4 +- 7 files changed, 149 insertions(+), 78 deletions(-) diff --git a/deepmd/env.py b/deepmd/env.py index b1d4958ed8..1a8da63f8e 100644 --- a/deepmd/env.py +++ b/deepmd/env.py @@ -1,5 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import logging import os +from typing import ( + Tuple, +) import numpy as np @@ -26,3 +30,82 @@ "low. Please set precision with environmental variable " "DP_INTERFACE_PREC." % dp_float_prec ) + + +def set_env_if_empty(key: str, value: str, verbose: bool = True): + """Set environment variable only if it is empty. + + Parameters + ---------- + key : str + env variable name + value : str + env variable value + verbose : bool, optional + if True action will be logged, by default True + """ + if os.environ.get(key) is None: + os.environ[key] = value + if verbose: + logging.warning( + f"Environment variable {key} is empty. Use the default value {value}" + ) + + +def set_default_nthreads(): + """Set internal number of threads to default=automatic selection. + + Notes + ----- + `DP_INTRA_OP_PARALLELISM_THREADS` and `DP_INTER_OP_PARALLELISM_THREADS` + control configuration of multithreading. + """ + if ( + "OMP_NUM_THREADS" not in os.environ + # for backward compatibility + or ( + "DP_INTRA_OP_PARALLELISM_THREADS" not in os.environ + and "TF_INTRA_OP_PARALLELISM_THREADS" not in os.environ + ) + or ( + "DP_INTER_OP_PARALLELISM_THREADS" not in os.environ + and "TF_INTER_OP_PARALLELISM_THREADS" not in os.environ + ) + ): + logging.warning( + "To get the best performance, it is recommended to adjust " + "the number of threads by setting the environment variables " + "OMP_NUM_THREADS, DP_INTRA_OP_PARALLELISM_THREADS, and " + "DP_INTER_OP_PARALLELISM_THREADS. See " + "https://deepmd.rtfd.io/parallelism/ for more information." + ) + if "TF_INTRA_OP_PARALLELISM_THREADS" not in os.environ: + set_env_if_empty("DP_INTRA_OP_PARALLELISM_THREADS", "0", verbose=False) + if "TF_INTER_OP_PARALLELISM_THREADS" not in os.environ: + set_env_if_empty("DP_INTER_OP_PARALLELISM_THREADS", "0", verbose=False) + + +def get_default_nthreads() -> Tuple[int, int]: + """Get paralellism settings. + + The method will first read the environment variables with the prefix `DP_`. + If not found, it will read the environment variables with the prefix `TF_` + for backward compatibility. + + Returns + ------- + Tuple[int, int] + number of `DP_INTRA_OP_PARALLELISM_THREADS` and + `DP_INTER_OP_PARALLELISM_THREADS` + """ + return int( + os.environ.get( + "DP_INTRA_OP_PARALLELISM_THREADS", + os.environ.get("TF_INTRA_OP_PARALLELISM_THREADS", "0"), + ) + ), int( + os.environ.get( + "DP_INTER_OP_PARALLELISM_THREADS", + os.environ.get("TF_INTRA_OP_PARALLELISM_THREADS", "0"), + ) + ) diff --git a/deepmd/pt/utils/env.py b/deepmd/pt/utils/env.py index 559dba0167..b51b03fdc2 100644 --- a/deepmd/pt/utils/env.py +++ b/deepmd/pt/utils/env.py @@ -4,6 +4,11 @@ import numpy as np import torch +from deepmd.env import ( + get_default_nthreads, + set_default_nthreads, +) + PRECISION = os.environ.get("PRECISION", "float64") GLOBAL_NP_FLOAT_PRECISION = getattr(np, PRECISION) GLOBAL_PT_FLOAT_PRECISION = getattr(torch, PRECISION) @@ -37,3 +42,11 @@ "double": torch.float64, } DEFAULT_PRECISION = "float64" + +# throw warnings if threads not set +set_default_nthreads() +inter_nthreads, intra_nthreads = get_default_nthreads() +if inter_nthreads > 0: # the behavior of 0 is not documented + torch.set_num_interop_threads(inter_nthreads) +if intra_nthreads > 0: + torch.set_num_threads(intra_nthreads) diff --git a/deepmd/tf/env.py b/deepmd/tf/env.py index 993768c4a4..6bc89664c7 100644 --- a/deepmd/tf/env.py +++ b/deepmd/tf/env.py @@ -2,7 +2,6 @@ """Module that sets tensorflow working environment and exports inportant constants.""" import ctypes -import logging import os import platform from configparser import ( @@ -19,7 +18,6 @@ TYPE_CHECKING, Any, Dict, - Tuple, ) import numpy as np @@ -31,8 +29,15 @@ from deepmd.env import ( GLOBAL_ENER_FLOAT_PRECISION, GLOBAL_NP_FLOAT_PRECISION, +) +from deepmd.env import get_default_nthreads as get_tf_default_nthreads +from deepmd.env import ( global_float_prec, ) +from deepmd.env import set_default_nthreads as set_tf_default_nthreads +from deepmd.env import ( + set_env_if_empty, +) if TYPE_CHECKING: from types import ( @@ -216,26 +221,6 @@ def dlopen_library(module: str, filename: str): } -def set_env_if_empty(key: str, value: str, verbose: bool = True): - """Set environment variable only if it is empty. - - Parameters - ---------- - key : str - env variable name - value : str - env variable value - verbose : bool, optional - if True action will be logged, by default True - """ - if os.environ.get(key) is None: - os.environ[key] = value - if verbose: - logging.warning( - f"Environment variable {key} is empty. Use the default value {value}" - ) - - def set_mkl(): """Tuning MKL for the best performance. @@ -270,44 +255,6 @@ def set_mkl(): reload(np) -def set_tf_default_nthreads(): - """Set TF internal number of threads to default=automatic selection. - - Notes - ----- - `TF_INTRA_OP_PARALLELISM_THREADS` and `TF_INTER_OP_PARALLELISM_THREADS` - control TF configuration of multithreading. - """ - if ( - "OMP_NUM_THREADS" not in os.environ - or "TF_INTRA_OP_PARALLELISM_THREADS" not in os.environ - or "TF_INTER_OP_PARALLELISM_THREADS" not in os.environ - ): - logging.warning( - "To get the best performance, it is recommended to adjust " - "the number of threads by setting the environment variables " - "OMP_NUM_THREADS, TF_INTRA_OP_PARALLELISM_THREADS, and " - "TF_INTER_OP_PARALLELISM_THREADS. See " - "https://deepmd.rtfd.io/parallelism/ for more information." - ) - set_env_if_empty("TF_INTRA_OP_PARALLELISM_THREADS", "0", verbose=False) - set_env_if_empty("TF_INTER_OP_PARALLELISM_THREADS", "0", verbose=False) - - -def get_tf_default_nthreads() -> Tuple[int, int]: - """Get TF paralellism settings. - - Returns - ------- - Tuple[int, int] - number of `TF_INTRA_OP_PARALLELISM_THREADS` and - `TF_INTER_OP_PARALLELISM_THREADS` - """ - return int(os.environ.get("TF_INTRA_OP_PARALLELISM_THREADS", "0")), int( - os.environ.get("TF_INTER_OP_PARALLELISM_THREADS", "0") - ) - - def get_tf_session_config() -> Any: """Configure tensorflow session. diff --git a/doc/troubleshooting/howtoset_num_nodes.md b/doc/troubleshooting/howtoset_num_nodes.md index 8a9beab857..18b1a133ee 100644 --- a/doc/troubleshooting/howtoset_num_nodes.md +++ b/doc/troubleshooting/howtoset_num_nodes.md @@ -22,10 +22,10 @@ Sometimes, `$num_nodes` and the nodes information can be directly given by the H ## Parallelism between independent operators -For CPU devices, TensorFlow use multiple streams to run independent operators (OP). +For CPU devices, TensorFlow and PyTorch use multiple streams to run independent operators (OP). ```bash -export TF_INTER_OP_PARALLELISM_THREADS=3 +export DP_INTER_OP_PARALLELISM_THREADS=3 ``` However, for GPU devices, TensorFlow uses only one compute stream and multiple copy streams. @@ -33,20 +33,35 @@ Note that some of DeePMD-kit OPs do not have GPU support, so it is still encoura ## Parallelism within an individual operators -For CPU devices, `TF_INTRA_OP_PARALLELISM_THREADS` controls parallelism within TensorFlow native OPs when TensorFlow is built against Eigen. +For CPU devices, `DP_INTRA_OP_PARALLELISM_THREADS` controls parallelism within TensorFlow (when TensorFlow is built against Eigen) and PyTorch native OPs. ```bash -export TF_INTRA_OP_PARALLELISM_THREADS=2 +export DP_INTRA_OP_PARALLELISM_THREADS=2 ``` -`OMP_NUM_THREADS` is threads for OpenMP parallelism. It controls parallelism within TensorFlow native OPs when TensorFlow is built by Intel OneDNN and DeePMD-kit custom CPU OPs. -It may also control parallelsim for NumPy when NumPy is built against OpenMP, so one who uses GPUs for training should also care this environmental variable. +`OMP_NUM_THREADS` is the number of threads for OpenMP parallelism. +It controls parallelism within TensorFlow (when TensorFlow is built upon Intel OneDNN) and PyTorch (when PyTorch is built upon OpenMP) native OPs and DeePMD-kit custom CPU OPs. +It may also control parallelism for NumPy when NumPy is built against OpenMP, so one who uses GPUs for training should also care this environmental variable. ```bash export OMP_NUM_THREADS=2 ``` -There are several other environmental variables for OpenMP, such as `KMP_BLOCKTIME`. See [Intel documentation](https://www.intel.com/content/www/us/en/developer/articles/technical/maximize-tensorflow-performance-on-cpu-considerations-and-recommendations-for-inference.html) for detailed information. +There are several other environmental variables for OpenMP, such as `KMP_BLOCKTIME`. + +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} + +See [Intel documentation](https://www.intel.com/content/www/us/en/developer/articles/technical/maximize-tensorflow-performance-on-cpu-considerations-and-recommendations-for-inference.html) for detailed information. + +::: +:::{tab-item} PyTorch {{ pytorch_icon }} + +See [PyTorch documentation](https://pytorch.org/tutorials/recipes/recipes/tuning_guide.html) for detailed information. + +::: +:::: ## Tune the performance @@ -56,8 +71,8 @@ Here are some empirical examples. If you wish to use 3 cores of 2 CPUs on one node, you may set the environmental variables and run DeePMD-kit as follows: ```bash export OMP_NUM_THREADS=3 -export TF_INTRA_OP_PARALLELISM_THREADS=3 -export TF_INTER_OP_PARALLELISM_THREADS=2 +export DP_INTRA_OP_PARALLELISM_THREADS=3 +export DP_INTER_OP_PARALLELISM_THREADS=2 dp train input.json ``` @@ -65,8 +80,8 @@ For a node with 128 cores, it is recommended to start with the following variabl ```bash export OMP_NUM_THREADS=16 -export TF_INTRA_OP_PARALLELISM_THREADS=16 -export TF_INTER_OP_PARALLELISM_THREADS=8 +export DP_INTRA_OP_PARALLELISM_THREADS=16 +export DP_INTER_OP_PARALLELISM_THREADS=8 ``` Again, in general, one should make sure the product of the parallel numbers is less than or equal to the number of cores available. diff --git a/source/api_cc/include/common.h b/source/api_cc/include/common.h index 0392747979..72382169f8 100644 --- a/source/api_cc/include/common.h +++ b/source/api_cc/include/common.h @@ -144,9 +144,9 @@ void select_map_inv(typename std::vector::iterator out, * @brief Get the number of threads from the environment variable. * @details A warning will be thrown if environmental variables are not set. * @param[out] num_intra_nthreads The number of intra threads. Read from - *TF_INTRA_OP_PARALLELISM_THREADS. + *DP_INTRA_OP_PARALLELISM_THREADS. * @param[out] num_inter_nthreads The number of inter threads. Read from - *TF_INTER_OP_PARALLELISM_THREADS. + *DP_INTER_OP_PARALLELISM_THREADS. **/ void get_env_nthreads(int& num_intra_nthreads, int& num_inter_nthreads); diff --git a/source/api_cc/src/common.cc b/source/api_cc/src/common.cc index 2923534fb7..d2923c8d9e 100644 --- a/source/api_cc/src/common.cc +++ b/source/api_cc/src/common.cc @@ -330,23 +330,36 @@ void deepmd::get_env_nthreads(int& num_intra_nthreads, num_intra_nthreads = 0; num_inter_nthreads = 0; const char* env_intra_nthreads = - std::getenv("TF_INTRA_OP_PARALLELISM_THREADS"); + std::getenv("DP_INTRA_OP_PARALLELISM_THREADS"); const char* env_inter_nthreads = + std::getenv("DP_INTER_OP_PARALLELISM_THREADS"); + // backward compatibility + const char* env_intra_nthreads_tf = + std::getenv("TF_INTRA_OP_PARALLELISM_THREADS"); + const char* env_inter_nthreads_tf = std::getenv("TF_INTER_OP_PARALLELISM_THREADS"); const char* env_omp_nthreads = std::getenv("OMP_NUM_THREADS"); if (env_intra_nthreads && std::string(env_intra_nthreads) != std::string("") && atoi(env_intra_nthreads) >= 0) { num_intra_nthreads = atoi(env_intra_nthreads); + } else if (env_intra_nthreads_tf && + std::string(env_intra_nthreads_tf) != std::string("") && + atoi(env_intra_nthreads_tf) >= 0) { + num_intra_nthreads = atoi(env_intra_nthreads_tf); } else { - throw_env_not_set_warning("TF_INTRA_OP_PARALLELISM_THREADS"); + throw_env_not_set_warning("DP_INTRA_OP_PARALLELISM_THREADS"); } if (env_inter_nthreads && std::string(env_inter_nthreads) != std::string("") && atoi(env_inter_nthreads) >= 0) { num_inter_nthreads = atoi(env_inter_nthreads); + } else if (env_inter_nthreads_tf && + std::string(env_inter_nthreads_tf) != std::string("") && + atoi(env_inter_nthreads_tf) >= 0) { + num_inter_nthreads = atoi(env_inter_nthreads_tf); } else { - throw_env_not_set_warning("TF_INTER_OP_PARALLELISM_THREADS"); + throw_env_not_set_warning("DP_INTER_OP_PARALLELISM_THREADS"); } if (!(env_omp_nthreads && std::string(env_omp_nthreads) != std::string("") && atoi(env_omp_nthreads) >= 0)) { diff --git a/source/tests/tf/test_env.py b/source/tests/tf/test_env.py index eb1b40e707..cd066b06a5 100644 --- a/source/tests/tf/test_env.py +++ b/source/tests/tf/test_env.py @@ -19,8 +19,8 @@ def test_empty(self): @mock.patch.dict( "os.environ", values={ - "TF_INTRA_OP_PARALLELISM_THREADS": "5", - "TF_INTER_OP_PARALLELISM_THREADS": "3", + "DP_INTRA_OP_PARALLELISM_THREADS": "5", + "DP_INTER_OP_PARALLELISM_THREADS": "3", }, ) def test_given(self): From 032fa7d1f0f87a3b33c6913e41567a7a8a908cbf Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 31 Jan 2024 09:27:49 -0500 Subject: [PATCH 037/686] pt: add tensorboard and profiler support (#3204) Use the same arguments as TF. [PyTorch on Tensorboard](https://pytorch.org/docs/stable/tensorboard.html): ![1706608497314](https://github.com/deepmodeling/deepmd-kit/assets/9496702/9d747ee2-2e76-43d3-8252-7dbd0cea6768) [PyTorch Profiler on Tensorboard](https://pytorch.org/tutorials/intermediate/tensorboard_profiler_tutorial.html): ![image](https://github.com/deepmodeling/deepmd-kit/assets/9496702/929d69b7-a696-45b1-8e9b-2b491177ad95) --------- Signed-off-by: Jinzhe Zeng --- deepmd/pt/train/training.py | 36 ++++++++++++++++++++++++++++++++++++ deepmd/utils/argcheck.py | 2 +- doc/train/tensorboard.md | 4 ++-- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index e4c672765b..ee0e7a54cc 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -438,6 +438,12 @@ def warm_up_linear(step, warmup_steps): assert sum_prob > 0.0, "Sum of model prob must be larger than 0!" self.model_prob = self.model_prob / sum_prob + # Tensorboard + self.enable_tensorboard = training_params.get("tensorboard", False) + self.tensorboard_log_dir = training_params.get("tensorboard_log_dir", "log") + self.tensorboard_freq = training_params.get("tensorboard_freq", 1) + self.enable_profiler = training_params.get("enable_profiler", False) + def run(self): fout = ( open(self.disp_file, mode="w", buffering=1) if self.rank == 0 else None @@ -448,8 +454,27 @@ def run(self): logging.info("Start to train %d steps.", self.num_steps) if dist.is_initialized(): logging.info(f"Rank: {dist.get_rank()}/{dist.get_world_size()}") + if self.enable_tensorboard: + from torch.utils.tensorboard import ( + SummaryWriter, + ) + + writer = SummaryWriter(log_dir=self.tensorboard_log_dir) + if self.enable_profiler: + prof = torch.profiler.profile( + schedule=torch.profiler.schedule(wait=1, warmup=1, active=3, repeat=1), + on_trace_ready=torch.profiler.tensorboard_trace_handler( + self.tensorboard_log_dir + ), + record_shapes=True, + with_stack=True, + ) + prof.start() def step(_step_id, task_key="Default"): + # PyTorch Profiler + if self.enable_profiler: + prof.step() self.wrapper.train() if isinstance(self.lr_exp, dict): _lr = self.lr_exp[task_key] @@ -654,6 +679,13 @@ def log_loss_valid(_task_key="Default"): with open("checkpoint", "w") as f: f.write(str(self.latest_model)) + # tensorboard + if self.enable_tensorboard and _step_id % self.tensorboard_freq == 0: + writer.add_scalar(f"{task_key}/lr", cur_lr, _step_id) + writer.add_scalar(f"{task_key}/loss", loss, _step_id) + for item in more_loss: + writer.add_scalar(f"{task_key}/{item}", more_loss[item], _step_id) + self.t0 = time.time() for step_id in range(self.num_steps): if step_id < self.start_step: @@ -691,6 +723,10 @@ def log_loss_valid(_task_key="Default"): fout.close() if SAMPLER_RECORD: fout1.close() + if self.enable_tensorboard: + writer.close() + if self.enable_profiler: + prof.stop() def save_model(self, save_path, lr=0.0, step=0): module = self.wrapper.module if dist.is_initialized() else self.wrapper diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 31b54b4d76..dbe4881952 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -1703,7 +1703,7 @@ def training_args(): # ! modified by Ziyao: data configuration isolated. doc_time_training = "Timing durining training." doc_profiling = "Profiling during training." doc_profiling_file = "Output file for profiling." - doc_enable_profiler = "Enable TensorFlow Profiler (available in TensorFlow 2.3) to analyze performance. The log will be saved to `tensorboard_log_dir`." + doc_enable_profiler = "Enable TensorFlow Profiler (available in TensorFlow 2.3) or PyTorch Profiler to analyze performance. The log will be saved to `tensorboard_log_dir`." doc_tensorboard = "Enable tensorboard" doc_tensorboard_log_dir = "The log directory of tensorboard outputs" doc_tensorboard_freq = "The frequency of writing tensorboard events." diff --git a/doc/train/tensorboard.md b/doc/train/tensorboard.md index 1d6c5f0d68..a6cfdccb68 100644 --- a/doc/train/tensorboard.md +++ b/doc/train/tensorboard.md @@ -1,7 +1,7 @@ -# TensorBoard Usage {{ tensorflow_icon }} +# TensorBoard Usage {{ tensorflow_icon }} {{ pytorch_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }} ::: TensorBoard provides the visualization and tooling needed for machine learning From eb9b2efedf4efc946894800a0d7abf5056f4bb7a Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Fri, 2 Feb 2024 14:00:48 +0800 Subject: [PATCH 038/686] feat: breaking: backend indepdent definition for dp model (#3208) Features: - abstract base classes for atomic model, fitting and descriptor. - dp model format for atomic models - dp model format for models. - torch support for atomic model format. - torch support `fparam` and `aparam`. This pr also introduces the following updates: - support region and nlist in numpy code. - class decorator like `fitting_check_output` gives human readable class names. - support int types in precision dict. - fix descriptor interfaces. - refactor torch atomic model impl. introduces dirty hacks to be fixed. - provide `format_nlist` that format the nlist in forward_lower method. Known limitations: - torch atomic model has dirty hacks - interfaces for descriptor, fitting and model statistics was not considered, should be fixed in future PRs. Will be fixed - [x] dp model module path is a mess to be refactorized. - [x] nlist consistency should be checked. if not format nlist. - [x] doc strings. - [x] `fparam` and `aparam` support. --------- Co-authored-by: Han Wang --- deepmd/dpmodel/__init__.py | 34 ++ deepmd/{model_format => dpmodel}/common.py | 6 +- deepmd/dpmodel/descriptor/__init__.py | 12 + deepmd/dpmodel/descriptor/base_descriptor.py | 8 + .../descriptor/make_base_descriptor.py | 106 +++++ .../descriptor}/se_e2_a.py | 52 ++- deepmd/dpmodel/fitting/__init__.py | 12 + deepmd/dpmodel/fitting/base_fitting.py | 8 + .../fitting/invar_fitting.py} | 26 +- deepmd/dpmodel/fitting/make_base_fitting.py | 68 +++ deepmd/dpmodel/model/__init__.py | 16 + deepmd/dpmodel/model/base_atomic_model.py | 8 + deepmd/dpmodel/model/dp_atomic_model.py | 141 ++++++ deepmd/dpmodel/model/dp_model.py | 9 + .../dpmodel/model/make_base_atomic_model.py | 115 +++++ deepmd/dpmodel/model/make_model.py | 275 ++++++++++++ deepmd/dpmodel/model/transform_output.py | 69 +++ .../{model_format => dpmodel}/output_def.py | 3 + .../utils}/__init__.py | 48 +-- .../utils}/env_mat.py | 2 +- .../utils}/network.py | 2 +- deepmd/dpmodel/utils/nlist.py | 252 +++++++++++ deepmd/dpmodel/utils/region.py | 103 +++++ deepmd/pt/model/descriptor/base_descriptor.py | 8 + deepmd/pt/model/descriptor/descriptor.py | 75 +--- deepmd/pt/model/descriptor/dpa1.py | 24 +- deepmd/pt/model/descriptor/dpa2.py | 23 +- deepmd/pt/model/descriptor/hybrid.py | 5 +- deepmd/pt/model/descriptor/repformers.py | 8 +- deepmd/pt/model/descriptor/se_a.py | 22 +- deepmd/pt/model/descriptor/se_atten.py | 8 +- deepmd/pt/model/model/__init__.py | 31 +- deepmd/pt/model/model/atomic_model.py | 70 --- deepmd/pt/model/model/base_atomic_model.py | 9 + deepmd/pt/model/model/dp_atomic_model.py | 104 +++-- deepmd/pt/model/model/ener.py | 66 +-- deepmd/pt/model/model/make_model.py | 174 +++++++- deepmd/pt/model/model/model.py | 4 - deepmd/pt/model/model/pair_tab.py | 18 +- deepmd/pt/model/model/transform_output.py | 3 +- deepmd/pt/model/network/mlp.py | 6 +- deepmd/pt/model/task/__init__.py | 8 +- deepmd/pt/model/task/atten_lcc.py | 6 +- deepmd/pt/model/task/base_fitting.py | 8 + deepmd/pt/model/task/denoise.py | 8 +- deepmd/pt/model/task/dipole.py | 6 +- deepmd/pt/model/task/ener.py | 18 +- deepmd/pt/model/task/fitting.py | 6 +- deepmd/pt/model/task/task.py | 17 - deepmd/pt/model/task/type_predict.py | 4 +- deepmd/pt/utils/env.py | 2 + deepmd/pt/utils/nlist.py | 206 ++------- deepmd/pt/utils/utils.py | 2 +- .../tests/common/test_model_format_utils.py | 407 +++++++++++++++++- source/tests/common/test_output_def.py | 4 +- source/tests/pt/test_descriptor_dpa1.py | 4 +- source/tests/pt/test_descriptor_dpa2.py | 2 +- source/tests/pt/test_dp_atomic_model.py | 112 +++++ source/tests/pt/test_dp_model.py | 388 +++++++++++++++++ source/tests/pt/test_ener_fitting.py | 2 +- source/tests/pt/test_env_mat.py | 25 +- source/tests/pt/test_mlp.py | 6 +- source/tests/pt/test_rotation.py | 22 +- source/tests/pt/test_se_e2_a.py | 4 +- source/tests/pt/test_utils.py | 2 +- 65 files changed, 2738 insertions(+), 564 deletions(-) create mode 100644 deepmd/dpmodel/__init__.py rename deepmd/{model_format => dpmodel}/common.py (85%) create mode 100644 deepmd/dpmodel/descriptor/__init__.py create mode 100644 deepmd/dpmodel/descriptor/base_descriptor.py create mode 100644 deepmd/dpmodel/descriptor/make_base_descriptor.py rename deepmd/{model_format => dpmodel/descriptor}/se_e2_a.py (88%) create mode 100644 deepmd/dpmodel/fitting/__init__.py create mode 100644 deepmd/dpmodel/fitting/base_fitting.py rename deepmd/{model_format/fitting.py => dpmodel/fitting/invar_fitting.py} (96%) create mode 100644 deepmd/dpmodel/fitting/make_base_fitting.py create mode 100644 deepmd/dpmodel/model/__init__.py create mode 100644 deepmd/dpmodel/model/base_atomic_model.py create mode 100644 deepmd/dpmodel/model/dp_atomic_model.py create mode 100644 deepmd/dpmodel/model/dp_model.py create mode 100644 deepmd/dpmodel/model/make_base_atomic_model.py create mode 100644 deepmd/dpmodel/model/make_model.py create mode 100644 deepmd/dpmodel/model/transform_output.py rename deepmd/{model_format => dpmodel}/output_def.py (98%) rename deepmd/{model_format => dpmodel/utils}/__init__.py (54%) rename deepmd/{model_format => dpmodel/utils}/env_mat.py (99%) rename deepmd/{model_format => dpmodel/utils}/network.py (99%) create mode 100644 deepmd/dpmodel/utils/nlist.py create mode 100644 deepmd/dpmodel/utils/region.py create mode 100644 deepmd/pt/model/descriptor/base_descriptor.py delete mode 100644 deepmd/pt/model/model/atomic_model.py create mode 100644 deepmd/pt/model/model/base_atomic_model.py create mode 100644 deepmd/pt/model/task/base_fitting.py create mode 100644 source/tests/pt/test_dp_atomic_model.py create mode 100644 source/tests/pt/test_dp_model.py diff --git a/deepmd/dpmodel/__init__.py b/deepmd/dpmodel/__init__.py new file mode 100644 index 0000000000..5a83bb7bd4 --- /dev/null +++ b/deepmd/dpmodel/__init__.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from .common import ( + DEFAULT_PRECISION, + PRECISION_DICT, + NativeOP, +) +from .model import ( + DPAtomicModel, + DPModel, +) +from .output_def import ( + FittingOutputDef, + ModelOutputDef, + OutputVariableDef, + fitting_check_output, + get_deriv_name, + get_reduce_name, + model_check_output, +) + +__all__ = [ + "DPModel", + "DPAtomicModel", + "PRECISION_DICT", + "DEFAULT_PRECISION", + "NativeOP", + "ModelOutputDef", + "FittingOutputDef", + "OutputVariableDef", + "model_check_output", + "fitting_check_output", + "get_reduce_name", + "get_deriv_name", +] diff --git a/deepmd/model_format/common.py b/deepmd/dpmodel/common.py similarity index 85% rename from deepmd/model_format/common.py rename to deepmd/dpmodel/common.py index d032e5d5df..1e35bd4d49 100644 --- a/deepmd/model_format/common.py +++ b/deepmd/dpmodel/common.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from abc import ( ABC, + abstractmethod, ) import numpy as np @@ -12,6 +13,8 @@ "half": np.float16, "single": np.float32, "double": np.float64, + "int32": np.int32, + "int64": np.int64, } DEFAULT_PRECISION = "float64" @@ -19,9 +22,10 @@ class NativeOP(ABC): """The unit operation of a native model.""" + @abstractmethod def call(self, *args, **kwargs): """Forward pass in NumPy implementation.""" - raise NotImplementedError + pass def __call__(self, *args, **kwargs): """Forward pass in NumPy implementation.""" diff --git a/deepmd/dpmodel/descriptor/__init__.py b/deepmd/dpmodel/descriptor/__init__.py new file mode 100644 index 0000000000..5eca26acc5 --- /dev/null +++ b/deepmd/dpmodel/descriptor/__init__.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from .make_base_descriptor import ( + make_base_descriptor, +) +from .se_e2_a import ( + DescrptSeA, +) + +__all__ = [ + "DescrptSeA", + "make_base_descriptor", +] diff --git a/deepmd/dpmodel/descriptor/base_descriptor.py b/deepmd/dpmodel/descriptor/base_descriptor.py new file mode 100644 index 0000000000..ca403d7f8e --- /dev/null +++ b/deepmd/dpmodel/descriptor/base_descriptor.py @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import numpy as np + +from .make_base_descriptor import ( + make_base_descriptor, +) + +BaseDescriptor = make_base_descriptor(np.ndarray, "call") diff --git a/deepmd/dpmodel/descriptor/make_base_descriptor.py b/deepmd/dpmodel/descriptor/make_base_descriptor.py new file mode 100644 index 0000000000..2b0025af07 --- /dev/null +++ b/deepmd/dpmodel/descriptor/make_base_descriptor.py @@ -0,0 +1,106 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from abc import ( + ABC, + abstractclassmethod, + abstractmethod, +) +from typing import ( + List, + Optional, +) + + +def make_base_descriptor( + t_tensor, + fwd_method_name: str = "forward", +): + """Make the base class for the descriptor. + + Parameters + ---------- + t_tensor + The type of the tensor. used in the type hint. + fwd_method_name + Name of the forward method. For dpmodels, it should be "call". + For torch models, it should be "forward". + + """ + + class BD(ABC): + """Base descriptor provides the interfaces of descriptor.""" + + @abstractmethod + def get_rcut(self) -> float: + """Returns the cut-off radius.""" + pass + + @abstractmethod + def get_sel(self) -> List[int]: + """Returns the number of selected neighboring atoms for each type.""" + pass + + def get_nsel(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + return sum(self.get_sel()) + + def get_nnei(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + return self.get_nsel() + + @abstractmethod + def get_ntypes(self) -> int: + """Returns the number of element types.""" + pass + + @abstractmethod + def get_dim_out(self) -> int: + """Returns the output descriptor dimension.""" + pass + + @abstractmethod + def get_dim_emb(self) -> int: + """Returns the embedding dimension of g2.""" + pass + + @abstractmethod + def distinguish_types(self) -> bool: + """Returns if the descriptor requires a neighbor list that distinguish different + atomic types or not. + """ + pass + + @abstractmethod + def compute_input_stats(self, merged): + """Update mean and stddev for descriptor elements.""" + pass + + @abstractmethod + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + """Initialize the model bias by the statistics.""" + pass + + @abstractmethod + def fwd( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[t_tensor] = None, + ): + """Calculate descriptor.""" + pass + + @abstractmethod + def serialize(self) -> dict: + """Serialize the obj to dict.""" + pass + + @abstractclassmethod + def deserialize(cls): + """Deserialize from a dict.""" + pass + + setattr(BD, fwd_method_name, BD.fwd) + delattr(BD, "fwd") + + return BD diff --git a/deepmd/model_format/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py similarity index 88% rename from deepmd/model_format/se_e2_a.py rename to deepmd/dpmodel/descriptor/se_e2_a.py index f179b10ac3..1cbaf69c49 100644 --- a/deepmd/model_format/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -13,20 +13,22 @@ Optional, ) -from .common import ( +from deepmd.dpmodel import ( DEFAULT_PRECISION, NativeOP, ) -from .env_mat import ( - EnvMat, -) -from .network import ( +from deepmd.dpmodel.utils import ( EmbeddingNet, + EnvMat, NetworkCollection, ) +from .base_descriptor import ( + BaseDescriptor, +) + -class DescrptSeA(NativeOP): +class DescrptSeA(NativeOP, BaseDescriptor): r"""DeepPot-SE constructed from all information (both angular and radial) of atomic configurations. The embedding takes the distance between atoms as input. @@ -193,9 +195,43 @@ def __getitem__(self, key): @property def dim_out(self): + """Returns the output dimension of this descriptor.""" + return self.get_dim_out() + + def get_dim_out(self): """Returns the output dimension of this descriptor.""" return self.neuron[-1] * self.axis_neuron + def get_dim_emb(self): + """Returns the embedding (g2) dimension of this descriptor.""" + return self.neuron[-1] + + def get_rcut(self): + """Returns cutoff radius.""" + return self.rcut + + def get_sel(self): + """Returns cutoff radius.""" + return self.sel + + def distinguish_types(self): + """Returns if the descriptor requires a neighbor list that distinguish different + atomic types or not. + """ + return True + + def get_ntypes(self) -> int: + """Returns the number of element types.""" + return self.ntypes + + def compute_input_stats(self, merged): + """Update mean and stddev for descriptor elements.""" + raise NotImplementedError + + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + """Initialize the model bias by the statistics.""" + raise NotImplementedError + def cal_g( self, ss, @@ -212,6 +248,7 @@ def call( coord_ext, atype_ext, nlist, + mapping: Optional[np.ndarray] = None, ): """Compute the descriptor. @@ -223,6 +260,8 @@ def call( The extended aotm types. shape: nf x nall nlist The neighbor list. shape: nf x nloc x nnei + mapping + The index mapping from extended to lcoal region. not used by this descriptor. Returns ------- @@ -240,6 +279,7 @@ def call( sw The smooth switch function. """ + del mapping # nf x nloc x nnei x 4 rr, ww = self.env_mat.call(coord_ext, atype_ext, nlist, self.davg, self.dstd) nf, nloc, nnei, _ = rr.shape diff --git a/deepmd/dpmodel/fitting/__init__.py b/deepmd/dpmodel/fitting/__init__.py new file mode 100644 index 0000000000..2bd5e23f5b --- /dev/null +++ b/deepmd/dpmodel/fitting/__init__.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from .invar_fitting import ( + InvarFitting, +) +from .make_base_fitting import ( + make_base_fitting, +) + +__all__ = [ + "InvarFitting", + "make_base_fitting", +] diff --git a/deepmd/dpmodel/fitting/base_fitting.py b/deepmd/dpmodel/fitting/base_fitting.py new file mode 100644 index 0000000000..bb1853a4a0 --- /dev/null +++ b/deepmd/dpmodel/fitting/base_fitting.py @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import numpy as np + +from .make_base_fitting import ( + make_base_fitting, +) + +BaseFitting = make_base_fitting(np.ndarray, fwd_method_name="call") diff --git a/deepmd/model_format/fitting.py b/deepmd/dpmodel/fitting/invar_fitting.py similarity index 96% rename from deepmd/model_format/fitting.py rename to deepmd/dpmodel/fitting/invar_fitting.py index 904fb42b76..efe2771323 100644 --- a/deepmd/model_format/fitting.py +++ b/deepmd/dpmodel/fitting/invar_fitting.py @@ -2,30 +2,35 @@ import copy from typing import ( Any, + Dict, List, Optional, ) import numpy as np -from .common import ( +from deepmd.dpmodel import ( DEFAULT_PRECISION, NativeOP, ) -from .network import ( - FittingNet, - NetworkCollection, -) -from .output_def import ( +from deepmd.dpmodel.output_def import ( FittingOutputDef, OutputVariableDef, fitting_check_output, ) +from deepmd.dpmodel.utils import ( + FittingNet, + NetworkCollection, +) + +from .base_fitting import ( + BaseFitting, +) @fitting_check_output -class InvarFitting(NativeOP): - r"""Fitting the energy (or a porperty of `dim_out`) of the system. The force and the virial can also be trained. +class InvarFitting(NativeOP, BaseFitting): + r"""Fitting the energy (or a rotationally invariant porperty of `dim_out`) of the system. The force and the virial can also be trained. Lets take the energy fitting task as an example. The potential energy :math:`E` is a fitting network function of the descriptor :math:`\mathcal{D}`: @@ -279,7 +284,7 @@ def call( h2: Optional[np.array] = None, fparam: Optional[np.array] = None, aparam: Optional[np.array] = None, - ): + ) -> Dict[str, np.array]: """Calculate the fitting. Parameters @@ -320,7 +325,7 @@ def call( "which is not consistent with {self.numb_fparam}.", ) fparam = (fparam - self.fparam_avg) * self.fparam_inv_std - fparam = np.tile(fparam.reshape([nf, 1, -1]), [1, nloc, 1]) + fparam = np.tile(fparam.reshape([nf, 1, self.numb_fparam]), [1, nloc, 1]) xx = np.concatenate( [xx, fparam], axis=-1, @@ -333,6 +338,7 @@ def call( "get an input aparam of dim {aparam.shape[-1]}, ", "which is not consistent with {self.numb_aparam}.", ) + aparam = aparam.reshape([nf, nloc, self.numb_aparam]) aparam = (aparam - self.aparam_avg) * self.aparam_inv_std xx = np.concatenate( [xx, aparam], diff --git a/deepmd/dpmodel/fitting/make_base_fitting.py b/deepmd/dpmodel/fitting/make_base_fitting.py new file mode 100644 index 0000000000..719ac6169e --- /dev/null +++ b/deepmd/dpmodel/fitting/make_base_fitting.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from abc import ( + ABC, + abstractclassmethod, + abstractmethod, +) +from typing import ( + Dict, + Optional, +) + +from deepmd.dpmodel.output_def import ( + FittingOutputDef, +) + + +def make_base_fitting( + t_tensor, + fwd_method_name: str = "forward", +): + """Make the base class for the fitting. + + Parameters + ---------- + t_tensor + The type of the tensor. used in the type hint. + fwd_method_name + Name of the forward method. For dpmodels, it should be "call". + For torch models, it should be "forward". + + """ + + class BF(ABC): + """Base fitting provides the interfaces of fitting net.""" + + @abstractmethod + def output_def(self) -> FittingOutputDef: + """Returns the output def of the fitting net.""" + pass + + @abstractmethod + def fwd( + self, + descriptor: t_tensor, + atype: t_tensor, + gr: Optional[t_tensor] = None, + g2: Optional[t_tensor] = None, + h2: Optional[t_tensor] = None, + fparam: Optional[t_tensor] = None, + aparam: Optional[t_tensor] = None, + ) -> Dict[str, t_tensor]: + """Calculate fitting.""" + pass + + @abstractmethod + def serialize(self) -> dict: + """Serialize the obj to dict.""" + pass + + @abstractclassmethod + def deserialize(cls): + """Deserialize from a dict.""" + pass + + setattr(BF, fwd_method_name, BF.fwd) + delattr(BF, "fwd") + + return BF diff --git a/deepmd/dpmodel/model/__init__.py b/deepmd/dpmodel/model/__init__.py new file mode 100644 index 0000000000..5c0a32673d --- /dev/null +++ b/deepmd/dpmodel/model/__init__.py @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from .dp_atomic_model import ( + DPAtomicModel, +) +from .dp_model import ( + DPModel, +) +from .make_base_atomic_model import ( + make_base_atomic_model, +) + +__all__ = [ + "DPModel", + "DPAtomicModel", + "make_base_atomic_model", +] diff --git a/deepmd/dpmodel/model/base_atomic_model.py b/deepmd/dpmodel/model/base_atomic_model.py new file mode 100644 index 0000000000..b9521cde8e --- /dev/null +++ b/deepmd/dpmodel/model/base_atomic_model.py @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import numpy as np + +from .make_base_atomic_model import ( + make_base_atomic_model, +) + +BaseAtomicModel = make_base_atomic_model(np.ndarray) diff --git a/deepmd/dpmodel/model/dp_atomic_model.py b/deepmd/dpmodel/model/dp_atomic_model.py new file mode 100644 index 0000000000..63c44aa1f8 --- /dev/null +++ b/deepmd/dpmodel/model/dp_atomic_model.py @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import sys +from typing import ( + Dict, + List, + Optional, +) + +import numpy as np + +from deepmd.dpmodel.descriptor import ( # noqa # TODO: should import all descriptors! + DescrptSeA, +) +from deepmd.dpmodel.fitting import ( # noqa # TODO: should import all fittings! + InvarFitting, +) +from deepmd.dpmodel.output_def import ( + FittingOutputDef, +) + +from .base_atomic_model import ( + BaseAtomicModel, +) + + +class DPAtomicModel(BaseAtomicModel): + """Model give atomic prediction of some physical property. + + Parameters + ---------- + descriptor + Descriptor + fitting_net + Fitting net + type_map + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. + + """ + + def __init__( + self, + descriptor, + fitting, + type_map: Optional[List[str]] = None, + ): + super().__init__() + self.type_map = type_map + self.descriptor = descriptor + self.fitting = fitting + + def fitting_output_def(self) -> FittingOutputDef: + """Get the output def of the fitting net.""" + return self.fitting.output_def() + + def get_rcut(self) -> float: + """Get the cut-off radius.""" + return self.descriptor.get_rcut() + + def get_sel(self) -> List[int]: + """Get the neighbor selection.""" + return self.descriptor.get_sel() + + def distinguish_types(self) -> bool: + """Returns if model requires a neighbor list that distinguish different + atomic types or not. + """ + return self.descriptor.distinguish_types() + + def forward_atomic( + self, + extended_coord: np.ndarray, + extended_atype: np.ndarray, + nlist: np.ndarray, + mapping: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + ) -> Dict[str, np.ndarray]: + """Models' atomic predictions. + + Parameters + ---------- + extended_coord + coodinates in extended region + extended_atype + atomic type in extended region + nlist + neighbor list. nf x nloc x nsel + mapping + mapps the extended indices to local indices. nf x nall + fparam + frame parameter. nf x ndf + aparam + atomic parameter. nf x nloc x nda + + Returns + ------- + result_dict + the result dict, defined by the `FittingOutputDef`. + + """ + nframes, nloc, nnei = nlist.shape + atype = extended_atype[:, :nloc] + descriptor, rot_mat, g2, h2, sw = self.descriptor( + extended_coord, + extended_atype, + nlist, + mapping=mapping, + ) + ret = self.fitting( + descriptor, + atype, + gr=rot_mat, + g2=g2, + h2=h2, + fparam=fparam, + aparam=aparam, + ) + return ret + + def serialize(self) -> dict: + return { + "type_map": self.type_map, + "descriptor": self.descriptor.serialize(), + "fitting": self.fitting.serialize(), + "descriptor_name": self.descriptor.__class__.__name__, + "fitting_name": self.fitting.__class__.__name__, + } + + @classmethod + def deserialize(cls, data) -> "DPAtomicModel": + data = copy.deepcopy(data) + descriptor_obj = getattr( + sys.modules[__name__], data["descriptor_name"] + ).deserialize(data["descriptor"]) + fitting_obj = getattr(sys.modules[__name__], data["fitting_name"]).deserialize( + data["fitting"] + ) + obj = cls(descriptor_obj, fitting_obj, type_map=data["type_map"]) + return obj diff --git a/deepmd/dpmodel/model/dp_model.py b/deepmd/dpmodel/model/dp_model.py new file mode 100644 index 0000000000..819d46450e --- /dev/null +++ b/deepmd/dpmodel/model/dp_model.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from .dp_atomic_model import ( + DPAtomicModel, +) +from .make_model import ( + make_model, +) + +DPModel = make_model(DPAtomicModel) diff --git a/deepmd/dpmodel/model/make_base_atomic_model.py b/deepmd/dpmodel/model/make_base_atomic_model.py new file mode 100644 index 0000000000..c057cd25f1 --- /dev/null +++ b/deepmd/dpmodel/model/make_base_atomic_model.py @@ -0,0 +1,115 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from abc import ( + ABC, + abstractclassmethod, + abstractmethod, +) +from typing import ( + Dict, + List, + Optional, +) + +from deepmd.dpmodel.output_def import ( + FittingOutputDef, +) + + +def make_base_atomic_model( + t_tensor, + fwd_method_name: str = "forward_atomic", +): + """Make the base class for the atomic model. + + Parameters + ---------- + t_tensor + The type of the tensor. used in the type hint. + fwd_method_name + Name of the forward method. For dpmodels, it should be "call". + For torch models, it should be "forward". + + """ + + class BAM(ABC): + """Base Atomic Model provides the interfaces of an atomic model.""" + + @abstractmethod + def fitting_output_def(self) -> FittingOutputDef: + """Get the fitting output def.""" + pass + + @abstractmethod + def get_rcut(self) -> float: + """Get the cut-off radius.""" + pass + + @abstractmethod + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + pass + + def get_nsel(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + return sum(self.get_sel()) + + def get_nnei(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + return self.get_nsel() + + @abstractmethod + def distinguish_types(self) -> bool: + """Returns if the model requires a neighbor list that distinguish different + atomic types or not. + """ + pass + + @abstractmethod + def fwd( + self, + extended_coord: t_tensor, + extended_atype: t_tensor, + nlist: t_tensor, + mapping: Optional[t_tensor] = None, + fparam: Optional[t_tensor] = None, + aparam: Optional[t_tensor] = None, + ) -> Dict[str, t_tensor]: + pass + + @abstractmethod + def serialize(self) -> dict: + pass + + @abstractclassmethod + def deserialize(cls): + pass + + def do_grad( + self, + var_name: Optional[str] = None, + ) -> bool: + """Tell if the output variable `var_name` is differentiable. + if var_name is None, returns if any of the variable is differentiable. + + """ + odef = self.fitting_output_def() + if var_name is None: + require: List[bool] = [] + for vv in odef.keys(): + require.append(self.do_grad_(vv)) + return any(require) + else: + return self.do_grad_(var_name) + + def do_grad_( + self, + var_name: str, + ) -> bool: + """Tell if the output variable `var_name` is differentiable.""" + assert var_name is not None + return self.fitting_output_def()[var_name].differentiable + + setattr(BAM, fwd_method_name, BAM.fwd) + delattr(BAM, "fwd") + + return BAM diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py new file mode 100644 index 0000000000..fec04255fa --- /dev/null +++ b/deepmd/dpmodel/model/make_model.py @@ -0,0 +1,275 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + Optional, +) + +import numpy as np + +from deepmd.dpmodel.output_def import ( + ModelOutputDef, +) +from deepmd.dpmodel.utils import ( + build_neighbor_list, + extend_coord_with_ghosts, + nlist_distinguish_types, + normalize_coord, +) + +from .transform_output import ( + communicate_extended_output, + fit_output_to_model_output, +) + + +def make_model(T_AtomicModel): + """Make a model as a derived class of an atomic model. + + The model provide two interfaces. + + 1. the `call_lower`, that takes extended coordinates, atyps and neighbor list, + and outputs the atomic and property and derivatives (if required) on the extended region. + + 2. the `call`, that takes coordinates, atypes and cell and predicts + the atomic and reduced property, and derivatives (if required) on the local region. + + Parameters + ---------- + T_AtomicModel + The atomic model. + + Returns + ------- + CM + The model. + + """ + + class CM(T_AtomicModel): + def __init__( + self, + *args, + **kwargs, + ): + super().__init__( + *args, + **kwargs, + ) + + def model_output_def(self): + """Get the output def for the model.""" + return ModelOutputDef(self.fitting_output_def()) + + def call( + self, + coord, + atype, + box: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, np.ndarray]: + """Return model prediction. + + Parameters + ---------- + coord + The coordinates of the atoms. + shape: nf x (nloc x 3) + atype + The type of atoms. shape: nf x nloc + box + The simulation box. shape: nf x 9 + fparam + frame parameter. nf x ndf + aparam + atomic parameter. nf x nloc x nda + do_atomic_virial + If calculate the atomic virial. + + Returns + ------- + ret_dict + The result dict of type Dict[str,np.ndarray]. + The keys are defined by the `ModelOutputDef`. + + """ + nframes, nloc = atype.shape[:2] + if box is not None: + coord_normalized = normalize_coord( + coord.reshape(nframes, nloc, 3), + box.reshape(nframes, 3, 3), + ) + else: + coord_normalized = coord.copy() + extended_coord, extended_atype, mapping = extend_coord_with_ghosts( + coord_normalized, atype, box, self.get_rcut() + ) + nlist = build_neighbor_list( + extended_coord, + extended_atype, + nloc, + self.get_rcut(), + self.get_sel(), + distinguish_types=self.distinguish_types(), + ) + extended_coord = extended_coord.reshape(nframes, -1, 3) + model_predict_lower = self.call_lower( + extended_coord, + extended_atype, + nlist, + mapping, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + model_predict = communicate_extended_output( + model_predict_lower, + self.model_output_def(), + mapping, + do_atomic_virial=do_atomic_virial, + ) + return model_predict + + def call_lower( + self, + extended_coord: np.ndarray, + extended_atype: np.ndarray, + nlist: np.ndarray, + mapping: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + do_atomic_virial: bool = False, + ): + """Return model prediction. Lower interface that takes + extended atomic coordinates and types, nlist, and mapping + as input, and returns the predictions on the extended region. + The predictions are not reduced. + + Parameters + ---------- + extended_coord + coodinates in extended region + extended_atype + atomic type in extended region + nlist + neighbor list. nf x nloc x nsel + mapping + mapps the extended indices to local indices + fparam + frame parameter. nf x ndf + aparam + atomic parameter. nf x nloc x nda + do_atomic_virial + whether calculate atomic virial + + Returns + ------- + result_dict + the result dict, defined by the `FittingOutputDef`. + + """ + nframes, nall = extended_atype.shape[:2] + extended_coord = extended_coord.reshape(nframes, -1, 3) + nlist = self.format_nlist(extended_coord, extended_atype, nlist) + atomic_ret = self.forward_atomic( + extended_coord, + extended_atype, + nlist, + mapping=mapping, + fparam=fparam, + aparam=aparam, + ) + model_predict = fit_output_to_model_output( + atomic_ret, + self.fitting_output_def(), + extended_coord, + do_atomic_virial=do_atomic_virial, + ) + return model_predict + + def format_nlist( + self, + extended_coord: np.ndarray, + extended_atype: np.ndarray, + nlist: np.ndarray, + ): + """Format the neighbor list. + + 1. If the number of neighbors in the `nlist` is equal to sum(self.sel), + it does nothong + + 2. If the number of neighbors in the `nlist` is smaller than sum(self.sel), + the `nlist` is pad with -1. + + 3. If the number of neighbors in the `nlist` is larger than sum(self.sel), + the nearest sum(sel) neighbors will be preseved. + + Known limitations: + + In the case of self.distinguish_types, the nlist is always formatted. + May have side effact on the efficiency. + + Parameters + ---------- + extended_coord + coodinates in extended region. nf x nall x 3 + extended_atype + atomic type in extended region. nf x nall + nlist + neighbor list. nf x nloc x nsel + + Returns + ------- + formated_nlist + the formated nlist. + + """ + n_nf, n_nloc, n_nnei = nlist.shape + distinguish_types = self.distinguish_types() + ret = self._format_nlist(extended_coord, nlist, sum(self.get_sel())) + if distinguish_types: + ret = nlist_distinguish_types(ret, extended_atype, self.get_sel()) + return ret + + def _format_nlist( + self, + extended_coord: np.ndarray, + nlist: np.ndarray, + nnei: int, + ): + n_nf, n_nloc, n_nnei = nlist.shape + extended_coord = extended_coord.reshape([n_nf, -1, 3]) + nall = extended_coord.shape[1] + rcut = self.get_rcut() + + if n_nnei < nnei: + # make a copy before revise + ret = np.concatenate( + [ + nlist, + -1 * np.ones([n_nf, n_nloc, nnei - n_nnei], dtype=nlist.dtype), + ], + axis=-1, + ) + elif n_nnei > nnei: + # make a copy before revise + m_real_nei = nlist >= 0 + ret = np.where(m_real_nei, nlist, 0) + coord0 = extended_coord[:, :n_nloc, :] + index = ret.reshape(n_nf, n_nloc * n_nnei, 1).repeat(3, axis=2) + coord1 = np.take_along_axis(extended_coord, index, axis=1) + coord1 = coord1.reshape(n_nf, n_nloc, n_nnei, 3) + rr = np.linalg.norm(coord0[:, :, None, :] - coord1, axis=-1) + rr = np.where(m_real_nei, rr, float("inf")) + rr, ret_mapping = np.sort(rr, axis=-1), np.argsort(rr, axis=-1) + ret = np.take_along_axis(ret, ret_mapping, axis=2) + ret = np.where(rr > rcut, -1, ret) + ret = ret[..., :nnei] + else: # n_nnei == nnei: + # copy anyway... + ret = nlist + assert ret.shape[-1] == nnei + return ret + + return CM diff --git a/deepmd/dpmodel/model/transform_output.py b/deepmd/dpmodel/model/transform_output.py new file mode 100644 index 0000000000..3c7917d847 --- /dev/null +++ b/deepmd/dpmodel/model/transform_output.py @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, +) + +import numpy as np + +from deepmd.dpmodel.output_def import ( + FittingOutputDef, + ModelOutputDef, + get_deriv_name, + get_reduce_name, +) + + +def fit_output_to_model_output( + fit_ret: Dict[str, np.ndarray], + fit_output_def: FittingOutputDef, + coord_ext: np.ndarray, + do_atomic_virial: bool = False, +) -> Dict[str, np.ndarray]: + """Transform the output of the fitting network to + the model output. + + """ + model_ret = dict(fit_ret.items()) + for kk, vv in fit_ret.items(): + vdef = fit_output_def[kk] + shap = vdef.shape + atom_axis = -(len(shap) + 1) + if vdef.reduciable: + kk_redu = get_reduce_name(kk) + model_ret[kk_redu] = np.sum(vv, axis=atom_axis) + if vdef.differentiable: + kk_derv_r, kk_derv_c = get_deriv_name(kk) + # name-holders + model_ret[kk_derv_r] = None + model_ret[kk_derv_c] = None + return model_ret + + +def communicate_extended_output( + model_ret: Dict[str, np.ndarray], + model_output_def: ModelOutputDef, + mapping: np.ndarray, # nf x nloc + do_atomic_virial: bool = False, +) -> Dict[str, np.ndarray]: + """Transform the output of the model network defined on + local and ghost (extended) atoms to local atoms. + + """ + new_ret = {} + for kk in model_output_def.keys_outp(): + vv = model_ret[kk] + vdef = model_output_def[kk] + new_ret[kk] = vv + if vdef.reduciable: + kk_redu = get_reduce_name(kk) + new_ret[kk_redu] = model_ret[kk_redu] + if vdef.differentiable: + kk_derv_r, kk_derv_c = get_deriv_name(kk) + # name holders + new_ret[kk_derv_r] = None + new_ret[kk_derv_c] = None + new_ret[kk_derv_c + "_redu"] = None + if not do_atomic_virial: + # pop atomic virial, because it is not correctly calculated. + new_ret.pop(kk_derv_c) + return new_ret diff --git a/deepmd/model_format/output_def.py b/deepmd/dpmodel/output_def.py similarity index 98% rename from deepmd/model_format/output_def.py rename to deepmd/dpmodel/output_def.py index 268dc21ea6..583f88491e 100644 --- a/deepmd/model_format/output_def.py +++ b/deepmd/dpmodel/output_def.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import functools from typing import ( Dict, List, @@ -42,6 +43,7 @@ def model_check_output(cls): """ + @functools.wraps(cls, updated=()) class wrapper(cls): def __init__( self, @@ -81,6 +83,7 @@ def fitting_check_output(cls): """ + @functools.wraps(cls, updated=()) class wrapper(cls): def __init__( self, diff --git a/deepmd/model_format/__init__.py b/deepmd/dpmodel/utils/__init__.py similarity index 54% rename from deepmd/model_format/__init__.py rename to deepmd/dpmodel/utils/__init__.py index e15f73758e..d3c31ae246 100644 --- a/deepmd/model_format/__init__.py +++ b/deepmd/dpmodel/utils/__init__.py @@ -1,15 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from .common import ( - DEFAULT_PRECISION, - PRECISION_DICT, - NativeOP, -) from .env_mat import ( EnvMat, ) -from .fitting import ( - InvarFitting, -) from .network import ( EmbeddingNet, FittingNet, @@ -23,22 +15,21 @@ save_dp_model, traverse_model_dict, ) -from .output_def import ( - FittingOutputDef, - ModelOutputDef, - OutputVariableDef, - fitting_check_output, - get_deriv_name, - get_reduce_name, - model_check_output, +from .nlist import ( + build_multiple_neighbor_list, + build_neighbor_list, + extend_coord_with_ghosts, + get_multiple_nlist_key, + nlist_distinguish_types, ) -from .se_e2_a import ( - DescrptSeA, +from .region import ( + inter2phys, + normalize_coord, + phys2inter, + to_face_distance, ) __all__ = [ - "InvarFitting", - "DescrptSeA", "EnvMat", "make_multilayer_network", "make_embedding_network", @@ -48,17 +39,18 @@ "NativeLayer", "NativeNet", "NetworkCollection", - "NativeOP", "load_dp_model", "save_dp_model", "traverse_model_dict", "PRECISION_DICT", "DEFAULT_PRECISION", - "ModelOutputDef", - "FittingOutputDef", - "OutputVariableDef", - "model_check_output", - "fitting_check_output", - "get_reduce_name", - "get_deriv_name", + "build_neighbor_list", + "nlist_distinguish_types", + "get_multiple_nlist_key", + "build_multiple_neighbor_list", + "extend_coord_with_ghosts", + "normalize_coord", + "inter2phys", + "phys2inter", + "to_face_distance", ] diff --git a/deepmd/model_format/env_mat.py b/deepmd/dpmodel/utils/env_mat.py similarity index 99% rename from deepmd/model_format/env_mat.py rename to deepmd/dpmodel/utils/env_mat.py index 7822bd7d0c..739b06208c 100644 --- a/deepmd/model_format/env_mat.py +++ b/deepmd/dpmodel/utils/env_mat.py @@ -6,7 +6,7 @@ import numpy as np -from .common import ( +from deepmd.dpmodel import ( NativeOP, ) diff --git a/deepmd/model_format/network.py b/deepmd/dpmodel/utils/network.py similarity index 99% rename from deepmd/model_format/network.py rename to deepmd/dpmodel/utils/network.py index f2056c0b95..17b3043612 100644 --- a/deepmd/model_format/network.py +++ b/deepmd/dpmodel/utils/network.py @@ -22,7 +22,7 @@ except ImportError: __version__ = "unknown" -from .common import ( +from deepmd.dpmodel import ( DEFAULT_PRECISION, PRECISION_DICT, NativeOP, diff --git a/deepmd/dpmodel/utils/nlist.py b/deepmd/dpmodel/utils/nlist.py new file mode 100644 index 0000000000..bc6592d52b --- /dev/null +++ b/deepmd/dpmodel/utils/nlist.py @@ -0,0 +1,252 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + List, + Optional, + Union, +) + +import numpy as np + +from .region import ( + to_face_distance, +) + + +## translated from torch implemantation by chatgpt +def build_neighbor_list( + coord1: np.ndarray, + atype: np.ndarray, + nloc: int, + rcut: float, + sel: Union[int, List[int]], + distinguish_types: bool = True, +) -> np.ndarray: + """Build neightbor list for a single frame. keeps nsel neighbors. + + Parameters + ---------- + coord1 : np.ndarray + exptended coordinates of shape [batch_size, nall x 3] + atype : np.ndarray + extended atomic types of shape [batch_size, nall] + nloc : int + number of local atoms. + rcut : float + cut-off radius + sel : int or List[int] + maximal number of neighbors (of each type). + if distinguish_types==True, nsel should be list and + the length of nsel should be equal to number of + types. + distinguish_types : bool + distinguish different types. + + Returns + ------- + neighbor_list : np.ndarray + Neighbor list of shape [batch_size, nloc, nsel], the neighbors + are stored in an ascending order. If the number of + neighbors is less than nsel, the positions are masked + with -1. The neighbor list of an atom looks like + |------ nsel ------| + xx xx xx xx -1 -1 -1 + if distinguish_types==True and we have two types + |---- nsel[0] -----| |---- nsel[1] -----| + xx xx xx xx -1 -1 -1 xx xx xx -1 -1 -1 -1 + + """ + batch_size = coord1.shape[0] + coord1 = coord1.reshape(batch_size, -1) + nall = coord1.shape[1] // 3 + if isinstance(sel, int): + sel = [sel] + nsel = sum(sel) + coord0 = coord1[:, : nloc * 3] + diff = ( + coord1.reshape([batch_size, -1, 3])[:, None, :, :] + - coord0.reshape([batch_size, -1, 3])[:, :, None, :] + ) + assert list(diff.shape) == [batch_size, nloc, nall, 3] + rr = np.linalg.norm(diff, axis=-1) + nlist = np.argsort(rr, axis=-1) + rr = np.sort(rr, axis=-1) + rr = rr[:, :, 1:] + nlist = nlist[:, :, 1:] + nnei = rr.shape[2] + if nsel <= nnei: + rr = rr[:, :, :nsel] + nlist = nlist[:, :, :nsel] + else: + rr = np.concatenate( + [rr, np.ones([batch_size, nloc, nsel - nnei]) + rcut], axis=-1 + ) + nlist = np.concatenate( + [nlist, np.ones([batch_size, nloc, nsel - nnei], dtype=nlist.dtype)], + axis=-1, + ) + assert list(nlist.shape) == [batch_size, nloc, nsel] + nlist = np.where((rr > rcut), -1, nlist) + + if distinguish_types: + return nlist_distinguish_types(nlist, atype, sel) + else: + return nlist + + +def nlist_distinguish_types( + nlist: np.ndarray, + atype: np.ndarray, + sel: List[int], +): + """Given a nlist that does not distinguish atom types, return a nlist that + distinguish atom types. + + """ + nf, nloc, _ = nlist.shape + ret_nlist = [] + tmp_atype = np.tile(atype[:, None], [1, nloc, 1]) + mask = nlist == -1 + tnlist_0 = nlist.copy() + tnlist_0[mask] = 0 + tnlist = np.take_along_axis(tmp_atype, tnlist_0, axis=2).squeeze() + tnlist = np.where(mask, -1, tnlist) + snsel = tnlist.shape[2] + for ii, ss in enumerate(sel): + pick_mask = (tnlist == ii).astype(np.int32) + sorted_indices = np.argsort(-pick_mask, kind="stable", axis=-1) + pick_mask_sorted = -np.sort(-pick_mask, axis=-1) + inlist = np.take_along_axis(nlist, sorted_indices, axis=2) + inlist = np.where(~pick_mask_sorted.astype(bool), -1, inlist) + ret_nlist.append(np.split(inlist, [ss, snsel - ss], axis=-1)[0]) + ret = np.concatenate(ret_nlist, axis=-1) + return ret + + +def get_multiple_nlist_key(rcut: float, nsel: int) -> str: + return str(rcut) + "_" + str(nsel) + + +## translated from torch implemantation by chatgpt +def build_multiple_neighbor_list( + coord: np.ndarray, + nlist: np.ndarray, + rcuts: List[float], + nsels: List[int], +) -> Dict[str, np.ndarray]: + """Input one neighbor list, and produce multiple neighbor lists with + different cutoff radius and numbers of selection out of it. The + required rcuts and nsels should be smaller or equal to the input nlist. + + Parameters + ---------- + coord : np.ndarray + exptended coordinates of shape [batch_size, nall x 3] + nlist : np.ndarray + Neighbor list of shape [batch_size, nloc, nsel], the neighbors + should be stored in an ascending order. + rcuts : List[float] + list of cut-off radius in ascending order. + nsels : List[int] + maximal number of neighbors in ascending order. + + Returns + ------- + nlist_dict : Dict[str, np.ndarray] + A dict of nlists, key given by get_multiple_nlist_key(rc, nsel) + value being the corresponding nlist. + + """ + assert len(rcuts) == len(nsels) + if len(rcuts) == 0: + return {} + nb, nloc, nsel = nlist.shape + if nsel < nsels[-1]: + pad = -1 * np.ones((nb, nloc, nsels[-1] - nsel), dtype=nlist.dtype) + nlist = np.concatenate([nlist, pad], axis=-1) + nsel = nsels[-1] + coord1 = coord.reshape(nb, -1, 3) + nall = coord1.shape[1] + coord0 = coord1[:, :nloc, :] + nlist_mask = nlist == -1 + tnlist_0 = nlist + tnlist_0[nlist_mask] = 0 + index = np.tile(tnlist_0.reshape(nb, nloc * nsel, 1), [1, 1, 3]) + coord2 = np.take_along_axis(coord1, index, axis=1).reshape(nb, nloc, nsel, 3) + diff = coord2 - coord0[:, :, None, :] + rr = np.linalg.norm(diff, axis=-1) + rr = np.where(nlist_mask, float("inf"), rr) + nlist0 = nlist + ret = {} + for rc, ns in zip(rcuts[::-1], nsels[::-1]): + tnlist_1 = np.copy(nlist0[:, :, :ns]) + tnlist_1[rr[:, :, :ns] > rc] = int(-1) + ret[get_multiple_nlist_key(rc, ns)] = tnlist_1 + return ret + + +## translated from torch implemantation by chatgpt +def extend_coord_with_ghosts( + coord: np.ndarray, + atype: np.ndarray, + cell: Optional[np.ndarray], + rcut: float, +): + """Extend the coordinates of the atoms by appending peridoc images. + The number of images is large enough to ensure all the neighbors + within rcut are appended. + + Parameters + ---------- + coord : np.ndarray + original coordinates of shape [-1, nloc*3]. + atype : np.ndarray + atom type of shape [-1, nloc]. + cell : np.ndarray + simulation cell tensor of shape [-1, 9]. + rcut : float + the cutoff radius + + Returns + ------- + extended_coord: np.ndarray + extended coordinates of shape [-1, nall*3]. + extended_atype: np.ndarray + extended atom type of shape [-1, nall]. + index_mapping: np.ndarray + maping extended index to the local index + + """ + nf, nloc = atype.shape + aidx = np.tile(np.arange(nloc)[np.newaxis, :], (nf, 1)) + if cell is None: + nall = nloc + extend_coord = coord.copy() + extend_atype = atype.copy() + extend_aidx = aidx.copy() + else: + coord = coord.reshape((nf, nloc, 3)) + cell = cell.reshape((nf, 3, 3)) + to_face = to_face_distance(cell) + nbuff = np.ceil(rcut / to_face).astype(int) + nbuff = np.max(nbuff, axis=0) + xi = np.arange(-nbuff[0], nbuff[0] + 1, 1) + yi = np.arange(-nbuff[1], nbuff[1] + 1, 1) + zi = np.arange(-nbuff[2], nbuff[2] + 1, 1) + xyz = np.outer(xi, np.array([1, 0, 0]))[:, np.newaxis, np.newaxis, :] + xyz = xyz + np.outer(yi, np.array([0, 1, 0]))[np.newaxis, :, np.newaxis, :] + xyz = xyz + np.outer(zi, np.array([0, 0, 1]))[np.newaxis, np.newaxis, :, :] + xyz = xyz.reshape(-1, 3) + shift_idx = xyz[np.argsort(np.linalg.norm(xyz, axis=1))] + ns, _ = shift_idx.shape + nall = ns * nloc + shift_vec = np.einsum("sd,fdk->fsk", shift_idx, cell) + extend_coord = coord[:, None, :, :] + shift_vec[:, :, None, :] + extend_atype = np.tile(atype[:, :, np.newaxis], (1, ns, 1)) + extend_aidx = np.tile(aidx[:, :, np.newaxis], (1, ns, 1)) + + return ( + extend_coord.reshape((nf, nall * 3)), + extend_atype.reshape((nf, nall)), + extend_aidx.reshape((nf, nall)), + ) diff --git a/deepmd/dpmodel/utils/region.py b/deepmd/dpmodel/utils/region.py new file mode 100644 index 0000000000..ddbc4b29b8 --- /dev/null +++ b/deepmd/dpmodel/utils/region.py @@ -0,0 +1,103 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import numpy as np + + +def phys2inter( + coord: np.ndarray, + cell: np.ndarray, +) -> np.ndarray: + """Convert physical coordinates to internal(direct) coordinates. + + Parameters + ---------- + coord : np.ndarray + physical coordinates of shape [*, na, 3]. + cell : np.ndarray + simulation cell tensor of shape [*, 3, 3]. + + Returns + ------- + inter_coord: np.ndarray + the internal coordinates + + """ + rec_cell = np.linalg.inv(cell) + return np.matmul(coord, rec_cell) + + +def inter2phys( + coord: np.ndarray, + cell: np.ndarray, +) -> np.ndarray: + """Convert internal(direct) coordinates to physical coordinates. + + Parameters + ---------- + coord : np.ndarray + internal coordinates of shape [*, na, 3]. + cell : np.ndarray + simulation cell tensor of shape [*, 3, 3]. + + Returns + ------- + phys_coord: np.ndarray + the physical coordinates + + """ + return np.matmul(coord, cell) + + +def normalize_coord( + coord: np.ndarray, + cell: np.ndarray, +) -> np.ndarray: + """Apply PBC according to the atomic coordinates. + + Parameters + ---------- + coord : np.ndarray + orignal coordinates of shape [*, na, 3]. + cell : np.ndarray + simulation cell shape [*, 3, 3]. + + Returns + ------- + wrapped_coord: np.ndarray + wrapped coordinates of shape [*, na, 3]. + + """ + icoord = phys2inter(coord, cell) + icoord = np.remainder(icoord, 1.0) + return inter2phys(icoord, cell) + + +def to_face_distance( + cell: np.ndarray, +) -> np.ndarray: + """Compute the to-face-distance of the simulation cell. + + Parameters + ---------- + cell : np.ndarray + simulation cell tensor of shape [*, 3, 3]. + + Returns + ------- + dist: np.ndarray + the to face distances of shape [*, 3] + + """ + cshape = cell.shape + dist = b_to_face_distance(cell.reshape([-1, 3, 3])) + return dist.reshape(list(cshape[:-2]) + [3]) # noqa:RUF005 + + +def b_to_face_distance(cell): + volume = np.linalg.det(cell) + c_yz = np.cross(cell[:, 1], cell[:, 2], axis=-1) + _h2yz = volume / np.linalg.norm(c_yz, axis=-1) + c_zx = np.cross(cell[:, 2], cell[:, 0], axis=-1) + _h2zx = volume / np.linalg.norm(c_zx, axis=-1) + c_xy = np.cross(cell[:, 0], cell[:, 1], axis=-1) + _h2xy = volume / np.linalg.norm(c_xy, axis=-1) + return np.stack([_h2yz, _h2zx, _h2xy], axis=1) diff --git a/deepmd/pt/model/descriptor/base_descriptor.py b/deepmd/pt/model/descriptor/base_descriptor.py new file mode 100644 index 0000000000..aa142b3acb --- /dev/null +++ b/deepmd/pt/model/descriptor/base_descriptor.py @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import torch + +from deepmd.dpmodel.descriptor import ( + make_base_descriptor, +) + +BaseDescriptor = make_base_descriptor(torch.Tensor, "forward") diff --git a/deepmd/pt/model/descriptor/descriptor.py b/deepmd/pt/model/descriptor/descriptor.py index bb98e8dc15..b4e866bb11 100644 --- a/deepmd/pt/model/descriptor/descriptor.py +++ b/deepmd/pt/model/descriptor/descriptor.py @@ -19,8 +19,12 @@ Plugin, ) +from .base_descriptor import ( + BaseDescriptor, +) + -class Descriptor(torch.nn.Module, ABC): +class Descriptor(torch.nn.Module, BaseDescriptor): """The descriptor. Given the atomic coordinates, atomic types and neighbor list, calculate the descriptor. @@ -29,52 +33,6 @@ class Descriptor(torch.nn.Module, ABC): __plugins = Plugin() local_cluster = False - @abstractmethod - def get_rcut(self) -> float: - """Returns the cut-off radius.""" - raise NotImplementedError - - @abstractmethod - def get_nsel(self) -> int: - """Returns the number of selected atoms in the cut-off radius.""" - raise NotImplementedError - - @abstractmethod - def get_sel(self) -> List[int]: - """Returns the number of selected atoms for each type.""" - raise NotImplementedError - - @abstractmethod - def get_ntype(self) -> int: - """Returns the number of element types.""" - raise NotImplementedError - - @abstractmethod - def get_dim_out(self) -> int: - """Returns the output dimension.""" - raise NotImplementedError - - @abstractmethod - def compute_input_stats(self, merged): - """Update mean and stddev for descriptor elements.""" - raise NotImplementedError - - @abstractmethod - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): - """Initialize the model bias by the statistics.""" - raise NotImplementedError - - @abstractmethod - def forward( - self, - extended_coord, - extended_atype, - nlist, - mapping: Optional[torch.Tensor] = None, - ): - """Calculate descriptor.""" - raise NotImplementedError - @staticmethod def register(key: str) -> Callable: """Register a descriptor plugin. @@ -166,42 +124,47 @@ def __new__(cls, *args, **kwargs): @abstractmethod def get_rcut(self) -> float: """Returns the cut-off radius.""" - raise NotImplementedError + pass @abstractmethod def get_nsel(self) -> int: """Returns the number of selected atoms in the cut-off radius.""" - raise NotImplementedError + pass @abstractmethod def get_sel(self) -> List[int]: """Returns the number of selected atoms for each type.""" - raise NotImplementedError + pass @abstractmethod - def get_ntype(self) -> int: + def get_ntypes(self) -> int: """Returns the number of element types.""" - raise NotImplementedError + pass @abstractmethod def get_dim_out(self) -> int: """Returns the output dimension.""" - raise NotImplementedError + pass @abstractmethod def get_dim_in(self) -> int: """Returns the output dimension.""" - raise NotImplementedError + pass + + @abstractmethod + def get_dim_emb(self) -> int: + """Returns the embedding dimension.""" + pass @abstractmethod def compute_input_stats(self, merged): """Update mean and stddev for DescriptorBlock elements.""" - raise NotImplementedError + pass @abstractmethod def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): """Initialize the model bias by the statistics.""" - raise NotImplementedError + pass def share_params(self, base_class, shared_level, resume=False): assert ( diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index 23f521b6d8..914c37ed51 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -91,9 +91,9 @@ def get_sel(self) -> List[int]: """Returns the number of selected atoms for each type.""" return self.se_atten.get_sel() - def get_ntype(self) -> int: + def get_ntypes(self) -> int: """Returns the number of element types.""" - return self.se_atten.get_ntype() + return self.se_atten.get_ntypes() def get_dim_out(self) -> int: """Returns the output dimension.""" @@ -102,13 +102,22 @@ def get_dim_out(self) -> int: ret += self.tebd_dim return ret + def get_dim_emb(self) -> int: + return self.se_atten.dim_emb + + def distinguish_types(self) -> bool: + """Returns if the descriptor requires a neighbor list that distinguish different + atomic types or not. + """ + return False + @property def dim_out(self): return self.get_dim_out() @property def dim_emb(self): - return self.se_atten.dim_emb + return self.get_dim_emb() def compute_input_stats(self, merged): return self.se_atten.compute_input_stats(merged) @@ -128,6 +137,15 @@ def get_data_process_key(cls, config): assert descrpt_type in ["dpa1", "se_atten"] return {"sel": config["sel"], "rcut": config["rcut"]} + def serialize(self) -> dict: + """Serialize the obj to dict.""" + raise NotImplementedError + + @classmethod + def deserialize(cls) -> "DescrptDPA1": + """Deserialize from a dict.""" + raise NotImplementedError + def forward( self, extended_coord: torch.Tensor, diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index 409b999262..b40e466ed4 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -256,7 +256,7 @@ def get_sel(self) -> List[int]: """Returns the number of selected atoms for each type.""" return self.sel - def get_ntype(self) -> int: + def get_ntypes(self) -> int: """Returns the number of element types.""" return self.ntypes @@ -267,6 +267,16 @@ def get_dim_out(self) -> int: ret += self.tebd_dim return ret + def get_dim_emb(self) -> int: + """Returns the embedding dimension of this descriptor.""" + return self.repformers.dim_emb + + def distinguish_types(self) -> bool: + """Returns if the descriptor requires a neighbor list that distinguish different + atomic types or not. + """ + return False + @property def dim_out(self): return self.get_dim_out() @@ -274,7 +284,7 @@ def dim_out(self): @property def dim_emb(self): """Returns the embedding dimension g2.""" - return self.repformers.dim_emb + return self.get_dim_emb() def compute_input_stats(self, merged): sumr, suma, sumn, sumr2, suma2 = [], [], [], [], [] @@ -322,6 +332,15 @@ def get_data_process_key(cls, config): "rcut": [config["repinit_rcut"], config["repformer_rcut"]], } + def serialize(self) -> dict: + """Serialize the obj to dict.""" + raise NotImplementedError + + @classmethod + def deserialize(cls) -> "DescrptDPA2": + """Deserialize from a dict.""" + raise NotImplementedError + def forward( self, extended_coord: torch.Tensor, diff --git a/deepmd/pt/model/descriptor/hybrid.py b/deepmd/pt/model/descriptor/hybrid.py index 11bbc80729..0698992659 100644 --- a/deepmd/pt/model/descriptor/hybrid.py +++ b/deepmd/pt/model/descriptor/hybrid.py @@ -88,7 +88,7 @@ def get_sel(self) -> List[int]: """Returns the number of selected atoms for each type.""" return self.sel - def get_ntype(self) -> int: + def get_ntypes(self) -> int: """Returns the number of element types.""" return self.ntypes @@ -100,6 +100,9 @@ def get_dim_in(self) -> int: """Returns the input dimension.""" return self.dim_in + def get_dim_emb(self): + return self.dim_emb + @property def dim_out(self): """Returns the output dimension of this descriptor.""" diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py index 141b5dc745..853962de69 100644 --- a/deepmd/pt/model/descriptor/repformers.py +++ b/deepmd/pt/model/descriptor/repformers.py @@ -162,7 +162,7 @@ def get_sel(self) -> List[int]: """Returns the number of selected atoms for each type.""" return self.sel - def get_ntype(self) -> int: + def get_ntypes(self) -> int: """Returns the number of element types.""" return self.ntypes @@ -174,6 +174,10 @@ def get_dim_in(self) -> int: """Returns the input dimension.""" return self.dim_in + def get_dim_emb(self) -> int: + """Returns the embedding dimension g2.""" + return self.g2_dim + @property def dim_out(self): """Returns the output dimension of this descriptor.""" @@ -187,7 +191,7 @@ def dim_in(self): @property def dim_emb(self): """Returns the embedding dimension g2.""" - return self.g2_dim + return self.get_dim_emb() def forward( self, diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 3f42736dca..23b78dcf34 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -28,7 +28,7 @@ except ImportError: from torch.jit import Final -from deepmd.model_format import EnvMat as DPEnvMat +from deepmd.dpmodel.utils import EnvMat as DPEnvMat from deepmd.pt.model.network.mlp import ( EmbeddingNet, NetworkCollection, @@ -81,14 +81,24 @@ def get_sel(self) -> List[int]: """Returns the number of selected atoms for each type.""" return self.sea.get_sel() - def get_ntype(self) -> int: + def get_ntypes(self) -> int: """Returns the number of element types.""" - return self.sea.get_ntype() + return self.sea.get_ntypes() def get_dim_out(self) -> int: """Returns the output dimension.""" return self.sea.get_dim_out() + def get_dim_emb(self) -> int: + """Returns the output dimension.""" + return self.sea.get_dim_emb() + + def distinguish_types(self): + """Returns if the descriptor requires a neighbor list that distinguish different + atomic types or not. + """ + return True + @property def dim_out(self): """Returns the output dimension of this descriptor.""" @@ -295,7 +305,7 @@ def get_sel(self) -> List[int]: """Returns the number of selected atoms for each type.""" return self.sel - def get_ntype(self) -> int: + def get_ntypes(self) -> int: """Returns the number of element types.""" return self.ntypes @@ -303,6 +313,10 @@ def get_dim_out(self) -> int: """Returns the output dimension.""" return self.dim_out + def get_dim_emb(self) -> int: + """Returns the output dimension.""" + return self.neuron[-1] + def get_dim_in(self) -> int: """Returns the input dimension.""" return self.dim_in diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index 78cba59da7..5d6e16fb96 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -145,7 +145,7 @@ def get_sel(self) -> List[int]: """Returns the number of selected atoms for each type.""" return self.sel - def get_ntype(self) -> int: + def get_ntypes(self) -> int: """Returns the number of element types.""" return self.ntypes @@ -157,6 +157,10 @@ def get_dim_out(self) -> int: """Returns the output dimension.""" return self.dim_out + def get_dim_emb(self) -> int: + """Returns the output dimension of embedding.""" + return self.filter_neuron[-1] + @property def dim_out(self): """Returns the output dimension of this descriptor.""" @@ -170,7 +174,7 @@ def dim_in(self): @property def dim_emb(self): """Returns the output dimension of embedding.""" - return self.filter_neuron[-1] + return self.get_dim_emb() def compute_input_stats(self, merged): """Update mean and stddev for descriptor elements.""" diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index a3db3dbdec..c4de02ed20 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -1,4 +1,13 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy + +from deepmd.pt.model.descriptor.descriptor import ( + Descriptor, +) +from deepmd.pt.model.task import ( + Fitting, +) + from .ener import ( EnergyModel, ) @@ -8,9 +17,27 @@ def get_model(model_params, sampled=None): + model_params = copy.deepcopy(model_params) + ntypes = len(model_params["type_map"]) + # descriptor + model_params["descriptor"]["ntypes"] = ntypes + descriptor = Descriptor(**model_params["descriptor"]) + # fitting + fitting_net = model_params.get("fitting_net", None) + fitting_net["type"] = fitting_net.get("type", "ener") + fitting_net["ntypes"] = descriptor.get_ntypes() + fitting_net["distinguish_types"] = descriptor.distinguish_types() + fitting_net["embedding_width"] = descriptor.get_dim_out() + grad_force = "direct" not in fitting_net["type"] + if not grad_force: + fitting_net["out_dim"] = descriptor.get_dim_emb() + if "ener" in fitting_net["type"]: + fitting_net["return_energy"] = True + fitting = Fitting(**fitting_net) + return EnergyModel( - descriptor=model_params["descriptor"], - fitting_net=model_params.get("fitting_net", None), + descriptor, + fitting, type_map=model_params["type_map"], type_embedding=model_params.get("type_embedding", None), resuming=model_params.get("resuming", False), diff --git a/deepmd/pt/model/model/atomic_model.py b/deepmd/pt/model/model/atomic_model.py deleted file mode 100644 index 9720bfa57d..0000000000 --- a/deepmd/pt/model/model/atomic_model.py +++ /dev/null @@ -1,70 +0,0 @@ -# SPDX-License-Identifier: LGPL-3.0-or-later -from abc import ( - ABC, - abstractmethod, -) -from typing import ( - Dict, - List, - Optional, -) - -import torch - -from deepmd.model_format import ( - FittingOutputDef, -) - - -class AtomicModel(ABC): - @abstractmethod - def get_fitting_output_def(self) -> FittingOutputDef: - raise NotImplementedError - - @abstractmethod - def get_rcut(self) -> float: - raise NotImplementedError - - @abstractmethod - def get_sel(self) -> List[int]: - raise NotImplementedError - - @abstractmethod - def distinguish_types(self) -> bool: - raise NotImplementedError - - @abstractmethod - def forward_atomic( - self, - extended_coord, - extended_atype, - nlist, - mapping: Optional[torch.Tensor] = None, - do_atomic_virial: bool = False, - ) -> Dict[str, torch.Tensor]: - raise NotImplementedError - - def do_grad( - self, - var_name: Optional[str] = None, - ) -> bool: - """Tell if the output variable `var_name` is differentiable. - if var_name is None, returns if any of the variable is differentiable. - - """ - odef = self.get_fitting_output_def() - if var_name is None: - require: List[bool] = [] - for vv in odef.keys(): - require.append(self.do_grad_(vv)) - return any(require) - else: - return self.do_grad_(var_name) - - def do_grad_( - self, - var_name: str, - ) -> bool: - """Tell if the output variable `var_name` is differentiable.""" - assert var_name is not None - return self.get_fitting_output_def()[var_name].differentiable diff --git a/deepmd/pt/model/model/base_atomic_model.py b/deepmd/pt/model/model/base_atomic_model.py new file mode 100644 index 0000000000..3f3e14257b --- /dev/null +++ b/deepmd/pt/model/model/base_atomic_model.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later + +import torch + +from deepmd.dpmodel.model import ( + make_base_atomic_model, +) + +BaseAtomicModel = make_base_atomic_model(torch.Tensor) diff --git a/deepmd/pt/model/model/dp_atomic_model.py b/deepmd/pt/model/model/dp_atomic_model.py index a222c8e6f6..b2ae48628b 100644 --- a/deepmd/pt/model/model/dp_atomic_model.py +++ b/deepmd/pt/model/model/dp_atomic_model.py @@ -1,4 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import sys from typing import ( Dict, List, @@ -7,25 +9,25 @@ import torch -from deepmd.model_format import ( +from deepmd.dpmodel import ( FittingOutputDef, ) -from deepmd.pt.model.descriptor.descriptor import ( - Descriptor, +from deepmd.pt.model.descriptor.se_a import ( # noqa # TODO: should import all descriptors!!! + DescrptSeA, ) -from deepmd.pt.model.task import ( - Fitting, +from deepmd.pt.model.task.ener import ( # noqa # TODO: should import all fittings! + InvarFitting, ) -from .atomic_model import ( - AtomicModel, +from .base_atomic_model import ( + BaseAtomicModel, ) from .model import ( BaseModel, ) -class DPAtomicModel(BaseModel, AtomicModel): +class DPAtomicModel(BaseModel, BaseAtomicModel): """Model give atomic prediction of some physical property. Parameters @@ -49,10 +51,11 @@ class DPAtomicModel(BaseModel, AtomicModel): Sampled frames to compute the statistics. """ + # I am enough with the shit interface! def __init__( self, - descriptor: dict, - fitting_net: dict, + descriptor, + fitting, type_map: Optional[List[str]], type_embedding: Optional[dict] = None, resuming: bool = False, @@ -62,26 +65,15 @@ def __init__( **kwargs, ): super().__init__() - # Descriptor + Type Embedding Net (Optional) ntypes = len(type_map) self.type_map = type_map self.ntypes = ntypes - descriptor["ntypes"] = ntypes - self.combination = descriptor.get("combination", False) - if self.combination: - self.prefactor = descriptor.get("prefactor", [0.5, 0.5]) - self.descriptor_type = descriptor["type"] - - self.type_split = True - if self.descriptor_type not in ["se_e2_a"]: - self.type_split = False - - self.descriptor = Descriptor(**descriptor) + self.descriptor = descriptor self.rcut = self.descriptor.get_rcut() self.sel = self.descriptor.get_sel() - self.split_nlist = False - + self.fitting_net = fitting # Statistics + fitting_net = None # TODO: hack!!! not sure if it is correct. self.compute_or_load_stat( fitting_net, ntypes, @@ -92,22 +84,7 @@ def __init__( sampled=sampled, ) - fitting_net["type"] = fitting_net.get("type", "ener") - fitting_net["ntypes"] = self.descriptor.get_ntype() - if self.descriptor_type in ["se_e2_a"]: - fitting_net["distinguish_types"] = True - else: - fitting_net["distinguish_types"] = False - fitting_net["embedding_width"] = self.descriptor.dim_out - - self.grad_force = "direct" not in fitting_net["type"] - if not self.grad_force: - fitting_net["out_dim"] = self.descriptor.dim_emb - if "ener" in fitting_net["type"]: - fitting_net["return_energy"] = True - self.fitting_net = Fitting(**fitting_net) - - def get_fitting_output_def(self) -> FittingOutputDef: + def fitting_output_def(self) -> FittingOutputDef: """Get the output def of the fitting net.""" return ( self.fitting_net.output_def() @@ -125,7 +102,34 @@ def get_sel(self) -> List[int]: def distinguish_types(self) -> bool: """If distinguish different types by sorting.""" - return self.type_split + return self.descriptor.distinguish_types() + + def serialize(self) -> dict: + return { + "type_map": self.type_map, + "descriptor": self.descriptor.serialize(), + "fitting": self.fitting_net.serialize(), + "descriptor_name": self.descriptor.__class__.__name__, + "fitting_name": self.fitting_net.__class__.__name__, + } + + @classmethod + def deserialize(cls, data) -> "DPAtomicModel": + data = copy.deepcopy(data) + descriptor_obj = getattr( + sys.modules[__name__], data["descriptor_name"] + ).deserialize(data["descriptor"]) + fitting_obj = getattr(sys.modules[__name__], data["fitting_name"]).deserialize( + data["fitting"] + ) + # TODO: dirty hack to provide type_map and avoid data stat!!! + obj = cls( + descriptor_obj, + fitting_obj, + type_map=data["type_map"], + resuming=True, + ) + return obj def forward_atomic( self, @@ -133,6 +137,8 @@ def forward_atomic( extended_atype, nlist, mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, ) -> Dict[str, torch.Tensor]: """Return atomic prediction. @@ -146,11 +152,15 @@ def forward_atomic( neighbor list. nf x nloc x nsel mapping mapps the extended indices to local indices + fparam + frame parameter. nf x ndf + aparam + atomic parameter. nf x nloc x nda Returns ------- result_dict - the result dict, defined by the fitting net output def. + the result dict, defined by the `FittingOutputDef`. """ nframes, nloc, nnei = nlist.shape @@ -165,5 +175,13 @@ def forward_atomic( ) assert descriptor is not None # energy, force - fit_ret = self.fitting_net(descriptor, atype, gr=rot_mat) + fit_ret = self.fitting_net( + descriptor, + atype, + gr=rot_mat, + g2=g2, + h2=h2, + fparam=fparam, + aparam=aparam, + ) return fit_ret diff --git a/deepmd/pt/model/model/ener.py b/deepmd/pt/model/model/ener.py index c316c99a86..a408689d8d 100644 --- a/deepmd/pt/model/model/ener.py +++ b/deepmd/pt/model/model/ener.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( Dict, - List, Optional, ) @@ -32,6 +31,8 @@ def forward( coord, atype, box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, do_atomic_virial: bool = False, ) -> Dict[str, torch.Tensor]: model_ret = self.forward_common( @@ -86,66 +87,3 @@ def forward_lower( else: model_predict = model_ret return model_predict - - -# should be a stand-alone function!!!! -def process_nlist( - nlist, - extended_atype, - mapping: Optional[torch.Tensor] = None, -): - # process the nlist_type and nlist_loc - nframes, nloc = nlist.shape[:2] - nmask = nlist == -1 - nlist[nmask] = 0 - if mapping is not None: - nlist_loc = torch.gather( - mapping, - dim=1, - index=nlist.reshape(nframes, -1), - ).reshape(nframes, nloc, -1) - nlist_loc[nmask] = -1 - else: - nlist_loc = None - nlist_type = torch.gather( - extended_atype, - dim=1, - index=nlist.reshape(nframes, -1), - ).reshape(nframes, nloc, -1) - nlist_type[nmask] = -1 - nlist[nmask] = -1 - return nlist_loc, nlist_type, nframes, nloc - - -def process_nlist_gathered( - nlist, - extended_atype, - split_sel: List[int], - mapping: Optional[torch.Tensor] = None, -): - nlist_list = list(torch.split(nlist, split_sel, -1)) - nframes, nloc = nlist_list[0].shape[:2] - nlist_type_list = [] - nlist_loc_list = [] - for nlist_item in nlist_list: - nmask = nlist_item == -1 - nlist_item[nmask] = 0 - if mapping is not None: - nlist_loc_item = torch.gather( - mapping, dim=1, index=nlist_item.reshape(nframes, -1) - ).reshape(nframes, nloc, -1) - nlist_loc_item[nmask] = -1 - nlist_loc_list.append(nlist_loc_item) - nlist_type_item = torch.gather( - extended_atype, dim=1, index=nlist_item.reshape(nframes, -1) - ).reshape(nframes, nloc, -1) - nlist_type_item[nmask] = -1 - nlist_type_list.append(nlist_type_item) - nlist_item[nmask] = -1 - - if mapping is not None: - nlist_loc = torch.cat(nlist_loc_list, -1) - else: - nlist_loc = None - nlist_type = torch.cat(nlist_type_list, -1) - return nlist_loc, nlist_type, nframes, nloc diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 3ddd21fbb8..c8c1e9450b 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -6,7 +6,7 @@ import torch -from deepmd.model_format import ( +from deepmd.dpmodel import ( ModelOutputDef, ) from deepmd.pt.model.model.transform_output import ( @@ -16,6 +16,7 @@ from deepmd.pt.utils.nlist import ( build_neighbor_list, extend_coord_with_ghosts, + nlist_distinguish_types, ) from deepmd.pt.utils.region import ( normalize_coord, @@ -23,6 +24,28 @@ def make_model(T_AtomicModel): + """Make a model as a derived class of an atomic model. + + The model provide two interfaces. + + 1. the `forward_common_lower`, that takes extended coordinates, atyps and neighbor list, + and outputs the atomic and property and derivatives (if required) on the extended region. + + 2. the `forward_common`, that takes coordinates, atypes and cell and predicts + the atomic and reduced property, and derivatives (if required) on the local region. + + Parameters + ---------- + T_AtomicModel + The atomic model. + + Returns + ------- + CM + The model. + + """ + class CM(T_AtomicModel): def __init__( self, @@ -34,8 +57,9 @@ def __init__( **kwargs, ) - def get_model_output_def(self): - return ModelOutputDef(self.get_fitting_output_def()) + def model_output_def(self): + """Get the output def for the model.""" + return ModelOutputDef(self.fitting_output_def()) # cannot use the name forward. torch script does not work def forward_common( @@ -43,24 +67,37 @@ def forward_common( coord, atype, box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, do_atomic_virial: bool = False, ) -> Dict[str, torch.Tensor]: - """Return total energy of the system. - Args: - - coord: Atom coordinates with shape [nframes, natoms[1]*3]. - - atype: Atom types with shape [nframes, natoms[1]]. - - natoms: Atom statisics with shape [self.ntypes+2]. - - box: Simulation box with shape [nframes, 9]. - - atomic_virial: Whether or not compoute the atomic virial. + """Return model prediction. + + Parameters + ---------- + coord + The coordinates of the atoms. + shape: nf x (nloc x 3) + atype + The type of atoms. shape: nf x nloc + box + The simulation box. shape: nf x 9 + do_atomic_virial + If calculate the atomic virial. Returns ------- - - energy: Energy per atom. - - force: XYZ force per atom. + ret_dict + The result dict of type Dict[str,torch.Tensor]. + The keys are defined by the `ModelOutputDef`. + """ nframes, nloc = atype.shape[:2] if box is not None: - coord_normalized = normalize_coord(coord, box.reshape(-1, 3, 3)) + coord_normalized = normalize_coord( + coord.view(nframes, nloc, 3), + box.reshape(nframes, 3, 3), + ) else: coord_normalized = coord.clone() extended_coord, extended_atype, mapping = extend_coord_with_ghosts( @@ -74,17 +111,19 @@ def forward_common( self.get_sel(), distinguish_types=self.distinguish_types(), ) - extended_coord = extended_coord.reshape(nframes, -1, 3) + extended_coord = extended_coord.view(nframes, -1, 3) model_predict_lower = self.forward_common_lower( extended_coord, extended_atype, nlist, mapping, do_atomic_virial=do_atomic_virial, + fparam=fparam, + aparam=aparam, ) model_predict = communicate_extended_output( model_predict_lower, - self.get_model_output_def(), + self.model_output_def(), mapping, do_atomic_virial=do_atomic_virial, ) @@ -96,9 +135,14 @@ def forward_common_lower( extended_atype, nlist, mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, do_atomic_virial: bool = False, ): - """Return model prediction. + """Return model prediction. Lower interface that takes + extended atomic coordinates and types, nlist, and mapping + as input, and returns the predictions on the extended region. + The predictions are not reduced. Parameters ---------- @@ -111,26 +155,118 @@ def forward_common_lower( mapping mapps the extended indices to local indices do_atomic_virial - whether do atomic virial + whether calculate atomic virial Returns ------- result_dict - the result dict, defined by the fitting net output def. + the result dict, defined by the `FittingOutputDef`. """ + nframes, nall = extended_atype.shape[:2] + extended_coord = extended_coord.view(nframes, -1, 3) + nlist = self.format_nlist(extended_coord, extended_atype, nlist) atomic_ret = self.forward_atomic( extended_coord, extended_atype, nlist, mapping=mapping, + fparam=fparam, + aparam=aparam, ) model_predict = fit_output_to_model_output( atomic_ret, - self.get_fitting_output_def(), + self.fitting_output_def(), extended_coord, do_atomic_virial=do_atomic_virial, ) return model_predict + def format_nlist( + self, + extended_coord: torch.Tensor, + extended_atype: torch.Tensor, + nlist: torch.Tensor, + ): + """Format the neighbor list. + + 1. If the number of neighbors in the `nlist` is equal to sum(self.sel), + it does nothong + + 2. If the number of neighbors in the `nlist` is smaller than sum(self.sel), + the `nlist` is pad with -1. + + 3. If the number of neighbors in the `nlist` is larger than sum(self.sel), + the nearest sum(sel) neighbors will be preseved. + + Known limitations: + + In the case of self.distinguish_types, the nlist is always formatted. + May have side effact on the efficiency. + + Parameters + ---------- + extended_coord + coodinates in extended region. nf x nall x 3 + extended_atype + atomic type in extended region. nf x nall + nlist + neighbor list. nf x nloc x nsel + + Returns + ------- + formated_nlist + the formated nlist. + + """ + distinguish_types = self.distinguish_types() + nlist = self._format_nlist(extended_coord, nlist, sum(self.get_sel())) + if distinguish_types: + nlist = nlist_distinguish_types(nlist, extended_atype, self.get_sel()) + return nlist + + def _format_nlist( + self, + extended_coord: torch.Tensor, + nlist: torch.Tensor, + nnei: int, + ): + n_nf, n_nloc, n_nnei = nlist.shape + # nf x nall x 3 + extended_coord = extended_coord.view([n_nf, -1, 3]) + rcut = self.get_rcut() + + if n_nnei < nnei: + nlist = torch.cat( + [ + nlist, + -1 + * torch.ones( + [n_nf, n_nloc, nnei - n_nnei], dtype=nlist.dtype + ).to(nlist.device), + ], + dim=-1, + ) + elif n_nnei > nnei: + m_real_nei = nlist >= 0 + nlist = torch.where(m_real_nei, nlist, 0) + # nf x nloc x 3 + coord0 = extended_coord[:, :n_nloc, :] + # nf x (nloc x nnei) x 3 + index = nlist.view(n_nf, n_nloc * n_nnei, 1).expand(-1, -1, 3) + coord1 = torch.gather(extended_coord, 1, index) + # nf x nloc x nnei x 3 + coord1 = coord1.view(n_nf, n_nloc, n_nnei, 3) + # nf x nloc x nnei + rr = torch.linalg.norm(coord0[:, :, None, :] - coord1, dim=-1) + rr = torch.where(m_real_nei, rr, float("inf")) + rr, nlist_mapping = torch.sort(rr, dim=-1) + nlist = torch.gather(nlist, 2, nlist_mapping) + nlist = torch.where(rr > rcut, -1, nlist) + nlist = nlist[..., :nnei] + else: # n_nnei == nnei: + pass # great! + assert nlist.shape[-1] == nnei + return nlist + return CM diff --git a/deepmd/pt/model/model/model.py b/deepmd/pt/model/model/model.py index 139744c1e9..01c2d7b9d6 100644 --- a/deepmd/pt/model/model/model.py +++ b/deepmd/pt/model/model/model.py @@ -18,10 +18,6 @@ def __init__(self): """Construct a basic model for different tasks.""" super().__init__() - def forward(self, *args, **kwargs): - """Model output.""" - raise NotImplementedError - def compute_or_load_stat( self, fitting_param, diff --git a/deepmd/pt/model/model/pair_tab.py b/deepmd/pt/model/model/pair_tab.py index 6f0782289a..430d090eb0 100644 --- a/deepmd/pt/model/model/pair_tab.py +++ b/deepmd/pt/model/model/pair_tab.py @@ -11,7 +11,7 @@ nn, ) -from deepmd.model_format import ( +from deepmd.dpmodel import ( FittingOutputDef, OutputVariableDef, ) @@ -19,12 +19,12 @@ PairTab, ) -from .atomic_model import ( - AtomicModel, +from .base_atomic_model import ( + BaseAtomicModel, ) -class PairTabModel(nn.Module, AtomicModel): +class PairTabModel(nn.Module, BaseAtomicModel): """Pairwise tabulation energy model. This model can be used to tabulate the pairwise energy between atoms for either @@ -72,7 +72,7 @@ def __init__( else: raise TypeError("sel must be int or list[int]") - def get_fitting_output_def(self) -> FittingOutputDef: + def fitting_output_def(self) -> FittingOutputDef: return FittingOutputDef( [ OutputVariableDef( @@ -91,6 +91,14 @@ def distinguish_types(self) -> bool: # to match DPA1 and DPA2. return False + def serialize(self) -> dict: + # place holder, implemantated in future PR + raise NotImplementedError + + def deserialize(cls): + # place holder, implemantated in future PR + raise NotImplementedError + def forward_atomic( self, extended_coord, diff --git a/deepmd/pt/model/model/transform_output.py b/deepmd/pt/model/model/transform_output.py index a14518e8a0..d942ed3ae8 100644 --- a/deepmd/pt/model/model/transform_output.py +++ b/deepmd/pt/model/model/transform_output.py @@ -7,7 +7,7 @@ import torch -from deepmd.model_format import ( +from deepmd.dpmodel import ( FittingOutputDef, ModelOutputDef, OutputVariableDef, @@ -152,6 +152,7 @@ def fit_output_to_model_output( ) model_ret[kk_derv_r] = dr model_ret[kk_derv_c] = dc + model_ret[kk_derv_c + "_redu"] = torch.sum(model_ret[kk_derv_c], dim=1) return model_ret diff --git a/deepmd/pt/model/network/mlp.py b/deepmd/pt/model/network/mlp.py index d76abd82f9..251150f945 100644 --- a/deepmd/pt/model/network/mlp.py +++ b/deepmd/pt/model/network/mlp.py @@ -15,11 +15,11 @@ device = env.DEVICE -from deepmd.model_format import ( +from deepmd.dpmodel.utils import ( NativeLayer, ) -from deepmd.model_format import NetworkCollection as DPNetworkCollection -from deepmd.model_format import ( +from deepmd.dpmodel.utils import NetworkCollection as DPNetworkCollection +from deepmd.dpmodel.utils import ( make_embedding_network, make_fitting_network, make_multilayer_network, diff --git a/deepmd/pt/model/task/__init__.py b/deepmd/pt/model/task/__init__.py index fcf46632f3..0b21033d31 100644 --- a/deepmd/pt/model/task/__init__.py +++ b/deepmd/pt/model/task/__init__.py @@ -2,6 +2,9 @@ from .atten_lcc import ( FittingNetAttenLcc, ) +from .base_fitting import ( + BaseFitting, +) from .denoise import ( DenoiseNet, ) @@ -15,9 +18,6 @@ from .fitting import ( Fitting, ) -from .task import ( - TaskBaseMethod, -) from .type_predict import ( TypePredictNet, ) @@ -29,6 +29,6 @@ "EnergyFittingNet", "EnergyFittingNetDirect", "Fitting", - "TaskBaseMethod", + "BaseFitting", "TypePredictNet", ] diff --git a/deepmd/pt/model/task/atten_lcc.py b/deepmd/pt/model/task/atten_lcc.py index 41ccf99330..e5961335ec 100644 --- a/deepmd/pt/model/task/atten_lcc.py +++ b/deepmd/pt/model/task/atten_lcc.py @@ -6,15 +6,15 @@ EnergyHead, NodeTaskHead, ) -from deepmd.pt.model.task.task import ( - TaskBaseMethod, +from deepmd.pt.model.task.fitting import ( + Fitting, ) from deepmd.pt.utils import ( env, ) -class FittingNetAttenLcc(TaskBaseMethod): +class FittingNetAttenLcc(Fitting): def __init__( self, embedding_width, bias_atom_e, pair_embed_dim, attention_heads, **kwargs ): diff --git a/deepmd/pt/model/task/base_fitting.py b/deepmd/pt/model/task/base_fitting.py new file mode 100644 index 0000000000..884a1bfe57 --- /dev/null +++ b/deepmd/pt/model/task/base_fitting.py @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import torch + +from deepmd.dpmodel.fitting import ( + make_base_fitting, +) + +BaseFitting = make_base_fitting(torch.Tensor, fwd_method_name="forward") diff --git a/deepmd/pt/model/task/denoise.py b/deepmd/pt/model/task/denoise.py index 7e6b6dcdb6..35846ed231 100644 --- a/deepmd/pt/model/task/denoise.py +++ b/deepmd/pt/model/task/denoise.py @@ -5,7 +5,7 @@ import torch -from deepmd.model_format import ( +from deepmd.dpmodel import ( FittingOutputDef, OutputVariableDef, fitting_check_output, @@ -14,8 +14,8 @@ MaskLMHead, NonLinearHead, ) -from deepmd.pt.model.task.task import ( - TaskBaseMethod, +from deepmd.pt.model.task.fitting import ( + Fitting, ) from deepmd.pt.utils import ( env, @@ -23,7 +23,7 @@ @fitting_check_output -class DenoiseNet(TaskBaseMethod): +class DenoiseNet(Fitting): def __init__( self, feature_dim, diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py index 8511c7dc29..4906987bf8 100644 --- a/deepmd/pt/model/task/dipole.py +++ b/deepmd/pt/model/task/dipole.py @@ -6,12 +6,12 @@ from deepmd.pt.model.network.network import ( ResidualDeep, ) -from deepmd.pt.model.task.task import ( - TaskBaseMethod, +from deepmd.pt.model.task.fitting import ( + Fitting, ) -class DipoleFittingNetType(TaskBaseMethod): +class DipoleFittingNetType(Fitting): def __init__( self, ntypes, embedding_width, neuron, out_dim, resnet_dt=True, **kwargs ): diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index e40a6bda44..484e477b6a 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -10,7 +10,7 @@ import numpy as np import torch -from deepmd.model_format import ( +from deepmd.dpmodel import ( FittingOutputDef, OutputVariableDef, fitting_check_output, @@ -292,6 +292,7 @@ def forward( "get an input fparam of dim {fparam.shape[-1]}, ", "which is not consistent with {self.numb_fparam}.", ) + fparam = fparam.view([nf, self.numb_fparam]) nb, _ = fparam.shape t_fparam_avg = self._extend_f_avg_std(self.fparam_avg, nb) t_fparam_inv_std = self._extend_f_avg_std(self.fparam_inv_std, nb) @@ -311,6 +312,7 @@ def forward( "get an input aparam of dim {aparam.shape[-1]}, ", "which is not consistent with {self.numb_aparam}.", ) + aparam = aparam.view([nf, nloc, self.numb_aparam]) nb, nloc, _ = aparam.shape t_aparam_avg = self._extend_a_avg_std(self.aparam_avg, nb, nloc) t_aparam_inv_std = self._extend_a_avg_std(self.aparam_inv_std, nb, nloc) @@ -396,7 +398,7 @@ def __init__( ntypes, embedding_width, neuron, - bias_atom_e, + bias_atom_e=None, out_dim=1, resnet_dt=True, use_tebd=True, @@ -417,6 +419,8 @@ def __init__( self.dim_descrpt = embedding_width self.use_tebd = use_tebd self.out_dim = out_dim + if bias_atom_e is None: + bias_atom_e = np.zeros([self.ntypes]) if not use_tebd: assert self.ntypes == len(bias_atom_e), "Element count mismatches!" bias_atom_e = torch.tensor(bias_atom_e) @@ -460,11 +464,21 @@ def output_def(self): ] ) + def serialize(self) -> dict: + raise NotImplementedError + + def deserialize(cls) -> "EnergyFittingNetDirect": + raise NotImplementedError + def forward( self, inputs: torch.Tensor, atype: torch.Tensor, gr: Optional[torch.Tensor] = None, + g2: Optional[torch.Tensor] = None, + h2: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, ) -> Tuple[torch.Tensor, None]: """Based on embedding net output, alculate total energy. diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index c6fb6b27e1..551fb9640b 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -7,8 +7,8 @@ import numpy as np import torch -from deepmd.pt.model.task.task import ( - TaskBaseMethod, +from deepmd.pt.model.task.base_fitting import ( + BaseFitting, ) from deepmd.pt.utils.dataloader import ( DpLoaderSet, @@ -24,7 +24,7 @@ ) -class Fitting(TaskBaseMethod): +class Fitting(torch.nn.Module, BaseFitting): __plugins = Plugin() @staticmethod diff --git a/deepmd/pt/model/task/task.py b/deepmd/pt/model/task/task.py index b2dc03e4bd..6ceb116d85 100644 --- a/deepmd/pt/model/task/task.py +++ b/deepmd/pt/model/task/task.py @@ -1,18 +1 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from abc import ( - ABC, - abstractmethod, -) - -import torch - -from deepmd.model_format import ( - FittingOutputDef, -) - - -class TaskBaseMethod(torch.nn.Module, ABC): - @abstractmethod - def output_def(self) -> FittingOutputDef: - """Definition for the task Output.""" - raise NotImplementedError diff --git a/deepmd/pt/model/task/type_predict.py b/deepmd/pt/model/task/type_predict.py index 57227004d0..c696590043 100644 --- a/deepmd/pt/model/task/type_predict.py +++ b/deepmd/pt/model/task/type_predict.py @@ -9,11 +9,11 @@ MaskLMHead, ) from deepmd.pt.model.task import ( - TaskBaseMethod, + Fitting, ) -class TypePredictNet(TaskBaseMethod): +class TypePredictNet(Fitting): def __init__(self, feature_dim, ntypes, activation_function="gelu", **kwargs): """Construct a type predict net. diff --git a/deepmd/pt/utils/env.py b/deepmd/pt/utils/env.py index b51b03fdc2..a679ccf1fa 100644 --- a/deepmd/pt/utils/env.py +++ b/deepmd/pt/utils/env.py @@ -40,6 +40,8 @@ "half": torch.float16, "single": torch.float32, "double": torch.float64, + "int32": torch.int32, + "int64": torch.int64, } DEFAULT_PRECISION = "float64" diff --git a/deepmd/pt/utils/nlist.py b/deepmd/pt/utils/nlist.py index 23a11684a5..fdb2627f04 100644 --- a/deepmd/pt/utils/nlist.py +++ b/deepmd/pt/utils/nlist.py @@ -16,143 +16,6 @@ ) -def _build_neighbor_list( - coord1: torch.Tensor, - nloc: int, - rcut: float, - nsel: int, - rmin: float = 1e-10, - cut_nearest: bool = True, -) -> torch.Tensor: - """Build neightbor list for a single frame. keeps nsel neighbors. - coord1 : [nall x 3]. - - ret: [nloc x nsel] stores indexes of coord1. - """ - nall = coord1.shape[-1] // 3 - coord0 = torch.split(coord1, [nloc * 3, (nall - nloc) * 3])[0] - # nloc x nall x 3 - diff = coord1.view([-1, 3])[None, :, :] - coord0.view([-1, 3])[:, None, :] - assert list(diff.shape) == [nloc, nall, 3] - # nloc x nall - rr = torch.linalg.norm(diff, dim=-1) - rr, nlist = torch.sort(rr, dim=-1) - if cut_nearest: - # nloc x (nall-1) - rr = torch.split(rr, [1, nall - 1], dim=-1)[-1] - nlist = torch.split(nlist, [1, nall - 1], dim=-1)[-1] - # nloc x nsel - nnei = rr.shape[1] - rr = torch.split(rr, [nsel, nnei - nsel], dim=-1)[0] - nlist = torch.split(nlist, [nsel, nnei - nsel], dim=-1)[0] - nlist = nlist.masked_fill((rr > rcut), -1) - return nlist - - -def build_neighbor_list_lower( - coord1: torch.Tensor, - atype: torch.Tensor, - nloc: int, - rcut: float, - sel: Union[int, List[int]], - distinguish_types: bool = True, -) -> torch.Tensor: - """Build neightbor list for a single frame. keeps nsel neighbors. - - Parameters - ---------- - coord1 : torch.Tensor - exptended coordinates of shape [nall x 3] - atype : torch.Tensor - extended atomic types of shape [nall] - nloc : int - number of local atoms. - rcut : float - cut-off radius - sel : int or List[int] - maximal number of neighbors (of each type). - if distinguish_types==True, nsel should be list and - the length of nsel should be equal to number of - types. - distinguish_types : bool - distinguish different types. - - Returns - ------- - neighbor_list : torch.Tensor - Neighbor list of shape [nloc, nsel], the neighbors - are stored in an ascending order. If the number of - neighbors is less than nsel, the positions are masked - with -1. The neighbor list of an atom looks like - |------ nsel ------| - xx xx xx xx -1 -1 -1 - if distinguish_types==True and we have two types - |---- nsel[0] -----| |---- nsel[1] -----| - xx xx xx xx -1 -1 -1 xx xx xx -1 -1 -1 -1 - - """ - nall = coord1.shape[0] // 3 - if isinstance(sel, int): - sel = [sel] - nsel = sum(sel) - # nloc x 3 - coord0 = coord1[: nloc * 3] - # nloc x nall x 3 - diff = coord1.view([-1, 3]).unsqueeze(0) - coord0.view([-1, 3]).unsqueeze(1) - assert list(diff.shape) == [nloc, nall, 3] - # nloc x nall - rr = torch.linalg.norm(diff, dim=-1) - rr, nlist = torch.sort(rr, dim=-1) - # nloc x (nall-1) - rr = rr[:, 1:] - nlist = nlist[:, 1:] - # nloc x nsel - nnei = rr.shape[1] - if nsel <= nnei: - rr = rr[:, :nsel] - nlist = nlist[:, :nsel] - else: - rr = torch.cat( - [rr, torch.ones([nloc, nsel - nnei]).to(rr.device) + rcut], dim=-1 - ) - nlist = torch.cat( - [nlist, torch.ones([nloc, nsel - nnei], dtype=torch.long).to(rr.device)], - dim=-1, - ) - assert list(nlist.shape) == [nloc, nsel] - nlist = nlist.masked_fill((rr > rcut), -1) - - if not distinguish_types: - return nlist - else: - ret_nlist = [] - # nloc x nall - tmp_atype = torch.tile(atype.unsqueeze(0), [nloc, 1]) - mask = nlist == -1 - # nloc x s(nsel) - tnlist = torch.gather( - tmp_atype, - 1, - nlist.masked_fill(mask, 0), - ) - tnlist = tnlist.masked_fill(mask, -1) - snsel = tnlist.shape[1] - for ii, ss in enumerate(sel): - # nloc x s(nsel) - # to int because bool cannot be sort on GPU - pick_mask = (tnlist == ii).to(torch.int32) - # nloc x s(nsel), stable sort, nearer neighbors first - pick_mask, imap = torch.sort( - pick_mask, dim=-1, descending=True, stable=True - ) - # nloc x s(nsel) - inlist = torch.gather(nlist, 1, imap) - inlist = inlist.masked_fill(~(pick_mask.to(torch.bool)), -1) - # nloc x nsel[ii] - ret_nlist.append(torch.split(inlist, [ss, snsel - ss], dim=-1)[0]) - return torch.concat(ret_nlist, dim=-1) - - def build_neighbor_list( coord1: torch.Tensor, atype: torch.Tensor, @@ -227,7 +90,7 @@ def build_neighbor_list( nlist = torch.cat( [ nlist, - torch.ones([batch_size, nloc, nsel - nnei], dtype=torch.long).to( + torch.ones([batch_size, nloc, nsel - nnei], dtype=nlist.dtype).to( rr.device ), ], @@ -236,35 +99,46 @@ def build_neighbor_list( assert list(nlist.shape) == [batch_size, nloc, nsel] nlist = nlist.masked_fill((rr > rcut), -1) - if not distinguish_types: - return nlist + if distinguish_types: + return nlist_distinguish_types(nlist, atype, sel) else: - ret_nlist = [] - # nloc x nall - tmp_atype = torch.tile(atype.unsqueeze(1), [1, nloc, 1]) - mask = nlist == -1 + return nlist + + +def nlist_distinguish_types( + nlist: torch.Tensor, + atype: torch.Tensor, + sel: List[int], +): + """Given a nlist that does not distinguish atom types, return a nlist that + distinguish atom types. + + """ + nf, nloc, nnei = nlist.shape + ret_nlist = [] + # nloc x nall + tmp_atype = torch.tile(atype.unsqueeze(1), [1, nloc, 1]) + mask = nlist == -1 + # nloc x s(nsel) + tnlist = torch.gather( + tmp_atype, + 2, + nlist.masked_fill(mask, 0), + ) + tnlist = tnlist.masked_fill(mask, -1) + snsel = tnlist.shape[2] + for ii, ss in enumerate(sel): # nloc x s(nsel) - tnlist = torch.gather( - tmp_atype, - 2, - nlist.masked_fill(mask, 0), - ) - tnlist = tnlist.masked_fill(mask, -1) - snsel = tnlist.shape[2] - for ii, ss in enumerate(sel): - # nloc x s(nsel) - # to int because bool cannot be sort on GPU - pick_mask = (tnlist == ii).to(torch.int32) - # nloc x s(nsel), stable sort, nearer neighbors first - pick_mask, imap = torch.sort( - pick_mask, dim=-1, descending=True, stable=True - ) - # nloc x s(nsel) - inlist = torch.gather(nlist, 2, imap) - inlist = inlist.masked_fill(~(pick_mask.to(torch.bool)), -1) - # nloc x nsel[ii] - ret_nlist.append(torch.split(inlist, [ss, snsel - ss], dim=-1)[0]) - return torch.concat(ret_nlist, dim=-1) + # to int because bool cannot be sort on GPU + pick_mask = (tnlist == ii).to(torch.int32) + # nloc x s(nsel), stable sort, nearer neighbors first + pick_mask, imap = torch.sort(pick_mask, dim=-1, descending=True, stable=True) + # nloc x s(nsel) + inlist = torch.gather(nlist, 2, imap) + inlist = inlist.masked_fill(~(pick_mask.to(torch.bool)), -1) + # nloc x nsel[ii] + ret_nlist.append(torch.split(inlist, [ss, snsel - ss], dim=-1)[0]) + return torch.concat(ret_nlist, dim=-1) # build_neighbor_list = torch.vmap( @@ -369,6 +243,8 @@ def extend_coord_with_ghosts( atom type of shape [-1, nloc]. cell : torch.Tensor simulation cell tensor of shape [-1, 9]. + rcut : float + the cutoff radius Returns ------- diff --git a/deepmd/pt/utils/utils.py b/deepmd/pt/utils/utils.py index e83e12f608..2b96925a51 100644 --- a/deepmd/pt/utils/utils.py +++ b/deepmd/pt/utils/utils.py @@ -8,7 +8,7 @@ import torch import torch.nn.functional as F -from deepmd.model_format.common import PRECISION_DICT as NP_PRECISION_DICT +from deepmd.dpmodel.common import PRECISION_DICT as NP_PRECISION_DICT from .env import ( DEVICE, diff --git a/source/tests/common/test_model_format_utils.py b/source/tests/common/test_model_format_utils.py index cb85fd2bb2..18a40ffdd9 100644 --- a/source/tests/common/test_model_format_utils.py +++ b/source/tests/common/test_model_format_utils.py @@ -8,17 +8,31 @@ import numpy as np -from deepmd.model_format import ( +from deepmd.dpmodel.descriptor import ( DescrptSeA, +) +from deepmd.dpmodel.fitting import ( + InvarFitting, +) +from deepmd.dpmodel.model import ( + DPAtomicModel, + DPModel, +) +from deepmd.dpmodel.utils import ( EmbeddingNet, EnvMat, FittingNet, - InvarFitting, NativeLayer, NativeNet, NetworkCollection, + build_multiple_neighbor_list, + build_neighbor_list, + extend_coord_with_ghosts, + get_multiple_nlist_key, + inter2phys, load_dp_model, save_dp_model, + to_face_distance, ) @@ -266,7 +280,7 @@ def test_zero_dim(self): ) -class TestDPModel(unittest.TestCase): +class TestSaveLoadDPModel(unittest.TestCase): def setUp(self) -> None: self.w = np.full((3, 2), 3.0) self.b = np.full((3,), 4.0) @@ -285,7 +299,7 @@ def setUp(self) -> None: }, ], } - self.filename = "test_dp_model_format.dp" + self.filename = "test_dp_dpmodel.dp" def test_save_load_model(self): save_dp_model(self.filename, deepcopy(self.model_dict)) @@ -321,7 +335,7 @@ def setUp(self): [ [1, 3, -1, -1, -1, 2, -1], [0, -1, -1, -1, -1, 2, -1], - [0, 1, -1, -1, -1, 0, -1], + [0, 1, -1, -1, -1, -1, -1], ], dtype=int, ).reshape([1, self.nloc, sum(self.sel)]) @@ -490,3 +504,386 @@ def test_get_set(self): ]: ifn0[ii] = foo np.testing.assert_allclose(foo, ifn0[ii]) + + +class TestDPAtomicModel(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_self_consistency( + self, + ): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ) + type_map = ["foo", "bar"] + md0 = DPAtomicModel(ds, ft, type_map=type_map) + md1 = DPAtomicModel.deserialize(md0.serialize()) + + ret0 = md0.forward_atomic(self.coord_ext, self.atype_ext, self.nlist) + ret1 = md1.forward_atomic(self.coord_ext, self.atype_ext, self.nlist) + + np.testing.assert_allclose(ret0["energy"], ret1["energy"]) + + +class TestDPModel(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_self_consistency( + self, + ): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ) + type_map = ["foo", "bar"] + md0 = DPModel(ds, ft, type_map=type_map) + md1 = DPModel.deserialize(md0.serialize()) + + ret0 = md0.call_lower(self.coord_ext, self.atype_ext, self.nlist) + ret1 = md1.call_lower(self.coord_ext, self.atype_ext, self.nlist) + + np.testing.assert_allclose(ret0["energy"], ret1["energy"]) + np.testing.assert_allclose(ret0["energy_redu"], ret1["energy_redu"]) + + +class TestDPModelFormatNlist(unittest.TestCase): + def setUp(self): + # nloc == 3, nall == 4 + self.nloc = 3 + self.nall = 5 + self.nf, self.nt = 1, 2 + self.coord_ext = np.array( + [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 1], + [0, -2, 0], + [2.3, 0, 0], + ], + dtype=np.float64, + ).reshape([1, self.nall * 3]) + # sel = [5, 2] + self.sel = [5, 2] + self.expected_nlist = np.array( + [ + [1, 3, -1, -1, -1, 2, -1], + [0, -1, -1, -1, -1, 2, -1], + [0, 1, -1, -1, -1, -1, -1], + ], + dtype=int, + ).reshape([1, self.nloc, sum(self.sel)]) + self.atype_ext = np.array([0, 0, 1, 0, 1], dtype=int).reshape([1, self.nall]) + self.rcut_smth = 0.4 + self.rcut = 2.1 + + nf, nloc, nnei = self.expected_nlist.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ) + type_map = ["foo", "bar"] + self.md = DPModel(ds, ft, type_map=type_map) + + def test_nlist_eq(self): + # n_nnei == nnei + nlist = np.array( + [ + [1, 3, -1, -1, -1, 2, -1], + [0, -1, -1, -1, -1, 2, -1], + [0, 1, -1, -1, -1, -1, -1], + ], + dtype=np.int64, + ).reshape([1, self.nloc, -1]) + nlist1 = self.md.format_nlist( + self.coord_ext, + self.atype_ext, + nlist, + ) + np.testing.assert_allclose(self.expected_nlist, nlist1) + + def test_nlist_st(self): + # n_nnei < nnei + nlist = np.array( + [ + [1, 3, -1, 2], + [0, -1, -1, 2], + [0, 1, -1, -1], + ], + dtype=np.int64, + ).reshape([1, self.nloc, -1]) + nlist1 = self.md.format_nlist( + self.coord_ext, + self.atype_ext, + nlist, + ) + np.testing.assert_allclose(self.expected_nlist, nlist1) + + def test_nlist_lt(self): + # n_nnei > nnei + nlist = np.array( + [ + [1, 3, -1, -1, -1, 2, -1, -1, 4], + [0, -1, 4, -1, -1, 2, -1, 3, -1], + [0, 1, -1, -1, -1, 4, -1, -1, 3], + ], + dtype=np.int64, + ).reshape([1, self.nloc, -1]) + nlist1 = self.md.format_nlist( + self.coord_ext, + self.atype_ext, + nlist, + ) + np.testing.assert_allclose(self.expected_nlist, nlist1) + + +class TestRegion(unittest.TestCase): + def setUp(self): + self.cell = np.array( + [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], + ) + self.cell = np.reshape(self.cell, [1, 1, -1, 3]) + self.cell = np.tile(self.cell, [4, 5, 1, 1]) + self.prec = 1e-8 + + def test_inter_to_phys(self): + rng = np.random.default_rng() + inter = rng.normal(size=[4, 5, 3, 3]) + phys = inter2phys(inter, self.cell) + for ii in range(4): + for jj in range(5): + expected_phys = np.matmul(inter[ii, jj], self.cell[ii, jj]) + np.testing.assert_allclose( + phys[ii, jj], expected_phys, rtol=self.prec, atol=self.prec + ) + + def test_to_face_dist(self): + cell0 = self.cell[0][0] + vol = np.linalg.det(cell0) + # area of surfaces xy, xz, yz + sxy = np.linalg.norm(np.cross(cell0[0], cell0[1])) + sxz = np.linalg.norm(np.cross(cell0[0], cell0[2])) + syz = np.linalg.norm(np.cross(cell0[1], cell0[2])) + # vol / area gives distance + dz = vol / sxy + dy = vol / sxz + dx = vol / syz + expected = np.array([dx, dy, dz]) + dists = to_face_distance(self.cell) + for ii in range(4): + for jj in range(5): + np.testing.assert_allclose( + dists[ii][jj], expected, rtol=self.prec, atol=self.prec + ) + + +dtype = np.float64 + + +class TestNeighList(unittest.TestCase): + def setUp(self): + self.nf = 3 + self.nloc = 2 + self.ns = 5 * 5 * 3 + self.nall = self.ns * self.nloc + self.cell = np.array([[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype) + self.icoord = np.array([[0, 0, 0], [0.5, 0.5, 0.1]], dtype=dtype) + self.atype = np.array([0, 1], dtype=np.int32) + [self.cell, self.icoord, self.atype] = [ + np.expand_dims(ii, 0) for ii in [self.cell, self.icoord, self.atype] + ] + self.coord = inter2phys(self.icoord, self.cell).reshape([-1, self.nloc * 3]) + self.cell = self.cell.reshape([-1, 9]) + [self.cell, self.coord, self.atype] = [ + np.tile(ii, [self.nf, 1]) for ii in [self.cell, self.coord, self.atype] + ] + self.rcut = 1.01 + self.prec = 1e-10 + self.nsel = [10, 10] + self.ref_nlist = np.array( + [ + [0, 0, 0, 0, 0, 0, -1, -1, -1, -1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1], + [0, 0, 0, 0, -1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1], + ] + ) + + def test_build_notype(self): + ecoord, eatype, mapping = extend_coord_with_ghosts( + self.coord, self.atype, self.cell, self.rcut + ) + nlist = build_neighbor_list( + ecoord, + eatype, + self.nloc, + self.rcut, + sum(self.nsel), + distinguish_types=False, + ) + np.testing.assert_allclose(nlist[0], nlist[1]) + nlist_mask = nlist[0] == -1 + nlist_loc = mapping[0][nlist[0]] + nlist_loc[nlist_mask] = -1 + np.testing.assert_allclose( + np.sort(nlist_loc, axis=-1), + np.sort(self.ref_nlist, axis=-1), + ) + + def test_build_type(self): + ecoord, eatype, mapping = extend_coord_with_ghosts( + self.coord, self.atype, self.cell, self.rcut + ) + nlist = build_neighbor_list( + ecoord, + eatype, + self.nloc, + self.rcut, + self.nsel, + distinguish_types=True, + ) + np.testing.assert_allclose(nlist[0], nlist[1]) + nlist_mask = nlist[0] == -1 + nlist_loc = mapping[0][nlist[0]] + nlist_loc[nlist_mask] = -1 + for ii in range(2): + np.testing.assert_allclose( + np.sort(np.split(nlist_loc, self.nsel, axis=-1)[ii], axis=-1), + np.sort(np.split(self.ref_nlist, self.nsel, axis=-1)[ii], axis=-1), + ) + + def test_build_multiple_nlist(self): + rcuts = [1.01, 2.01] + nsels = [20, 80] + ecoord, eatype, mapping = extend_coord_with_ghosts( + self.coord, self.atype, self.cell, max(rcuts) + ) + nlist1 = build_neighbor_list( + ecoord, + eatype, + self.nloc, + rcuts[1], + nsels[1] - 1, + distinguish_types=False, + ) + pad = -1 * np.ones([self.nf, self.nloc, 1], dtype=nlist1.dtype) + nlist2 = np.concatenate([nlist1, pad], axis=-1) + nlist0 = build_neighbor_list( + ecoord, + eatype, + self.nloc, + rcuts[0], + nsels[0], + distinguish_types=False, + ) + nlists = build_multiple_neighbor_list(ecoord, nlist1, rcuts, nsels) + for dd in range(2): + self.assertEqual( + nlists[get_multiple_nlist_key(rcuts[dd], nsels[dd])].shape[-1], + nsels[dd], + ) + np.testing.assert_allclose( + nlists[get_multiple_nlist_key(rcuts[0], nsels[0])], + nlist0, + ) + np.testing.assert_allclose( + nlists[get_multiple_nlist_key(rcuts[1], nsels[1])], + nlist2, + ) + + def test_extend_coord(self): + ecoord, eatype, mapping = extend_coord_with_ghosts( + self.coord, self.atype, self.cell, self.rcut + ) + # expected ncopy x nloc + self.assertEqual(list(ecoord.shape), [self.nf, self.nall * 3]) + self.assertEqual(list(eatype.shape), [self.nf, self.nall]) + self.assertEqual(list(mapping.shape), [self.nf, self.nall]) + # check the nloc part is identical with original coord + np.testing.assert_allclose( + ecoord[:, : self.nloc * 3], self.coord, rtol=self.prec, atol=self.prec + ) + # check the shift vectors are aligned with grid + shift_vec = ( + ecoord.reshape([-1, self.ns, self.nloc, 3]) + - self.coord.reshape([-1, self.nloc, 3])[:, None, :, :] + ) + shift_vec = shift_vec.reshape([-1, self.nall, 3]) + # hack!!! assumes identical cell across frames + shift_vec = np.matmul( + shift_vec, np.linalg.inv(self.cell.reshape([self.nf, 3, 3])[0]) + ) + # nf x nall x 3 + shift_vec = np.round(shift_vec) + # check: identical shift vecs + np.testing.assert_allclose( + shift_vec[0], shift_vec[1], rtol=self.prec, atol=self.prec + ) + # check: shift idx aligned with grid + mm, cc = np.unique(shift_vec[0][:, 0], return_counts=True) + np.testing.assert_allclose( + mm, + np.array([-2, -1, 0, 1, 2], dtype=dtype), + rtol=self.prec, + atol=self.prec, + ) + np.testing.assert_allclose( + cc, + np.array([30, 30, 30, 30, 30], dtype=np.int32), + rtol=self.prec, + atol=self.prec, + ) + mm, cc = np.unique(shift_vec[1][:, 1], return_counts=True) + np.testing.assert_allclose( + mm, + np.array([-2, -1, 0, 1, 2], dtype=dtype), + rtol=self.prec, + atol=self.prec, + ) + np.testing.assert_allclose( + cc, + np.array([30, 30, 30, 30, 30], dtype=np.int32), + rtol=self.prec, + atol=self.prec, + ) + mm, cc = np.unique(shift_vec[1][:, 2], return_counts=True) + np.testing.assert_allclose( + mm, + np.array([-1, 0, 1], dtype=dtype), + rtol=self.prec, + atol=self.prec, + ) + np.testing.assert_allclose( + cc, + np.array([50, 50, 50], dtype=np.int32), + rtol=self.prec, + atol=self.prec, + ) diff --git a/source/tests/common/test_output_def.py b/source/tests/common/test_output_def.py index 4316fa5982..d0cf822247 100644 --- a/source/tests/common/test_output_def.py +++ b/source/tests/common/test_output_def.py @@ -6,7 +6,7 @@ import numpy as np -from deepmd.model_format import ( +from deepmd.dpmodel import ( FittingOutputDef, ModelOutputDef, NativeOP, @@ -14,7 +14,7 @@ fitting_check_output, model_check_output, ) -from deepmd.model_format.output_def import ( +from deepmd.dpmodel.output_def import ( check_var, ) diff --git a/source/tests/pt/test_descriptor_dpa1.py b/source/tests/pt/test_descriptor_dpa1.py index 725369d68d..21a43803c9 100644 --- a/source/tests/pt/test_descriptor_dpa1.py +++ b/source/tests/pt/test_descriptor_dpa1.py @@ -277,7 +277,7 @@ def test_descriptor_block(self): self.assertEqual(descriptor.shape[-1], des.get_dim_out()) self.assertAlmostEqual(6.0, des.get_rcut()) self.assertEqual(30, des.get_nsel()) - self.assertEqual(2, des.get_ntype()) + self.assertEqual(2, des.get_ntypes()) torch.testing.assert_close( descriptor.view(-1), self.ref_d, atol=1e-10, rtol=1e-10 ) @@ -329,7 +329,7 @@ def test_descriptor(self): self.assertEqual(descriptor.shape[-1], des.get_dim_out()) self.assertAlmostEqual(6.0, des.get_rcut()) self.assertEqual(30, des.get_nsel()) - self.assertEqual(2, des.get_ntype()) + self.assertEqual(2, des.get_ntypes()) torch.testing.assert_close( descriptor.view(-1), self.ref_d, atol=1e-10, rtol=1e-10 ) diff --git a/source/tests/pt/test_descriptor_dpa2.py b/source/tests/pt/test_descriptor_dpa2.py index aa6b16964e..e614e64c2f 100644 --- a/source/tests/pt/test_descriptor_dpa2.py +++ b/source/tests/pt/test_descriptor_dpa2.py @@ -224,7 +224,7 @@ def test_descriptor(self): self.assertEqual(descriptor.shape[-1], des.get_dim_out()) self.assertAlmostEqual(6.0, des.get_rcut()) self.assertEqual(30, des.get_nsel()) - self.assertEqual(2, des.get_ntype()) + self.assertEqual(2, des.get_ntypes()) torch.testing.assert_close( descriptor.view(-1), self.ref_d, atol=1e-10, rtol=1e-10 ) diff --git a/source/tests/pt/test_dp_atomic_model.py b/source/tests/pt/test_dp_atomic_model.py new file mode 100644 index 0000000000..2960cb97cc --- /dev/null +++ b/source/tests/pt/test_dp_atomic_model.py @@ -0,0 +1,112 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np +import torch + +from deepmd.dpmodel import DPAtomicModel as DPDPAtomicModel +from deepmd.dpmodel.descriptor import DescrptSeA as DPDescrptSeA +from deepmd.dpmodel.fitting import InvarFitting as DPInvarFitting +from deepmd.pt.model.descriptor.se_a import ( + DescrptSeA, +) +from deepmd.pt.model.model.dp_atomic_model import ( + DPAtomicModel, +) +from deepmd.pt.model.task.ener import ( + InvarFitting, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, + to_torch_tensor, +) + +from .test_env_mat import ( + TestCaseSingleFrameWithNlist, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION + + +class TestDPAtomicModel(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_self_consistency(self): + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ).to(env.DEVICE) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ).to(env.DEVICE) + type_map = ["foo", "bar"] + # TODO: dirty hack to avoid data stat!!! + md0 = DPAtomicModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + md1 = DPAtomicModel.deserialize(md0.serialize()).to(env.DEVICE) + args = [ + to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] + ] + ret0 = md0.forward_atomic(*args) + ret1 = md1.forward_atomic(*args) + np.testing.assert_allclose( + to_numpy_array(ret0["energy"]), + to_numpy_array(ret1["energy"]), + ) + + def test_dp_consistency(self): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + ds = DPDescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = DPInvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ) + type_map = ["foo", "bar"] + md0 = DPDPAtomicModel(ds, ft, type_map=type_map) + md1 = DPAtomicModel.deserialize(md0.serialize()).to(env.DEVICE) + args0 = [self.coord_ext, self.atype_ext, self.nlist] + args1 = [ + to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] + ] + ret0 = md0.forward_atomic(*args0) + ret1 = md1.forward_atomic(*args1) + np.testing.assert_allclose( + ret0["energy"], + to_numpy_array(ret1["energy"]), + ) + + def test_jit(self): + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ).to(env.DEVICE) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ).to(env.DEVICE) + type_map = ["foo", "bar"] + # TODO: dirty hack to avoid data stat!!! + md0 = DPAtomicModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + torch.jit.script(md0) diff --git a/source/tests/pt/test_dp_model.py b/source/tests/pt/test_dp_model.py new file mode 100644 index 0000000000..79f65d26d6 --- /dev/null +++ b/source/tests/pt/test_dp_model.py @@ -0,0 +1,388 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np +import torch + +from deepmd.dpmodel import DPModel as DPDPModel +from deepmd.dpmodel.descriptor import DescrptSeA as DPDescrptSeA +from deepmd.dpmodel.fitting import InvarFitting as DPInvarFitting +from deepmd.pt.model.descriptor.se_a import ( + DescrptSeA, +) +from deepmd.pt.model.model.ener import ( + DPModel, +) +from deepmd.pt.model.task.ener import ( + InvarFitting, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.nlist import ( + build_neighbor_list, + extend_coord_with_ghosts, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, + to_torch_tensor, +) + +from .test_env_mat import ( + TestCaseSingleFrameWithNlist, + TestCaseSingleFrameWithoutNlist, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION + + +class TestDPModel(unittest.TestCase, TestCaseSingleFrameWithoutNlist): + def setUp(self): + TestCaseSingleFrameWithoutNlist.setUp(self) + + def test_self_consistency(self): + nf, nloc = self.atype.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ).to(env.DEVICE) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ).to(env.DEVICE) + type_map = ["foo", "bar"] + # TODO: dirty hack to avoid data stat!!! + md0 = DPModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + md1 = DPModel.deserialize(md0.serialize()).to(env.DEVICE) + args = [to_torch_tensor(ii) for ii in [self.coord, self.atype, self.cell]] + ret0 = md0.forward_common(*args) + ret1 = md1.forward_common(*args) + np.testing.assert_allclose( + to_numpy_array(ret0["energy"]), + to_numpy_array(ret1["energy"]), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["energy_redu"]), + to_numpy_array(ret1["energy_redu"]), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["energy_derv_r"]), + to_numpy_array(ret1["energy_derv_r"]), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["energy_derv_c_redu"]), + to_numpy_array(ret1["energy_derv_c_redu"]), + ) + ret0 = md0.forward_common(*args, do_atomic_virial=True) + ret1 = md1.forward_common(*args, do_atomic_virial=True) + np.testing.assert_allclose( + to_numpy_array(ret0["energy_derv_c"]), + to_numpy_array(ret1["energy_derv_c"]), + ) + + coord_ext, atype_ext, mapping = extend_coord_with_ghosts( + to_torch_tensor(self.coord), + to_torch_tensor(self.atype), + to_torch_tensor(self.cell), + self.rcut, + ) + nlist = build_neighbor_list( + coord_ext, + atype_ext, + self.nloc, + self.rcut, + self.sel, + distinguish_types=md0.distinguish_types(), + ) + args = [coord_ext, atype_ext, nlist] + ret2 = md0.forward_common_lower(*args, do_atomic_virial=True) + # check the consistency between the reduced virial from + # forward_common and forward_common_lower + np.testing.assert_allclose( + to_numpy_array(ret0["energy_derv_c_redu"]), + to_numpy_array(ret2["energy_derv_c_redu"]), + ) + + def test_dp_consistency(self): + nf, nloc = self.atype.shape + nfp, nap = 2, 3 + ds = DPDescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = DPInvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + numb_fparam=nfp, + numb_aparam=nap, + ) + type_map = ["foo", "bar"] + md0 = DPDPModel(ds, ft, type_map=type_map) + md1 = DPModel.deserialize(md0.serialize()).to(env.DEVICE) + + rng = np.random.default_rng() + fparam = rng.normal(size=[self.nf, nfp]) + aparam = rng.normal(size=[self.nf, nloc, nap]) + args0 = [self.coord, self.atype, self.cell] + args1 = [to_torch_tensor(ii) for ii in [self.coord, self.atype, self.cell]] + kwargs0 = {"fparam": fparam, "aparam": aparam} + kwargs1 = {kk: to_torch_tensor(vv) for kk, vv in kwargs0.items()} + ret0 = md0.call(*args0, **kwargs0) + ret1 = md1.forward_common(*args1, **kwargs1) + np.testing.assert_allclose( + ret0["energy"], + to_numpy_array(ret1["energy"]), + ) + np.testing.assert_allclose( + ret0["energy_redu"], + to_numpy_array(ret1["energy_redu"]), + ) + + def test_dp_consistency_nopbc(self): + nf, nloc = self.atype.shape + nfp, nap = 2, 3 + ds = DPDescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = DPInvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + numb_fparam=nfp, + numb_aparam=nap, + ) + type_map = ["foo", "bar"] + md0 = DPDPModel(ds, ft, type_map=type_map) + md1 = DPModel.deserialize(md0.serialize()).to(env.DEVICE) + + rng = np.random.default_rng() + fparam = rng.normal(size=[self.nf, nfp]) + aparam = rng.normal(size=[self.nf, self.nloc, nap]) + args0 = [self.coord, self.atype] + args1 = [to_torch_tensor(ii) for ii in args0] + kwargs0 = {"fparam": fparam, "aparam": aparam} + kwargs1 = {kk: to_torch_tensor(vv) for kk, vv in kwargs0.items()} + ret0 = md0.call(*args0, **kwargs0) + ret1 = md1.forward_common(*args1, **kwargs1) + np.testing.assert_allclose( + ret0["energy"], + to_numpy_array(ret1["energy"]), + ) + np.testing.assert_allclose( + ret0["energy_redu"], + to_numpy_array(ret1["energy_redu"]), + ) + + +class TestDPModelLower(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_self_consistency(self): + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ).to(env.DEVICE) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ).to(env.DEVICE) + type_map = ["foo", "bar"] + # TODO: dirty hack to avoid data stat!!! + md0 = DPModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + md1 = DPModel.deserialize(md0.serialize()).to(env.DEVICE) + args = [ + to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] + ] + ret0 = md0.forward_common_lower(*args) + ret1 = md1.forward_common_lower(*args) + np.testing.assert_allclose( + to_numpy_array(ret0["energy"]), + to_numpy_array(ret1["energy"]), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["energy_redu"]), + to_numpy_array(ret1["energy_redu"]), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["energy_derv_r"]), + to_numpy_array(ret1["energy_derv_r"]), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["energy_derv_c_redu"]), + to_numpy_array(ret1["energy_derv_c_redu"]), + ) + ret0 = md0.forward_common_lower(*args, do_atomic_virial=True) + ret1 = md1.forward_common_lower(*args, do_atomic_virial=True) + np.testing.assert_allclose( + to_numpy_array(ret0["energy_derv_c"]), + to_numpy_array(ret1["energy_derv_c"]), + ) + + def test_dp_consistency(self): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + ds = DPDescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = DPInvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ) + type_map = ["foo", "bar"] + md0 = DPDPModel(ds, ft, type_map=type_map) + md1 = DPModel.deserialize(md0.serialize()).to(env.DEVICE) + args0 = [self.coord_ext, self.atype_ext, self.nlist] + args1 = [ + to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] + ] + ret0 = md0.call_lower(*args0) + ret1 = md1.forward_common_lower(*args1) + np.testing.assert_allclose( + ret0["energy"], + to_numpy_array(ret1["energy"]), + ) + np.testing.assert_allclose( + ret0["energy_redu"], + to_numpy_array(ret1["energy_redu"]), + ) + + def test_jit(self): + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ).to(env.DEVICE) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ).to(env.DEVICE) + type_map = ["foo", "bar"] + # TODO: dirty hack to avoid data stat!!! + md0 = DPModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + torch.jit.script(md0) + + +class TestDPModelFormatNlist(unittest.TestCase): + def setUp(self): + # nloc == 3, nall == 4 + self.nloc = 3 + self.nall = 5 + self.nf, self.nt = 1, 2 + self.coord_ext = np.array( + [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 1], + [0, -2, 0], + [2.3, 0, 0], + ], + dtype=np.float64, + ).reshape([1, self.nall * 3]) + # sel = [5, 2] + self.sel = [5, 2] + self.expected_nlist = np.array( + [ + [1, 3, -1, -1, -1, 2, -1], + [0, -1, -1, -1, -1, 2, -1], + [0, 1, -1, -1, -1, -1, -1], + ], + dtype=int, + ).reshape([1, self.nloc, sum(self.sel)]) + self.atype_ext = np.array([0, 0, 1, 0, 1], dtype=int).reshape([1, self.nall]) + self.rcut_smth = 0.4 + self.rcut = 2.0 + + nf, nloc, nnei = self.expected_nlist.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ).to(env.DEVICE) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ).to(env.DEVICE) + type_map = ["foo", "bar"] + # TODO: dirty hack to avoid data stat!!! + self.md = DPModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + + def test_nlist_eq(self): + # n_nnei == nnei + nlist = np.array( + [ + [1, 3, -1, -1, -1, 2, -1], + [0, -1, -1, -1, -1, 2, -1], + [0, 1, -1, -1, -1, -1, -1], + ], + dtype=np.int64, + ).reshape([1, self.nloc, -1]) + nlist1 = self.md.format_nlist( + to_torch_tensor(self.coord_ext), + to_torch_tensor(self.atype_ext), + to_torch_tensor(nlist), + ) + np.testing.assert_allclose(self.expected_nlist, to_numpy_array(nlist1)) + + def test_nlist_st(self): + # n_nnei < nnei + nlist = np.array( + [ + [1, 3, -1, 2], + [0, -1, -1, 2], + [0, 1, -1, -1], + ], + dtype=np.int64, + ).reshape([1, self.nloc, -1]) + nlist1 = self.md.format_nlist( + to_torch_tensor(self.coord_ext), + to_torch_tensor(self.atype_ext), + to_torch_tensor(nlist), + ) + np.testing.assert_allclose(self.expected_nlist, to_numpy_array(nlist1)) + + def test_nlist_lt(self): + # n_nnei > nnei + nlist = np.array( + [ + [1, 3, -1, -1, -1, 2, -1, -1, 4], + [0, -1, 4, -1, -1, 2, -1, 3, -1], + [0, 1, -1, -1, -1, 4, -1, -1, 3], + ], + dtype=np.int64, + ).reshape([1, self.nloc, -1]) + nlist1 = self.md.format_nlist( + to_torch_tensor(self.coord_ext), + to_torch_tensor(self.atype_ext), + to_torch_tensor(nlist), + ) + np.testing.assert_allclose(self.expected_nlist, to_numpy_array(nlist1)) diff --git a/source/tests/pt/test_ener_fitting.py b/source/tests/pt/test_ener_fitting.py index eece8447df..cbddf34dd6 100644 --- a/source/tests/pt/test_ener_fitting.py +++ b/source/tests/pt/test_ener_fitting.py @@ -5,7 +5,7 @@ import numpy as np import torch -from deepmd.model_format import InvarFitting as DPInvarFitting +from deepmd.dpmodel.fitting import InvarFitting as DPInvarFitting from deepmd.pt.model.descriptor.se_a import ( DescrptSeA, ) diff --git a/source/tests/pt/test_env_mat.py b/source/tests/pt/test_env_mat.py index f4931e9ecc..b9f0ff1981 100644 --- a/source/tests/pt/test_env_mat.py +++ b/source/tests/pt/test_env_mat.py @@ -5,7 +5,7 @@ import torch try: - from deepmd.model_format import ( + from deepmd.dpmodel import ( EnvMat, ) @@ -47,7 +47,7 @@ def setUp(self): [ [1, 3, -1, -1, -1, 2, -1], [0, -1, -1, -1, -1, 2, -1], - [0, 1, -1, -1, -1, 0, -1], + [0, 1, -1, -1, -1, -1, -1], ], dtype=int, ).reshape([1, self.nloc, sum(self.sel)]) @@ -55,6 +55,27 @@ def setUp(self): self.rcut_smth = 2.2 +class TestCaseSingleFrameWithoutNlist: + def setUp(self): + # nloc == 3, nall == 4 + self.nloc = 3 + self.nf, self.nt = 1, 2 + self.coord = np.array( + [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 1], + ], + dtype=np.float64, + ).reshape([1, self.nloc * 3]) + self.atype = np.array([0, 0, 1], dtype=int).reshape([1, self.nloc]) + self.cell = 2.0 * np.eye(3).reshape([1, 9]) + # sel = [5, 2] + self.sel = [5, 2] + self.rcut = 0.4 + self.rcut_smth = 2.2 + + # to be merged with the tf test case @unittest.skipIf(not support_env_mat, "EnvMat not supported") class TestEnvMat(unittest.TestCase, TestCaseSingleFrameWithNlist): diff --git a/source/tests/pt/test_mlp.py b/source/tests/pt/test_mlp.py index 26f0041bf9..3a78b8294d 100644 --- a/source/tests/pt/test_mlp.py +++ b/source/tests/pt/test_mlp.py @@ -42,7 +42,7 @@ try: - from deepmd.model_format import ( + from deepmd.dpmodel import ( NativeLayer, NativeNet, ) @@ -54,7 +54,7 @@ support_native_net = False try: - from deepmd.model_format import EmbeddingNet as DPEmbeddingNet + from deepmd.dpmodel import EmbeddingNet as DPEmbeddingNet support_embedding_net = True except ModuleNotFoundError: @@ -63,7 +63,7 @@ support_embedding_net = False try: - from deepmd.model_format import FittingNet as DPFittingNet + from deepmd.dpmodel import FittingNet as DPFittingNet support_fitting_net = True except ModuleNotFoundError: diff --git a/source/tests/pt/test_rotation.py b/source/tests/pt/test_rotation.py index 58ec80e0d6..a62e04eb89 100644 --- a/source/tests/pt/test_rotation.py +++ b/source/tests/pt/test_rotation.py @@ -111,22 +111,18 @@ def test_rotation(self): result1 = self.model(**get_data(self.origin_batch)) result2 = self.model(**get_data(self.rotated_batch)) rotation = torch.from_numpy(self.rotation).to(env.DEVICE) - self.assertTrue(result1["energy"] == result2["energy"]) + torch.testing.assert_close(result1["energy"], result2["energy"]) if "force" in result1: - self.assertTrue( - torch.allclose( - result2["force"][0], torch.matmul(rotation, result1["force"][0].T).T - ) + torch.testing.assert_close( + result2["force"][0], torch.matmul(rotation, result1["force"][0].T).T ) if "virial" in result1: - self.assertTrue( - torch.allclose( - result2["virial"][0].view([3, 3]), - torch.matmul( - torch.matmul(rotation, result1["virial"][0].view([3, 3]).T), - rotation.T, - ), - ) + torch.testing.assert_close( + result2["virial"][0].view([3, 3]), + torch.matmul( + torch.matmul(rotation, result1["virial"][0].view([3, 3]).T), + rotation.T, + ), ) diff --git a/source/tests/pt/test_se_e2_a.py b/source/tests/pt/test_se_e2_a.py index 0da80ea1ea..ec49725929 100644 --- a/source/tests/pt/test_se_e2_a.py +++ b/source/tests/pt/test_se_e2_a.py @@ -6,8 +6,8 @@ import torch try: - # from deepmd.model_format import PRECISION_DICT as DP_PRECISION_DICT - from deepmd.model_format import DescrptSeA as DPDescrptSeA + # from deepmd.dpmodel import PRECISION_DICT as DP_PRECISION_DICT + from deepmd.dpmodel import DescrptSeA as DPDescrptSeA support_se_e2_a = True except ModuleNotFoundError: diff --git a/source/tests/pt/test_utils.py b/source/tests/pt/test_utils.py index 9c9a9479ad..145fe6c510 100644 --- a/source/tests/pt/test_utils.py +++ b/source/tests/pt/test_utils.py @@ -24,7 +24,7 @@ def test_to_numpy(self): onk = to_numpy_array(bar) self.assertEqual(onk.dtype, npp) with self.assertRaises(ValueError) as ee: - foo = foo.astype(np.int32) + foo = foo.astype(np.int8) bar = to_torch_tensor(foo) with self.assertRaises(ValueError) as ee: bar = to_torch_tensor(foo) From 701b9132efb6c46512b064f0e955f3bd13fa7fc3 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 2 Feb 2024 01:52:17 -0500 Subject: [PATCH 039/686] fix compile gromacs with precompiled C library (#3217) Fix #3214. In the gmx patch file, `${TENSORFLOW_ROOT}` is used other than `${TensorFlow_LIBRARY_PATH}$` or `${TENSORFLOW_INCLUDE_DIRS}`, so the fastest workaround is to set `${TENSORFLOW_ROOT}`. https://github.com/deepmodeling/deepmd-kit/blob/eb9b2efedf4efc946894800a0d7abf5056f4bb7a/source/gmx/patches/2020.2/CMakeLists.txt.patch.in#L14-L18 Signed-off-by: Jinzhe Zeng --- source/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index d6ee3d0958..a4971a4d2a 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -271,6 +271,7 @@ if(DEEPMD_C_ROOT) IMPORTED_LOCATION "${deepmd_c}" INTERFACE_INCLUDE_DIRECTORIES "${DEEPMD_INCLUDE_C_DIR}/deepmd") # use variable for TF path to set deepmd_c path + set(TENSORFLOW_ROOT "${DEEPMD_C_ROOT}") set(TensorFlow_LIBRARY_PATH "${DEEPMD_C_ROOT}/lib") set(TENSORFLOW_INCLUDE_DIRS "${DEEPMD_C_ROOT}/include") set(TORCH_LIBRARIES "${DEEPMD_C_ROOT}/lib/libtorch.so") From 677d936d8cc79341c7679e31bf8891ecb52e7cb8 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Sat, 3 Feb 2024 05:05:08 +0800 Subject: [PATCH 040/686] fix bug of output def: the reduced virial is not defined. (#3219) Co-authored-by: Han Wang --- deepmd/dpmodel/output_def.py | 21 +++++++++++++++------ source/tests/common/test_output_def.py | 7 +++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/deepmd/dpmodel/output_def.py b/deepmd/dpmodel/output_def.py index 583f88491e..6cd83fcf28 100644 --- a/deepmd/dpmodel/output_def.py +++ b/deepmd/dpmodel/output_def.py @@ -147,6 +147,8 @@ def __init__( self.differentiable = differentiable if not self.reduciable and self.differentiable: raise ValueError("only reduciable variable are differentiable") + if self.reduciable and not self.atomic: + raise ValueError("only reduciable variable should be atomic") class FittingOutputDef: @@ -201,14 +203,16 @@ def __init__( fit_defs: FittingOutputDef, ): self.def_outp = fit_defs - self.def_redu = do_reduce(self.def_outp) - self.def_derv_r, self.def_derv_c = do_derivative(self.def_outp) + self.def_redu = do_reduce(self.def_outp.get_data()) + self.def_derv_r, self.def_derv_c = do_derivative(self.def_outp.get_data()) + self.def_derv_c_redu = do_reduce(self.def_derv_c) self.var_defs: Dict[str, OutputVariableDef] = {} for ii in [ self.def_outp.get_data(), self.def_redu, self.def_derv_c, self.def_derv_r, + self.def_derv_c_redu, ]: self.var_defs.update(ii) @@ -239,6 +243,9 @@ def keys_derv_r(self): def keys_derv_c(self): return self.def_derv_c.keys() + def keys_derv_c_redu(self): + return self.def_derv_c_redu.keys() + def get_reduce_name(name: str) -> str: return name + "_redu" @@ -249,10 +256,10 @@ def get_deriv_name(name: str) -> Tuple[str, str]: def do_reduce( - def_outp: FittingOutputDef, + def_outp_data: Dict[str, OutputVariableDef], ) -> Dict[str, OutputVariableDef]: def_redu: Dict[str, OutputVariableDef] = {} - for kk, vv in def_outp.get_data().items(): + for kk, vv in def_outp_data.items(): if vv.reduciable: rk = get_reduce_name(kk) def_redu[rk] = OutputVariableDef( @@ -262,11 +269,11 @@ def do_reduce( def do_derivative( - def_outp: FittingOutputDef, + def_outp_data: Dict[str, OutputVariableDef], ) -> Tuple[Dict[str, OutputVariableDef], Dict[str, OutputVariableDef]]: def_derv_r: Dict[str, OutputVariableDef] = {} def_derv_c: Dict[str, OutputVariableDef] = {} - for kk, vv in def_outp.get_data().items(): + for kk, vv in def_outp_data.items(): if vv.differentiable: rkr, rkc = get_deriv_name(kk) def_derv_r[rkr] = OutputVariableDef( @@ -274,11 +281,13 @@ def do_derivative( vv.shape + [3], # noqa: RUF005 reduciable=False, differentiable=False, + atomic=True, ) def_derv_c[rkc] = OutputVariableDef( rkc, vv.shape + [3, 3], # noqa: RUF005 reduciable=True, differentiable=False, + atomic=True, ) return def_derv_r, def_derv_c diff --git a/source/tests/common/test_output_def.py b/source/tests/common/test_output_def.py index d0cf822247..aaabdc0ba6 100644 --- a/source/tests/common/test_output_def.py +++ b/source/tests/common/test_output_def.py @@ -70,6 +70,7 @@ def test_model_output_def(self): "energy_redu", "energy_derv_r", "energy_derv_c", + "energy_derv_c_redu", "dos_redu", ] self.assertEqual( @@ -93,6 +94,7 @@ def test_model_output_def(self): self.assertEqual(md["energy_redu"].shape, [1]) self.assertEqual(md["energy_derv_r"].shape, [1, 3]) self.assertEqual(md["energy_derv_c"].shape, [1, 3, 3]) + self.assertEqual(md["energy_derv_c_redu"].shape, [1, 3, 3]) # atomic self.assertEqual(md["energy"].atomic, True) self.assertEqual(md["dos"].atomic, True) @@ -100,11 +102,16 @@ def test_model_output_def(self): self.assertEqual(md["energy_redu"].atomic, False) self.assertEqual(md["energy_derv_r"].atomic, True) self.assertEqual(md["energy_derv_c"].atomic, True) + self.assertEqual(md["energy_derv_c_redu"].atomic, False) def test_raise_no_redu_deriv(self): with self.assertRaises(ValueError) as context: (OutputVariableDef("energy", [1], False, True),) + def test_raise_redu_not_atomic(self): + with self.assertRaises(ValueError) as context: + (OutputVariableDef("energy", [1], True, False, atomic=False),) + def test_model_decorator(self): nf = 2 nloc = 3 From 412c8122b3cbd6ef0b629b06f753be72bb259e01 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 2 Feb 2024 23:39:02 -0500 Subject: [PATCH 041/686] gmx: fix include directive (#3221) Fix #3214. Signed-off-by: Jinzhe Zeng --- source/gmx/CMakeLists.txt | 1 + source/gmx/include/gmx_plugin.h | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/source/gmx/CMakeLists.txt b/source/gmx/CMakeLists.txt index c119e4b212..d445479d39 100644 --- a/source/gmx/CMakeLists.txt +++ b/source/gmx/CMakeLists.txt @@ -19,6 +19,7 @@ else() target_link_libraries(${libgmxname} PUBLIC ${LIB_DEEPMD_CC}) target_compile_definitions(${libgmxname} PUBLIC "DP_USE_CXX_API") endif() +target_compile_definitions(${libgmxname} PRIVATE "DP_GMX_PLUGIN_INTERNAL") target_include_directories(${libgmxname} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_include_directories(${libgmxname} diff --git a/source/gmx/include/gmx_plugin.h b/source/gmx/include/gmx_plugin.h index 51eae8ca7e..430ca2fe0d 100644 --- a/source/gmx/include/gmx_plugin.h +++ b/source/gmx/include/gmx_plugin.h @@ -5,7 +5,11 @@ #include "DeepPot.h" namespace deepmd_compat = deepmd; #else +#ifdef DP_GMX_PLUGIN_INTERNAL #include "deepmd.hpp" +#else +#include "deepmd/deepmd.hpp" +#endif namespace deepmd_compat = deepmd::hpp; #endif From ab2c5514c5f2578bc6320c802eecdf8047e0c1c4 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 3 Feb 2024 20:04:35 -0500 Subject: [PATCH 042/686] c: fix all memory leaks; add sanitizer checks (#3223) Fix #3045. All memory leaks have been fixed! --------- Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/test_cc.yml | 7 ++++ doc/inference/cxx.md | 2 +- examples/infer_water/infer_water.c | 2 +- source/api_c/include/c_api.h | 35 +++++++++++++++++++ source/api_c/include/deepmd.hpp | 51 +++++++++++++++++++++++----- source/api_c/src/c_api.cc | 10 ++++++ source/api_c/tests/test_deeppot_a.cc | 32 +++++++++++++++-- 7 files changed, 127 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test_cc.yml b/.github/workflows/test_cc.yml index 1ded666070..2253a25ee0 100644 --- a/.github/workflows/test_cc.yml +++ b/.github/workflows/test_cc.yml @@ -6,6 +6,9 @@ jobs: testcc: name: Test C++ runs-on: ubuntu-latest + strategy: + matrix: + check_memleak: [true, false] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -31,6 +34,7 @@ jobs: TF_INTER_OP_PARALLELISM_THREADS: 1 LMP_CXX11_ABI_0: 1 CMAKE_GENERATOR: Ninja + CXXFLAGS: ${{ matrix.check_memleak && '-fsanitize=leak' || '' }} # test lammps # ASE issue: https://gitlab.com/ase/ase/-/merge_requests/2843 # TODO: remove ase version when ase has new release @@ -39,6 +43,7 @@ jobs: python -m pip install -e .[cpu,test,lmp] "ase @ https://gitlab.com/ase/ase/-/archive/8c5aa5fd6448c5cfb517a014dccf2b214a9dfa8f/ase-8c5aa5fd6448c5cfb517a014dccf2b214a9dfa8f.tar.gz" env: DP_BUILD_TESTING: 1 + if: ${{ !matrix.check_memleak }} - run: pytest --cov=deepmd source/lmp/tests env: OMP_NUM_THREADS: 1 @@ -46,6 +51,7 @@ jobs: TF_INTER_OP_PARALLELISM_THREADS: 1 LAMMPS_PLUGIN_PATH: ${{ github.workspace }}/dp_test/lib/deepmd_lmp LD_LIBRARY_PATH: ${{ github.workspace }}/dp_test/lib + if: ${{ !matrix.check_memleak }} # test ipi - run: pytest --cov=deepmd source/ipi/tests env: @@ -54,6 +60,7 @@ jobs: TF_INTER_OP_PARALLELISM_THREADS: 1 PATH: ${{ github.workspace }}/dp_test/bin:$PATH LD_LIBRARY_PATH: ${{ github.workspace }}/dp_test/lib + if: ${{ !matrix.check_memleak }} - uses: codecov/codecov-action@v3 with: gcov: true diff --git a/doc/inference/cxx.md b/doc/inference/cxx.md index 6188daba4c..cc7e7be540 100644 --- a/doc/inference/cxx.md +++ b/doc/inference/cxx.md @@ -62,7 +62,7 @@ int main(){ free(v); free(ae); free(av); - free(dp); + DP_DeleteDeepPot(dp); } ``` diff --git a/examples/infer_water/infer_water.c b/examples/infer_water/infer_water.c index f4eeae147f..cf13f45e3a 100644 --- a/examples/infer_water/infer_water.c +++ b/examples/infer_water/infer_water.c @@ -32,5 +32,5 @@ int main() { free(v); free(ae); free(av); - free(dp); + DP_DeleteDeepPot(dp); } diff --git a/source/api_c/include/c_api.h b/source/api_c/include/c_api.h index d05f790bf9..4baa7dd4a0 100644 --- a/source/api_c/include/c_api.h +++ b/source/api_c/include/c_api.h @@ -25,6 +25,13 @@ extern DP_Nlist* DP_NewNlist(int inum_, int* numneigh_, int** firstneigh_); +/** + * @brief Delete a neighbor list. + * + * @param nl Neighbor list to delete. + */ +extern void DP_DeleteNlist(DP_Nlist* nl); + /** * @brief Check if there is any exceptions throw. * @@ -72,6 +79,13 @@ extern DP_DeepPot* DP_NewDeepPotWithParam2(const char* c_model, const char* c_file_content, const int size_file_content); +/** + * @brief Delete a Deep Potential. + * + * @param dp Deep Potential to delete. + */ +extern void DP_DeleteDeepPot(DP_DeepPot* dp); + /** * @brief Evaluate the energy, force and virial by using a DP. (double version) * @attention The number of frames is assumed to be 1. @@ -491,6 +505,13 @@ extern DP_DeepPotModelDevi* DP_NewDeepPotModelDeviWithParam( const int n_file_contents, const int* size_file_contents); +/** + * @brief Delete a Deep Potential Model Deviation. + * + * @param dp Deep Potential to delete. + */ +extern void DP_DeleteDeepPotModelDevi(DP_DeepPotModelDevi* dp); + /** * @brief Evaluate the energy, force and virial by using a DP model deviation *with neighbor list. (double version) @@ -792,6 +813,13 @@ extern DP_DeepTensor* DP_NewDeepTensorWithParam(const char* c_model, const int gpu_rank, const char* c_name_scope); +/** + * @brief Delete a Deep Tensor. + * + * @param dp Deep Tensor to delete. + */ +extern void DP_DeleteDeepTensor(DP_DeepTensor* dt); + /** * @brief Evaluate the tensor by using a DP. (double version) * @param[in] dt The Deep Tensor to use. @@ -1094,6 +1122,13 @@ extern DP_DipoleChargeModifier* DP_NewDipoleChargeModifier(const char* c_model); extern DP_DipoleChargeModifier* DP_NewDipoleChargeModifierWithParam( const char* c_model, const int gpu_rank, const char* c_name_scope); +/** + * @brief Delete a Dipole Charge Modifier. + * + * @param dp Dipole Charge Modifier to delete. + */ +extern void DP_DeleteDipoleChargeModifier(DP_DipoleChargeModifier* dcm); + /** * @brief Evaluate the force and virial correction by using a dipole charge *modifier with the neighbor list. (double version) diff --git a/source/api_c/include/deepmd.hpp b/source/api_c/include/deepmd.hpp index 06a50ee3f0..966cc1f24e 100644 --- a/source/api_c/include/deepmd.hpp +++ b/source/api_c/include/deepmd.hpp @@ -522,6 +522,7 @@ struct InputNlist { nl(DP_NewNlist(inum_, ilist_, numneigh_, firstneigh_)) { DP_CHECK_OK(DP_NlistCheckOK, nl); }; + ~InputNlist() { DP_DeleteNlist(nl); }; /// @brief C API neighbor list. DP_Nlist *nl; /// @brief Number of core region atoms @@ -556,6 +557,8 @@ void inline convert_nlist(InputNlist &to_nlist, to_nlist.numneigh[ii] = from_nlist[ii].size(); to_nlist.firstneigh[ii] = &from_nlist[ii][0]; } + // delete the original nl + DP_DeleteNlist(to_nlist.nl); to_nlist.nl = DP_NewNlist(to_nlist.inum, to_nlist.ilist, to_nlist.numneigh, to_nlist.firstneigh); } @@ -568,7 +571,7 @@ class DeepPot { * @brief DP constructor without initialization. **/ DeepPot() : dp(nullptr){}; - ~DeepPot(){}; + ~DeepPot() { DP_DeleteDeepPot(dp); }; /** * @brief DP constructor with initialization. * @param[in] model The name of the frozen model file. @@ -579,7 +582,15 @@ class DeepPot { const int &gpu_rank = 0, const std::string &file_content = "") : dp(nullptr) { - init(model, gpu_rank, file_content); + try { + init(model, gpu_rank, file_content); + } catch (...) { + // Clean up and rethrow, as the destructor will not be called + if (dp) { + DP_DeleteDeepPot(dp); + } + throw; + } }; /** * @brief Initialize the DP. @@ -1100,13 +1111,21 @@ class DeepPotModelDevi { * @brief DP model deviation constructor without initialization. **/ DeepPotModelDevi() : dp(nullptr){}; - ~DeepPotModelDevi(){}; + ~DeepPotModelDevi() { DP_DeleteDeepPotModelDevi(dp); }; /** * @brief DP model deviation constructor with initialization. * @param[in] models The names of the frozen model file. **/ DeepPotModelDevi(const std::vector &models) : dp(nullptr) { - init(models); + try { + init(models); + } catch (...) { + // Clean up and rethrow, as the destructor will not be called + if (dp) { + DP_DeleteDeepPotModelDevi(dp); + } + throw; + } }; /** * @brief Initialize the DP model deviation. @@ -1523,7 +1542,7 @@ class DeepTensor { * @brief Deep Tensor constructor without initialization. **/ DeepTensor() : dt(nullptr){}; - ~DeepTensor(){}; + ~DeepTensor() { DP_DeleteDeepTensor(dt); }; /** * @brief DeepTensor constructor with initialization. * @param[in] model The name of the frozen model file. @@ -1532,7 +1551,15 @@ class DeepTensor { const int &gpu_rank = 0, const std::string &name_scope = "") : dt(nullptr) { - init(model, gpu_rank, name_scope); + try { + init(model, gpu_rank, name_scope); + } catch (...) { + // Clean up and rethrow, as the destructor will not be called + if (dt) { + DP_DeleteDeepTensor(dt); + } + throw; + } }; /** * @brief Initialize the DeepTensor. @@ -1891,7 +1918,7 @@ class DipoleChargeModifier { * @brief DipoleChargeModifier constructor without initialization. **/ DipoleChargeModifier() : dcm(nullptr){}; - ~DipoleChargeModifier(){}; + ~DipoleChargeModifier() { DP_DeleteDipoleChargeModifier(dcm); }; /** * @brief DipoleChargeModifier constructor with initialization. * @param[in] model The name of the frozen model file. @@ -1902,7 +1929,15 @@ class DipoleChargeModifier { const int &gpu_rank = 0, const std::string &name_scope = "") : dcm(nullptr) { - init(model, gpu_rank, name_scope); + try { + init(model, gpu_rank, name_scope); + } catch (...) { + // Clean up and rethrow, as the destructor will not be called + if (dcm) { + DP_DeleteDipoleChargeModifier(dcm); + } + throw; + } }; /** * @brief Initialize the DipoleChargeModifier. diff --git a/source/api_c/src/c_api.cc b/source/api_c/src/c_api.cc index bc6178702f..029d020f45 100644 --- a/source/api_c/src/c_api.cc +++ b/source/api_c/src/c_api.cc @@ -25,6 +25,8 @@ DP_Nlist* DP_NewNlist(int inum_, DP_Nlist* new_nl = new DP_Nlist(nl); return new_nl;) } +void DP_DeleteNlist(DP_Nlist* nl) { delete nl; } + DP_DeepPot::DP_DeepPot() {} DP_DeepPot::DP_DeepPot(deepmd::DeepPot& dp) : dp(dp) { dfparam = dp.dim_fparam(); @@ -61,6 +63,8 @@ DP_DeepPot* DP_NewDeepPotWithParam2(const char* c_model, DP_DeepPot* new_dp = new DP_DeepPot(dp); return new_dp;) } +void DP_DeleteDeepPot(DP_DeepPot* dp) { delete dp; } + DP_DeepPotModelDevi::DP_DeepPotModelDevi() {} DP_DeepPotModelDevi::DP_DeepPotModelDevi(deepmd::DeepPotModelDevi& dp) : dp(dp) { @@ -97,6 +101,8 @@ DP_DeepPotModelDevi* DP_NewDeepPotModelDeviWithParam( return new_dp;) } +void DP_DeleteDeepPotModelDevi(DP_DeepPotModelDevi* dp) { delete dp; } + DP_DeepTensor::DP_DeepTensor() {} DP_DeepTensor::DP_DeepTensor(deepmd::DeepTensor& dt) : dt(dt) {} @@ -115,6 +121,8 @@ DP_DeepTensor* DP_NewDeepTensorWithParam(const char* c_model, DP_DeepTensor* new_dt = new DP_DeepTensor(dt); return new_dt;) } +void DP_DeleteDeepTensor(DP_DeepTensor* dt) { delete dt; } + DP_DipoleChargeModifier::DP_DipoleChargeModifier() {} DP_DipoleChargeModifier::DP_DipoleChargeModifier( deepmd::DipoleChargeModifier& dcm) @@ -137,6 +145,8 @@ DP_DipoleChargeModifier* DP_NewDipoleChargeModifierWithParam( return new_dcm;) } +void DP_DeleteDipoleChargeModifier(DP_DipoleChargeModifier* dcm) { delete dcm; } + } // extern "C" template diff --git a/source/api_c/tests/test_deeppot_a.cc b/source/api_c/tests/test_deeppot_a.cc index 63f53e16e9..b4a9a81f92 100644 --- a/source/api_c/tests/test_deeppot_a.cc +++ b/source/api_c/tests/test_deeppot_a.cc @@ -86,7 +86,10 @@ class TestInferDeepPotA : public ::testing::Test { } }; - void TearDown() override { remove("deeppot.pb"); }; + void TearDown() override { + remove("deeppot.pb"); + DP_DeleteDeepPot(dp); + }; }; TEST_F(TestInferDeepPotA, double_infer) { @@ -119,6 +122,12 @@ TEST_F(TestInferDeepPotA, double_infer) { for (int ii = 0; ii < natoms * 9; ++ii) { EXPECT_LT(fabs(atomic_virial[ii] - expected_v[ii]), 1e-10); } + + delete ener_; + delete[] force_; + delete[] virial_; + delete[] atomic_ener_; + delete[] atomic_virial_; } TEST_F(TestInferDeepPotA, float_infer) { @@ -151,6 +160,11 @@ TEST_F(TestInferDeepPotA, float_infer) { for (int ii = 0; ii < natoms * 9; ++ii) { EXPECT_LT(fabs(atomic_virial[ii] - expected_v[ii]), 1e-6); } + delete ener_; + delete[] force_; + delete[] virial_; + delete[] atomic_ener_; + delete[] atomic_virial_; } TEST_F(TestInferDeepPotA, cutoff) { @@ -253,7 +267,11 @@ class TestInferDeepPotANoPBC : public ::testing::Test { } }; - void TearDown() override { remove("deeppot.pb"); }; + void TearDown() override { + remove("deeppot.pb"); + + DP_DeleteDeepPot(dp); + }; }; TEST_F(TestInferDeepPotANoPBC, double_infer) { @@ -286,6 +304,11 @@ TEST_F(TestInferDeepPotANoPBC, double_infer) { for (int ii = 0; ii < natoms * 9; ++ii) { EXPECT_LT(fabs(atomic_virial[ii] - expected_v[ii]), 1e-10); } + delete ener_; + delete[] force_; + delete[] virial_; + delete[] atomic_ener_; + delete[] atomic_virial_; } TEST_F(TestInferDeepPotANoPBC, float_infer) { @@ -318,4 +341,9 @@ TEST_F(TestInferDeepPotANoPBC, float_infer) { for (int ii = 0; ii < natoms * 9; ++ii) { EXPECT_LT(fabs(atomic_virial[ii] - expected_v[ii]), 1e-6); } + delete ener_; + delete[] force_; + delete[] virial_; + delete[] atomic_ener_; + delete[] atomic_virial_; } From 7db1fde4aca447f075082e712966555ae9ce9d0f Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Sun, 4 Feb 2024 13:50:04 +0800 Subject: [PATCH 043/686] Feat: numpy pairtab model (#3212) This PR is to provide backend independent implementation of PairTabModel in `numpy`. Also the cross framework `serialization` and `deserialization` are added. --------- Co-authored-by: Anyang Peng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/dpmodel/model/pair_tab_model.py | 296 ++++++++++++++++++ .../model/{pair_tab.py => pair_tab_model.py} | 45 ++- deepmd/utils/pair_tab.py | 33 ++ .../tests/common/test_pairtab_preprocess.py | 12 + source/tests/dpmodel/__init__.py | 1 + source/tests/dpmodel/test_pairtab.py | 203 ++++++++++++ source/tests/pt/test_pairtab.py | 63 +++- 7 files changed, 630 insertions(+), 23 deletions(-) create mode 100644 deepmd/dpmodel/model/pair_tab_model.py rename deepmd/pt/model/model/{pair_tab.py => pair_tab_model.py} (88%) create mode 100644 source/tests/dpmodel/__init__.py create mode 100644 source/tests/dpmodel/test_pairtab.py diff --git a/deepmd/dpmodel/model/pair_tab_model.py b/deepmd/dpmodel/model/pair_tab_model.py new file mode 100644 index 0000000000..d62ac5c859 --- /dev/null +++ b/deepmd/dpmodel/model/pair_tab_model.py @@ -0,0 +1,296 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + List, + Optional, + Union, +) + +import numpy as np + +from deepmd.dpmodel.output_def import ( + FittingOutputDef, + OutputVariableDef, +) +from deepmd.utils.pair_tab import ( + PairTab, +) + +from .base_atomic_model import ( + BaseAtomicModel, +) + + +class PairTabModel(BaseAtomicModel): + """Pairwise tabulation energy model. + + This model can be used to tabulate the pairwise energy between atoms for either + short-range or long-range interactions, such as D3, LJ, ZBL, etc. It should not + be used alone, but rather as one submodel of a linear (sum) model, such as + DP+D3. + + Do not put the model on the first model of a linear model, since the linear + model fetches the type map from the first model. + + At this moment, the model does not smooth the energy at the cutoff radius, so + one needs to make sure the energy has been smoothed to zero. + + Parameters + ---------- + tab_file : str + The path to the tabulation file. + rcut : float + The cutoff radius. + sel : int or list[int] + The maxmum number of atoms in the cut-off radius. + """ + + def __init__( + self, tab_file: str, rcut: float, sel: Union[int, List[int]], **kwargs + ): + super().__init__() + self.tab_file = tab_file + self.rcut = rcut + + self.tab = PairTab(self.tab_file, rcut=rcut) + + if self.tab_file is not None: + self.tab_info, self.tab_data = self.tab.get() + else: + self.tab_info, self.tab_data = None, None + + if isinstance(sel, int): + self.sel = sel + elif isinstance(sel, list): + self.sel = sum(sel) + else: + raise TypeError("sel must be int or list[int]") + + def fitting_output_def(self) -> FittingOutputDef: + return FittingOutputDef( + [ + OutputVariableDef( + name="energy", shape=[1], reduciable=True, differentiable=True + ) + ] + ) + + def get_rcut(self) -> float: + return self.rcut + + def get_sel(self) -> int: + return self.sel + + def distinguish_types(self) -> bool: + # to match DPA1 and DPA2. + return False + + def serialize(self) -> dict: + return {"tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel} + + @classmethod + def deserialize(cls, data) -> "PairTabModel": + rcut = data["rcut"] + sel = data["sel"] + tab = PairTab.deserialize(data["tab"]) + tab_model = cls(None, rcut, sel) + tab_model.tab = tab + tab_model.tab_info = tab_model.tab.tab_info + tab_model.tab_data = tab_model.tab.tab_data + return tab_model + + def forward_atomic( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[np.ndarray] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, np.ndarray]: + self.nframes, self.nloc, self.nnei = nlist.shape + extended_coord = extended_coord.reshape(self.nframes, -1, 3) + + # this will mask all -1 in the nlist + masked_nlist = np.clip(nlist, 0, None) + + atype = extended_atype[:, : self.nloc] # (nframes, nloc) + pairwise_dr = self._get_pairwise_dist( + extended_coord + ) # (nframes, nall, nall, 3) + pairwise_rr = np.sqrt( + np.sum(np.power(pairwise_dr, 2), axis=-1) + ) # (nframes, nall, nall) + self.tab_data = self.tab_data.reshape( + self.tab.ntypes, self.tab.ntypes, self.tab.nspline, 4 + ) + + # (nframes, nloc, nnei) + j_type = extended_atype[ + np.arange(extended_atype.shape[0])[:, None, None], masked_nlist + ] + + # slice rr to get (nframes, nloc, nnei) + rr = np.take_along_axis(pairwise_rr[:, : self.nloc, :], masked_nlist, 2) + raw_atomic_energy = self._pair_tabulated_inter(nlist, atype, j_type, rr) + atomic_energy = 0.5 * np.sum( + np.where(nlist != -1, raw_atomic_energy, np.zeros_like(raw_atomic_energy)), + axis=-1, + ).reshape(self.nframes, self.nloc, 1) + + return {"energy": atomic_energy} + + def _pair_tabulated_inter( + self, + nlist: np.ndarray, + i_type: np.ndarray, + j_type: np.ndarray, + rr: np.ndarray, + ) -> np.ndarray: + """Pairwise tabulated energy. + + Parameters + ---------- + nlist : np.ndarray + The unmasked neighbour list. (nframes, nloc) + i_type : np.ndarray + The integer representation of atom type for all local atoms for all frames. (nframes, nloc) + j_type : np.ndarray + The integer representation of atom type for all neighbour atoms of all local atoms for all frames. (nframes, nloc, nnei) + rr : np.ndarray + The salar distance vector between two atoms. (nframes, nloc, nnei) + + Returns + ------- + np.ndarray + The masked atomic energy for all local atoms for all frames. (nframes, nloc, nnei) + + Raises + ------ + Exception + If the distance is beyond the table. + + Notes + ----- + This function is used to calculate the pairwise energy between two atoms. + It uses a table containing cubic spline coefficients calculated in PairTab. + """ + rmin = self.tab_info[0] + hh = self.tab_info[1] + hi = 1.0 / hh + + self.nspline = int(self.tab_info[2] + 0.1) + + uu = (rr - rmin) * hi # this is broadcasted to (nframes,nloc,nnei) + + # if nnei of atom 0 has -1 in the nlist, uu would be 0. + # this is to handle the nlist where the mask is set to 0, so that we don't raise exception for those atoms. + uu = np.where(nlist != -1, uu, self.nspline + 1) + + if np.any(uu < 0): + raise Exception("coord go beyond table lower boundary") + + idx = uu.astype(int) + + uu -= idx + table_coef = self._extract_spline_coefficient( + i_type, j_type, idx, self.tab_data, self.nspline + ) + table_coef = table_coef.reshape(self.nframes, self.nloc, self.nnei, 4) + ener = self._calcualte_ener(table_coef, uu) + # here we need to overwrite energy to zero at rcut and beyond. + mask_beyond_rcut = rr >= self.rcut + # also overwrite values beyond extrapolation to zero + extrapolation_mask = rr >= self.tab.rmin + self.nspline * self.tab.hh + ener[mask_beyond_rcut] = 0 + ener[extrapolation_mask] = 0 + + return ener + + @staticmethod + def _get_pairwise_dist(coords: np.ndarray) -> np.ndarray: + """Get pairwise distance `dr`. + + Parameters + ---------- + coords : np.ndarray + The coordinate of the atoms shape of (nframes, nall, 3). + + Returns + ------- + np.ndarray + The pairwise distance between the atoms (nframes, nall, nall, 3). + """ + return np.expand_dims(coords, 2) - np.expand_dims(coords, 1) + + @staticmethod + def _extract_spline_coefficient( + i_type: np.ndarray, + j_type: np.ndarray, + idx: np.ndarray, + tab_data: np.ndarray, + nspline: int, + ) -> np.ndarray: + """Extract the spline coefficient from the table. + + Parameters + ---------- + i_type : np.ndarray + The integer representation of atom type for all local atoms for all frames. (nframes, nloc) + j_type : np.ndarray + The integer representation of atom type for all neighbour atoms of all local atoms for all frames. (nframes, nloc, nnei) + idx : np.ndarray + The index of the spline coefficient. (nframes, nloc, nnei) + tab_data : np.ndarray + The table storing all the spline coefficient. (ntype, ntype, nspline, 4) + nspline : int + The number of splines in the table. + + Returns + ------- + np.ndarray + The spline coefficient. (nframes, nloc, nnei, 4), shape may be squeezed. + """ + # (nframes, nloc, nnei) + expanded_i_type = np.broadcast_to( + i_type[:, :, np.newaxis], + (i_type.shape[0], i_type.shape[1], j_type.shape[-1]), + ) + + # (nframes, nloc, nnei, nspline, 4) + expanded_tab_data = tab_data[expanded_i_type, j_type] + + # (nframes, nloc, nnei, 1, 4) + expanded_idx = np.broadcast_to( + idx[..., np.newaxis, np.newaxis], (*idx.shape, 1, 4) + ) + clipped_indices = np.clip(expanded_idx, 0, nspline - 1).astype(int) + + # (nframes, nloc, nnei, 4) + final_coef = np.squeeze( + np.take_along_axis(expanded_tab_data, clipped_indices, 3) + ) + + # when the spline idx is beyond the table, all spline coefficients are set to `0`, and the resulting ener corresponding to the idx is also `0`. + final_coef[expanded_idx.squeeze() > nspline] = 0 + return final_coef + + @staticmethod + def _calcualte_ener(coef: np.ndarray, uu: np.ndarray) -> np.ndarray: + """Calculate energy using spline coeeficients. + + Parameters + ---------- + coef : np.ndarray + The spline coefficients. (nframes, nloc, nnei, 4) + uu : np.ndarray + The atom displancemnt used in interpolation and extrapolation (nframes, nloc, nnei) + + Returns + ------- + np.ndarray + The atomic energy for all local atoms for all frames. (nframes, nloc, nnei) + """ + a3, a2, a1, a0 = coef[..., 0], coef[..., 1], coef[..., 2], coef[..., 3] + etmp = (a3 * uu + a2) * uu + a1 # this should be elementwise operations. + ener = etmp * uu + a0 # this energy has the extrapolated value when rcut > rmax + return ener diff --git a/deepmd/pt/model/model/pair_tab.py b/deepmd/pt/model/model/pair_tab_model.py similarity index 88% rename from deepmd/pt/model/model/pair_tab.py rename to deepmd/pt/model/model/pair_tab_model.py index 430d090eb0..1a415d633d 100644 --- a/deepmd/pt/model/model/pair_tab.py +++ b/deepmd/pt/model/model/pair_tab_model.py @@ -54,13 +54,19 @@ def __init__( super().__init__() self.tab_file = tab_file self.rcut = rcut - self.tab = PairTab(self.tab_file, rcut=rcut) - self.ntypes = self.tab.ntypes - tab_info, tab_data = self.tab.get() # this returns -> Tuple[np.array, np.array] - self.tab_info = torch.from_numpy(tab_info) - self.tab_data = torch.from_numpy(tab_data) + # handle deserialization with no input file + if self.tab_file is not None: + ( + tab_info, + tab_data, + ) = self.tab.get() # this returns -> Tuple[np.array, np.array] + self.tab_info = torch.from_numpy(tab_info) + self.tab_data = torch.from_numpy(tab_data) + else: + self.tab_info = None + self.tab_data = None # self.model_type = "ener" # self.model_version = MODEL_VERSION ## this shoud be in the parent class @@ -92,12 +98,18 @@ def distinguish_types(self) -> bool: return False def serialize(self) -> dict: - # place holder, implemantated in future PR - raise NotImplementedError - - def deserialize(cls): - # place holder, implemantated in future PR - raise NotImplementedError + return {"tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel} + + @classmethod + def deserialize(cls, data) -> "PairTabModel": + rcut = data["rcut"] + sel = data["sel"] + tab = PairTab.deserialize(data["tab"]) + tab_model = cls(None, rcut, sel) + tab_model.tab = tab + tab_model.tab_info = torch.from_numpy(tab_model.tab.tab_info) + tab_model.tab_data = torch.from_numpy(tab_model.tab.tab_data) + return tab_model def forward_atomic( self, @@ -108,6 +120,7 @@ def forward_atomic( do_atomic_virial: bool = False, ) -> Dict[str, torch.Tensor]: self.nframes, self.nloc, self.nnei = nlist.shape + extended_coord = extended_coord.view(self.nframes, -1, 3) # this will mask all -1 in the nlist masked_nlist = torch.clamp(nlist, 0) @@ -118,7 +131,7 @@ def forward_atomic( ) # (nframes, nall, nall, 3) pairwise_rr = pairwise_dr.pow(2).sum(-1).sqrt() # (nframes, nall, nall) - self.tab_data = self.tab_data.reshape( + self.tab_data = self.tab_data.view( self.tab.ntypes, self.tab.ntypes, self.tab.nspline, 4 ) @@ -139,7 +152,7 @@ def forward_atomic( nlist != -1, raw_atomic_energy, torch.zeros_like(raw_atomic_energy) ), dim=-1, - ) + ).unsqueeze(-1) return {"energy": atomic_energy} @@ -200,7 +213,7 @@ def _pair_tabulated_inter( table_coef = self._extract_spline_coefficient( i_type, j_type, idx, self.tab_data, self.nspline ) - table_coef = table_coef.reshape(self.nframes, self.nloc, self.nnei, 4) + table_coef = table_coef.view(self.nframes, self.nloc, self.nnei, 4) ener = self._calcualte_ener(table_coef, uu) # here we need to overwrite energy to zero at rcut and beyond. @@ -219,12 +232,12 @@ def _get_pairwise_dist(coords: torch.Tensor) -> torch.Tensor: Parameters ---------- coords : torch.Tensor - The coordinate of the atoms shape of (nframes * nall * 3). + The coordinate of the atoms shape of (nframes, nall, 3). Returns ------- torch.Tensor - The pairwise distance between the atoms (nframes * nall * nall * 3). + The pairwise distance between the atoms (nframes, nall, nall, 3). Examples -------- diff --git a/deepmd/utils/pair_tab.py b/deepmd/utils/pair_tab.py index 56f8e618df..c97aefc108 100644 --- a/deepmd/utils/pair_tab.py +++ b/deepmd/utils/pair_tab.py @@ -44,6 +44,9 @@ def reinit(self, filename: str, rcut: Optional[float] = None) -> None: For example we have two atom types, 0 and 1. The columes from 2nd to 4th are for 0-0, 0-1 and 1-1 correspondingly. """ + if filename is None: + self.tab_info, self.tab_data = None, None + return self.vdata = np.loadtxt(filename) self.rmin = self.vdata[0][0] self.rmax = self.vdata[-1][0] @@ -65,6 +68,36 @@ def reinit(self, filename: str, rcut: Optional[float] = None) -> None: self.tab_info = np.array([self.rmin, self.hh, self.nspline, self.ntypes]) self.tab_data = self._make_data() + def serialize(self) -> dict: + return { + "rmin": self.rmin, + "rmax": self.rmax, + "hh": self.hh, + "ntypes": self.ntypes, + "rcut": self.rcut, + "nspline": self.nspline, + "@variables": { + "vdata": self.vdata, + "tab_info": self.tab_info, + "tab_data": self.tab_data, + }, + } + + @classmethod + def deserialize(cls, data) -> "PairTab": + variables = data.pop("@variables") + tab = PairTab(None, None) + tab.vdata = variables["vdata"] + tab.rmin = data["rmin"] + tab.rmax = data["rmax"] + tab.hh = data["hh"] + tab.ntypes = data["ntypes"] + tab.rcut = data["rcut"] + tab.nspline = data["nspline"] + tab.tab_info = variables["tab_info"] + tab.tab_data = variables["tab_data"] + return tab + def _check_table_upper_boundary(self) -> None: """Update User Provided Table Based on `rcut`. diff --git a/source/tests/common/test_pairtab_preprocess.py b/source/tests/common/test_pairtab_preprocess.py index a866c42236..26f96a3ca4 100644 --- a/source/tests/common/test_pairtab_preprocess.py +++ b/source/tests/common/test_pairtab_preprocess.py @@ -30,6 +30,18 @@ def setUp(self, mock_loadtxt) -> None: self.tab4 = PairTab(filename=file_path, rcut=0.03) self.tab5 = PairTab(filename=file_path, rcut=0.032) + def test_deserialize(self): + deserialized_tab = PairTab.deserialize(self.tab1.serialize()) + np.testing.assert_allclose(self.tab1.vdata, deserialized_tab.vdata) + np.testing.assert_allclose(self.tab1.rmin, deserialized_tab.rmin) + np.testing.assert_allclose(self.tab1.rmax, deserialized_tab.rmax) + np.testing.assert_allclose(self.tab1.hh, deserialized_tab.hh) + np.testing.assert_allclose(self.tab1.ntypes, deserialized_tab.ntypes) + np.testing.assert_allclose(self.tab1.rcut, deserialized_tab.rcut) + np.testing.assert_allclose(self.tab1.nspline, deserialized_tab.nspline) + np.testing.assert_allclose(self.tab1.tab_info, deserialized_tab.tab_info) + np.testing.assert_allclose(self.tab1.tab_data, deserialized_tab.tab_data) + def test_preprocess(self): np.testing.assert_allclose( self.tab1.vdata, diff --git a/source/tests/dpmodel/__init__.py b/source/tests/dpmodel/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/source/tests/dpmodel/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/source/tests/dpmodel/test_pairtab.py b/source/tests/dpmodel/test_pairtab.py new file mode 100644 index 0000000000..3713d33510 --- /dev/null +++ b/source/tests/dpmodel/test_pairtab.py @@ -0,0 +1,203 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest +from unittest.mock import ( + patch, +) + +import numpy as np + +from deepmd.dpmodel.model.pair_tab_model import ( + PairTabModel, +) + + +class TestPairTab(unittest.TestCase): + @patch("numpy.loadtxt") + def setUp(self, mock_loadtxt) -> None: + file_path = "dummy_path" + mock_loadtxt.return_value = np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + ] + ) + + self.model = PairTabModel(tab_file=file_path, rcut=0.02, sel=2) + + self.extended_coord = np.array( + [ + [ + [0.01, 0.01, 0.01], + [0.01, 0.02, 0.01], + [0.01, 0.01, 0.02], + [0.02, 0.01, 0.01], + ], + [ + [0.01, 0.01, 0.01], + [0.01, 0.02, 0.01], + [0.01, 0.01, 0.02], + [0.05, 0.01, 0.01], + ], + ] + ) + + # nframes=2, nall=4 + self.extended_atype = np.array([[0, 1, 0, 1], [0, 0, 1, 1]]) + + # nframes=2, nloc=2, nnei=2 + self.nlist = np.array([[[1, 2], [0, 2]], [[1, 2], [0, 3]]]) + + def test_without_mask(self): + result = self.model.forward_atomic( + self.extended_coord, self.extended_atype, self.nlist + ) + expected_result = np.array([[[1.2000], [1.3614]], [[1.2000], [0.4000]]]) + + np.testing.assert_allclose(result["energy"], expected_result, 0.0001, 0.0001) + + def test_with_mask(self): + self.nlist = np.array([[[1, -1], [0, 2]], [[1, 2], [0, 3]]]) + + result = self.model.forward_atomic( + self.extended_coord, self.extended_atype, self.nlist + ) + expected_result = np.array([[[0.8000], [1.3614]], [[1.2000], [0.4000]]]) + + np.testing.assert_allclose(result["energy"], expected_result, 0.0001, 0.0001) + + def test_deserialize(self): + model1 = PairTabModel.deserialize(self.model.serialize()) + np.testing.assert_allclose(self.model.tab_data, model1.tab_data) + np.testing.assert_allclose(self.model.tab_info, model1.tab_info) + + self.nlist = np.array([[[1, -1], [0, 2]], [[1, 2], [0, 3]]]) + result = model1.forward_atomic( + self.extended_coord, self.extended_atype, self.nlist + ) + expected_result = self.model.forward_atomic( + self.extended_coord, self.extended_atype, self.nlist + ) + + np.testing.assert_allclose( + result["energy"], expected_result["energy"], 0.0001, 0.0001 + ) + + +class TestPairTabTwoAtoms(unittest.TestCase): + @patch("numpy.loadtxt") + def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: + """Scenarios to test. + + rcut < rmax: + rr < rcut: use table values, or interpolate. + rr == rcut: use table values, or interpolate. + rr > rcut: should be 0 + rcut == rmax: + rr < rcut: use table values, or interpolate. + rr == rcut: use table values, or interpolate. + rr > rcut: should be 0 + rcut > rmax: + rr < rmax: use table values, or interpolate. + rr == rmax: use table values, or interpolate. + rmax < rr < rcut: extrapolate + rr >= rcut: should be 0 + + """ + file_path = "dummy_path" + mock_loadtxt.return_value = np.array( + [ + [0.005, 1.0], + [0.01, 0.8], + [0.015, 0.5], + [0.02, 0.25], + ] + ) + + # nframes=1, nall=2 + extended_atype = np.array([[0, 0]]) + + # nframes=1, nloc=2, nnei=1 + nlist = np.array([[[1], [-1]]]) + + results = [] + + for dist, rcut in zip( + [ + 0.01, + 0.015, + 0.020, + 0.015, + 0.02, + 0.021, + 0.015, + 0.02, + 0.021, + 0.025, + 0.026, + 0.025, + 0.025, + 0.0216161, + ], + [ + 0.015, + 0.015, + 0.015, + 0.02, + 0.02, + 0.02, + 0.022, + 0.022, + 0.022, + 0.025, + 0.025, + 0.03, + 0.035, + 0.025, + ], + ): + extended_coord = np.array( + [ + [ + [0.0, 0.0, 0.0], + [0.0, dist, 0.0], + ], + ] + ) + + model = PairTabModel(tab_file=file_path, rcut=rcut, sel=2) + results.append( + model.forward_atomic(extended_coord, extended_atype, nlist)["energy"] + ) + + expected_result = np.stack( + [ + np.array( + [ + [ + [0.4, 0], + [0.0, 0], + [0.0, 0], + [0.25, 0], + [0, 0], + [0, 0], + [0.25, 0], + [0.125, 0], + [0.0922, 0], + [0, 0], + [0, 0], + [0, 0], + [0.0923, 0], + [0.0713, 0], + ] + ] + ) + ] + ).reshape(14, 2) + results = np.stack(results).reshape(14, 2) + + np.testing.assert_allclose(results, expected_result, 0.0001, 0.0001) + + if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_pairtab.py b/source/tests/pt/test_pairtab.py index b4dbda6702..e27e2cf2a1 100644 --- a/source/tests/pt/test_pairtab.py +++ b/source/tests/pt/test_pairtab.py @@ -7,7 +7,8 @@ import numpy as np import torch -from deepmd.pt.model.model.pair_tab import ( +from deepmd.dpmodel.model.pair_tab_model import PairTabModel as DPPairTabModel +from deepmd.pt.model.model.pair_tab_model import ( PairTabModel, ) @@ -54,9 +55,13 @@ def test_without_mask(self): result = self.model.forward_atomic( self.extended_coord, self.extended_atype, self.nlist ) - expected_result = torch.tensor([[1.2000, 1.3614], [1.2000, 0.4000]]) + expected_result = torch.tensor( + [[[1.2000], [1.3614]], [[1.2000], [0.4000]]], dtype=torch.float64 + ) - torch.testing.assert_allclose(result["energy"], expected_result, 0.0001, 0.0001) + torch.testing.assert_close( + result["energy"], expected_result, rtol=0.0001, atol=0.0001 + ) def test_with_mask(self): self.nlist = torch.tensor([[[1, -1], [0, 2]], [[1, 2], [0, 3]]]) @@ -64,13 +69,56 @@ def test_with_mask(self): result = self.model.forward_atomic( self.extended_coord, self.extended_atype, self.nlist ) - expected_result = torch.tensor([[0.8000, 1.3614], [1.2000, 0.4000]]) + expected_result = torch.tensor( + [[[0.8000], [1.3614]], [[1.2000], [0.4000]]], dtype=torch.float64 + ) - torch.testing.assert_allclose(result["energy"], expected_result, 0.0001, 0.0001) + torch.testing.assert_close( + result["energy"], expected_result, rtol=0.0001, atol=0.0001 + ) def test_jit(self): model = torch.jit.script(self.model) + def test_deserialize(self): + model1 = PairTabModel.deserialize(self.model.serialize()) + torch.testing.assert_close(self.model.tab_data, model1.tab_data) + torch.testing.assert_close(self.model.tab_info, model1.tab_info) + + self.nlist = torch.tensor([[[1, -1], [0, 2]], [[1, 2], [0, 3]]]) + result = model1.forward_atomic( + self.extended_coord, self.extended_atype, self.nlist + ) + expected_result = self.model.forward_atomic( + self.extended_coord, self.extended_atype, self.nlist + ) + + torch.testing.assert_close( + result["energy"], expected_result["energy"], rtol=0.0001, atol=0.0001 + ) + + model1 = torch.jit.script(model1) + + def test_cross_deserialize(self): + model_dict = self.model.serialize() # pytorch model to dict + model1 = DPPairTabModel.deserialize(model_dict) # dict to numpy model + np.testing.assert_allclose(self.model.tab_data, model1.tab_data) + np.testing.assert_allclose(self.model.tab_info, model1.tab_info) + + self.nlist = np.array([[[1, -1], [0, 2]], [[1, 2], [0, 3]]]) + result = model1.forward_atomic( + self.extended_coord.numpy(), + self.extended_atype.numpy(), + self.nlist, + ) + expected_result = self.model.forward_atomic( + self.extended_coord, self.extended_atype, torch.from_numpy(self.nlist) + ) + + np.testing.assert_allclose( + result["energy"], expected_result["energy"], 0.0001, 0.0001 + ) + class TestPairTabTwoAtoms(unittest.TestCase): @patch("numpy.loadtxt") @@ -178,13 +226,14 @@ def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: [0.0923, 0], [0.0713, 0], ] - ] + ], + dtype=torch.float64, ) ] ).reshape(14, 2) results = torch.stack(results).reshape(14, 2) - torch.testing.assert_allclose(results, expected_result, 0.0001, 0.0001) + torch.testing.assert_close(results, expected_result, rtol=0.0001, atol=0.0001) if __name__ == "__main__": unittest.main() From cd77429dcbdaf42892ac3319b6fb72b9a2879a65 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Sun, 4 Feb 2024 15:43:31 +0800 Subject: [PATCH 044/686] fix model doc str: add shape hint to doc string. (#3225) add shape hit to doc string. Co-authored-by: Han Wang --- deepmd/dpmodel/model/make_model.py | 8 ++++---- deepmd/pt/model/model/make_model.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py index fec04255fa..f507374ff2 100644 --- a/deepmd/dpmodel/model/make_model.py +++ b/deepmd/dpmodel/model/make_model.py @@ -149,13 +149,13 @@ def call_lower( Parameters ---------- extended_coord - coodinates in extended region + coodinates in extended region. nf x (nall x 3). extended_atype - atomic type in extended region + atomic type in extended region. nf x nall. nlist - neighbor list. nf x nloc x nsel + neighbor list. nf x nloc x nsel. mapping - mapps the extended indices to local indices + mapps the extended indices to local indices. nf x nall. fparam frame parameter. nf x ndf aparam diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index c8c1e9450b..1e76c6a468 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -147,15 +147,15 @@ def forward_common_lower( Parameters ---------- extended_coord - coodinates in extended region + coodinates in extended region. nf x (nall x 3) extended_atype - atomic type in extended region + atomic type in extended region. nf x nall nlist - neighbor list. nf x nloc x nsel + neighbor list. nf x nloc x nsel. mapping - mapps the extended indices to local indices + mapps the extended indices to local indices. nf x nall. do_atomic_virial - whether calculate atomic virial + whether calculate atomic virial. Returns ------- From 17f2c35cafd36e57784f2eaa6fbf574cf7ad5278 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 4 Feb 2024 20:39:24 -0500 Subject: [PATCH 045/686] pt: rename atomic_virial to atom_virial in the model output (#3226) To be consistent with TF, as discussed in https://github.com/deepmodeling/deepmd-kit/pull/3213#discussion_r1477183841. Old PT models are expected to be incompatible. Signed-off-by: Jinzhe Zeng --- deepmd/pt/infer/deep_eval.py | 12 ++++++------ deepmd/pt/model/model/ener.py | 2 +- source/tests/pt/test_model.py | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index b5d596ed0f..4ba9e17b52 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -208,9 +208,9 @@ def _eval_model( batch_output["force"].reshape(nframes, natoms, 3).detach().cpu().numpy() ) virial_out = batch_output["virial"].reshape(nframes, 9).detach().cpu().numpy() - if "atomic_virial" in batch_output: + if "atom_virial" in batch_output: atomic_virial_out = ( - batch_output["atomic_virial"] + batch_output["atom_virial"] .reshape(nframes, natoms, 9) .detach() .cpu() @@ -326,9 +326,9 @@ def eval_model( force_out.append(batch_output["force"].detach().cpu().numpy()) if "virial" in batch_output: virial_out.append(batch_output["virial"].detach().cpu().numpy()) - if "atomic_virial" in batch_output: + if "atom_virial" in batch_output: atomic_virial_out.append( - batch_output["atomic_virial"].detach().cpu().numpy() + batch_output["atom_virial"].detach().cpu().numpy() ) if "updated_coord" in batch_output: updated_coord_out.append( @@ -345,8 +345,8 @@ def eval_model( force_out.append(batch_output["force"]) if "virial" in batch_output: virial_out.append(batch_output["virial"]) - if "atomic_virial" in batch_output: - atomic_virial_out.append(batch_output["atomic_virial"]) + if "atom_virial" in batch_output: + atomic_virial_out.append(batch_output["atom_virial"]) if "updated_coord" in batch_output: updated_coord_out.append(batch_output["updated_coord"]) if "logits" in batch_output: diff --git a/deepmd/pt/model/model/ener.py b/deepmd/pt/model/model/ener.py index a408689d8d..1930936336 100644 --- a/deepmd/pt/model/model/ener.py +++ b/deepmd/pt/model/model/ener.py @@ -45,7 +45,7 @@ def forward( if self.do_grad("energy"): model_predict["force"] = model_ret["energy_derv_r"].squeeze(-2) if do_atomic_virial: - model_predict["atomic_virial"] = model_ret["energy_derv_c"].squeeze( + model_predict["atom_virial"] = model_ret["energy_derv_c"].squeeze( -3 ) model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-3) diff --git a/source/tests/pt/test_model.py b/source/tests/pt/test_model.py index e87a53969c..bb99759d16 100644 --- a/source/tests/pt/test_model.py +++ b/source/tests/pt/test_model.py @@ -146,7 +146,7 @@ def get_intermediate_state(self, num_steps=1): "energy": model_pred["energy"], "force": model_pred["force"], "virial": model_pred["virial"], - "atomic_virial": model_pred["atom_virial"], + "atom_virial": model_pred["atom_virial"], } # Get statistics of each component @@ -359,7 +359,7 @@ def test_consistency(self): model_predict["energy"], model_predict["force"], model_predict["virial"], - model_predict["atomic_virial"], + model_predict["atom_virial"], ) cur_lr = my_lr.value(self.wanted_step) model_pred = { @@ -395,10 +395,10 @@ def test_consistency(self): .detach() .numpy(), ) - self.assertIsNone(model_predict_1.get("atomic_virial", None)) + self.assertIsNone(model_predict_1.get("atom_virial", None)) np.testing.assert_allclose( - head_dict["atomic_virial"], - p_atomic_virial.view(*head_dict["atomic_virial"].shape) + head_dict["atom_virial"], + p_atomic_virial.view(*head_dict["atom_virial"].shape) .cpu() .detach() .numpy(), From 22197f520289ebd5ccfd05059e09986400be85ef Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 4 Feb 2024 20:39:41 -0500 Subject: [PATCH 046/686] pt: apply global logger to pt (#3222) We can change the format of the global logger in the future if the additional information is helpful (e.g., time, path, etc). --------- Signed-off-by: Jinzhe Zeng --- deepmd/env.py | 6 ++++-- deepmd/pt/entrypoints/main.py | 34 +++++++++++++-------------------- deepmd/pt/infer/inference.py | 31 +++++++++++++++--------------- deepmd/pt/model/model/model.py | 10 ++++++---- deepmd/pt/model/task/dipole.py | 4 +++- deepmd/pt/model/task/ener.py | 6 ++++-- deepmd/pt/model/task/fitting.py | 8 +++++--- deepmd/pt/optimizer/LKF.py | 4 +++- deepmd/pt/train/training.py | 20 ++++++++++--------- deepmd/pt/train/wrapper.py | 8 ++++++-- deepmd/pt/utils/dataloader.py | 11 ++++++----- deepmd/pt/utils/dataset.py | 2 +- deepmd/pt/utils/finetune.py | 10 ++++++---- deepmd/pt/utils/preprocess.py | 4 +++- deepmd/pt/utils/stat.py | 4 +++- deepmd/utils/pair_tab.py | 8 +++++--- 16 files changed, 94 insertions(+), 76 deletions(-) diff --git a/deepmd/env.py b/deepmd/env.py index 1a8da63f8e..451b79d94f 100644 --- a/deepmd/env.py +++ b/deepmd/env.py @@ -13,6 +13,8 @@ "global_float_prec", ] +log = logging.getLogger(__name__) + # FLOAT_PREC dp_float_prec = os.environ.get("DP_INTERFACE_PREC", "high").lower() if dp_float_prec in ("high", ""): @@ -47,7 +49,7 @@ def set_env_if_empty(key: str, value: str, verbose: bool = True): if os.environ.get(key) is None: os.environ[key] = value if verbose: - logging.warning( + log.warning( f"Environment variable {key} is empty. Use the default value {value}" ) @@ -72,7 +74,7 @@ def set_default_nthreads(): and "TF_INTER_OP_PARALLELISM_THREADS" not in os.environ ) ): - logging.warning( + log.warning( "To get the best performance, it is recommended to adjust " "the number of threads by setting the environment variables " "OMP_NUM_THREADS, DP_INTRA_OP_PARALLELISM_THREADS, and " diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index ad5e92d495..680d3313a6 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -30,6 +30,9 @@ from deepmd.infer.model_devi import ( make_model_devi, ) +from deepmd.loggers.loggers import ( + set_log_handles, +) from deepmd.main import ( parse_args, ) @@ -42,9 +45,6 @@ from deepmd.pt.train import ( training, ) -from deepmd.pt.utils import ( - env, -) from deepmd.pt.utils.dataloader import ( DpLoaderSet, ) @@ -58,6 +58,8 @@ make_stat_input, ) +log = logging.getLogger(__name__) + def get_trainer( config, @@ -237,7 +239,7 @@ def prepare_trainer_input_single( def train(FLAGS): - logging.info("Configuration path: %s", FLAGS.INPUT) + log.info("Configuration path: %s", FLAGS.INPUT) with open(FLAGS.INPUT) as fin: config = json.load(fin) trainer = get_trainer( @@ -278,28 +280,18 @@ def freeze(FLAGS): ) -# avoid logger conflicts of tf version -def clean_loggers(): - logger = logging.getLogger() - while logger.hasHandlers(): - logger.removeHandler(logger.handlers[0]) - - @record def main(args: Optional[Union[List[str], argparse.Namespace]] = None): - clean_loggers() - if not isinstance(args, argparse.Namespace): FLAGS = parse_args(args=args) else: FLAGS = args dict_args = vars(FLAGS) - logging.basicConfig( - level=logging.WARNING if env.LOCAL_RANK else logging.INFO, - format=f"%(asctime)-15s {os.environ.get('RANK') or ''} [%(filename)s:%(lineno)d] %(levelname)s %(message)s", - ) - logging.info("DeepMD version: %s", __version__) + set_log_handles(FLAGS.log_level, FLAGS.log_path, mpi_log=None) + log.debug("Log handles were successfully set") + + log.info("DeepMD version: %s", __version__) if FLAGS.command == "train": train(FLAGS) @@ -315,9 +307,9 @@ def main(args: Optional[Union[List[str], argparse.Namespace]] = None): FLAGS.model = FLAGS.checkpoint_folder FLAGS.output = str(Path(FLAGS.output).with_suffix(".pth")) freeze(FLAGS) - elif args.command == "doc-train-input": + elif FLAGS.command == "doc-train-input": doc_train_input(**dict_args) - elif args.command == "model-devi": + elif FLAGS.command == "model-devi": dict_args["models"] = [ str(Path(mm).with_suffix(".pt")) if Path(mm).suffix not in (".pb", ".pt") @@ -325,7 +317,7 @@ def main(args: Optional[Union[List[str], argparse.Namespace]] = None): for mm in dict_args["models"] ] make_model_devi(**dict_args) - elif args.command == "gui": + elif FLAGS.command == "gui": start_dpgui(**dict_args) else: raise RuntimeError(f"Invalid command {FLAGS.command}!") diff --git a/deepmd/pt/infer/inference.py b/deepmd/pt/infer/inference.py index 4906bb7a46..6a9f0d99d2 100644 --- a/deepmd/pt/infer/inference.py +++ b/deepmd/pt/infer/inference.py @@ -40,6 +40,7 @@ if torch.__version__.startswith("2"): import torch._dynamo +log = logging.getLogger(__name__) class Tester: @@ -95,9 +96,7 @@ def __init__( ), f"Validation systems not found in {input_script}!" self.systems = training_params["validation_data"]["systems"] self.batchsize = training_params["validation_data"]["batch_size"] - logging.info( - f"Testing validation systems in input script: {input_script}" - ) + log.info(f"Testing validation systems in input script: {input_script}") else: assert ( "data_dict" in training_params @@ -115,18 +114,18 @@ def __init__( self.batchsize = training_params["data_dict"][head]["validation_data"][ "batch_size" ] - logging.info( + log.info( f"Testing validation systems in head {head} of input script: {input_script}" ) elif system is not None: self.systems = expand_sys_str(system) self.batchsize = "auto" - logging.info("Testing systems in path: %s", system) + log.info("Testing systems in path: %s", system) elif datafile is not None: with open(datafile) as fin: self.systems = fin.read().splitlines() self.batchsize = "auto" - logging.info("Testing systems in file: %s", datafile) + log.info("Testing systems in file: %s", datafile) else: self.systems = None self.batchsize = None @@ -210,8 +209,8 @@ def run(self): system_results = {} global_sum_natoms = 0 for cc, system in enumerate(systems): - logging.info("# ---------------output of dp test--------------- ") - logging.info(f"# testing system : {system}") + log.info("# ---------------output of dp test--------------- ") + log.info(f"# testing system : {system}") system_pred = [] system_label = [] dataset = DpLoaderSet( @@ -226,7 +225,7 @@ def run(self): dataset, replacement=True, num_samples=dataset.total_batch ) if sampler is None: - logging.warning( + log.warning( "Sampler not specified!" ) # None sampler will lead to a premature stop iteration. Replacement should be True in attribute of the sampler to produce expected number of items in one iteration. dataloader = DataLoader( @@ -296,8 +295,8 @@ def run(self): for k, v in single_results.items() } for item in sorted(results.keys()): - logging.info(f"{item}: {results[item]:.4f}") - logging.info("# ----------------------------------------------- ") + log.info(f"{item}: {results[item]:.4f}") + log.info("# ----------------------------------------------- ") for k, v in single_results.items(): system_results[k] = system_results.get(k, 0.0) + v global_sum_natoms += sum_natoms @@ -306,14 +305,14 @@ def run(self): k: v / global_sum_natoms if "mae" in k else math.sqrt(v / global_sum_natoms) for k, v in system_results.items() } - logging.info("# ----------weighted average of errors----------- ") + log.info("# ----------weighted average of errors----------- ") if not self.multi_task: - logging.info(f"# number of systems : {len(systems)}") + log.info(f"# number of systems : {len(systems)}") else: - logging.info(f"# number of systems for {self.head}: {len(systems)}") + log.info(f"# number of systems for {self.head}: {len(systems)}") for item in sorted(global_results.keys()): - logging.info(f"{item}: {global_results[item]:.4f}") - logging.info("# ----------------------------------------------- ") + log.info(f"{item}: {global_results[item]:.4f}") + log.info("# ----------------------------------------------- ") return global_results diff --git a/deepmd/pt/model/model/model.py b/deepmd/pt/model/model/model.py index 01c2d7b9d6..000746a213 100644 --- a/deepmd/pt/model/model/model.py +++ b/deepmd/pt/model/model/model.py @@ -12,6 +12,8 @@ compute_output_stats, ) +log = logging.getLogger(__name__) + class BaseModel(torch.nn.Module): def __init__(self): @@ -55,7 +57,7 @@ def compute_or_load_stat( if not os.path.exists(stat_file_dir): os.mkdir(stat_file_dir) if not isinstance(stat_file_path, list): - logging.info(f"Saving stat file to {stat_file_path}") + log.info(f"Saving stat file to {stat_file_path}") np.savez_compressed( stat_file_path, sumr=sumr, @@ -68,7 +70,7 @@ def compute_or_load_stat( ) else: for ii, file_path in enumerate(stat_file_path): - logging.info(f"Saving stat file to {file_path}") + log.info(f"Saving stat file to {file_path}") np.savez_compressed( file_path, sumr=sumr[ii], @@ -82,7 +84,7 @@ def compute_or_load_stat( else: # load stat target_type_map = type_map if not isinstance(stat_file_path, list): - logging.info(f"Loading stat file from {stat_file_path}") + log.info(f"Loading stat file from {stat_file_path}") stats = np.load(stat_file_path) stat_type_map = list(stats["type_map"]) missing_type = [ @@ -105,7 +107,7 @@ def compute_or_load_stat( sumr, suma, sumn, sumr2, suma2 = [], [], [], [], [] id_bias_atom_e = None for ii, file_path in enumerate(stat_file_path): - logging.info(f"Loading stat file from {file_path}") + log.info(f"Loading stat file from {file_path}") stats = np.load(file_path) stat_type_map = list(stats["type_map"]) missing_type = [ diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py index 4906987bf8..d911613a5b 100644 --- a/deepmd/pt/model/task/dipole.py +++ b/deepmd/pt/model/task/dipole.py @@ -10,6 +10,8 @@ Fitting, ) +log = logging.getLogger(__name__) + class DipoleFittingNetType(Fitting): def __init__( @@ -37,7 +39,7 @@ def __init__( self.filter_layers = torch.nn.ModuleList(filter_layers) if "seed" in kwargs: - logging.info("Set seed to %d in fitting net.", kwargs["seed"]) + log.info("Set seed to %d in fitting net.", kwargs["seed"]) torch.manual_seed(kwargs["seed"]) def forward(self, inputs, atype, atype_tebd, rot_mat): diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 484e477b6a..5e3cd87367 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -40,6 +40,8 @@ dtype = env.GLOBAL_PT_FLOAT_PRECISION device = env.DEVICE +log = logging.getLogger(__name__) + @fitting_check_output class InvarFitting(Fitting): @@ -153,7 +155,7 @@ def __init__( # very bad design... if "seed" in kwargs: - logging.info("Set seed to %d in fitting net.", kwargs["seed"]) + log.info("Set seed to %d in fitting net.", kwargs["seed"]) torch.manual_seed(kwargs["seed"]) def output_def(self) -> FittingOutputDef: @@ -451,7 +453,7 @@ def __init__( self.filter_layers = torch.nn.ModuleList(filter_layers) if "seed" in kwargs: - logging.info("Set seed to %d in fitting net.", kwargs["seed"]) + log.info("Set seed to %d in fitting net.", kwargs["seed"]) torch.manual_seed(kwargs["seed"]) def output_def(self): diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 551fb9640b..b03aee7539 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -23,6 +23,8 @@ make_stat_input, ) +log = logging.getLogger(__name__) + class Fitting(torch.nn.Module, BaseFitting): __plugins = Plugin() @@ -115,7 +117,7 @@ def change_energy_bias( ntest : int The number of test samples in a system to change the energy bias. """ - logging.info( + log.info( "Changing energy bias in pretrained model for types {}... " "(this step may take long time)".format(str(new_type_map)) ) @@ -188,7 +190,7 @@ def change_energy_bias( self.bias_atom_e[idx_type_map] += torch.from_numpy( delta_bias.reshape(-1) ).to(DEVICE) - logging.info( + log.info( f"RMSE of atomic energy after linear regression is: {rmse_ae:10.5e} eV/atom." ) elif bias_shift == "statistic": @@ -202,7 +204,7 @@ def change_energy_bias( ) else: raise RuntimeError("Unknown bias_shift mode: " + bias_shift) - logging.info( + log.info( "Change energy bias of {} from {} to {}.".format( str(new_type_map), str(old_bias.detach().cpu().numpy()), diff --git a/deepmd/pt/optimizer/LKF.py b/deepmd/pt/optimizer/LKF.py index 5e18797c7b..ebc9242d49 100644 --- a/deepmd/pt/optimizer/LKF.py +++ b/deepmd/pt/optimizer/LKF.py @@ -7,6 +7,8 @@ Optimizer, ) +log = logging.getLogger(__name__) + class LKFOptimizer(Optimizer): def __init__( @@ -59,7 +61,7 @@ def __init_P(self): P = [] params_packed_index = [] - logging.info("LKF parameter nums: %s" % param_nums) + log.info("LKF parameter nums: %s" % param_nums) for param_num in param_nums: if param_num >= block_size: block_num = math.ceil(param_num / block_size) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index ee0e7a54cc..02367f4aee 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -59,6 +59,8 @@ DataLoader, ) +log = logging.getLogger(__name__) + class Trainer: def __init__( @@ -140,7 +142,7 @@ def get_data_loader(_training_data, _validation_data, _training_params): valid_sampler = get_weighted_sampler(_validation_data, "prob_sys_size") if train_sampler is None or valid_sampler is None: - logging.warning( + log.warning( "Sampler not specified!" ) # None sampler will lead to a premature stop iteration. Replacement should be True in attribute of the sampler to produce expected number of items in one iteration. training_dataloader = DataLoader( @@ -299,7 +301,7 @@ def get_loss(loss_params, start_lr, _ntypes): origin_model = ( finetune_model if finetune_model is not None else resume_model ) - logging.info(f"Resuming from {origin_model}.") + log.info(f"Resuming from {origin_model}.") state_dict = torch.load(origin_model, map_location=DEVICE) if "model" in state_dict: optimizer_state_dict = ( @@ -332,7 +334,7 @@ def get_loss(loss_params, start_lr, _ntypes): tmp_keys = ".".join(item.split(".")[:3]) slim_keys.append(tmp_keys) slim_keys = [i + ".*" for i in slim_keys] - logging.warning( + log.warning( f"Force load mode allowed! These keys are not in ckpt and will re-init: {slim_keys}" ) elif self.finetune_multi_task: @@ -451,9 +453,9 @@ def run(self): if SAMPLER_RECORD: record_file = f"Sample_rank_{self.rank}.txt" fout1 = open(record_file, mode="w", buffering=1) - logging.info("Start to train %d steps.", self.num_steps) + log.info("Start to train %d steps.", self.num_steps) if dist.is_initialized(): - logging.info(f"Rank: {dist.get_rank()}/{dist.get_world_size()}") + log.info(f"Rank: {dist.get_rank()}/{dist.get_world_size()}") if self.enable_tensorboard: from torch.utils.tensorboard import ( SummaryWriter, @@ -655,7 +657,7 @@ def log_loss_valid(_task_key="Default"): train_time = time.time() - self.t0 self.t0 = time.time() msg += f", speed={train_time:.2f} s/{self.disp_freq if _step_id else 1} batches" - logging.info(msg) + log.info(msg) if fout: if self.lcurve_should_print_header: @@ -674,7 +676,7 @@ def log_loss_valid(_task_key="Default"): module = self.wrapper.module if dist.is_initialized() else self.wrapper self.save_model(self.latest_model, lr=cur_lr, step=_step_id) - logging.info(f"Saved model to {self.latest_model}") + log.info(f"Saved model to {self.latest_model}") symlink_prefix_files(self.latest_model.stem, self.save_ckpt) with open("checkpoint", "w") as f: f.write(str(self.latest_model)) @@ -714,10 +716,10 @@ def log_loss_valid(_task_key="Default"): "frozen_model.pth" # We use .pth to denote the frozen model ) self.model.save(pth_model_path) - logging.info( + log.info( f"Frozen model for inferencing has been saved to {pth_model_path}" ) - logging.info(f"Trained model has been saved to: {self.save_ckpt}") + log.info(f"Trained model has been saved to: {self.save_ckpt}") if fout: fout.close() diff --git a/deepmd/pt/train/wrapper.py b/deepmd/pt/train/wrapper.py index fe423e6318..2207f111a0 100644 --- a/deepmd/pt/train/wrapper.py +++ b/deepmd/pt/train/wrapper.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import logging from typing import ( Dict, Optional, @@ -11,6 +12,9 @@ import torch._dynamo +log = logging.getLogger(__name__) + + class ModelWrapper(torch.nn.Module): def __init__( self, @@ -124,7 +128,7 @@ def share_params(self, shared_links, resume=False): link_class.share_params( base_class, shared_level_link, resume=resume ) - print( + log.warning( f"Shared params of {model_key_base}.{class_type_base} and {model_key_link}.{class_type_link}!" ) else: @@ -146,7 +150,7 @@ def share_params(self, shared_links, resume=False): link_class.share_params( base_class, shared_level_link, resume=resume ) - print( + log.warning( f"Shared params of {model_key_base}.{class_type_base} and {model_key_link}.{class_type_link}!" ) diff --git a/deepmd/pt/utils/dataloader.py b/deepmd/pt/utils/dataloader.py index 7a6684e82e..0ec43f5a75 100644 --- a/deepmd/pt/utils/dataloader.py +++ b/deepmd/pt/utils/dataloader.py @@ -40,6 +40,7 @@ process_sys_probs, ) +log = logging.getLogger(__name__) torch.multiprocessing.set_sharing_strategy("file_system") @@ -69,7 +70,7 @@ def __init__( self.systems: List[DeepmdDataSetForLoader] = [] if len(systems) >= 100: - logging.info(f"Constructing DataLoaders from {len(systems)} systems") + log.info(f"Constructing DataLoaders from {len(systems)} systems") def construct_dataset(system): ### this design requires "rcut" and "sel" in the descriptor @@ -119,7 +120,7 @@ def construct_dataset(system): rule = int(batch_size.split(":")[1]) else: rule = None - logging.error("Unsupported batch size type") + log.error("Unsupported batch size type") self.batch_size = rule // system._natoms if self.batch_size * system._natoms < rule: self.batch_size += 1 @@ -155,7 +156,7 @@ def __len__(self): return len(self.dataloaders) def __getitem__(self, idx): - # logging.warning(str(torch.distributed.get_rank())+" idx: "+str(idx)+" index: "+str(self.index[idx])) + # log.warning(str(torch.distributed.get_rank())+" idx: "+str(idx)+" index: "+str(self.index[idx])) try: batch = next(self.iters[idx]) except StopIteration: @@ -216,7 +217,7 @@ def __next__(self): self.warning_time is None or time.time() - self.warning_time > 15 * 60 ): - logging.warning( + log.warning( "Data loading buffer is empty or nearly empty. This may " "indicate a data loading bottleneck, and increasing the " "number of workers (--num-workers) may help." @@ -310,7 +311,7 @@ def get_weighted_sampler(training_data, prob_style, sys_prob=False): probs = prob_sys_size_ext(style, len(training_data), training_data.index) else: probs = process_sys_probs(prob_style, training_data.index) - logging.info("Generated weighted sampler with prob array: " + str(probs)) + log.info("Generated weighted sampler with prob array: " + str(probs)) # training_data.total_batch is the size of one epoch, you can increase it to avoid too many rebuilding of iteraters len_sampler = training_data.total_batch * max(env.NUM_WORKERS, 1) sampler = WeightedRandomSampler(probs, len_sampler, replacement=True) diff --git a/deepmd/pt/utils/dataset.py b/deepmd/pt/utils/dataset.py index 68d4a09ce4..aca4a9ce5b 100644 --- a/deepmd/pt/utils/dataset.py +++ b/deepmd/pt/utils/dataset.py @@ -449,7 +449,7 @@ def _load_data( if atomic: ndof *= self._natoms path = os.path.join(set_name, key + ".npy") - # logging.info('Loading data from: %s', path) + # log.info('Loading data from: %s', path) if os.path.isfile(path): if high_prec: data = np.load(path).astype(env.GLOBAL_ENER_FLOAT_PRECISION) diff --git a/deepmd/pt/utils/finetune.py b/deepmd/pt/utils/finetune.py index 9d82783cc0..13749da151 100644 --- a/deepmd/pt/utils/finetune.py +++ b/deepmd/pt/utils/finetune.py @@ -7,6 +7,8 @@ env, ) +log = logging.getLogger(__name__) + def change_finetune_model_params( ckpt, finetune_model, model_config, multi_task=False, model_branch="" @@ -19,7 +21,7 @@ def change_finetune_model_params( """ if multi_task: # TODO - print("finetune mode need modification for multitask mode!") + log.error("finetune mode need modification for multitask mode!") if finetune_model is not None: state_dict = torch.load(finetune_model, map_location=env.DEVICE) if "model" in state_dict: @@ -45,7 +47,7 @@ def change_finetune_model_params( old_type_map ), "Only support for smaller type map when finetuning or resuming." model_config = last_model_params - logging.info( + log.info( "Change the model configurations according to the pretrained one..." ) model_config["new_type_map"] = new_type_map @@ -57,7 +59,7 @@ def change_finetune_model_params( model_branch_chosen = next(iter(model_dict_params.keys())) new_fitting = True model_config["bias_shift"] = "statistic" # fitting net re-init - print( + log.warning( "The fitting net will be re-init instead of using that in the pretrained model! " "The bias_shift will be statistic!" ) @@ -83,7 +85,7 @@ def change_finetune_model_params( model_config["fitting_net"] = model_dict_params[model_branch_chosen][ "fitting_net" ] - logging.info( + log.info( f"Change the model configurations according to the model branch " f"{model_branch_chosen} in the pretrained one..." ) diff --git a/deepmd/pt/utils/preprocess.py b/deepmd/pt/utils/preprocess.py index 18c798138e..806bacdcd2 100644 --- a/deepmd/pt/utils/preprocess.py +++ b/deepmd/pt/utils/preprocess.py @@ -10,6 +10,8 @@ env, ) +log = logging.getLogger(__name__) + class Region3D: def __init__(self, boxt): @@ -263,7 +265,7 @@ def make_env_mat( ) merged_coord = coord[merged_mapping] - merged_coord_shift if merged_coord.shape[0] <= coord.shape[0]: - logging.warning("No ghost atom is added for system ") + log.warning("No ghost atom is added for system ") else: merged_coord_shift = torch.zeros_like(coord) merged_atype = atype.clone() diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index eec7179bcd..5fde03c74a 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -8,6 +8,8 @@ env, ) +log = logging.getLogger(__name__) + def make_stat_input(datasets, dataloaders, nbatches): """Pack data for statistics. @@ -36,7 +38,7 @@ def make_stat_input(datasets, dataloaders, nbatches): ] if datasets[0].mixed_type: keys.append("real_natoms_vec") - logging.info(f"Packing data for statistics from {len(datasets)} systems") + log.info(f"Packing data for statistics from {len(datasets)} systems") for i in range(len(datasets)): sys_stat = {key: [] for key in keys} iterator = iter(dataloaders[i]) diff --git a/deepmd/utils/pair_tab.py b/deepmd/utils/pair_tab.py index c97aefc108..b807354171 100644 --- a/deepmd/utils/pair_tab.py +++ b/deepmd/utils/pair_tab.py @@ -12,6 +12,8 @@ CubicSpline, ) +log = logging.getLogger(__name__) + class PairTab: """Pairwise tabulated potential. @@ -147,7 +149,7 @@ def _check_table_upper_boundary(self) -> None: if np.all(upper_val == 0): # if table values decay to `0` after rcut if self.rcut < self.rmax and np.any(self.vdata[rcut_idx - 1][1:] != 0): - logging.warning( + log.warning( "The energy provided in the table does not decay to 0 at rcut." ) # if table values decay to `0` at rcut, do nothing @@ -164,12 +166,12 @@ def _check_table_upper_boundary(self) -> None: else: # if table values do not decay to `0` at rcut if self.rcut <= self.rmax: - logging.warning( + log.warning( "The energy provided in the table does not decay to 0 at rcut." ) # if rcut goes beyond table upper bond, need extrapolation, ensure values decay to `0` before rcut. else: - logging.warning( + log.warning( "The rcut goes beyond table upper boundary, performing extrapolation." ) pad_extrapolation = np.zeros((rcut_idx - upper_idx, self.ncol)) From 7f5d67ca9013619c18862a6be67c4b4b01c5d9d9 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 4 Feb 2024 20:40:16 -0500 Subject: [PATCH 047/686] pt: apply global user set precision to pt (#3220) Signed-off-by: Jinzhe Zeng --- deepmd/pt/utils/env.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/deepmd/pt/utils/env.py b/deepmd/pt/utils/env.py index a679ccf1fa..81499b5063 100644 --- a/deepmd/pt/utils/env.py +++ b/deepmd/pt/utils/env.py @@ -5,14 +5,12 @@ import torch from deepmd.env import ( + GLOBAL_ENER_FLOAT_PRECISION, + GLOBAL_NP_FLOAT_PRECISION, get_default_nthreads, set_default_nthreads, ) -PRECISION = os.environ.get("PRECISION", "float64") -GLOBAL_NP_FLOAT_PRECISION = getattr(np, PRECISION) -GLOBAL_PT_FLOAT_PRECISION = getattr(torch, PRECISION) -GLOBAL_ENER_FLOAT_PRECISION = getattr(np, PRECISION) SAMPLER_RECORD = os.environ.get("SAMPLER_RECORD", False) try: # only linux @@ -43,6 +41,7 @@ "int32": torch.int32, "int64": torch.int64, } +GLOBAL_PT_FLOAT_PRECISION = PRECISION_DICT[np.dtype(GLOBAL_NP_FLOAT_PRECISION).name] DEFAULT_PRECISION = "float64" # throw warnings if threads not set @@ -52,3 +51,18 @@ torch.set_num_interop_threads(inter_nthreads) if intra_nthreads > 0: torch.set_num_threads(intra_nthreads) + +__all__ = [ + "GLOBAL_ENER_FLOAT_PRECISION", + "GLOBAL_NP_FLOAT_PRECISION", + "GLOBAL_PT_FLOAT_PRECISION", + "DEFAULT_PRECISION", + "PRECISION_DICT", + "SAMPLER_RECORD", + "NUM_WORKERS", + "DEVICE", + "JIT", + "CACHE_PER_SYS", + "ENERGY_BIAS_TRAINABLE", + "LOCAL_RANK", +] From f5bb131fd964ba62a212d395e91e1e1e994b23cf Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 5 Feb 2024 04:25:19 -0500 Subject: [PATCH 048/686] fix DP_ENABLE_TENSORFLOW support (#3229) Avoid installing tensorflow as build requires when `DP_ENABLE_TENSORFLOW` is `0`. Signed-off-by: Jinzhe Zeng --- backend/find_tensorflow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/find_tensorflow.py b/backend/find_tensorflow.py index b43b32f954..c4d58ea0cd 100644 --- a/backend/find_tensorflow.py +++ b/backend/find_tensorflow.py @@ -47,6 +47,8 @@ def find_tensorflow() -> Tuple[Optional[str], List[str]]: list of str TensorFlow requirement if not found. Empty if found. """ + if os.environ.get("DP_ENABLE_TENSORFLOW", "1") == "0": + return None, [] requires = [] tf_spec = None From 18c43f619b9e1568c3ac6fe0564344786ec88778 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:21:37 +0800 Subject: [PATCH 049/686] test: better structure for the dpmodel uts (#3232) - mv all dpmodel related UTs to a separate directory - split the large test_model_format_utils.py as tests for different modules. --------- Co-authored-by: Han Wang --- source/tests/common/dpmodel/README | 1 + source/tests/{ => common}/dpmodel/__init__.py | 0 .../dpmodel/case_single_frame_with_nlist.py | 32 + .../common/dpmodel/test_descriptor_se_e2_a.py | 35 + .../common/dpmodel/test_dp_atomic_model.py | 47 + source/tests/common/dpmodel/test_dp_model.py | 49 + source/tests/common/dpmodel/test_env_mat.py | 32 + .../dpmodel/test_fitting_invar_fitting.py | 136 +++ source/tests/common/dpmodel/test_network.py | 296 ++++++ source/tests/common/dpmodel/test_nlist.py | 301 ++++++ .../common/{ => dpmodel}/test_output_def.py | 0 .../{ => common}/dpmodel/test_pairtab.py | 0 .../{ => dpmodel}/test_pairtab_preprocess.py | 0 source/tests/common/dpmodel/test_region.py | 49 + .../tests/common/test_model_format_utils.py | 889 ------------------ 15 files changed, 978 insertions(+), 889 deletions(-) create mode 100644 source/tests/common/dpmodel/README rename source/tests/{ => common}/dpmodel/__init__.py (100%) create mode 100644 source/tests/common/dpmodel/case_single_frame_with_nlist.py create mode 100644 source/tests/common/dpmodel/test_descriptor_se_e2_a.py create mode 100644 source/tests/common/dpmodel/test_dp_atomic_model.py create mode 100644 source/tests/common/dpmodel/test_dp_model.py create mode 100644 source/tests/common/dpmodel/test_env_mat.py create mode 100644 source/tests/common/dpmodel/test_fitting_invar_fitting.py create mode 100644 source/tests/common/dpmodel/test_network.py create mode 100644 source/tests/common/dpmodel/test_nlist.py rename source/tests/common/{ => dpmodel}/test_output_def.py (100%) rename source/tests/{ => common}/dpmodel/test_pairtab.py (100%) rename source/tests/common/{ => dpmodel}/test_pairtab_preprocess.py (100%) create mode 100644 source/tests/common/dpmodel/test_region.py delete mode 100644 source/tests/common/test_model_format_utils.py diff --git a/source/tests/common/dpmodel/README b/source/tests/common/dpmodel/README new file mode 100644 index 0000000000..de6d061bdd --- /dev/null +++ b/source/tests/common/dpmodel/README @@ -0,0 +1 @@ +test deepmd-kit/source/deepmd/dpmodel diff --git a/source/tests/dpmodel/__init__.py b/source/tests/common/dpmodel/__init__.py similarity index 100% rename from source/tests/dpmodel/__init__.py rename to source/tests/common/dpmodel/__init__.py diff --git a/source/tests/common/dpmodel/case_single_frame_with_nlist.py b/source/tests/common/dpmodel/case_single_frame_with_nlist.py new file mode 100644 index 0000000000..93a85c2d66 --- /dev/null +++ b/source/tests/common/dpmodel/case_single_frame_with_nlist.py @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import numpy as np + + +class TestCaseSingleFrameWithNlist: + def setUp(self): + # nloc == 3, nall == 4 + self.nloc = 3 + self.nall = 4 + self.nf, self.nt = 1, 2 + self.coord_ext = np.array( + [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 1], + [0, -2, 0], + ], + dtype=np.float64, + ).reshape([1, self.nall * 3]) + self.atype_ext = np.array([0, 0, 1, 0], dtype=int).reshape([1, self.nall]) + # sel = [5, 2] + self.sel = [5, 2] + self.nlist = np.array( + [ + [1, 3, -1, -1, -1, 2, -1], + [0, -1, -1, -1, -1, 2, -1], + [0, 1, -1, -1, -1, -1, -1], + ], + dtype=int, + ).reshape([1, self.nloc, sum(self.sel)]) + self.rcut = 0.4 + self.rcut_smth = 2.2 diff --git a/source/tests/common/dpmodel/test_descriptor_se_e2_a.py b/source/tests/common/dpmodel/test_descriptor_se_e2_a.py new file mode 100644 index 0000000000..17c27cf9f1 --- /dev/null +++ b/source/tests/common/dpmodel/test_descriptor_se_e2_a.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np + +from deepmd.dpmodel.descriptor import ( + DescrptSeA, +) + +from .case_single_frame_with_nlist import ( + TestCaseSingleFrameWithNlist, +) + + +class TestDescrptSeA(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_self_consistency( + self, + ): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + davg = rng.normal(size=(self.nt, nnei, 4)) + dstd = rng.normal(size=(self.nt, nnei, 4)) + dstd = 0.1 + np.abs(dstd) + + em0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel) + em0.davg = davg + em0.dstd = dstd + em1 = DescrptSeA.deserialize(em0.serialize()) + mm0 = em0.call(self.coord_ext, self.atype_ext, self.nlist) + mm1 = em1.call(self.coord_ext, self.atype_ext, self.nlist) + for ii in [0, 1, 4]: + np.testing.assert_allclose(mm0[ii], mm1[ii]) diff --git a/source/tests/common/dpmodel/test_dp_atomic_model.py b/source/tests/common/dpmodel/test_dp_atomic_model.py new file mode 100644 index 0000000000..684bea45c6 --- /dev/null +++ b/source/tests/common/dpmodel/test_dp_atomic_model.py @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np + +from deepmd.dpmodel.descriptor import ( + DescrptSeA, +) +from deepmd.dpmodel.fitting import ( + InvarFitting, +) +from deepmd.dpmodel.model import ( + DPAtomicModel, +) + +from .case_single_frame_with_nlist import ( + TestCaseSingleFrameWithNlist, +) + + +class TestDPAtomicModel(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_self_consistency( + self, + ): + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ) + type_map = ["foo", "bar"] + md0 = DPAtomicModel(ds, ft, type_map=type_map) + md1 = DPAtomicModel.deserialize(md0.serialize()) + + ret0 = md0.forward_atomic(self.coord_ext, self.atype_ext, self.nlist) + ret1 = md1.forward_atomic(self.coord_ext, self.atype_ext, self.nlist) + + np.testing.assert_allclose(ret0["energy"], ret1["energy"]) diff --git a/source/tests/common/dpmodel/test_dp_model.py b/source/tests/common/dpmodel/test_dp_model.py new file mode 100644 index 0000000000..d22cd274c7 --- /dev/null +++ b/source/tests/common/dpmodel/test_dp_model.py @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np + +from deepmd.dpmodel.descriptor import ( + DescrptSeA, +) +from deepmd.dpmodel.fitting import ( + InvarFitting, +) +from deepmd.dpmodel.model import ( + DPModel, +) + +from .case_single_frame_with_nlist import ( + TestCaseSingleFrameWithNlist, +) + + +class TestDPModel(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_self_consistency( + self, + ): + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ) + type_map = ["foo", "bar"] + md0 = DPModel(ds, ft, type_map=type_map) + md1 = DPModel.deserialize(md0.serialize()) + + ret0 = md0.call_lower(self.coord_ext, self.atype_ext, self.nlist) + ret1 = md1.call_lower(self.coord_ext, self.atype_ext, self.nlist) + + np.testing.assert_allclose(ret0["energy"], ret1["energy"]) + np.testing.assert_allclose(ret0["energy_redu"], ret1["energy_redu"]) diff --git a/source/tests/common/dpmodel/test_env_mat.py b/source/tests/common/dpmodel/test_env_mat.py new file mode 100644 index 0000000000..7e1ce7cddd --- /dev/null +++ b/source/tests/common/dpmodel/test_env_mat.py @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np + +from deepmd.dpmodel.utils import ( + EnvMat, +) + +from .case_single_frame_with_nlist import ( + TestCaseSingleFrameWithNlist, +) + + +class TestEnvMat(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_self_consistency( + self, + ): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + davg = rng.normal(size=(self.nt, nnei, 4)) + dstd = rng.normal(size=(self.nt, nnei, 4)) + dstd = 0.1 + np.abs(dstd) + em0 = EnvMat(self.rcut, self.rcut_smth) + em1 = EnvMat.deserialize(em0.serialize()) + mm0, ww0 = em0.call(self.coord_ext, self.atype_ext, self.nlist, davg, dstd) + mm1, ww1 = em1.call(self.coord_ext, self.atype_ext, self.nlist, davg, dstd) + np.testing.assert_allclose(mm0, mm1) + np.testing.assert_allclose(ww0, ww1) diff --git a/source/tests/common/dpmodel/test_fitting_invar_fitting.py b/source/tests/common/dpmodel/test_fitting_invar_fitting.py new file mode 100644 index 0000000000..ea70e98f7c --- /dev/null +++ b/source/tests/common/dpmodel/test_fitting_invar_fitting.py @@ -0,0 +1,136 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import itertools +import unittest + +import numpy as np + +from deepmd.dpmodel.descriptor import ( + DescrptSeA, +) +from deepmd.dpmodel.fitting import ( + InvarFitting, +) + +from .case_single_frame_with_nlist import ( + TestCaseSingleFrameWithNlist, +) + + +class TestInvarFitting(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_self_consistency( + self, + ): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA(self.rcut, self.rcut_smth, self.sel) + dd = ds.call(self.coord_ext, self.atype_ext, self.nlist) + atype = self.atype_ext[:, :nloc] + + for ( + distinguish_types, + od, + nfp, + nap, + ) in itertools.product( + [True, False], + [1, 2], + [0, 3], + [0, 4], + ): + ifn0 = InvarFitting( + "energy", + self.nt, + ds.dim_out, + od, + numb_fparam=nfp, + numb_aparam=nap, + distinguish_types=distinguish_types, + ) + ifn1 = InvarFitting.deserialize(ifn0.serialize()) + if nfp > 0: + ifp = rng.normal(size=(self.nf, nfp)) + else: + ifp = None + if nap > 0: + iap = rng.normal(size=(self.nf, self.nloc, nap)) + else: + iap = None + ret0 = ifn0(dd[0], atype, fparam=ifp, aparam=iap) + ret1 = ifn1(dd[0], atype, fparam=ifp, aparam=iap) + np.testing.assert_allclose(ret0["energy"], ret1["energy"]) + + def test_self_exception( + self, + ): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA(self.rcut, self.rcut_smth, self.sel) + dd = ds.call(self.coord_ext, self.atype_ext, self.nlist) + atype = self.atype_ext[:, :nloc] + + for ( + distinguish_types, + od, + nfp, + nap, + ) in itertools.product( + [True, False], + [1, 2], + [0, 3], + [0, 4], + ): + ifn0 = InvarFitting( + "energy", + self.nt, + ds.dim_out, + od, + numb_fparam=nfp, + numb_aparam=nap, + distinguish_types=distinguish_types, + ) + + if nfp > 0: + ifp = rng.normal(size=(self.nf, nfp)) + else: + ifp = None + if nap > 0: + iap = rng.normal(size=(self.nf, self.nloc, nap)) + else: + iap = None + with self.assertRaises(ValueError) as context: + ret0 = ifn0(dd[0][:, :, :-2], atype, fparam=ifp, aparam=iap) + self.assertIn("input descriptor", context.exception) + + if nfp > 0: + ifp = rng.normal(size=(self.nf, nfp - 1)) + with self.assertRaises(ValueError) as context: + ret0 = ifn0(dd[0], atype, fparam=ifp, aparam=iap) + self.assertIn("input fparam", context.exception) + + if nap > 0: + iap = rng.normal(size=(self.nf, self.nloc, nap - 1)) + with self.assertRaises(ValueError) as context: + ifn0(dd[0], atype, fparam=ifp, aparam=iap) + self.assertIn("input aparam", context.exception) + + def test_get_set(self): + ifn0 = InvarFitting( + "energy", + self.nt, + 3, + 1, + ) + rng = np.random.default_rng() + foo = rng.normal([3, 4]) + for ii in [ + "bias_atom_e", + "fparam_avg", + "fparam_inv_std", + "aparam_avg", + "aparam_inv_std", + ]: + ifn0[ii] = foo + np.testing.assert_allclose(foo, ifn0[ii]) diff --git a/source/tests/common/dpmodel/test_network.py b/source/tests/common/dpmodel/test_network.py new file mode 100644 index 0000000000..bfed8da45a --- /dev/null +++ b/source/tests/common/dpmodel/test_network.py @@ -0,0 +1,296 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import itertools +import os +import unittest +from copy import ( + deepcopy, +) + +import numpy as np + +from deepmd.dpmodel.utils import ( + EmbeddingNet, + FittingNet, + NativeLayer, + NativeNet, + NetworkCollection, + load_dp_model, + save_dp_model, +) + + +class TestNativeLayer(unittest.TestCase): + def test_serialize_deserize(self): + for ( + ni, + no, + ), bias, ut, activation_function, resnet, ashp, prec in itertools.product( + [(5, 5), (5, 10), (5, 9), (9, 5)], + [True, False], + [True, False], + ["tanh", "none"], + [True, False], + [None, [4], [3, 2]], + ["float32", "float64", "single", "double"], + ): + nl0 = NativeLayer( + ni, + no, + bias=bias, + use_timestep=ut, + activation_function=activation_function, + resnet=resnet, + precision=prec, + ) + nl1 = NativeLayer.deserialize(nl0.serialize()) + inp_shap = [ni] + if ashp is not None: + inp_shap = ashp + inp_shap + inp = np.arange(np.prod(inp_shap)).reshape(inp_shap) + np.testing.assert_allclose(nl0.call(inp), nl1.call(inp)) + + def test_shape_error(self): + self.w0 = np.full((2, 3), 3.0) + self.b0 = np.full((2,), 4.0) + self.b1 = np.full((3,), 4.0) + self.idt0 = np.full((2,), 4.0) + with self.assertRaises(ValueError) as context: + NativeLayer.deserialize( + { + "activation_function": "tanh", + "resnet": True, + "@variables": {"w": self.w0, "b": self.b0}, + } + ) + assert "not equalt to shape of b" in context.exception + with self.assertRaises(ValueError) as context: + NativeLayer.deserialize( + { + "activation_function": "tanh", + "resnet": True, + "@variables": {"w": self.w0, "b": self.b1, "idt": self.idt0}, + } + ) + assert "not equalt to shape of idt" in context.exception + + +class TestNativeNet(unittest.TestCase): + def setUp(self) -> None: + self.w0 = np.full((2, 3), 3.0) + self.b0 = np.full((3,), 4.0) + self.w1 = np.full((3, 4), 3.0) + self.b1 = np.full((4,), 4.0) + + def test_serialize(self): + network = NativeNet( + [ + NativeLayer(2, 3).serialize(), + NativeLayer(3, 4).serialize(), + ] + ) + network[1]["w"] = self.w1 + network[1]["b"] = self.b1 + network[0]["w"] = self.w0 + network[0]["b"] = self.b0 + network[1]["activation_function"] = "tanh" + network[0]["activation_function"] = "tanh" + network[1]["resnet"] = True + network[0]["resnet"] = True + jdata = network.serialize() + np.testing.assert_array_equal(jdata["layers"][0]["@variables"]["w"], self.w0) + np.testing.assert_array_equal(jdata["layers"][0]["@variables"]["b"], self.b0) + np.testing.assert_array_equal(jdata["layers"][1]["@variables"]["w"], self.w1) + np.testing.assert_array_equal(jdata["layers"][1]["@variables"]["b"], self.b1) + np.testing.assert_array_equal(jdata["layers"][0]["activation_function"], "tanh") + np.testing.assert_array_equal(jdata["layers"][1]["activation_function"], "tanh") + np.testing.assert_array_equal(jdata["layers"][0]["resnet"], True) + np.testing.assert_array_equal(jdata["layers"][1]["resnet"], True) + + def test_deserialize(self): + network = NativeNet.deserialize( + { + "layers": [ + { + "activation_function": "tanh", + "resnet": True, + "@variables": {"w": self.w0, "b": self.b0}, + }, + { + "activation_function": "tanh", + "resnet": True, + "@variables": {"w": self.w1, "b": self.b1}, + }, + ], + } + ) + np.testing.assert_array_equal(network[0]["w"], self.w0) + np.testing.assert_array_equal(network[0]["b"], self.b0) + np.testing.assert_array_equal(network[1]["w"], self.w1) + np.testing.assert_array_equal(network[1]["b"], self.b1) + np.testing.assert_array_equal(network[0]["activation_function"], "tanh") + np.testing.assert_array_equal(network[1]["activation_function"], "tanh") + np.testing.assert_array_equal(network[0]["resnet"], True) + np.testing.assert_array_equal(network[1]["resnet"], True) + + def test_shape_error(self): + with self.assertRaises(ValueError) as context: + NativeNet.deserialize( + { + "layers": [ + { + "activation_function": "tanh", + "resnet": True, + "@variables": {"w": self.w0, "b": self.b0}, + }, + { + "activation_function": "tanh", + "resnet": True, + "@variables": {"w": self.w0, "b": self.b0}, + }, + ], + } + ) + assert "does not match the dim of layer" in context.exception + + +class TestEmbeddingNet(unittest.TestCase): + def test_embedding_net(self): + for ni, act, idt, prec in itertools.product( + [1, 10], + ["tanh", "none"], + [True, False], + ["double", "single"], + ): + en0 = EmbeddingNet( + ni, + activation_function=act, + precision=prec, + resnet_dt=idt, + ) + en1 = EmbeddingNet.deserialize(en0.serialize()) + inp = np.ones([ni]) + np.testing.assert_allclose(en0.call(inp), en1.call(inp)) + + +class TestFittingNet(unittest.TestCase): + def test_fitting_net(self): + for ni, no, act, idt, prec, bo in itertools.product( + [1, 10], + [1, 7], + ["tanh", "none"], + [True, False], + ["double", "single"], + [True, False], + ): + en0 = FittingNet( + ni, + no, + activation_function=act, + precision=prec, + resnet_dt=idt, + bias_out=bo, + ) + en1 = FittingNet.deserialize(en0.serialize()) + inp = np.ones([ni]) + en0.call(inp) + en1.call(inp) + np.testing.assert_allclose(en0.call(inp), en1.call(inp)) + + +class TestNetworkCollection(unittest.TestCase): + def setUp(self) -> None: + w0 = np.full((2, 3), 3.0) + b0 = np.full((3,), 4.0) + w1 = np.full((3, 4), 3.0) + b1 = np.full((4,), 4.0) + self.network = { + "layers": [ + { + "activation_function": "tanh", + "resnet": True, + "@variables": {"w": w0, "b": b0}, + }, + { + "activation_function": "tanh", + "resnet": True, + "@variables": {"w": w1, "b": b1}, + }, + ], + } + + def test_two_dim(self): + networks = NetworkCollection(ndim=2, ntypes=2) + networks[(0, 0)] = self.network + networks[(1, 1)] = self.network + networks[(0, 1)] = self.network + with self.assertRaises(RuntimeError): + networks.check_completeness() + networks[(1, 0)] = self.network + networks.check_completeness() + np.testing.assert_equal( + networks.serialize(), + NetworkCollection.deserialize(networks.serialize()).serialize(), + ) + np.testing.assert_equal( + networks[(0, 0)].serialize(), networks.serialize()["networks"][0] + ) + + def test_one_dim(self): + networks = NetworkCollection(ndim=1, ntypes=2) + networks[(0,)] = self.network + with self.assertRaises(RuntimeError): + networks.check_completeness() + networks[(1,)] = self.network + networks.check_completeness() + np.testing.assert_equal( + networks.serialize(), + NetworkCollection.deserialize(networks.serialize()).serialize(), + ) + np.testing.assert_equal( + networks[(0,)].serialize(), networks.serialize()["networks"][0] + ) + + def test_zero_dim(self): + networks = NetworkCollection(ndim=0, ntypes=2) + networks[()] = self.network + networks.check_completeness() + np.testing.assert_equal( + networks.serialize(), + NetworkCollection.deserialize(networks.serialize()).serialize(), + ) + np.testing.assert_equal( + networks[()].serialize(), networks.serialize()["networks"][0] + ) + + +class TestSaveLoadDPModel(unittest.TestCase): + def setUp(self) -> None: + self.w = np.full((3, 2), 3.0) + self.b = np.full((3,), 4.0) + self.model_dict = { + "type": "some_type", + "layers": [ + { + "activation_function": "tanh", + "resnet": True, + "@variables": {"w": self.w, "b": self.b}, + }, + { + "activation_function": "tanh", + "resnet": True, + "@variables": {"w": self.w, "b": self.b}, + }, + ], + } + self.filename = "test_dp_dpmodel.dp" + + def test_save_load_model(self): + save_dp_model(self.filename, deepcopy(self.model_dict)) + model = load_dp_model(self.filename) + np.testing.assert_equal(model["model"], self.model_dict) + assert "software" in model + assert "version" in model + + def tearDown(self) -> None: + if os.path.exists(self.filename): + os.remove(self.filename) diff --git a/source/tests/common/dpmodel/test_nlist.py b/source/tests/common/dpmodel/test_nlist.py new file mode 100644 index 0000000000..a36fa66d40 --- /dev/null +++ b/source/tests/common/dpmodel/test_nlist.py @@ -0,0 +1,301 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np + +from deepmd.dpmodel.descriptor import ( + DescrptSeA, +) +from deepmd.dpmodel.fitting import ( + InvarFitting, +) +from deepmd.dpmodel.model import ( + DPModel, +) +from deepmd.dpmodel.utils import ( + build_multiple_neighbor_list, + build_neighbor_list, + extend_coord_with_ghosts, + get_multiple_nlist_key, + inter2phys, +) + + +class TestDPModelFormatNlist(unittest.TestCase): + def setUp(self): + # nloc == 3, nall == 4 + self.nloc = 3 + self.nall = 5 + self.nf, self.nt = 1, 2 + self.coord_ext = np.array( + [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 1], + [0, -2, 0], + [2.3, 0, 0], + ], + dtype=np.float64, + ).reshape([1, self.nall * 3]) + # sel = [5, 2] + self.sel = [5, 2] + self.expected_nlist = np.array( + [ + [1, 3, -1, -1, -1, 2, -1], + [0, -1, -1, -1, -1, 2, -1], + [0, 1, -1, -1, -1, -1, -1], + ], + dtype=int, + ).reshape([1, self.nloc, sum(self.sel)]) + self.atype_ext = np.array([0, 0, 1, 0, 1], dtype=int).reshape([1, self.nall]) + self.rcut_smth = 0.4 + self.rcut = 2.1 + + nf, nloc, nnei = self.expected_nlist.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ) + type_map = ["foo", "bar"] + self.md = DPModel(ds, ft, type_map=type_map) + + def test_nlist_eq(self): + # n_nnei == nnei + nlist = np.array( + [ + [1, 3, -1, -1, -1, 2, -1], + [0, -1, -1, -1, -1, 2, -1], + [0, 1, -1, -1, -1, -1, -1], + ], + dtype=np.int64, + ).reshape([1, self.nloc, -1]) + nlist1 = self.md.format_nlist( + self.coord_ext, + self.atype_ext, + nlist, + ) + np.testing.assert_allclose(self.expected_nlist, nlist1) + + def test_nlist_st(self): + # n_nnei < nnei + nlist = np.array( + [ + [1, 3, -1, 2], + [0, -1, -1, 2], + [0, 1, -1, -1], + ], + dtype=np.int64, + ).reshape([1, self.nloc, -1]) + nlist1 = self.md.format_nlist( + self.coord_ext, + self.atype_ext, + nlist, + ) + np.testing.assert_allclose(self.expected_nlist, nlist1) + + def test_nlist_lt(self): + # n_nnei > nnei + nlist = np.array( + [ + [1, 3, -1, -1, -1, 2, -1, -1, 4], + [0, -1, 4, -1, -1, 2, -1, 3, -1], + [0, 1, -1, -1, -1, 4, -1, -1, 3], + ], + dtype=np.int64, + ).reshape([1, self.nloc, -1]) + nlist1 = self.md.format_nlist( + self.coord_ext, + self.atype_ext, + nlist, + ) + np.testing.assert_allclose(self.expected_nlist, nlist1) + + +dtype = np.float64 + + +class TestNeighList(unittest.TestCase): + def setUp(self): + self.nf = 3 + self.nloc = 2 + self.ns = 5 * 5 * 3 + self.nall = self.ns * self.nloc + self.cell = np.array([[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype) + self.icoord = np.array([[0, 0, 0], [0.5, 0.5, 0.1]], dtype=dtype) + self.atype = np.array([0, 1], dtype=np.int32) + [self.cell, self.icoord, self.atype] = [ + np.expand_dims(ii, 0) for ii in [self.cell, self.icoord, self.atype] + ] + self.coord = inter2phys(self.icoord, self.cell).reshape([-1, self.nloc * 3]) + self.cell = self.cell.reshape([-1, 9]) + [self.cell, self.coord, self.atype] = [ + np.tile(ii, [self.nf, 1]) for ii in [self.cell, self.coord, self.atype] + ] + self.rcut = 1.01 + self.prec = 1e-10 + self.nsel = [10, 10] + self.ref_nlist = np.array( + [ + [0, 0, 0, 0, 0, 0, -1, -1, -1, -1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1], + [0, 0, 0, 0, -1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1], + ] + ) + + def test_build_notype(self): + ecoord, eatype, mapping = extend_coord_with_ghosts( + self.coord, self.atype, self.cell, self.rcut + ) + nlist = build_neighbor_list( + ecoord, + eatype, + self.nloc, + self.rcut, + sum(self.nsel), + distinguish_types=False, + ) + np.testing.assert_allclose(nlist[0], nlist[1]) + nlist_mask = nlist[0] == -1 + nlist_loc = mapping[0][nlist[0]] + nlist_loc[nlist_mask] = -1 + np.testing.assert_allclose( + np.sort(nlist_loc, axis=-1), + np.sort(self.ref_nlist, axis=-1), + ) + + def test_build_type(self): + ecoord, eatype, mapping = extend_coord_with_ghosts( + self.coord, self.atype, self.cell, self.rcut + ) + nlist = build_neighbor_list( + ecoord, + eatype, + self.nloc, + self.rcut, + self.nsel, + distinguish_types=True, + ) + np.testing.assert_allclose(nlist[0], nlist[1]) + nlist_mask = nlist[0] == -1 + nlist_loc = mapping[0][nlist[0]] + nlist_loc[nlist_mask] = -1 + for ii in range(2): + np.testing.assert_allclose( + np.sort(np.split(nlist_loc, self.nsel, axis=-1)[ii], axis=-1), + np.sort(np.split(self.ref_nlist, self.nsel, axis=-1)[ii], axis=-1), + ) + + def test_build_multiple_nlist(self): + rcuts = [1.01, 2.01] + nsels = [20, 80] + ecoord, eatype, mapping = extend_coord_with_ghosts( + self.coord, self.atype, self.cell, max(rcuts) + ) + nlist1 = build_neighbor_list( + ecoord, + eatype, + self.nloc, + rcuts[1], + nsels[1] - 1, + distinguish_types=False, + ) + pad = -1 * np.ones([self.nf, self.nloc, 1], dtype=nlist1.dtype) + nlist2 = np.concatenate([nlist1, pad], axis=-1) + nlist0 = build_neighbor_list( + ecoord, + eatype, + self.nloc, + rcuts[0], + nsels[0], + distinguish_types=False, + ) + nlists = build_multiple_neighbor_list(ecoord, nlist1, rcuts, nsels) + for dd in range(2): + self.assertEqual( + nlists[get_multiple_nlist_key(rcuts[dd], nsels[dd])].shape[-1], + nsels[dd], + ) + np.testing.assert_allclose( + nlists[get_multiple_nlist_key(rcuts[0], nsels[0])], + nlist0, + ) + np.testing.assert_allclose( + nlists[get_multiple_nlist_key(rcuts[1], nsels[1])], + nlist2, + ) + + def test_extend_coord(self): + ecoord, eatype, mapping = extend_coord_with_ghosts( + self.coord, self.atype, self.cell, self.rcut + ) + # expected ncopy x nloc + self.assertEqual(list(ecoord.shape), [self.nf, self.nall * 3]) + self.assertEqual(list(eatype.shape), [self.nf, self.nall]) + self.assertEqual(list(mapping.shape), [self.nf, self.nall]) + # check the nloc part is identical with original coord + np.testing.assert_allclose( + ecoord[:, : self.nloc * 3], self.coord, rtol=self.prec, atol=self.prec + ) + # check the shift vectors are aligned with grid + shift_vec = ( + ecoord.reshape([-1, self.ns, self.nloc, 3]) + - self.coord.reshape([-1, self.nloc, 3])[:, None, :, :] + ) + shift_vec = shift_vec.reshape([-1, self.nall, 3]) + # hack!!! assumes identical cell across frames + shift_vec = np.matmul( + shift_vec, np.linalg.inv(self.cell.reshape([self.nf, 3, 3])[0]) + ) + # nf x nall x 3 + shift_vec = np.round(shift_vec) + # check: identical shift vecs + np.testing.assert_allclose( + shift_vec[0], shift_vec[1], rtol=self.prec, atol=self.prec + ) + # check: shift idx aligned with grid + mm, cc = np.unique(shift_vec[0][:, 0], return_counts=True) + np.testing.assert_allclose( + mm, + np.array([-2, -1, 0, 1, 2], dtype=dtype), + rtol=self.prec, + atol=self.prec, + ) + np.testing.assert_allclose( + cc, + np.array([30, 30, 30, 30, 30], dtype=np.int32), + rtol=self.prec, + atol=self.prec, + ) + mm, cc = np.unique(shift_vec[1][:, 1], return_counts=True) + np.testing.assert_allclose( + mm, + np.array([-2, -1, 0, 1, 2], dtype=dtype), + rtol=self.prec, + atol=self.prec, + ) + np.testing.assert_allclose( + cc, + np.array([30, 30, 30, 30, 30], dtype=np.int32), + rtol=self.prec, + atol=self.prec, + ) + mm, cc = np.unique(shift_vec[1][:, 2], return_counts=True) + np.testing.assert_allclose( + mm, + np.array([-1, 0, 1], dtype=dtype), + rtol=self.prec, + atol=self.prec, + ) + np.testing.assert_allclose( + cc, + np.array([50, 50, 50], dtype=np.int32), + rtol=self.prec, + atol=self.prec, + ) diff --git a/source/tests/common/test_output_def.py b/source/tests/common/dpmodel/test_output_def.py similarity index 100% rename from source/tests/common/test_output_def.py rename to source/tests/common/dpmodel/test_output_def.py diff --git a/source/tests/dpmodel/test_pairtab.py b/source/tests/common/dpmodel/test_pairtab.py similarity index 100% rename from source/tests/dpmodel/test_pairtab.py rename to source/tests/common/dpmodel/test_pairtab.py diff --git a/source/tests/common/test_pairtab_preprocess.py b/source/tests/common/dpmodel/test_pairtab_preprocess.py similarity index 100% rename from source/tests/common/test_pairtab_preprocess.py rename to source/tests/common/dpmodel/test_pairtab_preprocess.py diff --git a/source/tests/common/dpmodel/test_region.py b/source/tests/common/dpmodel/test_region.py new file mode 100644 index 0000000000..8043c8c985 --- /dev/null +++ b/source/tests/common/dpmodel/test_region.py @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np + +from deepmd.dpmodel.utils import ( + inter2phys, + to_face_distance, +) + + +class TestRegion(unittest.TestCase): + def setUp(self): + self.cell = np.array( + [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], + ) + self.cell = np.reshape(self.cell, [1, 1, -1, 3]) + self.cell = np.tile(self.cell, [4, 5, 1, 1]) + self.prec = 1e-8 + + def test_inter_to_phys(self): + rng = np.random.default_rng() + inter = rng.normal(size=[4, 5, 3, 3]) + phys = inter2phys(inter, self.cell) + for ii in range(4): + for jj in range(5): + expected_phys = np.matmul(inter[ii, jj], self.cell[ii, jj]) + np.testing.assert_allclose( + phys[ii, jj], expected_phys, rtol=self.prec, atol=self.prec + ) + + def test_to_face_dist(self): + cell0 = self.cell[0][0] + vol = np.linalg.det(cell0) + # area of surfaces xy, xz, yz + sxy = np.linalg.norm(np.cross(cell0[0], cell0[1])) + sxz = np.linalg.norm(np.cross(cell0[0], cell0[2])) + syz = np.linalg.norm(np.cross(cell0[1], cell0[2])) + # vol / area gives distance + dz = vol / sxy + dy = vol / sxz + dx = vol / syz + expected = np.array([dx, dy, dz]) + dists = to_face_distance(self.cell) + for ii in range(4): + for jj in range(5): + np.testing.assert_allclose( + dists[ii][jj], expected, rtol=self.prec, atol=self.prec + ) diff --git a/source/tests/common/test_model_format_utils.py b/source/tests/common/test_model_format_utils.py deleted file mode 100644 index 18a40ffdd9..0000000000 --- a/source/tests/common/test_model_format_utils.py +++ /dev/null @@ -1,889 +0,0 @@ -# SPDX-License-Identifier: LGPL-3.0-or-later -import itertools -import os -import unittest -from copy import ( - deepcopy, -) - -import numpy as np - -from deepmd.dpmodel.descriptor import ( - DescrptSeA, -) -from deepmd.dpmodel.fitting import ( - InvarFitting, -) -from deepmd.dpmodel.model import ( - DPAtomicModel, - DPModel, -) -from deepmd.dpmodel.utils import ( - EmbeddingNet, - EnvMat, - FittingNet, - NativeLayer, - NativeNet, - NetworkCollection, - build_multiple_neighbor_list, - build_neighbor_list, - extend_coord_with_ghosts, - get_multiple_nlist_key, - inter2phys, - load_dp_model, - save_dp_model, - to_face_distance, -) - - -class TestNativeLayer(unittest.TestCase): - def test_serialize_deserize(self): - for ( - ni, - no, - ), bias, ut, activation_function, resnet, ashp, prec in itertools.product( - [(5, 5), (5, 10), (5, 9), (9, 5)], - [True, False], - [True, False], - ["tanh", "none"], - [True, False], - [None, [4], [3, 2]], - ["float32", "float64", "single", "double"], - ): - nl0 = NativeLayer( - ni, - no, - bias=bias, - use_timestep=ut, - activation_function=activation_function, - resnet=resnet, - precision=prec, - ) - nl1 = NativeLayer.deserialize(nl0.serialize()) - inp_shap = [ni] - if ashp is not None: - inp_shap = ashp + inp_shap - inp = np.arange(np.prod(inp_shap)).reshape(inp_shap) - np.testing.assert_allclose(nl0.call(inp), nl1.call(inp)) - - def test_shape_error(self): - self.w0 = np.full((2, 3), 3.0) - self.b0 = np.full((2,), 4.0) - self.b1 = np.full((3,), 4.0) - self.idt0 = np.full((2,), 4.0) - with self.assertRaises(ValueError) as context: - network = NativeLayer.deserialize( - { - "activation_function": "tanh", - "resnet": True, - "@variables": {"w": self.w0, "b": self.b0}, - } - ) - assert "not equalt to shape of b" in context.exception - with self.assertRaises(ValueError) as context: - network = NativeLayer.deserialize( - { - "activation_function": "tanh", - "resnet": True, - "@variables": {"w": self.w0, "b": self.b1, "idt": self.idt0}, - } - ) - assert "not equalt to shape of idt" in context.exception - - -class TestNativeNet(unittest.TestCase): - def setUp(self) -> None: - self.w0 = np.full((2, 3), 3.0) - self.b0 = np.full((3,), 4.0) - self.w1 = np.full((3, 4), 3.0) - self.b1 = np.full((4,), 4.0) - - def test_serialize(self): - network = NativeNet( - [ - NativeLayer(2, 3).serialize(), - NativeLayer(3, 4).serialize(), - ] - ) - network[1]["w"] = self.w1 - network[1]["b"] = self.b1 - network[0]["w"] = self.w0 - network[0]["b"] = self.b0 - network[1]["activation_function"] = "tanh" - network[0]["activation_function"] = "tanh" - network[1]["resnet"] = True - network[0]["resnet"] = True - jdata = network.serialize() - np.testing.assert_array_equal(jdata["layers"][0]["@variables"]["w"], self.w0) - np.testing.assert_array_equal(jdata["layers"][0]["@variables"]["b"], self.b0) - np.testing.assert_array_equal(jdata["layers"][1]["@variables"]["w"], self.w1) - np.testing.assert_array_equal(jdata["layers"][1]["@variables"]["b"], self.b1) - np.testing.assert_array_equal(jdata["layers"][0]["activation_function"], "tanh") - np.testing.assert_array_equal(jdata["layers"][1]["activation_function"], "tanh") - np.testing.assert_array_equal(jdata["layers"][0]["resnet"], True) - np.testing.assert_array_equal(jdata["layers"][1]["resnet"], True) - - def test_deserialize(self): - network = NativeNet.deserialize( - { - "layers": [ - { - "activation_function": "tanh", - "resnet": True, - "@variables": {"w": self.w0, "b": self.b0}, - }, - { - "activation_function": "tanh", - "resnet": True, - "@variables": {"w": self.w1, "b": self.b1}, - }, - ], - } - ) - np.testing.assert_array_equal(network[0]["w"], self.w0) - np.testing.assert_array_equal(network[0]["b"], self.b0) - np.testing.assert_array_equal(network[1]["w"], self.w1) - np.testing.assert_array_equal(network[1]["b"], self.b1) - np.testing.assert_array_equal(network[0]["activation_function"], "tanh") - np.testing.assert_array_equal(network[1]["activation_function"], "tanh") - np.testing.assert_array_equal(network[0]["resnet"], True) - np.testing.assert_array_equal(network[1]["resnet"], True) - - def test_shape_error(self): - with self.assertRaises(ValueError) as context: - network = NativeNet.deserialize( - { - "layers": [ - { - "activation_function": "tanh", - "resnet": True, - "@variables": {"w": self.w0, "b": self.b0}, - }, - { - "activation_function": "tanh", - "resnet": True, - "@variables": {"w": self.w0, "b": self.b0}, - }, - ], - } - ) - assert "does not match the dim of layer" in context.exception - - -class TestEmbeddingNet(unittest.TestCase): - def test_embedding_net(self): - for ni, act, idt, prec in itertools.product( - [1, 10], - ["tanh", "none"], - [True, False], - ["double", "single"], - ): - en0 = EmbeddingNet( - ni, - activation_function=act, - precision=prec, - resnet_dt=idt, - ) - en1 = EmbeddingNet.deserialize(en0.serialize()) - inp = np.ones([ni]) - np.testing.assert_allclose(en0.call(inp), en1.call(inp)) - - -class TestFittingNet(unittest.TestCase): - def test_fitting_net(self): - for ni, no, act, idt, prec, bo in itertools.product( - [1, 10], - [1, 7], - ["tanh", "none"], - [True, False], - ["double", "single"], - [True, False], - ): - en0 = FittingNet( - ni, - no, - activation_function=act, - precision=prec, - resnet_dt=idt, - bias_out=bo, - ) - en1 = FittingNet.deserialize(en0.serialize()) - inp = np.ones([ni]) - en0.call(inp) - en1.call(inp) - np.testing.assert_allclose(en0.call(inp), en1.call(inp)) - - -class TestNetworkCollection(unittest.TestCase): - def setUp(self) -> None: - w0 = np.full((2, 3), 3.0) - b0 = np.full((3,), 4.0) - w1 = np.full((3, 4), 3.0) - b1 = np.full((4,), 4.0) - self.network = { - "layers": [ - { - "activation_function": "tanh", - "resnet": True, - "@variables": {"w": w0, "b": b0}, - }, - { - "activation_function": "tanh", - "resnet": True, - "@variables": {"w": w1, "b": b1}, - }, - ], - } - - def test_two_dim(self): - networks = NetworkCollection(ndim=2, ntypes=2) - networks[(0, 0)] = self.network - networks[(1, 1)] = self.network - networks[(0, 1)] = self.network - with self.assertRaises(RuntimeError): - networks.check_completeness() - networks[(1, 0)] = self.network - networks.check_completeness() - np.testing.assert_equal( - networks.serialize(), - NetworkCollection.deserialize(networks.serialize()).serialize(), - ) - np.testing.assert_equal( - networks[(0, 0)].serialize(), networks.serialize()["networks"][0] - ) - - def test_one_dim(self): - networks = NetworkCollection(ndim=1, ntypes=2) - networks[(0,)] = self.network - with self.assertRaises(RuntimeError): - networks.check_completeness() - networks[(1,)] = self.network - networks.check_completeness() - np.testing.assert_equal( - networks.serialize(), - NetworkCollection.deserialize(networks.serialize()).serialize(), - ) - np.testing.assert_equal( - networks[(0,)].serialize(), networks.serialize()["networks"][0] - ) - - def test_zero_dim(self): - networks = NetworkCollection(ndim=0, ntypes=2) - networks[()] = self.network - networks.check_completeness() - np.testing.assert_equal( - networks.serialize(), - NetworkCollection.deserialize(networks.serialize()).serialize(), - ) - np.testing.assert_equal( - networks[()].serialize(), networks.serialize()["networks"][0] - ) - - -class TestSaveLoadDPModel(unittest.TestCase): - def setUp(self) -> None: - self.w = np.full((3, 2), 3.0) - self.b = np.full((3,), 4.0) - self.model_dict = { - "type": "some_type", - "layers": [ - { - "activation_function": "tanh", - "resnet": True, - "@variables": {"w": self.w, "b": self.b}, - }, - { - "activation_function": "tanh", - "resnet": True, - "@variables": {"w": self.w, "b": self.b}, - }, - ], - } - self.filename = "test_dp_dpmodel.dp" - - def test_save_load_model(self): - save_dp_model(self.filename, deepcopy(self.model_dict)) - model = load_dp_model(self.filename) - np.testing.assert_equal(model["model"], self.model_dict) - assert "software" in model - assert "version" in model - - def tearDown(self) -> None: - if os.path.exists(self.filename): - os.remove(self.filename) - - -class TestCaseSingleFrameWithNlist: - def setUp(self): - # nloc == 3, nall == 4 - self.nloc = 3 - self.nall = 4 - self.nf, self.nt = 1, 2 - self.coord_ext = np.array( - [ - [0, 0, 0], - [0, 1, 0], - [0, 0, 1], - [0, -2, 0], - ], - dtype=np.float64, - ).reshape([1, self.nall * 3]) - self.atype_ext = np.array([0, 0, 1, 0], dtype=int).reshape([1, self.nall]) - # sel = [5, 2] - self.sel = [5, 2] - self.nlist = np.array( - [ - [1, 3, -1, -1, -1, 2, -1], - [0, -1, -1, -1, -1, 2, -1], - [0, 1, -1, -1, -1, -1, -1], - ], - dtype=int, - ).reshape([1, self.nloc, sum(self.sel)]) - self.rcut = 0.4 - self.rcut_smth = 2.2 - - -class TestEnvMat(unittest.TestCase, TestCaseSingleFrameWithNlist): - def setUp(self): - TestCaseSingleFrameWithNlist.setUp(self) - - def test_self_consistency( - self, - ): - rng = np.random.default_rng() - nf, nloc, nnei = self.nlist.shape - davg = rng.normal(size=(self.nt, nnei, 4)) - dstd = rng.normal(size=(self.nt, nnei, 4)) - dstd = 0.1 + np.abs(dstd) - em0 = EnvMat(self.rcut, self.rcut_smth) - em1 = EnvMat.deserialize(em0.serialize()) - mm0, ww0 = em0.call(self.coord_ext, self.atype_ext, self.nlist, davg, dstd) - mm1, ww1 = em1.call(self.coord_ext, self.atype_ext, self.nlist, davg, dstd) - np.testing.assert_allclose(mm0, mm1) - np.testing.assert_allclose(ww0, ww1) - - -class TestDescrptSeA(unittest.TestCase, TestCaseSingleFrameWithNlist): - def setUp(self): - TestCaseSingleFrameWithNlist.setUp(self) - - def test_self_consistency( - self, - ): - rng = np.random.default_rng() - nf, nloc, nnei = self.nlist.shape - davg = rng.normal(size=(self.nt, nnei, 4)) - dstd = rng.normal(size=(self.nt, nnei, 4)) - dstd = 0.1 + np.abs(dstd) - - em0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel) - em0.davg = davg - em0.dstd = dstd - em1 = DescrptSeA.deserialize(em0.serialize()) - mm0 = em0.call(self.coord_ext, self.atype_ext, self.nlist) - mm1 = em1.call(self.coord_ext, self.atype_ext, self.nlist) - for ii in [0, 1, 4]: - np.testing.assert_allclose(mm0[ii], mm1[ii]) - - -class TestInvarFitting(unittest.TestCase, TestCaseSingleFrameWithNlist): - def setUp(self): - TestCaseSingleFrameWithNlist.setUp(self) - - def test_self_consistency( - self, - ): - rng = np.random.default_rng() - nf, nloc, nnei = self.nlist.shape - ds = DescrptSeA(self.rcut, self.rcut_smth, self.sel) - dd = ds.call(self.coord_ext, self.atype_ext, self.nlist) - atype = self.atype_ext[:, :nloc] - - for ( - distinguish_types, - od, - nfp, - nap, - ) in itertools.product( - [True, False], - [1, 2], - [0, 3], - [0, 4], - ): - ifn0 = InvarFitting( - "energy", - self.nt, - ds.dim_out, - od, - numb_fparam=nfp, - numb_aparam=nap, - distinguish_types=distinguish_types, - ) - ifn1 = InvarFitting.deserialize(ifn0.serialize()) - if nfp > 0: - ifp = rng.normal(size=(self.nf, nfp)) - else: - ifp = None - if nap > 0: - iap = rng.normal(size=(self.nf, self.nloc, nap)) - else: - iap = None - ret0 = ifn0(dd[0], atype, fparam=ifp, aparam=iap) - ret1 = ifn1(dd[0], atype, fparam=ifp, aparam=iap) - np.testing.assert_allclose(ret0["energy"], ret1["energy"]) - - def test_self_exception( - self, - ): - rng = np.random.default_rng() - nf, nloc, nnei = self.nlist.shape - ds = DescrptSeA(self.rcut, self.rcut_smth, self.sel) - dd = ds.call(self.coord_ext, self.atype_ext, self.nlist) - atype = self.atype_ext[:, :nloc] - - for ( - distinguish_types, - od, - nfp, - nap, - ) in itertools.product( - [True, False], - [1, 2], - [0, 3], - [0, 4], - ): - ifn0 = InvarFitting( - "energy", - self.nt, - ds.dim_out, - od, - numb_fparam=nfp, - numb_aparam=nap, - distinguish_types=distinguish_types, - ) - - if nfp > 0: - ifp = rng.normal(size=(self.nf, nfp)) - else: - ifp = None - if nap > 0: - iap = rng.normal(size=(self.nf, self.nloc, nap)) - else: - iap = None - with self.assertRaises(ValueError) as context: - ret0 = ifn0(dd[0][:, :, :-2], atype, fparam=ifp, aparam=iap) - self.assertIn("input descriptor", context.exception) - - if nfp > 0: - ifp = rng.normal(size=(self.nf, nfp - 1)) - with self.assertRaises(ValueError) as context: - ret0 = ifn0(dd[0], atype, fparam=ifp, aparam=iap) - self.assertIn("input fparam", context.exception) - - if nap > 0: - iap = rng.normal(size=(self.nf, self.nloc, nap - 1)) - with self.assertRaises(ValueError) as context: - ret0 = ifn0(dd[0], atype, fparam=ifp, aparam=iap) - self.assertIn("input aparam", context.exception) - - def test_get_set(self): - ifn0 = InvarFitting( - "energy", - self.nt, - 3, - 1, - ) - rng = np.random.default_rng() - foo = rng.normal([3, 4]) - for ii in [ - "bias_atom_e", - "fparam_avg", - "fparam_inv_std", - "aparam_avg", - "aparam_inv_std", - ]: - ifn0[ii] = foo - np.testing.assert_allclose(foo, ifn0[ii]) - - -class TestDPAtomicModel(unittest.TestCase, TestCaseSingleFrameWithNlist): - def setUp(self): - TestCaseSingleFrameWithNlist.setUp(self) - - def test_self_consistency( - self, - ): - rng = np.random.default_rng() - nf, nloc, nnei = self.nlist.shape - ds = DescrptSeA( - self.rcut, - self.rcut_smth, - self.sel, - ) - ft = InvarFitting( - "energy", - self.nt, - ds.get_dim_out(), - 1, - distinguish_types=ds.distinguish_types(), - ) - type_map = ["foo", "bar"] - md0 = DPAtomicModel(ds, ft, type_map=type_map) - md1 = DPAtomicModel.deserialize(md0.serialize()) - - ret0 = md0.forward_atomic(self.coord_ext, self.atype_ext, self.nlist) - ret1 = md1.forward_atomic(self.coord_ext, self.atype_ext, self.nlist) - - np.testing.assert_allclose(ret0["energy"], ret1["energy"]) - - -class TestDPModel(unittest.TestCase, TestCaseSingleFrameWithNlist): - def setUp(self): - TestCaseSingleFrameWithNlist.setUp(self) - - def test_self_consistency( - self, - ): - rng = np.random.default_rng() - nf, nloc, nnei = self.nlist.shape - ds = DescrptSeA( - self.rcut, - self.rcut_smth, - self.sel, - ) - ft = InvarFitting( - "energy", - self.nt, - ds.get_dim_out(), - 1, - distinguish_types=ds.distinguish_types(), - ) - type_map = ["foo", "bar"] - md0 = DPModel(ds, ft, type_map=type_map) - md1 = DPModel.deserialize(md0.serialize()) - - ret0 = md0.call_lower(self.coord_ext, self.atype_ext, self.nlist) - ret1 = md1.call_lower(self.coord_ext, self.atype_ext, self.nlist) - - np.testing.assert_allclose(ret0["energy"], ret1["energy"]) - np.testing.assert_allclose(ret0["energy_redu"], ret1["energy_redu"]) - - -class TestDPModelFormatNlist(unittest.TestCase): - def setUp(self): - # nloc == 3, nall == 4 - self.nloc = 3 - self.nall = 5 - self.nf, self.nt = 1, 2 - self.coord_ext = np.array( - [ - [0, 0, 0], - [0, 1, 0], - [0, 0, 1], - [0, -2, 0], - [2.3, 0, 0], - ], - dtype=np.float64, - ).reshape([1, self.nall * 3]) - # sel = [5, 2] - self.sel = [5, 2] - self.expected_nlist = np.array( - [ - [1, 3, -1, -1, -1, 2, -1], - [0, -1, -1, -1, -1, 2, -1], - [0, 1, -1, -1, -1, -1, -1], - ], - dtype=int, - ).reshape([1, self.nloc, sum(self.sel)]) - self.atype_ext = np.array([0, 0, 1, 0, 1], dtype=int).reshape([1, self.nall]) - self.rcut_smth = 0.4 - self.rcut = 2.1 - - nf, nloc, nnei = self.expected_nlist.shape - ds = DescrptSeA( - self.rcut, - self.rcut_smth, - self.sel, - ) - ft = InvarFitting( - "energy", - self.nt, - ds.get_dim_out(), - 1, - distinguish_types=ds.distinguish_types(), - ) - type_map = ["foo", "bar"] - self.md = DPModel(ds, ft, type_map=type_map) - - def test_nlist_eq(self): - # n_nnei == nnei - nlist = np.array( - [ - [1, 3, -1, -1, -1, 2, -1], - [0, -1, -1, -1, -1, 2, -1], - [0, 1, -1, -1, -1, -1, -1], - ], - dtype=np.int64, - ).reshape([1, self.nloc, -1]) - nlist1 = self.md.format_nlist( - self.coord_ext, - self.atype_ext, - nlist, - ) - np.testing.assert_allclose(self.expected_nlist, nlist1) - - def test_nlist_st(self): - # n_nnei < nnei - nlist = np.array( - [ - [1, 3, -1, 2], - [0, -1, -1, 2], - [0, 1, -1, -1], - ], - dtype=np.int64, - ).reshape([1, self.nloc, -1]) - nlist1 = self.md.format_nlist( - self.coord_ext, - self.atype_ext, - nlist, - ) - np.testing.assert_allclose(self.expected_nlist, nlist1) - - def test_nlist_lt(self): - # n_nnei > nnei - nlist = np.array( - [ - [1, 3, -1, -1, -1, 2, -1, -1, 4], - [0, -1, 4, -1, -1, 2, -1, 3, -1], - [0, 1, -1, -1, -1, 4, -1, -1, 3], - ], - dtype=np.int64, - ).reshape([1, self.nloc, -1]) - nlist1 = self.md.format_nlist( - self.coord_ext, - self.atype_ext, - nlist, - ) - np.testing.assert_allclose(self.expected_nlist, nlist1) - - -class TestRegion(unittest.TestCase): - def setUp(self): - self.cell = np.array( - [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], - ) - self.cell = np.reshape(self.cell, [1, 1, -1, 3]) - self.cell = np.tile(self.cell, [4, 5, 1, 1]) - self.prec = 1e-8 - - def test_inter_to_phys(self): - rng = np.random.default_rng() - inter = rng.normal(size=[4, 5, 3, 3]) - phys = inter2phys(inter, self.cell) - for ii in range(4): - for jj in range(5): - expected_phys = np.matmul(inter[ii, jj], self.cell[ii, jj]) - np.testing.assert_allclose( - phys[ii, jj], expected_phys, rtol=self.prec, atol=self.prec - ) - - def test_to_face_dist(self): - cell0 = self.cell[0][0] - vol = np.linalg.det(cell0) - # area of surfaces xy, xz, yz - sxy = np.linalg.norm(np.cross(cell0[0], cell0[1])) - sxz = np.linalg.norm(np.cross(cell0[0], cell0[2])) - syz = np.linalg.norm(np.cross(cell0[1], cell0[2])) - # vol / area gives distance - dz = vol / sxy - dy = vol / sxz - dx = vol / syz - expected = np.array([dx, dy, dz]) - dists = to_face_distance(self.cell) - for ii in range(4): - for jj in range(5): - np.testing.assert_allclose( - dists[ii][jj], expected, rtol=self.prec, atol=self.prec - ) - - -dtype = np.float64 - - -class TestNeighList(unittest.TestCase): - def setUp(self): - self.nf = 3 - self.nloc = 2 - self.ns = 5 * 5 * 3 - self.nall = self.ns * self.nloc - self.cell = np.array([[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype) - self.icoord = np.array([[0, 0, 0], [0.5, 0.5, 0.1]], dtype=dtype) - self.atype = np.array([0, 1], dtype=np.int32) - [self.cell, self.icoord, self.atype] = [ - np.expand_dims(ii, 0) for ii in [self.cell, self.icoord, self.atype] - ] - self.coord = inter2phys(self.icoord, self.cell).reshape([-1, self.nloc * 3]) - self.cell = self.cell.reshape([-1, 9]) - [self.cell, self.coord, self.atype] = [ - np.tile(ii, [self.nf, 1]) for ii in [self.cell, self.coord, self.atype] - ] - self.rcut = 1.01 - self.prec = 1e-10 - self.nsel = [10, 10] - self.ref_nlist = np.array( - [ - [0, 0, 0, 0, 0, 0, -1, -1, -1, -1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1], - [0, 0, 0, 0, -1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1], - ] - ) - - def test_build_notype(self): - ecoord, eatype, mapping = extend_coord_with_ghosts( - self.coord, self.atype, self.cell, self.rcut - ) - nlist = build_neighbor_list( - ecoord, - eatype, - self.nloc, - self.rcut, - sum(self.nsel), - distinguish_types=False, - ) - np.testing.assert_allclose(nlist[0], nlist[1]) - nlist_mask = nlist[0] == -1 - nlist_loc = mapping[0][nlist[0]] - nlist_loc[nlist_mask] = -1 - np.testing.assert_allclose( - np.sort(nlist_loc, axis=-1), - np.sort(self.ref_nlist, axis=-1), - ) - - def test_build_type(self): - ecoord, eatype, mapping = extend_coord_with_ghosts( - self.coord, self.atype, self.cell, self.rcut - ) - nlist = build_neighbor_list( - ecoord, - eatype, - self.nloc, - self.rcut, - self.nsel, - distinguish_types=True, - ) - np.testing.assert_allclose(nlist[0], nlist[1]) - nlist_mask = nlist[0] == -1 - nlist_loc = mapping[0][nlist[0]] - nlist_loc[nlist_mask] = -1 - for ii in range(2): - np.testing.assert_allclose( - np.sort(np.split(nlist_loc, self.nsel, axis=-1)[ii], axis=-1), - np.sort(np.split(self.ref_nlist, self.nsel, axis=-1)[ii], axis=-1), - ) - - def test_build_multiple_nlist(self): - rcuts = [1.01, 2.01] - nsels = [20, 80] - ecoord, eatype, mapping = extend_coord_with_ghosts( - self.coord, self.atype, self.cell, max(rcuts) - ) - nlist1 = build_neighbor_list( - ecoord, - eatype, - self.nloc, - rcuts[1], - nsels[1] - 1, - distinguish_types=False, - ) - pad = -1 * np.ones([self.nf, self.nloc, 1], dtype=nlist1.dtype) - nlist2 = np.concatenate([nlist1, pad], axis=-1) - nlist0 = build_neighbor_list( - ecoord, - eatype, - self.nloc, - rcuts[0], - nsels[0], - distinguish_types=False, - ) - nlists = build_multiple_neighbor_list(ecoord, nlist1, rcuts, nsels) - for dd in range(2): - self.assertEqual( - nlists[get_multiple_nlist_key(rcuts[dd], nsels[dd])].shape[-1], - nsels[dd], - ) - np.testing.assert_allclose( - nlists[get_multiple_nlist_key(rcuts[0], nsels[0])], - nlist0, - ) - np.testing.assert_allclose( - nlists[get_multiple_nlist_key(rcuts[1], nsels[1])], - nlist2, - ) - - def test_extend_coord(self): - ecoord, eatype, mapping = extend_coord_with_ghosts( - self.coord, self.atype, self.cell, self.rcut - ) - # expected ncopy x nloc - self.assertEqual(list(ecoord.shape), [self.nf, self.nall * 3]) - self.assertEqual(list(eatype.shape), [self.nf, self.nall]) - self.assertEqual(list(mapping.shape), [self.nf, self.nall]) - # check the nloc part is identical with original coord - np.testing.assert_allclose( - ecoord[:, : self.nloc * 3], self.coord, rtol=self.prec, atol=self.prec - ) - # check the shift vectors are aligned with grid - shift_vec = ( - ecoord.reshape([-1, self.ns, self.nloc, 3]) - - self.coord.reshape([-1, self.nloc, 3])[:, None, :, :] - ) - shift_vec = shift_vec.reshape([-1, self.nall, 3]) - # hack!!! assumes identical cell across frames - shift_vec = np.matmul( - shift_vec, np.linalg.inv(self.cell.reshape([self.nf, 3, 3])[0]) - ) - # nf x nall x 3 - shift_vec = np.round(shift_vec) - # check: identical shift vecs - np.testing.assert_allclose( - shift_vec[0], shift_vec[1], rtol=self.prec, atol=self.prec - ) - # check: shift idx aligned with grid - mm, cc = np.unique(shift_vec[0][:, 0], return_counts=True) - np.testing.assert_allclose( - mm, - np.array([-2, -1, 0, 1, 2], dtype=dtype), - rtol=self.prec, - atol=self.prec, - ) - np.testing.assert_allclose( - cc, - np.array([30, 30, 30, 30, 30], dtype=np.int32), - rtol=self.prec, - atol=self.prec, - ) - mm, cc = np.unique(shift_vec[1][:, 1], return_counts=True) - np.testing.assert_allclose( - mm, - np.array([-2, -1, 0, 1, 2], dtype=dtype), - rtol=self.prec, - atol=self.prec, - ) - np.testing.assert_allclose( - cc, - np.array([30, 30, 30, 30, 30], dtype=np.int32), - rtol=self.prec, - atol=self.prec, - ) - mm, cc = np.unique(shift_vec[1][:, 2], return_counts=True) - np.testing.assert_allclose( - mm, - np.array([-1, 0, 1], dtype=dtype), - rtol=self.prec, - atol=self.prec, - ) - np.testing.assert_allclose( - cc, - np.array([50, 50, 50], dtype=np.int32), - rtol=self.prec, - atol=self.prec, - ) From 13a781f44392c2e8f6826a4906be7847be856810 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:25:04 +0800 Subject: [PATCH 050/686] fix: pt: energy model forward lower is not tested and has bugs. (#3235) Co-authored-by: Han Wang --- deepmd/pt/model/model/ener.py | 7 +- source/tests/pt/test_dp_model.py | 143 +++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 3 deletions(-) diff --git a/deepmd/pt/model/model/ener.py b/deepmd/pt/model/model/ener.py index 1930936336..9a6e60d963 100644 --- a/deepmd/pt/model/model/ener.py +++ b/deepmd/pt/model/model/ener.py @@ -48,7 +48,7 @@ def forward( model_predict["atom_virial"] = model_ret["energy_derv_c"].squeeze( -3 ) - model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-3) + model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) else: model_predict["force"] = model_ret["dforce"] else: @@ -64,7 +64,7 @@ def forward_lower( mapping: Optional[torch.Tensor] = None, do_atomic_virial: bool = False, ): - model_ret = self.common_forward_lower( + model_ret = self.forward_common_lower( extended_coord, extended_atype, nlist, @@ -77,10 +77,11 @@ def forward_lower( model_predict["energy"] = model_ret["energy_redu"] if self.do_grad("energy"): model_predict["extended_force"] = model_ret["energy_derv_r"].squeeze(-2) + model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) if do_atomic_virial: model_predict["extended_virial"] = model_ret[ "energy_derv_c" - ].squeeze(-3) + ].squeeze(-2) else: assert model_ret["dforce"] is not None model_predict["dforce"] = model_ret["dforce"] diff --git a/source/tests/pt/test_dp_model.py b/source/tests/pt/test_dp_model.py index 79f65d26d6..51aa5d92f6 100644 --- a/source/tests/pt/test_dp_model.py +++ b/source/tests/pt/test_dp_model.py @@ -12,6 +12,7 @@ ) from deepmd.pt.model.model.ener import ( DPModel, + EnergyModel, ) from deepmd.pt.model.task.ener import ( InvarFitting, @@ -386,3 +387,145 @@ def test_nlist_lt(self): to_torch_tensor(nlist), ) np.testing.assert_allclose(self.expected_nlist, to_numpy_array(nlist1)) + + +class TestEnergyModel(unittest.TestCase, TestCaseSingleFrameWithoutNlist): + def setUp(self): + TestCaseSingleFrameWithoutNlist.setUp(self) + + def test_self_consistency(self): + nf, nloc = self.atype.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ).to(env.DEVICE) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ).to(env.DEVICE) + type_map = ["foo", "bar"] + # TODO: dirty hack to avoid data stat!!! + md0 = EnergyModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + md1 = EnergyModel.deserialize(md0.serialize()).to(env.DEVICE) + args = [to_torch_tensor(ii) for ii in [self.coord, self.atype, self.cell]] + ret0 = md0.forward(*args) + ret1 = md1.forward(*args) + np.testing.assert_allclose( + to_numpy_array(ret0["atom_energy"]), + to_numpy_array(ret1["atom_energy"]), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["energy"]), + to_numpy_array(ret1["energy"]), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["force"]), + to_numpy_array(ret1["force"]), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["virial"]), + to_numpy_array(ret1["virial"]), + ) + ret0 = md0.forward(*args, do_atomic_virial=True) + ret1 = md1.forward(*args, do_atomic_virial=True) + np.testing.assert_allclose( + to_numpy_array(ret0["atom_virial"]), + to_numpy_array(ret1["atom_virial"]), + ) + + coord_ext, atype_ext, mapping = extend_coord_with_ghosts( + to_torch_tensor(self.coord), + to_torch_tensor(self.atype), + to_torch_tensor(self.cell), + self.rcut, + ) + nlist = build_neighbor_list( + coord_ext, + atype_ext, + self.nloc, + self.rcut, + self.sel, + distinguish_types=md0.distinguish_types(), + ) + args = [coord_ext, atype_ext, nlist] + ret2 = md0.forward_lower(*args, do_atomic_virial=True) + # check the consistency between the reduced virial from + # forward and forward_lower + np.testing.assert_allclose( + to_numpy_array(ret0["virial"]), + to_numpy_array(ret2["virial"]), + ) + + +class TestEnergyModelLower(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_self_consistency(self): + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ).to(env.DEVICE) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ).to(env.DEVICE) + type_map = ["foo", "bar"] + # TODO: dirty hack to avoid data stat!!! + md0 = EnergyModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + md1 = EnergyModel.deserialize(md0.serialize()).to(env.DEVICE) + args = [ + to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] + ] + ret0 = md0.forward_lower(*args) + ret1 = md1.forward_lower(*args) + np.testing.assert_allclose( + to_numpy_array(ret0["atom_energy"]), + to_numpy_array(ret1["atom_energy"]), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["energy"]), + to_numpy_array(ret1["energy"]), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["extended_force"]), + to_numpy_array(ret1["extended_force"]), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["virial"]), + to_numpy_array(ret1["virial"]), + ) + ret0 = md0.forward_lower(*args, do_atomic_virial=True) + ret1 = md1.forward_lower(*args, do_atomic_virial=True) + np.testing.assert_allclose( + to_numpy_array(ret0["extended_virial"]), + to_numpy_array(ret1["extended_virial"]), + ) + + def test_jit(self): + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ).to(env.DEVICE) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ).to(env.DEVICE) + type_map = ["foo", "bar"] + # TODO: dirty hack to avoid data stat!!! + md0 = EnergyModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + torch.jit.script(md0) From f96e61409ea27a6b2c18980c65dbe5d14c567490 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:29:39 +0800 Subject: [PATCH 051/686] build(deps): bump codecov/codecov-action from 3 to 4 (#3231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.

Release notes

Sourced from codecov/codecov-action's releases.

v4.0.0

v4 of the Codecov Action uses the CLI as the underlying upload. The CLI has helped to power new features including local upload, the global upload token, and new upcoming features.

Breaking Changes

  • The Codecov Action runs as a node20 action due to node16 deprecation. See this post from GitHub on how to migrate.
  • Tokenless uploading is unsupported. However, PRs made from forks to the upstream public repos will support tokenless (e.g. contributors to OS projects do not need the upstream repo's Codecov token). This doc shows instructions on how to add the Codecov token.
  • OS platforms have been added, though some may not be automatically detected. To see a list of platforms, see our CLI download page
  • Various arguments to the Action have been changed. Please be aware that the arguments match with the CLI's needs

v3 versions and below will not have access to CLI features (e.g. global upload token, ATS).

What's Changed

... (truncated)

Changelog

Sourced from codecov/codecov-action's changelog.

4.0.0-beta.2

Fixes

  • #1085 not adding -n if empty to do-upload command

4.0.0-beta.1

v4 represents a move from the universal uploader to the Codecov CLI. Although this will unlock new features for our users, the CLI is not yet at feature parity with the universal uploader.

Breaking Changes

  • No current support for aarch64 and alpine architectures.
  • Tokenless uploading is unsuported
  • Various arguments to the Action have been removed

3.1.4

Fixes

  • #967 Fix typo in README.md
  • #971 fix: add back in working dir
  • #969 fix: CLI option names for uploader

Dependencies

  • #970 build(deps-dev): bump @​types/node from 18.15.12 to 18.16.3
  • #979 build(deps-dev): bump @​types/node from 20.1.0 to 20.1.2
  • #981 build(deps-dev): bump @​types/node from 20.1.2 to 20.1.4

3.1.3

Fixes

  • #960 fix: allow for aarch64 build

Dependencies

  • #957 build(deps-dev): bump jest-junit from 15.0.0 to 16.0.0
  • #958 build(deps): bump openpgp from 5.7.0 to 5.8.0
  • #959 build(deps-dev): bump @​types/node from 18.15.10 to 18.15.12

3.1.2

Fixes

  • #718 Update README.md
  • #851 Remove unsupported path_to_write_report argument
  • #898 codeql-analysis.yml
  • #901 Update README to contain correct information - inputs and negate feature
  • #955 fix: add in all the extra arguments for uploader

Dependencies

  • #819 build(deps): bump openpgp from 5.4.0 to 5.5.0
  • #835 build(deps): bump node-fetch from 3.2.4 to 3.2.10
  • #840 build(deps): bump ossf/scorecard-action from 1.1.1 to 2.0.4
  • #841 build(deps): bump @​actions/core from 1.9.1 to 1.10.0
  • #843 build(deps): bump @​actions/github from 5.0.3 to 5.1.1
  • #869 build(deps): bump node-fetch from 3.2.10 to 3.3.0
  • #872 build(deps-dev): bump jest-junit from 13.2.0 to 15.0.0
  • #879 build(deps): bump decode-uri-component from 0.2.0 to 0.2.2

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codecov/codecov-action&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jinzhe Zeng --- .github/workflows/test_cc.yml | 6 +++--- .github/workflows/test_cuda.yml | 6 +++--- .github/workflows/test_python.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test_cc.yml b/.github/workflows/test_cc.yml index 2253a25ee0..f2af6f45ae 100644 --- a/.github/workflows/test_cc.yml +++ b/.github/workflows/test_cc.yml @@ -61,9 +61,9 @@ jobs: PATH: ${{ github.workspace }}/dp_test/bin:$PATH LD_LIBRARY_PATH: ${{ github.workspace }}/dp_test/lib if: ${{ !matrix.check_memleak }} - - uses: codecov/codecov-action@v3 - with: - gcov: true + - uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} pass: name: Pass testing C++ needs: [testcc] diff --git a/.github/workflows/test_cuda.yml b/.github/workflows/test_cuda.yml index 4e9725103a..7c08e50912 100644 --- a/.github/workflows/test_cuda.yml +++ b/.github/workflows/test_cuda.yml @@ -66,6 +66,6 @@ jobs: TF_INTER_OP_PARALLELISM_THREADS: 1 LAMMPS_PLUGIN_PATH: ${{ github.workspace }}/dp_test/lib/deepmd_lmp CUDA_PATH: /usr/local/cuda-12.2 - - uses: codecov/codecov-action@v3 - with: - gcov: true + - uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index 091a2a61f8..e1ac3a716c 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -42,9 +42,9 @@ jobs: - run: pytest --cov=deepmd source/tests --durations=0 env: NUM_WORKERS: 0 - - uses: codecov/codecov-action@v3 - with: - gcov: true + - uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} pass: name: Pass testing Python needs: [testpython] From e281a8c99d0161a1a99541e3f76e1b408420ff6f Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 5 Feb 2024 20:30:55 -0500 Subject: [PATCH 052/686] c: change the required shape of electric field to nloc * 3 (#3237) [A segfault](https://github.com/deepmodeling/deepmd-kit/actions/runs/7782245372/job/21218255452) sometimes appears in the tests after #3223. The reason is that the required shape of the electric field is set to nall * 3 in the C interface, but a vector of nloc * 3 is given in the tests and lammps and used in the C++ interface. It was not caught before, as the program didn't write to these addresses (only reading it usually won't cause a segfault, unless the address is invalid). Perhaps fix #2895. --------- Signed-off-by: Jinzhe Zeng --- source/api_c/include/c_api.h | 8 ++++---- source/api_c/include/deepmd.hpp | 6 +++--- source/api_c/src/c_api.cc | 2 +- source/api_cc/include/DataModifier.h | 6 +++--- source/api_cc/include/DataModifierTF.h | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/source/api_c/include/c_api.h b/source/api_c/include/c_api.h index 4baa7dd4a0..911813e428 100644 --- a/source/api_c/include/c_api.h +++ b/source/api_c/include/c_api.h @@ -1134,20 +1134,20 @@ extern void DP_DeleteDipoleChargeModifier(DP_DipoleChargeModifier* dcm); *modifier with the neighbor list. (double version) * @param[in] dcm The dipole charge modifier to use. * @param[in] natoms The number of atoms. - * @param[in] coord The coordinates of atoms. The array should be of size natoms + * @param[in] coord The coordinates of atoms. The array should be of size nall *x 3. - * @param[in] atype The atom types. The array should contain natoms ints. + * @param[in] atype The atom types. The array should contain nall ints. * @param[in] cell The cell of the region. The array should be of size 9. Pass *NULL if pbc is not used. * @param[in] pairs The pairs of atoms. The list should contain npairs pairs of *ints. * @param[in] npairs The number of pairs. * @param[in] delef_ The electric field on each atom. The array should be of - *size nframes x natoms x 3. + *size nframes x nloc x 3. * @param[in] nghost The number of ghost atoms. * @param[in] nlist The neighbor list. * @param[out] dfcorr_ Output force correction. The array should be of size - *natoms x 3. + *nall x 3. * @param[out] dvcorr_ Output virial correction. The array should be of size 9. * @warning The output arrays should be allocated before calling this function. *Pass NULL if not required. diff --git a/source/api_c/include/deepmd.hpp b/source/api_c/include/deepmd.hpp index 966cc1f24e..16b8f08cad 100644 --- a/source/api_c/include/deepmd.hpp +++ b/source/api_c/include/deepmd.hpp @@ -1965,13 +1965,13 @@ class DipoleChargeModifier { * @param[out] dfcorr_ The force correction on each atom. * @param[out] dvcorr_ The virial correction. * @param[in] dcoord_ The coordinates of atoms. The array should be of size - *natoms x 3. - * @param[in] datype_ The atom types. The list should contain natoms ints. + *nall x 3. + * @param[in] datype_ The atom types. The list should contain nall ints. * @param[in] dbox The cell of the region. The array should be of size 9. * @param[in] pairs The pairs of atoms. The list should contain npairs pairs *of ints. * @param[in] delef_ The electric field on each atom. The array should be of - *size natoms x 3. + *size nghost x 3. * @param[in] nghost The number of ghost atoms. * @param[in] lmp_list The neighbor list. **/ diff --git a/source/api_c/src/c_api.cc b/source/api_c/src/c_api.cc index 029d020f45..79dc486e0d 100644 --- a/source/api_c/src/c_api.cc +++ b/source/api_c/src/c_api.cc @@ -785,7 +785,7 @@ inline void DP_DipoleChargeModifierComputeNList_variant( for (int i = 0; i < npairs; i++) { pairs_.push_back(std::make_pair(pairs[i * 2], pairs[i * 2 + 1])); } - std::vector delef_(delef, delef + natoms * 3); + std::vector delef_(delef, delef + (natoms - nghost) * 3); std::vector df, dv; DP_REQUIRES_OK(dcm, dcm->dcm.compute(df, dv, coord_, atype_, cell_, pairs_, diff --git a/source/api_cc/include/DataModifier.h b/source/api_cc/include/DataModifier.h index 6d443d9b9c..0f46b5e0f8 100644 --- a/source/api_cc/include/DataModifier.h +++ b/source/api_cc/include/DataModifier.h @@ -127,13 +127,13 @@ class DipoleChargeModifier { * @param[out] dfcorr_ The force correction on each atom. * @param[out] dvcorr_ The virial correction. * @param[in] dcoord_ The coordinates of atoms. The array should be of size - *natoms x 3. - * @param[in] datype_ The atom types. The list should contain natoms ints. + *nall x 3. + * @param[in] datype_ The atom types. The list should contain nall ints. * @param[in] dbox The cell of the region. The array should be of size 9. * @param[in] pairs The pairs of atoms. The list should contain npairs pairs *of ints. * @param[in] delef_ The electric field on each atom. The array should be of - *size natoms x 3. + *size nloc x 3. * @param[in] nghost The number of ghost atoms. * @param[in] lmp_list The neighbor list. **/ diff --git a/source/api_cc/include/DataModifierTF.h b/source/api_cc/include/DataModifierTF.h index cd1d696c3c..b2f610db3c 100644 --- a/source/api_cc/include/DataModifierTF.h +++ b/source/api_cc/include/DataModifierTF.h @@ -42,13 +42,13 @@ class DipoleChargeModifierTF : public DipoleChargeModifierBase { * @param[out] dfcorr_ The force correction on each atom. * @param[out] dvcorr_ The virial correction. * @param[in] dcoord_ The coordinates of atoms. The array should be of size - *natoms x 3. - * @param[in] datype_ The atom types. The list should contain natoms ints. + *nall x 3. + * @param[in] datype_ The atom types. The list should contain nall ints. * @param[in] dbox The cell of the region. The array should be of size 9. * @param[in] pairs The pairs of atoms. The list should contain npairs pairs *of ints. * @param[in] delef_ The electric field on each atom. The array should be of - *size natoms x 3. + *size nloc x 3. * @param[in] nghost The number of ghost atoms. * @param[in] lmp_list The neighbor list. **/ From 37cdccf06077c74269f87e1158c2fa5bda07e677 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:32:11 +0800 Subject: [PATCH 053/686] [pre-commit.ci] pre-commit autoupdate (#3236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.14 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.14...v0.2.0) --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jinzhe Zeng --- .pre-commit-config.yaml | 2 +- deepmd/tf/descriptor/loc_frame.py | 5 ++++- deepmd/tf/descriptor/se_a.py | 5 ++++- deepmd/tf/descriptor/se_r.py | 5 ++++- deepmd/tf/descriptor/se_t.py | 5 ++++- pyproject.toml | 4 ++-- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0fd2d1b40f..85486e3901 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^source/3rdparty - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.1.14 + rev: v0.2.0 hooks: - id: ruff args: ["--fix"] diff --git a/deepmd/tf/descriptor/loc_frame.py b/deepmd/tf/descriptor/loc_frame.py index b43678c381..185c5062aa 100644 --- a/deepmd/tf/descriptor/loc_frame.py +++ b/deepmd/tf/descriptor/loc_frame.py @@ -343,7 +343,10 @@ def prod_force_virial( tf.summary.histogram("net_derivative", net_deriv) net_deriv_reshape = tf.reshape( net_deriv, - [np.cast["int64"](-1), natoms[0] * np.cast["int64"](self.ndescrpt)], + [ + np.asarray(-1, dtype=np.int64), + natoms[0] * np.asarray(self.ndescrpt, dtype=np.int64), + ], ) force = op_module.prod_force( net_deriv_reshape, diff --git a/deepmd/tf/descriptor/se_a.py b/deepmd/tf/descriptor/se_a.py index f1f90451fc..01c4ee8844 100644 --- a/deepmd/tf/descriptor/se_a.py +++ b/deepmd/tf/descriptor/se_a.py @@ -708,7 +708,10 @@ def prod_force_virial( tf.summary.histogram("net_derivative", net_deriv) net_deriv_reshape = tf.reshape( net_deriv, - [np.cast["int64"](-1), natoms[0] * np.cast["int64"](self.ndescrpt)], + [ + np.asarray(-1, dtype=np.int64), + natoms[0] * np.asarray(self.ndescrpt, dtype=np.int64), + ], ) force = op_module.prod_force_se_a( net_deriv_reshape, diff --git a/deepmd/tf/descriptor/se_r.py b/deepmd/tf/descriptor/se_r.py index ac94ec0614..f790d0a8fb 100644 --- a/deepmd/tf/descriptor/se_r.py +++ b/deepmd/tf/descriptor/se_r.py @@ -500,7 +500,10 @@ def prod_force_virial( tf.summary.histogram("net_derivative", net_deriv) net_deriv_reshape = tf.reshape( net_deriv, - [np.cast["int64"](-1), natoms[0] * np.cast["int64"](self.ndescrpt)], + [ + np.asarray(-1, dtype=np.int64), + natoms[0] * np.asarray(self.ndescrpt, dtype=np.int64), + ], ) force = op_module.prod_force_se_r( net_deriv_reshape, self.descrpt_deriv, self.nlist, natoms diff --git a/deepmd/tf/descriptor/se_t.py b/deepmd/tf/descriptor/se_t.py index 98f4cf8212..c72296daa7 100644 --- a/deepmd/tf/descriptor/se_t.py +++ b/deepmd/tf/descriptor/se_t.py @@ -513,7 +513,10 @@ def prod_force_virial( [net_deriv] = tf.gradients(atom_ener, self.descrpt_reshape) net_deriv_reshape = tf.reshape( net_deriv, - [np.cast["int64"](-1), natoms[0] * np.cast["int64"](self.ndescrpt)], + [ + np.asarray(-1, dtype=np.int64), + natoms[0] * np.asarray(self.ndescrpt, dtype=np.int64), + ], ) force = op_module.prod_force_se_a( net_deriv_reshape, diff --git a/pyproject.toml b/pyproject.toml index aa0da4725d..e4096b37b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,7 +224,7 @@ ignore = "D413, D416, D203, D107, D213" profile = "black" force_grid_wrap = 1 -[tool.ruff] +[tool.ruff.lint] select = [ "E", # errors "F", # pyflakes @@ -252,7 +252,7 @@ ignore = [ ] ignore-init-module-imports = true -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "numpy" [tool.pytest.ini_options] From 1c8d63598c039f6c51e55a8e3f554a0f3ec44af5 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 5 Feb 2024 20:32:48 -0500 Subject: [PATCH 054/686] pin docker actions to major versions (#3238) We don't need to pin them to a specific tag. This can close #3230. Signed-off-by: Jinzhe Zeng --- .github/workflows/build_wheel.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 392ce7ac5b..3795a5ccce 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -147,7 +147,7 @@ jobs: path: source/install/docker/dist merge-multiple: true - name: Log in to the Container registry - uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -155,12 +155,12 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@dbef88086f6cef02e264edb7dbf63250c17cef6c + uses: docker/metadata-action@v5 with: images: ghcr.io/deepmodeling/deepmd-kit - name: Build and push Docker image - uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 + uses: docker/build-push-action@v5 with: context: source/install/docker push: ${{ github.repository_owner == 'deepmodeling' && github.event_name == 'push' && github.actor != 'dependabot[bot]' }} From 6672a2844f98400b22ceb91bec56381c94c91e99 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 5 Feb 2024 21:14:17 -0500 Subject: [PATCH 055/686] drop `deepmd.tf.cluster.slurm` (#3239) The `deepmd.tf.cluster.slurm` is too specialized - we are not able to support every cluster. This PR uses mpi4py as the alternatives, considering the documentation has asked users to install mpi4py. --------- Signed-off-by: Jinzhe Zeng --- deepmd/tf/cluster/__init__.py | 7 +--- deepmd/tf/cluster/local.py | 7 ++-- deepmd/tf/cluster/slurm.py | 59 -------------------------- deepmd/utils/hostlist.py | 34 +++++++++++++++ pyproject.toml | 1 - source/tests/tf/test_cluster.py | 73 --------------------------------- 6 files changed, 39 insertions(+), 142 deletions(-) delete mode 100644 deepmd/tf/cluster/slurm.py create mode 100644 deepmd/utils/hostlist.py diff --git a/deepmd/tf/cluster/__init__.py b/deepmd/tf/cluster/__init__.py index 3c15778fe5..6735ce92f4 100644 --- a/deepmd/tf/cluster/__init__.py +++ b/deepmd/tf/cluster/__init__.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Module that reads node resources, auto detects if running local or on SLURM.""" -import os from typing import ( List, Optional, @@ -9,7 +8,6 @@ ) from .local import get_resource as get_local_res -from .slurm import get_resource as get_slurm_res __all__ = ["get_resource"] @@ -22,7 +20,4 @@ def get_resource() -> Tuple[str, List[str], Optional[List[int]]]: Tuple[str, List[str], Optional[List[int]]] nodename, nodelist, and gpus """ - if "SLURM_JOB_NODELIST" in os.environ: - return get_slurm_res() - else: - return get_local_res() + return get_local_res() diff --git a/deepmd/tf/cluster/local.py b/deepmd/tf/cluster/local.py index bd0e4c86aa..60961a0d65 100644 --- a/deepmd/tf/cluster/local.py +++ b/deepmd/tf/cluster/local.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Get local GPU resources.""" -import socket import subprocess as sp import sys from typing import ( @@ -13,6 +12,9 @@ from deepmd.tf.env import ( tf, ) +from deepmd.utils.hostlist import ( + get_host_names, +) __all__ = ["get_gpus", "get_resource"] @@ -57,7 +59,6 @@ def get_resource() -> Tuple[str, List[str], Optional[List[int]]]: Tuple[str, List[str], Optional[List[int]]] nodename, nodelist, and gpus """ - nodename = socket.gethostname() - nodelist = [nodename] + nodename, nodelist = get_host_names() gpus = get_gpus() return nodename, nodelist, gpus diff --git a/deepmd/tf/cluster/slurm.py b/deepmd/tf/cluster/slurm.py deleted file mode 100644 index 7a7ebcee3e..0000000000 --- a/deepmd/tf/cluster/slurm.py +++ /dev/null @@ -1,59 +0,0 @@ -# SPDX-License-Identifier: LGPL-3.0-or-later -"""MOdule to get resources on SLURM cluster. - -References ----------- -https://github.com/deepsense-ai/tensorflow_on_slurm #### -""" - -import os -from typing import ( - List, - Optional, - Tuple, -) - -import hostlist - -from deepmd.tf.cluster import ( - local, -) - -__all__ = ["get_resource"] - - -def get_resource() -> Tuple[str, List[str], Optional[List[int]]]: - """Get SLURM resources: nodename, nodelist, and gpus. - - Returns - ------- - Tuple[str, List[str], Optional[List[int]]] - nodename, nodelist, and gpus - - Raises - ------ - RuntimeError - if number of nodes could not be retrieved - ValueError - list of nodes is not of the same length sa number of nodes - ValueError - if current nodename is not found in node list - """ - nodelist = hostlist.expand_hostlist(os.environ["SLURM_JOB_NODELIST"]) - nodename = os.environ["SLURMD_NODENAME"] - num_nodes_env = os.getenv("SLURM_JOB_NUM_NODES") - if num_nodes_env: - num_nodes = int(num_nodes_env) - else: - raise RuntimeError("Could not get SLURM number of nodes") - - if len(nodelist) != num_nodes: - raise ValueError( - f"Number of slurm nodes {len(nodelist)} not equal to {num_nodes}" - ) - if nodename not in nodelist: - raise ValueError( - f"Nodename({nodename}) not in nodelist({nodelist}). This should not happen!" - ) - gpus = local.get_gpus() - return nodename, nodelist, gpus diff --git a/deepmd/utils/hostlist.py b/deepmd/utils/hostlist.py new file mode 100644 index 0000000000..d09a8d8bf1 --- /dev/null +++ b/deepmd/utils/hostlist.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import socket +from typing import ( + List, + Tuple, +) + + +def get_host_names() -> Tuple[str, List[str]]: + """Get host names of all nodes in the cluster. + + If mpi4py is not installed or MPI is not used, then the + host name of the current node is returned as those of all nodes. + + Returns + ------- + str + Host name of the current node + List[str] + List of host names of all nodes in the cluster + """ + host_name = socket.gethostname() + try: + from mpi4py import ( + MPI, + ) + except ImportError: + return host_name, [host_name] + + comm = MPI.COMM_WORLD + if comm.Get_size() == 1: + return host_name, [host_name] + host_names = comm.allgather(host_name) + return host_name, list(set(host_names)) diff --git a/pyproject.toml b/pyproject.toml index e4096b37b4..1701e10bb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,6 @@ dependencies = [ 'scipy', 'pyyaml', 'dargs >= 0.4.1', - 'python-hostlist >= 1.21', 'typing_extensions; python_version < "3.8"', 'importlib_metadata>=1.4; python_version < "3.8"', 'h5py', diff --git a/source/tests/tf/test_cluster.py b/source/tests/tf/test_cluster.py index 27526a3ccf..ea90e1ea6d 100644 --- a/source/tests/tf/test_cluster.py +++ b/source/tests/tf/test_cluster.py @@ -6,7 +6,6 @@ from deepmd.tf.cluster import ( local, - slurm, ) kHostName = "compute-b24-1" @@ -70,75 +69,3 @@ def test_resource(self, mock_gethostname): nodename, nodelist, _ = local.get_resource() self.assertEqual(nodename, kHostName) self.assertEqual(nodelist, [kHostName]) - - -class TestSlurm(unittest.TestCase): - @mock.patch.dict( - "os.environ", - values={ - "SLURM_JOB_NODELIST": kHostName, - "SLURMD_NODENAME": kHostName, - "SLURM_JOB_NUM_NODES": "1", - }, - ) - def test_single(self): - nodename, nodelist, _ = slurm.get_resource() - self.assertEqual(nodename, kHostName) - self.assertEqual(nodelist, [kHostName]) - - @mock.patch.dict( - "os.environ", - values={ - "SLURM_JOB_NODELIST": "compute-b24-[1-3,5-9],compute-b25-[4,8]", - "SLURMD_NODENAME": "compute-b24-2", - "SLURM_JOB_NUM_NODES": "10", - }, - ) - def test_multiple(self): - nodename, nodelist, _ = slurm.get_resource() - self.assertEqual(nodename, "compute-b24-2") - self.assertEqual( - nodelist, - [ - "compute-b24-1", - "compute-b24-2", - "compute-b24-3", - "compute-b24-5", - "compute-b24-6", - "compute-b24-7", - "compute-b24-8", - "compute-b24-9", - "compute-b25-4", - "compute-b25-8", - ], - ) - - def test_illegal(self): - environ = { - "SLURM_JOB_NODELIST": "compute-b24-[3-5]", - "SLURMD_NODENAME": "compute-b24-4", - } - with mock.patch.dict("os.environ", environ): - with self.assertRaises(RuntimeError) as cm: - _ = slurm.get_resource() - self.assertIn("Could not get SLURM number", str(cm.exception)) - - environ = { - "SLURM_JOB_NODELIST": "compute-b24-1,compute-b25-2", - "SLURMD_NODENAME": "compute-b25-2", - "SLURM_JOB_NUM_NODES": "4", - } - with mock.patch.dict("os.environ", environ): - with self.assertRaises(ValueError) as cm: - _ = slurm.get_resource() - self.assertIn("Number of slurm nodes 2", str(cm.exception)) - - environ = { - "SLURM_JOB_NODELIST": "compute-b24-1,compute-b25-3", - "SLURMD_NODENAME": "compute-b25-2", - "SLURM_JOB_NUM_NODES": "2", - } - with mock.patch.dict("os.environ", environ): - with self.assertRaises(ValueError) as cm: - _ = slurm.get_resource() - self.assertIn("Nodename(compute-b25-2", str(cm.exception)) From 79f98cac8049b4ca22d2f42fe57bc715b5a65e01 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Tue, 6 Feb 2024 10:52:43 +0800 Subject: [PATCH 056/686] test: pt: mv all model related tests to the pt/test/model folder (#3234) Co-authored-by: Han Wang --- source/tests/pt/model/__init__.py | 1 + source/tests/pt/{ => model}/models/dpa1.json | 0 source/tests/pt/{ => model}/models/dpa1.pth | Bin source/tests/pt/{ => model}/models/dpa2.json | 0 source/tests/pt/{ => model}/models/dpa2.pth | Bin source/tests/pt/{ => model}/models/dpa2_hyb.json | 0 source/tests/pt/{ => model}/models/dpa2_tebd.pth | Bin source/tests/pt/{ => model}/test_autodiff.py | 0 source/tests/pt/{ => model}/test_deeppot.py | 0 source/tests/pt/{ => model}/test_descriptor.py | 0 source/tests/pt/{ => model}/test_descriptor_dpa1.py | 0 source/tests/pt/{ => model}/test_descriptor_dpa2.py | 0 source/tests/pt/{ => model}/test_dp_atomic_model.py | 0 source/tests/pt/{ => model}/test_dp_model.py | 0 source/tests/pt/{ => model}/test_embedding_net.py | 0 source/tests/pt/{ => model}/test_ener_fitting.py | 0 source/tests/pt/{ => model}/test_env_mat.py | 0 source/tests/pt/{ => model}/test_fitting_net.py | 0 source/tests/pt/{ => model}/test_force_grad.py | 0 source/tests/pt/{ => model}/test_jit.py | 0 source/tests/pt/{ => model}/test_mlp.py | 0 source/tests/pt/{ => model}/test_model.py | 0 source/tests/pt/{ => model}/test_nlist.py | 0 source/tests/pt/{ => model}/test_pairtab.py | 0 source/tests/pt/{ => model}/test_permutation.py | 0 .../pt/{ => model}/test_permutation_denoise.py | 0 source/tests/pt/{ => model}/test_region.py | 0 source/tests/pt/{ => model}/test_rot.py | 0 source/tests/pt/{ => model}/test_rot_denoise.py | 0 source/tests/pt/{ => model}/test_rotation.py | 0 source/tests/pt/{ => model}/test_saveload_dpa1.py | 0 .../tests/pt/{ => model}/test_saveload_se_e2_a.py | 0 source/tests/pt/{ => model}/test_se_e2_a.py | 0 source/tests/pt/{ => model}/test_smooth.py | 0 source/tests/pt/{ => model}/test_smooth_denoise.py | 0 source/tests/pt/{ => model}/test_trans.py | 0 source/tests/pt/{ => model}/test_trans_denoise.py | 0 source/tests/pt/{ => model}/test_unused_params.py | 0 .../{ => model}/water/data/data_0/set.000/box.npy | Bin .../{ => model}/water/data/data_0/set.000/coord.npy | Bin .../water/data/data_0/set.000/energy.npy | Bin .../{ => model}/water/data/data_0/set.000/force.npy | Bin .../tests/pt/{ => model}/water/data/data_0/type.raw | 0 .../pt/{ => model}/water/data/data_0/type_map.raw | 0 .../{ => model}/water/data/single/set.000/box.npy | Bin .../{ => model}/water/data/single/set.000/coord.npy | Bin .../water/data/single/set.000/energy.npy | Bin .../{ => model}/water/data/single/set.000/force.npy | Bin .../tests/pt/{ => model}/water/data/single/type.raw | 0 .../pt/{ => model}/water/data/single/type_map.raw | 0 source/tests/pt/{ => model}/water/lkf.json | 0 source/tests/pt/{ => model}/water/se_atten.json | 0 source/tests/pt/{ => model}/water/se_e2_a.json | 0 source/tests/pt/test_training.py | 2 +- source/tests/pt/water | 1 + 55 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 source/tests/pt/model/__init__.py rename source/tests/pt/{ => model}/models/dpa1.json (100%) rename source/tests/pt/{ => model}/models/dpa1.pth (100%) rename source/tests/pt/{ => model}/models/dpa2.json (100%) rename source/tests/pt/{ => model}/models/dpa2.pth (100%) rename source/tests/pt/{ => model}/models/dpa2_hyb.json (100%) rename source/tests/pt/{ => model}/models/dpa2_tebd.pth (100%) rename source/tests/pt/{ => model}/test_autodiff.py (100%) rename source/tests/pt/{ => model}/test_deeppot.py (100%) rename source/tests/pt/{ => model}/test_descriptor.py (100%) rename source/tests/pt/{ => model}/test_descriptor_dpa1.py (100%) rename source/tests/pt/{ => model}/test_descriptor_dpa2.py (100%) rename source/tests/pt/{ => model}/test_dp_atomic_model.py (100%) rename source/tests/pt/{ => model}/test_dp_model.py (100%) rename source/tests/pt/{ => model}/test_embedding_net.py (100%) rename source/tests/pt/{ => model}/test_ener_fitting.py (100%) rename source/tests/pt/{ => model}/test_env_mat.py (100%) rename source/tests/pt/{ => model}/test_fitting_net.py (100%) rename source/tests/pt/{ => model}/test_force_grad.py (100%) rename source/tests/pt/{ => model}/test_jit.py (100%) rename source/tests/pt/{ => model}/test_mlp.py (100%) rename source/tests/pt/{ => model}/test_model.py (100%) rename source/tests/pt/{ => model}/test_nlist.py (100%) rename source/tests/pt/{ => model}/test_pairtab.py (100%) rename source/tests/pt/{ => model}/test_permutation.py (100%) rename source/tests/pt/{ => model}/test_permutation_denoise.py (100%) rename source/tests/pt/{ => model}/test_region.py (100%) rename source/tests/pt/{ => model}/test_rot.py (100%) rename source/tests/pt/{ => model}/test_rot_denoise.py (100%) rename source/tests/pt/{ => model}/test_rotation.py (100%) rename source/tests/pt/{ => model}/test_saveload_dpa1.py (100%) rename source/tests/pt/{ => model}/test_saveload_se_e2_a.py (100%) rename source/tests/pt/{ => model}/test_se_e2_a.py (100%) rename source/tests/pt/{ => model}/test_smooth.py (100%) rename source/tests/pt/{ => model}/test_smooth_denoise.py (100%) rename source/tests/pt/{ => model}/test_trans.py (100%) rename source/tests/pt/{ => model}/test_trans_denoise.py (100%) rename source/tests/pt/{ => model}/test_unused_params.py (100%) rename source/tests/pt/{ => model}/water/data/data_0/set.000/box.npy (100%) rename source/tests/pt/{ => model}/water/data/data_0/set.000/coord.npy (100%) rename source/tests/pt/{ => model}/water/data/data_0/set.000/energy.npy (100%) rename source/tests/pt/{ => model}/water/data/data_0/set.000/force.npy (100%) rename source/tests/pt/{ => model}/water/data/data_0/type.raw (100%) rename source/tests/pt/{ => model}/water/data/data_0/type_map.raw (100%) rename source/tests/pt/{ => model}/water/data/single/set.000/box.npy (100%) rename source/tests/pt/{ => model}/water/data/single/set.000/coord.npy (100%) rename source/tests/pt/{ => model}/water/data/single/set.000/energy.npy (100%) rename source/tests/pt/{ => model}/water/data/single/set.000/force.npy (100%) rename source/tests/pt/{ => model}/water/data/single/type.raw (100%) rename source/tests/pt/{ => model}/water/data/single/type_map.raw (100%) rename source/tests/pt/{ => model}/water/lkf.json (100%) rename source/tests/pt/{ => model}/water/se_atten.json (100%) rename source/tests/pt/{ => model}/water/se_e2_a.json (100%) create mode 120000 source/tests/pt/water diff --git a/source/tests/pt/model/__init__.py b/source/tests/pt/model/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/source/tests/pt/model/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/source/tests/pt/models/dpa1.json b/source/tests/pt/model/models/dpa1.json similarity index 100% rename from source/tests/pt/models/dpa1.json rename to source/tests/pt/model/models/dpa1.json diff --git a/source/tests/pt/models/dpa1.pth b/source/tests/pt/model/models/dpa1.pth similarity index 100% rename from source/tests/pt/models/dpa1.pth rename to source/tests/pt/model/models/dpa1.pth diff --git a/source/tests/pt/models/dpa2.json b/source/tests/pt/model/models/dpa2.json similarity index 100% rename from source/tests/pt/models/dpa2.json rename to source/tests/pt/model/models/dpa2.json diff --git a/source/tests/pt/models/dpa2.pth b/source/tests/pt/model/models/dpa2.pth similarity index 100% rename from source/tests/pt/models/dpa2.pth rename to source/tests/pt/model/models/dpa2.pth diff --git a/source/tests/pt/models/dpa2_hyb.json b/source/tests/pt/model/models/dpa2_hyb.json similarity index 100% rename from source/tests/pt/models/dpa2_hyb.json rename to source/tests/pt/model/models/dpa2_hyb.json diff --git a/source/tests/pt/models/dpa2_tebd.pth b/source/tests/pt/model/models/dpa2_tebd.pth similarity index 100% rename from source/tests/pt/models/dpa2_tebd.pth rename to source/tests/pt/model/models/dpa2_tebd.pth diff --git a/source/tests/pt/test_autodiff.py b/source/tests/pt/model/test_autodiff.py similarity index 100% rename from source/tests/pt/test_autodiff.py rename to source/tests/pt/model/test_autodiff.py diff --git a/source/tests/pt/test_deeppot.py b/source/tests/pt/model/test_deeppot.py similarity index 100% rename from source/tests/pt/test_deeppot.py rename to source/tests/pt/model/test_deeppot.py diff --git a/source/tests/pt/test_descriptor.py b/source/tests/pt/model/test_descriptor.py similarity index 100% rename from source/tests/pt/test_descriptor.py rename to source/tests/pt/model/test_descriptor.py diff --git a/source/tests/pt/test_descriptor_dpa1.py b/source/tests/pt/model/test_descriptor_dpa1.py similarity index 100% rename from source/tests/pt/test_descriptor_dpa1.py rename to source/tests/pt/model/test_descriptor_dpa1.py diff --git a/source/tests/pt/test_descriptor_dpa2.py b/source/tests/pt/model/test_descriptor_dpa2.py similarity index 100% rename from source/tests/pt/test_descriptor_dpa2.py rename to source/tests/pt/model/test_descriptor_dpa2.py diff --git a/source/tests/pt/test_dp_atomic_model.py b/source/tests/pt/model/test_dp_atomic_model.py similarity index 100% rename from source/tests/pt/test_dp_atomic_model.py rename to source/tests/pt/model/test_dp_atomic_model.py diff --git a/source/tests/pt/test_dp_model.py b/source/tests/pt/model/test_dp_model.py similarity index 100% rename from source/tests/pt/test_dp_model.py rename to source/tests/pt/model/test_dp_model.py diff --git a/source/tests/pt/test_embedding_net.py b/source/tests/pt/model/test_embedding_net.py similarity index 100% rename from source/tests/pt/test_embedding_net.py rename to source/tests/pt/model/test_embedding_net.py diff --git a/source/tests/pt/test_ener_fitting.py b/source/tests/pt/model/test_ener_fitting.py similarity index 100% rename from source/tests/pt/test_ener_fitting.py rename to source/tests/pt/model/test_ener_fitting.py diff --git a/source/tests/pt/test_env_mat.py b/source/tests/pt/model/test_env_mat.py similarity index 100% rename from source/tests/pt/test_env_mat.py rename to source/tests/pt/model/test_env_mat.py diff --git a/source/tests/pt/test_fitting_net.py b/source/tests/pt/model/test_fitting_net.py similarity index 100% rename from source/tests/pt/test_fitting_net.py rename to source/tests/pt/model/test_fitting_net.py diff --git a/source/tests/pt/test_force_grad.py b/source/tests/pt/model/test_force_grad.py similarity index 100% rename from source/tests/pt/test_force_grad.py rename to source/tests/pt/model/test_force_grad.py diff --git a/source/tests/pt/test_jit.py b/source/tests/pt/model/test_jit.py similarity index 100% rename from source/tests/pt/test_jit.py rename to source/tests/pt/model/test_jit.py diff --git a/source/tests/pt/test_mlp.py b/source/tests/pt/model/test_mlp.py similarity index 100% rename from source/tests/pt/test_mlp.py rename to source/tests/pt/model/test_mlp.py diff --git a/source/tests/pt/test_model.py b/source/tests/pt/model/test_model.py similarity index 100% rename from source/tests/pt/test_model.py rename to source/tests/pt/model/test_model.py diff --git a/source/tests/pt/test_nlist.py b/source/tests/pt/model/test_nlist.py similarity index 100% rename from source/tests/pt/test_nlist.py rename to source/tests/pt/model/test_nlist.py diff --git a/source/tests/pt/test_pairtab.py b/source/tests/pt/model/test_pairtab.py similarity index 100% rename from source/tests/pt/test_pairtab.py rename to source/tests/pt/model/test_pairtab.py diff --git a/source/tests/pt/test_permutation.py b/source/tests/pt/model/test_permutation.py similarity index 100% rename from source/tests/pt/test_permutation.py rename to source/tests/pt/model/test_permutation.py diff --git a/source/tests/pt/test_permutation_denoise.py b/source/tests/pt/model/test_permutation_denoise.py similarity index 100% rename from source/tests/pt/test_permutation_denoise.py rename to source/tests/pt/model/test_permutation_denoise.py diff --git a/source/tests/pt/test_region.py b/source/tests/pt/model/test_region.py similarity index 100% rename from source/tests/pt/test_region.py rename to source/tests/pt/model/test_region.py diff --git a/source/tests/pt/test_rot.py b/source/tests/pt/model/test_rot.py similarity index 100% rename from source/tests/pt/test_rot.py rename to source/tests/pt/model/test_rot.py diff --git a/source/tests/pt/test_rot_denoise.py b/source/tests/pt/model/test_rot_denoise.py similarity index 100% rename from source/tests/pt/test_rot_denoise.py rename to source/tests/pt/model/test_rot_denoise.py diff --git a/source/tests/pt/test_rotation.py b/source/tests/pt/model/test_rotation.py similarity index 100% rename from source/tests/pt/test_rotation.py rename to source/tests/pt/model/test_rotation.py diff --git a/source/tests/pt/test_saveload_dpa1.py b/source/tests/pt/model/test_saveload_dpa1.py similarity index 100% rename from source/tests/pt/test_saveload_dpa1.py rename to source/tests/pt/model/test_saveload_dpa1.py diff --git a/source/tests/pt/test_saveload_se_e2_a.py b/source/tests/pt/model/test_saveload_se_e2_a.py similarity index 100% rename from source/tests/pt/test_saveload_se_e2_a.py rename to source/tests/pt/model/test_saveload_se_e2_a.py diff --git a/source/tests/pt/test_se_e2_a.py b/source/tests/pt/model/test_se_e2_a.py similarity index 100% rename from source/tests/pt/test_se_e2_a.py rename to source/tests/pt/model/test_se_e2_a.py diff --git a/source/tests/pt/test_smooth.py b/source/tests/pt/model/test_smooth.py similarity index 100% rename from source/tests/pt/test_smooth.py rename to source/tests/pt/model/test_smooth.py diff --git a/source/tests/pt/test_smooth_denoise.py b/source/tests/pt/model/test_smooth_denoise.py similarity index 100% rename from source/tests/pt/test_smooth_denoise.py rename to source/tests/pt/model/test_smooth_denoise.py diff --git a/source/tests/pt/test_trans.py b/source/tests/pt/model/test_trans.py similarity index 100% rename from source/tests/pt/test_trans.py rename to source/tests/pt/model/test_trans.py diff --git a/source/tests/pt/test_trans_denoise.py b/source/tests/pt/model/test_trans_denoise.py similarity index 100% rename from source/tests/pt/test_trans_denoise.py rename to source/tests/pt/model/test_trans_denoise.py diff --git a/source/tests/pt/test_unused_params.py b/source/tests/pt/model/test_unused_params.py similarity index 100% rename from source/tests/pt/test_unused_params.py rename to source/tests/pt/model/test_unused_params.py diff --git a/source/tests/pt/water/data/data_0/set.000/box.npy b/source/tests/pt/model/water/data/data_0/set.000/box.npy similarity index 100% rename from source/tests/pt/water/data/data_0/set.000/box.npy rename to source/tests/pt/model/water/data/data_0/set.000/box.npy diff --git a/source/tests/pt/water/data/data_0/set.000/coord.npy b/source/tests/pt/model/water/data/data_0/set.000/coord.npy similarity index 100% rename from source/tests/pt/water/data/data_0/set.000/coord.npy rename to source/tests/pt/model/water/data/data_0/set.000/coord.npy diff --git a/source/tests/pt/water/data/data_0/set.000/energy.npy b/source/tests/pt/model/water/data/data_0/set.000/energy.npy similarity index 100% rename from source/tests/pt/water/data/data_0/set.000/energy.npy rename to source/tests/pt/model/water/data/data_0/set.000/energy.npy diff --git a/source/tests/pt/water/data/data_0/set.000/force.npy b/source/tests/pt/model/water/data/data_0/set.000/force.npy similarity index 100% rename from source/tests/pt/water/data/data_0/set.000/force.npy rename to source/tests/pt/model/water/data/data_0/set.000/force.npy diff --git a/source/tests/pt/water/data/data_0/type.raw b/source/tests/pt/model/water/data/data_0/type.raw similarity index 100% rename from source/tests/pt/water/data/data_0/type.raw rename to source/tests/pt/model/water/data/data_0/type.raw diff --git a/source/tests/pt/water/data/data_0/type_map.raw b/source/tests/pt/model/water/data/data_0/type_map.raw similarity index 100% rename from source/tests/pt/water/data/data_0/type_map.raw rename to source/tests/pt/model/water/data/data_0/type_map.raw diff --git a/source/tests/pt/water/data/single/set.000/box.npy b/source/tests/pt/model/water/data/single/set.000/box.npy similarity index 100% rename from source/tests/pt/water/data/single/set.000/box.npy rename to source/tests/pt/model/water/data/single/set.000/box.npy diff --git a/source/tests/pt/water/data/single/set.000/coord.npy b/source/tests/pt/model/water/data/single/set.000/coord.npy similarity index 100% rename from source/tests/pt/water/data/single/set.000/coord.npy rename to source/tests/pt/model/water/data/single/set.000/coord.npy diff --git a/source/tests/pt/water/data/single/set.000/energy.npy b/source/tests/pt/model/water/data/single/set.000/energy.npy similarity index 100% rename from source/tests/pt/water/data/single/set.000/energy.npy rename to source/tests/pt/model/water/data/single/set.000/energy.npy diff --git a/source/tests/pt/water/data/single/set.000/force.npy b/source/tests/pt/model/water/data/single/set.000/force.npy similarity index 100% rename from source/tests/pt/water/data/single/set.000/force.npy rename to source/tests/pt/model/water/data/single/set.000/force.npy diff --git a/source/tests/pt/water/data/single/type.raw b/source/tests/pt/model/water/data/single/type.raw similarity index 100% rename from source/tests/pt/water/data/single/type.raw rename to source/tests/pt/model/water/data/single/type.raw diff --git a/source/tests/pt/water/data/single/type_map.raw b/source/tests/pt/model/water/data/single/type_map.raw similarity index 100% rename from source/tests/pt/water/data/single/type_map.raw rename to source/tests/pt/model/water/data/single/type_map.raw diff --git a/source/tests/pt/water/lkf.json b/source/tests/pt/model/water/lkf.json similarity index 100% rename from source/tests/pt/water/lkf.json rename to source/tests/pt/model/water/lkf.json diff --git a/source/tests/pt/water/se_atten.json b/source/tests/pt/model/water/se_atten.json similarity index 100% rename from source/tests/pt/water/se_atten.json rename to source/tests/pt/model/water/se_atten.json diff --git a/source/tests/pt/water/se_e2_a.json b/source/tests/pt/model/water/se_e2_a.json similarity index 100% rename from source/tests/pt/water/se_e2_a.json rename to source/tests/pt/model/water/se_e2_a.json diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index 574ca8688e..2186467788 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -14,7 +14,7 @@ get_trainer, ) -from .test_permutation import ( +from .model.test_permutation import ( model_dpa1, model_dpa2, model_hybrid, diff --git a/source/tests/pt/water b/source/tests/pt/water new file mode 120000 index 0000000000..7e5219651f --- /dev/null +++ b/source/tests/pt/water @@ -0,0 +1 @@ +model/water \ No newline at end of file From 6c1238038c28009e69154288ef31ebf3b03cbb04 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 5 Feb 2024 23:04:58 -0500 Subject: [PATCH 057/686] add category property to OutputVariableDef (#3228) Signed-off-by: Jinzhe Zeng Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- deepmd/dpmodel/output_def.py | 103 +++++++++++++++++- .../tests/common/dpmodel/test_output_def.py | 98 +++++++++++++++++ 2 files changed, 199 insertions(+), 2 deletions(-) diff --git a/deepmd/dpmodel/output_def.py b/deepmd/dpmodel/output_def.py index 6cd83fcf28..9e3570d2ff 100644 --- a/deepmd/dpmodel/output_def.py +++ b/deepmd/dpmodel/output_def.py @@ -1,5 +1,8 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import functools +from enum import ( + IntEnum, +) from typing import ( Dict, List, @@ -107,6 +110,38 @@ def __call__( return wrapper +class OutputVariableOperation(IntEnum): + """Defines the operation of the output variable.""" + + _NONE = 0 + """No operation.""" + REDU = 1 + """Reduce the output variable.""" + DERV_R = 2 + """Derivative w.r.t. coordinates.""" + DERV_C = 4 + """Derivative w.r.t. cell.""" + _SEC_DERV_R = 8 + """Second derivative w.r.t. coordinates.""" + + +class OutputVariableCategory(IntEnum): + """Defines the category of the output variable.""" + + OUT = OutputVariableOperation._NONE + """Output variable. (e.g. atom energy)""" + REDU = OutputVariableOperation.REDU + """Reduced output variable. (e.g. system energy)""" + DERV_R = OutputVariableOperation.DERV_R + """Negative derivative w.r.t. coordinates. (e.g. force)""" + DERV_C = OutputVariableOperation.DERV_C + """Atomic component of the virial, see PRB 104, 224202 (2021) """ + DERV_C_REDU = OutputVariableOperation.DERV_C | OutputVariableOperation.REDU + """Virial, the transposed negative gradient with cell tensor times cell tensor, see eq 40 JCP 159, 054801 (2023). """ + DERV_R_DERV_R = OutputVariableOperation.DERV_R | OutputVariableOperation._SEC_DERV_R + """Hession matrix, the second derivative w.r.t. coordinates.""" + + class OutputVariableDef: """Defines the shape and other properties of the one output variable. @@ -129,7 +164,8 @@ class OutputVariableDef: If the variable is differentiated with respect to coordinates of atoms and cell tensor (pbc case). Only reduciable variable are differentiable. - + category : int + The category of the output variable. """ def __init__( @@ -139,6 +175,7 @@ def __init__( reduciable: bool = False, differentiable: bool = False, atomic: bool = True, + category: int = OutputVariableCategory.OUT.value, ): self.name = name self.shape = list(shape) @@ -149,6 +186,7 @@ def __init__( raise ValueError("only reduciable variable are differentiable") if self.reduciable and not self.atomic: raise ValueError("only reduciable variable should be atomic") + self.category = category class FittingOutputDef: @@ -255,6 +293,60 @@ def get_deriv_name(name: str) -> Tuple[str, str]: return name + "_derv_r", name + "_derv_c" +def apply_operation(var_def: OutputVariableDef, op: OutputVariableOperation) -> int: + """Apply a operation to the category of a variable definition. + + Parameters + ---------- + var_def : OutputVariableDef + The variable definition. + op : OutputVariableOperation + The operation to be applied. + + Returns + ------- + int + The new category of the variable definition. + + Raises + ------ + ValueError + If the operation has been applied to the variable definition, + and exceed the maximum limitation. + """ + if op == OutputVariableOperation.REDU or op == OutputVariableOperation.DERV_C: + if check_operation_applied(var_def, op): + raise ValueError(f"operation {op} has been applied") + elif op == OutputVariableOperation.DERV_R: + if check_operation_applied(var_def, OutputVariableOperation.DERV_R): + op = OutputVariableOperation._SEC_DERV_R + if check_operation_applied(var_def, OutputVariableOperation._SEC_DERV_R): + raise ValueError(f"operation {op} has been applied twice") + else: + raise ValueError(f"operation {op} not supported") + return var_def.category | op.value + + +def check_operation_applied( + var_def: OutputVariableDef, op: OutputVariableOperation +) -> bool: + """Check if a operation has been applied to a variable definition. + + Parameters + ---------- + var_def : OutputVariableDef + The variable definition. + op : OutputVariableOperation + The operation to be checked. + + Returns + ------- + bool + True if the operation has been applied, False otherwise. + """ + return var_def.category & op.value == op.value + + def do_reduce( def_outp_data: Dict[str, OutputVariableDef], ) -> Dict[str, OutputVariableDef]: @@ -263,7 +355,12 @@ def do_reduce( if vv.reduciable: rk = get_reduce_name(kk) def_redu[rk] = OutputVariableDef( - rk, vv.shape, reduciable=False, differentiable=False, atomic=False + rk, + vv.shape, + reduciable=False, + differentiable=False, + atomic=False, + category=apply_operation(vv, OutputVariableOperation.REDU), ) return def_redu @@ -282,6 +379,7 @@ def do_derivative( reduciable=False, differentiable=False, atomic=True, + category=apply_operation(vv, OutputVariableOperation.DERV_R), ) def_derv_c[rkc] = OutputVariableDef( rkc, @@ -289,5 +387,6 @@ def do_derivative( reduciable=True, differentiable=False, atomic=True, + category=apply_operation(vv, OutputVariableOperation.DERV_C), ) return def_derv_r, def_derv_c diff --git a/source/tests/common/dpmodel/test_output_def.py b/source/tests/common/dpmodel/test_output_def.py index aaabdc0ba6..3f7544f597 100644 --- a/source/tests/common/dpmodel/test_output_def.py +++ b/source/tests/common/dpmodel/test_output_def.py @@ -15,6 +15,9 @@ model_check_output, ) from deepmd.dpmodel.output_def import ( + OutputVariableCategory, + OutputVariableOperation, + apply_operation, check_var, ) @@ -103,6 +106,101 @@ def test_model_output_def(self): self.assertEqual(md["energy_derv_r"].atomic, True) self.assertEqual(md["energy_derv_c"].atomic, True) self.assertEqual(md["energy_derv_c_redu"].atomic, False) + # category + self.assertEqual(md["energy"].category, OutputVariableCategory.OUT) + self.assertEqual(md["dos"].category, OutputVariableCategory.OUT) + self.assertEqual(md["foo"].category, OutputVariableCategory.OUT) + self.assertEqual(md["energy_redu"].category, OutputVariableCategory.REDU) + self.assertEqual(md["energy_derv_r"].category, OutputVariableCategory.DERV_R) + self.assertEqual(md["energy_derv_c"].category, OutputVariableCategory.DERV_C) + self.assertEqual( + md["energy_derv_c_redu"].category, OutputVariableCategory.DERV_C_REDU + ) + # flag + self.assertEqual(md["energy"].category & OutputVariableOperation.REDU, 0) + self.assertEqual(md["energy"].category & OutputVariableOperation.DERV_R, 0) + self.assertEqual(md["energy"].category & OutputVariableOperation.DERV_C, 0) + self.assertEqual(md["dos"].category & OutputVariableOperation.REDU, 0) + self.assertEqual(md["dos"].category & OutputVariableOperation.DERV_R, 0) + self.assertEqual(md["dos"].category & OutputVariableOperation.DERV_C, 0) + self.assertEqual(md["foo"].category & OutputVariableOperation.REDU, 0) + self.assertEqual(md["foo"].category & OutputVariableOperation.DERV_R, 0) + self.assertEqual(md["foo"].category & OutputVariableOperation.DERV_C, 0) + self.assertEqual( + md["energy_redu"].category & OutputVariableOperation.REDU, + OutputVariableOperation.REDU, + ) + self.assertEqual(md["energy_redu"].category & OutputVariableOperation.DERV_R, 0) + self.assertEqual(md["energy_redu"].category & OutputVariableOperation.DERV_C, 0) + self.assertEqual(md["energy_derv_r"].category & OutputVariableOperation.REDU, 0) + self.assertEqual( + md["energy_derv_r"].category & OutputVariableOperation.DERV_R, + OutputVariableOperation.DERV_R, + ) + self.assertEqual( + md["energy_derv_r"].category & OutputVariableOperation.DERV_C, 0 + ) + self.assertEqual(md["energy_derv_c"].category & OutputVariableOperation.REDU, 0) + self.assertEqual( + md["energy_derv_c"].category & OutputVariableOperation.DERV_R, 0 + ) + self.assertEqual( + md["energy_derv_c"].category & OutputVariableOperation.DERV_C, + OutputVariableOperation.DERV_C, + ) + self.assertEqual( + md["energy_derv_c_redu"].category & OutputVariableOperation.REDU, + OutputVariableOperation.REDU, + ) + self.assertEqual( + md["energy_derv_c_redu"].category & OutputVariableOperation.DERV_R, 0 + ) + self.assertEqual( + md["energy_derv_c_redu"].category & OutputVariableOperation.DERV_C, + OutputVariableOperation.DERV_C, + ) + + # apply_operation + self.assertEqual( + apply_operation(md["energy"], OutputVariableOperation.REDU), + md["energy_redu"].category, + ) + self.assertEqual( + apply_operation(md["energy"], OutputVariableOperation.DERV_R), + md["energy_derv_r"].category, + ) + self.assertEqual( + apply_operation(md["energy"], OutputVariableOperation.DERV_C), + md["energy_derv_c"].category, + ) + self.assertEqual( + apply_operation(md["energy_derv_c"], OutputVariableOperation.REDU), + md["energy_derv_c_redu"].category, + ) + # raise ValueError + with self.assertRaises(ValueError): + apply_operation(md["energy_redu"], OutputVariableOperation.REDU) + with self.assertRaises(ValueError): + apply_operation(md["energy_derv_c"], OutputVariableOperation.DERV_C) + with self.assertRaises(ValueError): + apply_operation(md["energy_derv_c_redu"], OutputVariableOperation.REDU) + # hession + hession_cat = apply_operation( + md["energy_derv_r"], OutputVariableOperation.DERV_R + ) + self.assertEqual( + hession_cat & OutputVariableOperation.DERV_R, OutputVariableOperation.DERV_R + ) + self.assertEqual( + hession_cat & OutputVariableOperation._SEC_DERV_R, + OutputVariableOperation._SEC_DERV_R, + ) + self.assertEqual(hession_cat, OutputVariableCategory.DERV_R_DERV_R) + hession_vardef = OutputVariableDef( + "energy_derv_r_derv_r", [1], False, False, category=hession_cat + ) + with self.assertRaises(ValueError): + apply_operation(hession_vardef, OutputVariableOperation.DERV_R) def test_raise_no_redu_deriv(self): with self.assertRaises(ValueError) as context: From 07a45109a71cd6f12d688961154bf8808863d65a Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Tue, 6 Feb 2024 16:12:01 +0800 Subject: [PATCH 058/686] fix skipped tests (#3241) Co-authored-by: Han Wang --- source/tests/pt/model/test_env_mat.py | 15 ++---- source/tests/pt/model/test_mlp.py | 75 +++++---------------------- source/tests/pt/model/test_se_e2_a.py | 12 +---- 3 files changed, 16 insertions(+), 86 deletions(-) diff --git a/source/tests/pt/model/test_env_mat.py b/source/tests/pt/model/test_env_mat.py index b9f0ff1981..08890cc963 100644 --- a/source/tests/pt/model/test_env_mat.py +++ b/source/tests/pt/model/test_env_mat.py @@ -4,17 +4,9 @@ import numpy as np import torch -try: - from deepmd.dpmodel import ( - EnvMat, - ) - - support_env_mat = True -except ModuleNotFoundError: - support_env_mat = False -except ImportError: - support_env_mat = False - +from deepmd.dpmodel.utils import ( + EnvMat, +) from deepmd.pt.model.descriptor.env_mat import ( prod_env_mat_se_a, ) @@ -77,7 +69,6 @@ def setUp(self): # to be merged with the tf test case -@unittest.skipIf(not support_env_mat, "EnvMat not supported") class TestEnvMat(unittest.TestCase, TestCaseSingleFrameWithNlist): def setUp(self): TestCaseSingleFrameWithNlist.setUp(self) diff --git a/source/tests/pt/model/test_mlp.py b/source/tests/pt/model/test_mlp.py index 3a78b8294d..ca2bb6d2cf 100644 --- a/source/tests/pt/model/test_mlp.py +++ b/source/tests/pt/model/test_mlp.py @@ -5,6 +5,18 @@ import numpy as np import torch +from deepmd.dpmodel.utils import EmbeddingNet as DPEmbeddingNet +from deepmd.dpmodel.utils import FittingNet as DPFittingNet +from deepmd.dpmodel.utils import ( + NativeLayer, + NativeNet, +) +from deepmd.pt.model.network.mlp import ( + MLP, + EmbeddingNet, + FittingNet, + MLPLayer, +) from deepmd.pt.utils import ( env, ) @@ -12,65 +24,6 @@ PRECISION_DICT, ) -try: - from deepmd.pt.model.network.mlp import ( - MLP, - MLPLayer, - ) - - support_native_net = True -except ModuleNotFoundError: - support_native_net = False - -try: - from deepmd.pt.model.network.mlp import ( - EmbeddingNet, - ) - - support_embedding_net = True -except ModuleNotFoundError: - support_embedding_net = False - -try: - from deepmd.pt.model.network.mlp import ( - FittingNet, - ) - - support_fitting_net = True -except ModuleNotFoundError: - support_fitting_net = False - - -try: - from deepmd.dpmodel import ( - NativeLayer, - NativeNet, - ) - - support_native_net = True -except ModuleNotFoundError: - support_native_net = False -except ImportError: - support_native_net = False - -try: - from deepmd.dpmodel import EmbeddingNet as DPEmbeddingNet - - support_embedding_net = True -except ModuleNotFoundError: - support_embedding_net = False -except ImportError: - support_embedding_net = False - -try: - from deepmd.dpmodel import FittingNet as DPFittingNet - - support_fitting_net = True -except ModuleNotFoundError: - support_fitting_net = False -except ImportError: - support_fitting_net = False - def get_tols(prec): if prec in ["single", "float32"]: @@ -84,7 +37,6 @@ def get_tols(prec): return rtol, atol -@unittest.skipIf(not support_native_net, "NativeLayer not supported") class TestMLPLayer(unittest.TestCase): def setUp(self): self.test_cases = itertools.product( @@ -141,7 +93,6 @@ def test_jit(self): model = torch.jit.script(ml1) -@unittest.skipIf(not support_native_net, "NativeLayer not supported") class TestMLP(unittest.TestCase): def setUp(self): self.test_cases = itertools.product( @@ -210,7 +161,6 @@ def test_jit(self): model = torch.jit.script(ml1) -@unittest.skipIf(not support_embedding_net, "EmbeddingNet not supported") class TestEmbeddingNet(unittest.TestCase): def setUp(self): self.test_cases = itertools.product( @@ -261,7 +211,6 @@ def test_jit( model = torch.jit.script(ml1) -@unittest.skipIf(not support_fitting_net, "FittingNet not supported") class TestFittingNet(unittest.TestCase): def setUp(self): self.test_cases = itertools.product( diff --git a/source/tests/pt/model/test_se_e2_a.py b/source/tests/pt/model/test_se_e2_a.py index ec49725929..520fdfcdfa 100644 --- a/source/tests/pt/model/test_se_e2_a.py +++ b/source/tests/pt/model/test_se_e2_a.py @@ -5,16 +5,7 @@ import numpy as np import torch -try: - # from deepmd.dpmodel import PRECISION_DICT as DP_PRECISION_DICT - from deepmd.dpmodel import DescrptSeA as DPDescrptSeA - - support_se_e2_a = True -except ModuleNotFoundError: - support_se_e2_a = False -except ImportError: - support_se_e2_a = False - +from deepmd.dpmodel.descriptor import DescrptSeA as DPDescrptSeA from deepmd.pt.model.descriptor.se_a import ( DescrptSeA, ) @@ -36,7 +27,6 @@ # to be merged with the tf test case -@unittest.skipIf(not support_se_e2_a, "EnvMat not supported") class TestDescrptSeA(unittest.TestCase, TestCaseSingleFrameWithNlist): def setUp(self): TestCaseSingleFrameWithNlist.setUp(self) From f1baf198b2c703637215942428d2ef24575b0e92 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:30:14 +0800 Subject: [PATCH 059/686] support separate r_differentiable and c_differentiable (#3240) solve some virial shape issue: should be [9] rather than [3,3] --------- Co-authored-by: Han Wang --- deepmd/dpmodel/fitting/invar_fitting.py | 6 +- .../dpmodel/model/make_base_atomic_model.py | 5 +- deepmd/dpmodel/model/pair_tab_model.py | 6 +- deepmd/dpmodel/model/transform_output.py | 10 +- deepmd/dpmodel/output_def.py | 48 +++++--- deepmd/pt/model/model/pair_tab_model.py | 6 +- deepmd/pt/model/model/transform_output.py | 64 ++++++---- deepmd/pt/model/task/denoise.py | 12 +- deepmd/pt/model/task/ener.py | 20 ++- .../tests/common/dpmodel/test_output_def.py | 115 ++++++++++++++---- 10 files changed, 225 insertions(+), 67 deletions(-) diff --git a/deepmd/dpmodel/fitting/invar_fitting.py b/deepmd/dpmodel/fitting/invar_fitting.py index efe2771323..820f422ef0 100644 --- a/deepmd/dpmodel/fitting/invar_fitting.py +++ b/deepmd/dpmodel/fitting/invar_fitting.py @@ -199,7 +199,11 @@ def output_def(self): return FittingOutputDef( [ OutputVariableDef( - self.var_name, [self.dim_out], reduciable=True, differentiable=True + self.var_name, + [self.dim_out], + reduciable=True, + r_differentiable=True, + c_differentiable=True, ), ] ) diff --git a/deepmd/dpmodel/model/make_base_atomic_model.py b/deepmd/dpmodel/model/make_base_atomic_model.py index c057cd25f1..84e685b973 100644 --- a/deepmd/dpmodel/model/make_base_atomic_model.py +++ b/deepmd/dpmodel/model/make_base_atomic_model.py @@ -107,7 +107,10 @@ def do_grad_( ) -> bool: """Tell if the output variable `var_name` is differentiable.""" assert var_name is not None - return self.fitting_output_def()[var_name].differentiable + return ( + self.fitting_output_def()[var_name].r_differentiable + or self.fitting_output_def()[var_name].c_differentiable + ) setattr(BAM, fwd_method_name, BAM.fwd) delattr(BAM, "fwd") diff --git a/deepmd/dpmodel/model/pair_tab_model.py b/deepmd/dpmodel/model/pair_tab_model.py index d62ac5c859..dc658d8662 100644 --- a/deepmd/dpmodel/model/pair_tab_model.py +++ b/deepmd/dpmodel/model/pair_tab_model.py @@ -70,7 +70,11 @@ def fitting_output_def(self) -> FittingOutputDef: return FittingOutputDef( [ OutputVariableDef( - name="energy", shape=[1], reduciable=True, differentiable=True + name="energy", + shape=[1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, ) ] ) diff --git a/deepmd/dpmodel/model/transform_output.py b/deepmd/dpmodel/model/transform_output.py index 3c7917d847..49368849ca 100644 --- a/deepmd/dpmodel/model/transform_output.py +++ b/deepmd/dpmodel/model/transform_output.py @@ -31,10 +31,13 @@ def fit_output_to_model_output( if vdef.reduciable: kk_redu = get_reduce_name(kk) model_ret[kk_redu] = np.sum(vv, axis=atom_axis) - if vdef.differentiable: + if vdef.r_differentiable: kk_derv_r, kk_derv_c = get_deriv_name(kk) # name-holders model_ret[kk_derv_r] = None + if vdef.c_differentiable: + assert vdef.r_differentiable + kk_derv_r, kk_derv_c = get_deriv_name(kk) model_ret[kk_derv_c] = None return model_ret @@ -57,10 +60,13 @@ def communicate_extended_output( if vdef.reduciable: kk_redu = get_reduce_name(kk) new_ret[kk_redu] = model_ret[kk_redu] - if vdef.differentiable: + if vdef.r_differentiable: kk_derv_r, kk_derv_c = get_deriv_name(kk) # name holders new_ret[kk_derv_r] = None + if vdef.c_differentiable: + assert vdef.r_differentiable + kk_derv_r, kk_derv_c = get_deriv_name(kk) new_ret[kk_derv_c] = None new_ret[kk_derv_c + "_redu"] = None if not do_atomic_virial: diff --git a/deepmd/dpmodel/output_def.py b/deepmd/dpmodel/output_def.py index 9e3570d2ff..8b190ed5de 100644 --- a/deepmd/dpmodel/output_def.py +++ b/deepmd/dpmodel/output_def.py @@ -68,9 +68,11 @@ def __call__( if dd.reduciable: rk = get_reduce_name(kk) check_var(ret[rk], self.md[rk]) - if dd.differentiable: + if dd.r_differentiable: dnr, dnc = get_deriv_name(kk) check_var(ret[dnr], self.md[dnr]) + if dd.c_differentiable: + assert dd.r_differentiable check_var(ret[dnc], self.md[dnc]) return ret @@ -160,10 +162,16 @@ class OutputVariableDef: dipole should be [3], polarizabilty should be [3,3]. reduciable If the variable is reduced. - differentiable + r_differentiable If the variable is differentiated with respect to coordinates - of atoms and cell tensor (pbc case). Only reduciable variable + of atoms. Only reduciable variable are differentiable. + Negative derivative w.r.t. coordinates will be calcualted. (e.g. force) + c_differentiable + If the variable is differentiated with respect to the + cell tensor (pbc case). Only reduciable variable are differentiable. + Virial, the transposed negative gradient with cell tensor times + cell tensor, will be calculated, see eq 40 JCP 159, 054801 (2023). category : int The category of the output variable. """ @@ -173,7 +181,8 @@ def __init__( name: str, shape: List[int], reduciable: bool = False, - differentiable: bool = False, + r_differentiable: bool = False, + c_differentiable: bool = False, atomic: bool = True, category: int = OutputVariableCategory.OUT.value, ): @@ -181,11 +190,16 @@ def __init__( self.shape = list(shape) self.atomic = atomic self.reduciable = reduciable - self.differentiable = differentiable - if not self.reduciable and self.differentiable: - raise ValueError("only reduciable variable are differentiable") + self.r_differentiable = r_differentiable + self.c_differentiable = c_differentiable + if self.c_differentiable and not self.r_differentiable: + raise ValueError("c differentiable requires r_differentiable") + if not self.reduciable and self.r_differentiable: + raise ValueError("only reduciable variable are r differentiable") + if not self.reduciable and self.c_differentiable: + raise ValueError("only reduciable variable are c differentiable") if self.reduciable and not self.atomic: - raise ValueError("only reduciable variable should be atomic") + raise ValueError("a reduciable variable should be atomic") self.category = category @@ -358,7 +372,8 @@ def do_reduce( rk, vv.shape, reduciable=False, - differentiable=False, + r_differentiable=False, + c_differentiable=False, atomic=False, category=apply_operation(vv, OutputVariableOperation.REDU), ) @@ -371,21 +386,26 @@ def do_derivative( def_derv_r: Dict[str, OutputVariableDef] = {} def_derv_c: Dict[str, OutputVariableDef] = {} for kk, vv in def_outp_data.items(): - if vv.differentiable: - rkr, rkc = get_deriv_name(kk) + rkr, rkc = get_deriv_name(kk) + if vv.r_differentiable: def_derv_r[rkr] = OutputVariableDef( rkr, vv.shape + [3], # noqa: RUF005 reduciable=False, - differentiable=False, + r_differentiable=False, + c_differentiable=False, atomic=True, category=apply_operation(vv, OutputVariableOperation.DERV_R), ) + if vv.c_differentiable: + assert vv.r_differentiable + rkr, rkc = get_deriv_name(kk) def_derv_c[rkc] = OutputVariableDef( rkc, - vv.shape + [3, 3], # noqa: RUF005 + vv.shape + [9], # noqa: RUF005 reduciable=True, - differentiable=False, + r_differentiable=False, + c_differentiable=False, atomic=True, category=apply_operation(vv, OutputVariableOperation.DERV_C), ) diff --git a/deepmd/pt/model/model/pair_tab_model.py b/deepmd/pt/model/model/pair_tab_model.py index 1a415d633d..83089f86b4 100644 --- a/deepmd/pt/model/model/pair_tab_model.py +++ b/deepmd/pt/model/model/pair_tab_model.py @@ -82,7 +82,11 @@ def fitting_output_def(self) -> FittingOutputDef: return FittingOutputDef( [ OutputVariableDef( - name="energy", shape=[1], reduciable=True, differentiable=True + name="energy", + shape=[1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, ) ] ) diff --git a/deepmd/pt/model/model/transform_output.py b/deepmd/pt/model/model/transform_output.py index d942ed3ae8..27e014640d 100644 --- a/deepmd/pt/model/model/transform_output.py +++ b/deepmd/pt/model/model/transform_output.py @@ -56,6 +56,7 @@ def task_deriv_one( atom_energy: torch.Tensor, energy: torch.Tensor, extended_coord: torch.Tensor, + do_virial: bool = True, do_atomic_virial: bool = False, ): faked_grad = torch.ones_like(energy) @@ -65,13 +66,16 @@ def task_deriv_one( )[0] assert extended_force is not None extended_force = -extended_force - extended_virial = extended_force.unsqueeze(-1) @ extended_coord.unsqueeze(-2) - # the correction sums to zero, which does not contribute to global virial - if do_atomic_virial: - extended_virial_corr = atomic_virial_corr(extended_coord, atom_energy) - extended_virial = extended_virial + extended_virial_corr - # to [...,3,3] -> [...,9] - extended_virial = extended_virial.view(list(extended_virial.shape[:-2]) + [9]) # noqa:RUF005 + if do_virial: + extended_virial = extended_force.unsqueeze(-1) @ extended_coord.unsqueeze(-2) + # the correction sums to zero, which does not contribute to global virial + if do_atomic_virial: + extended_virial_corr = atomic_virial_corr(extended_coord, atom_energy) + extended_virial = extended_virial + extended_virial_corr + # to [...,3,3] -> [...,9] + extended_virial = extended_virial.view(list(extended_virial.shape[:-2]) + [9]) # noqa:RUF005 + else: + extended_virial = None return extended_force, extended_virial @@ -97,6 +101,7 @@ def take_deriv( svv: torch.Tensor, vdef: OutputVariableDef, coord_ext: torch.Tensor, + do_virial: bool = False, do_atomic_virial: bool = False, ): size = 1 @@ -110,16 +115,25 @@ def take_deriv( for vvi, svvi in zip(split_vv1, split_svv1): # nf x nloc x 3, nf x nloc x 9 ffi, aviri = task_deriv_one( - vvi, svvi, coord_ext, do_atomic_virial=do_atomic_virial + vvi, + svvi, + coord_ext, + do_virial=do_virial, + do_atomic_virial=do_atomic_virial, ) # nf x nloc x 1 x 3, nf x nloc x 1 x 9 ffi = ffi.unsqueeze(-2) - aviri = aviri.unsqueeze(-2) split_ff.append(ffi) - split_avir.append(aviri) + if do_virial: + assert aviri is not None + aviri = aviri.unsqueeze(-2) + split_avir.append(aviri) # nf x nloc x v_dim x 3, nf x nloc x v_dim x 9 ff = torch.concat(split_ff, dim=-2) - avir = torch.concat(split_avir, dim=-2) + if do_virial: + avir = torch.concat(split_avir, dim=-2) + else: + avir = None return ff, avir @@ -141,18 +155,23 @@ def fit_output_to_model_output( if vdef.reduciable: kk_redu = get_reduce_name(kk) model_ret[kk_redu] = torch.sum(vv, dim=atom_axis) - if vdef.differentiable: + if vdef.r_differentiable: kk_derv_r, kk_derv_c = get_deriv_name(kk) dr, dc = take_deriv( vv, model_ret[kk_redu], vdef, coord_ext, + do_virial=vdef.c_differentiable, do_atomic_virial=do_atomic_virial, ) model_ret[kk_derv_r] = dr - model_ret[kk_derv_c] = dc - model_ret[kk_derv_c + "_redu"] = torch.sum(model_ret[kk_derv_c], dim=1) + if vdef.c_differentiable: + assert dc is not None + model_ret[kk_derv_c] = dc + model_ret[kk_derv_c + "_redu"] = torch.sum( + model_ret[kk_derv_c], dim=1 + ) return model_ret @@ -174,12 +193,12 @@ def communicate_extended_output( if vdef.reduciable: kk_redu = get_reduce_name(kk) new_ret[kk_redu] = model_ret[kk_redu] - if vdef.differentiable: - # nf x nloc - vldims = get_leading_dims(vv, vdef) - # nf x nall - mldims = list(mapping.shape) - kk_derv_r, kk_derv_c = get_deriv_name(kk) + # nf x nloc + vldims = get_leading_dims(vv, vdef) + # nf x nall + mldims = list(mapping.shape) + kk_derv_r, kk_derv_c = get_deriv_name(kk) + if vdef.r_differentiable: # vdim x 3 derv_r_ext_dims = list(vdef.shape) + [3] # noqa:RUF005 mapping = mapping.view(mldims + [1] * len(derv_r_ext_dims)).expand( @@ -196,10 +215,13 @@ def communicate_extended_output( src=model_ret[kk_derv_r], reduce="sum", ) + if vdef.c_differentiable: + assert vdef.r_differentiable derv_c_ext_dims = list(vdef.shape) + [9] # noqa:RUF005 # nf x nloc x nvar x 3 -> nf x nloc x nvar x 9 mapping = torch.tile( - mapping, [1] * (len(mldims) + len(vdef.shape)) + [3] + mapping, + [1] * (len(mldims) + len(vdef.shape)) + [3], ) virial = torch.zeros( vldims + derv_c_ext_dims, dtype=vv.dtype, device=vv.device diff --git a/deepmd/pt/model/task/denoise.py b/deepmd/pt/model/task/denoise.py index 35846ed231..5f1e780de3 100644 --- a/deepmd/pt/model/task/denoise.py +++ b/deepmd/pt/model/task/denoise.py @@ -75,10 +75,18 @@ def output_def(self): return FittingOutputDef( [ OutputVariableDef( - "updated_coord", [3], reduciable=False, differentiable=False + "updated_coord", + [3], + reduciable=False, + r_differentiable=False, + c_differentiable=False, ), OutputVariableDef( - "logits", [-1], reduciable=False, differentiable=False + "logits", + [-1], + reduciable=False, + r_differentiable=False, + c_differentiable=False, ), ] ) diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 5e3cd87367..d73c33545e 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -162,7 +162,11 @@ def output_def(self) -> FittingOutputDef: return FittingOutputDef( [ OutputVariableDef( - self.var_name, [self.dim_out], reduciable=True, differentiable=True + self.var_name, + [self.dim_out], + reduciable=True, + r_differentiable=True, + c_differentiable=True, ), ] ) @@ -459,9 +463,19 @@ def __init__( def output_def(self): return FittingOutputDef( [ - OutputVariableDef("energy", [1], reduciable=True, differentiable=False), OutputVariableDef( - "dforce", [3], reduciable=False, differentiable=False + "energy", + [1], + reduciable=True, + r_differentiable=False, + c_differentiable=False, + ), + OutputVariableDef( + "dforce", + [3], + reduciable=False, + r_differentiable=False, + c_differentiable=False, ), ] ) diff --git a/source/tests/common/dpmodel/test_output_def.py b/source/tests/common/dpmodel/test_output_def.py index 3f7544f597..9e794e0f32 100644 --- a/source/tests/common/dpmodel/test_output_def.py +++ b/source/tests/common/dpmodel/test_output_def.py @@ -37,9 +37,30 @@ def __init__( class TestDef(unittest.TestCase): def test_model_output_def(self): defs = [ - OutputVariableDef("energy", [1], True, True), - OutputVariableDef("dos", [10], True, False), - OutputVariableDef("foo", [3], False, False), + OutputVariableDef( + "energy", + [1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + atomic=True, + ), + OutputVariableDef( + "dos", + [10], + reduciable=True, + r_differentiable=False, + c_differentiable=False, + atomic=True, + ), + OutputVariableDef( + "foo", + [3], + reduciable=False, + r_differentiable=False, + c_differentiable=False, + atomic=True, + ), ] # fitting definition fd = FittingOutputDef(defs) @@ -61,9 +82,12 @@ def test_model_output_def(self): self.assertEqual(fd["dos"].reduciable, True) self.assertEqual(fd["foo"].reduciable, False) # derivative - self.assertEqual(fd["energy"].differentiable, True) - self.assertEqual(fd["dos"].differentiable, False) - self.assertEqual(fd["foo"].differentiable, False) + self.assertEqual(fd["energy"].r_differentiable, True) + self.assertEqual(fd["energy"].c_differentiable, True) + self.assertEqual(fd["dos"].r_differentiable, False) + self.assertEqual(fd["foo"].r_differentiable, False) + self.assertEqual(fd["dos"].c_differentiable, False) + self.assertEqual(fd["foo"].c_differentiable, False) # model definition md = ModelOutputDef(fd) expected_keys = [ @@ -87,17 +111,20 @@ def test_model_output_def(self): self.assertEqual(md["dos"].reduciable, True) self.assertEqual(md["foo"].reduciable, False) # derivative - self.assertEqual(md["energy"].differentiable, True) - self.assertEqual(md["dos"].differentiable, False) - self.assertEqual(md["foo"].differentiable, False) + self.assertEqual(md["energy"].r_differentiable, True) + self.assertEqual(md["energy"].c_differentiable, True) + self.assertEqual(md["dos"].r_differentiable, False) + self.assertEqual(md["foo"].r_differentiable, False) + self.assertEqual(md["dos"].c_differentiable, False) + self.assertEqual(md["foo"].c_differentiable, False) # shape self.assertEqual(md["energy"].shape, [1]) self.assertEqual(md["dos"].shape, [10]) self.assertEqual(md["foo"].shape, [3]) self.assertEqual(md["energy_redu"].shape, [1]) self.assertEqual(md["energy_derv_r"].shape, [1, 3]) - self.assertEqual(md["energy_derv_c"].shape, [1, 3, 3]) - self.assertEqual(md["energy_derv_c_redu"].shape, [1, 3, 3]) + self.assertEqual(md["energy_derv_c"].shape, [1, 9]) + self.assertEqual(md["energy_derv_c_redu"].shape, [1, 9]) # atomic self.assertEqual(md["energy"].atomic, True) self.assertEqual(md["dos"].atomic, True) @@ -204,11 +231,27 @@ def test_model_output_def(self): def test_raise_no_redu_deriv(self): with self.assertRaises(ValueError) as context: - (OutputVariableDef("energy", [1], False, True),) + OutputVariableDef( + "energy", + [1], + reduciable=False, + r_differentiable=True, + c_differentiable=False, + ) + + def test_raise_requires_r_deriv(self): + with self.assertRaises(ValueError) as context: + OutputVariableDef( + "energy", + [1], + reduciable=True, + r_differentiable=False, + c_differentiable=True, + ) def test_raise_redu_not_atomic(self): with self.assertRaises(ValueError) as context: - (OutputVariableDef("energy", [1], True, False, atomic=False),) + (OutputVariableDef("energy", [1], reduciable=True, atomic=False),) def test_model_decorator(self): nf = 2 @@ -219,7 +262,13 @@ def test_model_decorator(self): class Foo(NativeOP): def output_def(self): defs = [ - OutputVariableDef("energy", [1], True, True), + OutputVariableDef( + "energy", + [1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ), ] return ModelOutputDef(FittingOutputDef(defs)) @@ -228,7 +277,7 @@ def call(self): "energy": np.zeros([nf, nloc, 1]), "energy_redu": np.zeros([nf, 1]), "energy_derv_r": np.zeros([nf, nall, 1, 3]), - "energy_derv_c": np.zeros([nf, nall, 1, 3, 3]), + "energy_derv_c": np.zeros([nf, nall, 1, 9]), } ff = Foo() @@ -246,7 +295,13 @@ def __init__(self): def output_def(self): defs = [ - OutputVariableDef("energy", [1], True, True), + OutputVariableDef( + "energy", + [1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ), ] return ModelOutputDef(FittingOutputDef(defs)) @@ -254,7 +309,7 @@ def call(self): return { "energy": np.zeros([nf, nloc, 1]), "energy_redu": np.zeros([nf, 1]), - "energy_derv_c": np.zeros([nf, nall, 1, 3, 3]), + "energy_derv_c": np.zeros([nf, nall, 1, 9]), } ff = Foo() @@ -278,7 +333,13 @@ def __init__( def output_def(self): defs = [ - OutputVariableDef("energy", [1], True, True), + OutputVariableDef( + "energy", + [1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ), ] return ModelOutputDef(FittingOutputDef(defs)) @@ -287,7 +348,7 @@ def call(self): "energy": np.zeros([nf, nloc, 1]), "energy_redu": np.zeros(self.shape_rd), "energy_derv_r": np.zeros(self.shape_dr), - "energy_derv_c": np.zeros([nf, nall, 1, 3, 3]), + "energy_derv_c": np.zeros([nf, nall, 1, 9]), } ff = Foo() @@ -324,7 +385,13 @@ def test_fitting_decorator(self): class Foo(NativeOP): def output_def(self): defs = [ - OutputVariableDef("energy", [1], True, True), + OutputVariableDef( + "energy", + [1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ), ] return FittingOutputDef(defs) @@ -350,7 +417,13 @@ def __init__( def output_def(self): defs = [ - OutputVariableDef("energy", [1], True, True), + OutputVariableDef( + "energy", + [1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ), ] return FittingOutputDef(defs) From a7153b12014a8d7c89b317ff2eb613c280416751 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 6 Feb 2024 21:18:10 -0500 Subject: [PATCH 060/686] issue template: change TF version to backend version (#3244) --- .github/ISSUE_TEMPLATE/bug-report.yml | 6 +++--- .github/ISSUE_TEMPLATE/generic-issue.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index f13b187dfb..49918e47ac 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -21,10 +21,10 @@ body: validations: required: true - type: input - id: tf-version + id: backend-version attributes: - label: TensorFlow Version - description: "The version will be printed when running DeePMD-kit." + label: Backend and its version + description: "The backend and its version will be printed when running DeePMD-kit, e.g. TensorFlow v2.15.0." validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/generic-issue.yml b/.github/ISSUE_TEMPLATE/generic-issue.yml index af9f01c64d..f84097580e 100644 --- a/.github/ISSUE_TEMPLATE/generic-issue.yml +++ b/.github/ISSUE_TEMPLATE/generic-issue.yml @@ -21,10 +21,10 @@ body: validations: required: true - type: input - id: tf-version + id: backend-version attributes: - label: TensorFlow Version - description: "The version will be printed when running DeePMD-kit." + label: Backend and its version + description: "The backend and its version will be printed when running DeePMD-kit, e.g. TensorFlow v2.15.0." validations: required: true - type: textarea From b7f123905d57cda4b386aa9f55e3755b9ae4922f Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Thu, 8 Feb 2024 14:50:05 +0800 Subject: [PATCH 061/686] Feat: add ZBL weighted DP model (#3210) This PR is to implement general linear combination of several DPAtomicModels with user defined weights, as well as a special case ZBLModel with weights calculated based on this paper: `Appl. Phys. Lett. 114, 244101 (2019); doi: 10.1063/1.5098061` --------- Co-authored-by: Anyang Peng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Co-authored-by: Han Wang --- deepmd/dpmodel/model/linear_atomic_model.py | 300 +++++ ...r_tab_model.py => pairtab_atomic_model.py} | 67 +- deepmd/dpmodel/utils/nlist.py | 2 +- deepmd/pt/model/model/__init__.py | 44 + deepmd/pt/model/model/ener.py | 73 ++ deepmd/pt/model/model/linear_atomic_model.py | 315 ++++++ ...r_tab_model.py => pairtab_atomic_model.py} | 103 +- .../dpmodel/test_linear_atomic_model.py | 167 +++ ...airtab.py => test_pairtab_atomic_model.py} | 7 +- .../common/dpmodel/test_pairtab_preprocess.py | 4 + source/tests/pt/model/test_autodiff.py | 18 + .../pt/model/test_linear_atomic_model.py | 183 +++ ...airtab.py => test_pairtab_atomic_model.py} | 15 +- source/tests/pt/model/test_permutation.py | 33 + source/tests/pt/model/test_rot.py | 10 + source/tests/pt/model/test_smooth.py | 11 + source/tests/pt/model/test_trans.py | 10 + .../zbl_tab_potential/H2O_tab_potential.txt | 1000 +++++++++++++++++ 18 files changed, 2271 insertions(+), 91 deletions(-) create mode 100644 deepmd/dpmodel/model/linear_atomic_model.py rename deepmd/dpmodel/model/{pair_tab_model.py => pairtab_atomic_model.py} (82%) create mode 100644 deepmd/pt/model/model/linear_atomic_model.py rename deepmd/pt/model/model/{pair_tab_model.py => pairtab_atomic_model.py} (79%) create mode 100644 source/tests/common/dpmodel/test_linear_atomic_model.py rename source/tests/common/dpmodel/{test_pairtab.py => test_pairtab_atomic_model.py} (97%) create mode 100644 source/tests/pt/model/test_linear_atomic_model.py rename source/tests/pt/model/{test_pairtab.py => test_pairtab_atomic_model.py} (95%) create mode 100644 source/tests/pt/model/water/data/zbl_tab_potential/H2O_tab_potential.txt diff --git a/deepmd/dpmodel/model/linear_atomic_model.py b/deepmd/dpmodel/model/linear_atomic_model.py new file mode 100644 index 0000000000..dc7e9996c8 --- /dev/null +++ b/deepmd/dpmodel/model/linear_atomic_model.py @@ -0,0 +1,300 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import sys +from abc import ( + abstractmethod, +) +from typing import ( + Dict, + List, + Optional, + Tuple, + Union, +) + +import numpy as np + +from deepmd.dpmodel import ( + FittingOutputDef, + OutputVariableDef, +) +from deepmd.dpmodel.utils.nlist import ( + build_multiple_neighbor_list, + get_multiple_nlist_key, + nlist_distinguish_types, +) + +from .base_atomic_model import ( + BaseAtomicModel, +) +from .dp_atomic_model import ( + DPAtomicModel, +) +from .pairtab_atomic_model import ( + PairTabModel, +) + + +class LinearAtomicModel(BaseAtomicModel): + """Linear model make linear combinations of several existing models. + + Parameters + ---------- + models : list[DPAtomicModel or PairTabModel] + A list of models to be combined. PairTabModel must be used together with a DPAtomicModel. + """ + + def __init__( + self, + models: List[BaseAtomicModel], + **kwargs, + ): + super().__init__() + self.models = models + self.distinguish_type_list = [ + model.distinguish_types() for model in self.models + ] + + def distinguish_types(self) -> bool: + """If distinguish different types by sorting.""" + return False + + def get_rcut(self) -> float: + """Get the cut-off radius.""" + return max(self.get_model_rcuts()) + + def get_model_rcuts(self) -> List[float]: + """Get the cut-off radius for each individual models.""" + return [model.get_rcut() for model in self.models] + + def get_sel(self) -> List[int]: + return [max([model.get_nsel() for model in self.models])] + + def get_model_nsels(self) -> List[int]: + """Get the processed sels for each individual models. Not distinguishing types.""" + return [model.get_nsel() for model in self.models] + + def get_model_sels(self) -> List[Union[int, List[int]]]: + """Get the sels for each individual models.""" + return [model.get_sel() for model in self.models] + + def _sort_rcuts_sels(self) -> Tuple[List[float], List[int]]: + # sort the pair of rcut and sels in ascending order, first based on sel, then on rcut. + zipped = sorted( + zip(self.get_model_rcuts(), self.get_model_nsels()), + key=lambda x: (x[1], x[0]), + ) + return [p[0] for p in zipped], [p[1] for p in zipped] + + def forward_atomic( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + ) -> Dict[str, np.ndarray]: + """Return atomic prediction. + + Parameters + ---------- + extended_coord + coodinates in extended region, (nframes, nall * 3) + extended_atype + atomic type in extended region, (nframes, nall) + nlist + neighbor list, (nframes, nloc, nsel). + mapping + mapps the extended indices to local indices. + fparam + frame parameter. (nframes, ndf) + aparam + atomic parameter. (nframes, nloc, nda) + + Returns + ------- + result_dict + the result dict, defined by the fitting net output def. + """ + nframes, nloc, nnei = nlist.shape + extended_coord = extended_coord.reshape(nframes, -1, 3) + sorted_rcuts, sorted_sels = self._sort_rcuts_sels() + nlists = build_multiple_neighbor_list( + extended_coord, + nlist, + sorted_rcuts, + sorted_sels, + ) + raw_nlists = [ + nlists[get_multiple_nlist_key(rcut, sel)] + for rcut, sel in zip(self.get_model_rcuts(), self.get_model_nsels()) + ] + nlists_ = [ + nl if not dt else nlist_distinguish_types(nl, extended_atype, sel) + for dt, nl, sel in zip( + self.distinguish_type_list, raw_nlists, self.get_model_sels() + ) + ] + ener_list = [ + model.forward_atomic( + extended_coord, + extended_atype, + nl, + mapping, + fparam, + aparam, + )["energy"] + for model, nl in zip(self.models, nlists_) + ] + self.weights = self._compute_weight(extended_coord, extended_atype, nlists_) + self.atomic_bias = None + if self.atomic_bias is not None: + raise NotImplementedError("Need to add bias in a future PR.") + else: + fit_ret = { + "energy": np.sum(np.stack(ener_list) * np.stack(self.weights), axis=0), + } # (nframes, nloc, 1) + return fit_ret + + def fitting_output_def(self) -> FittingOutputDef: + return FittingOutputDef( + [ + OutputVariableDef( + name="energy", + shape=[1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ) + ] + ) + + @staticmethod + def serialize(models) -> dict: + return { + "models": [model.serialize() for model in models], + "model_name": [model.__class__.__name__ for model in models], + } + + @staticmethod + def deserialize(data) -> List[BaseAtomicModel]: + model_names = data["model_name"] + models = [ + getattr(sys.modules[__name__], name).deserialize(model) + for name, model in zip(model_names, data["models"]) + ] + return models + + @abstractmethod + def _compute_weight( + self, + extended_coord: np.ndarray, + extended_atype: np.ndarray, + nlists_: List[np.ndarray], + ) -> np.ndarray: + """This should be a list of user defined weights that matches the number of models to be combined.""" + raise NotImplementedError + + +class DPZBLLinearAtomicModel(LinearAtomicModel): + """Model linearly combine a list of AtomicModels. + + Parameters + ---------- + models + This linear model should take a DPAtomicModel and a PairTable model. + """ + + def __init__( + self, + dp_model: DPAtomicModel, + zbl_model: PairTabModel, + sw_rmin: float, + sw_rmax: float, + smin_alpha: Optional[float] = 0.1, + **kwargs, + ): + models = [dp_model, zbl_model] + super().__init__(models, **kwargs) + self.dp_model = dp_model + self.zbl_model = zbl_model + + self.sw_rmin = sw_rmin + self.sw_rmax = sw_rmax + self.smin_alpha = smin_alpha + + def serialize(self) -> dict: + return { + "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), + "sw_rmin": self.sw_rmin, + "sw_rmax": self.sw_rmax, + "smin_alpha": self.smin_alpha, + } + + @classmethod + def deserialize(cls, data) -> "DPZBLLinearAtomicModel": + sw_rmin = data["sw_rmin"] + sw_rmax = data["sw_rmax"] + smin_alpha = data["smin_alpha"] + + dp_model, zbl_model = LinearAtomicModel.deserialize(data["models"]) + + return cls( + dp_model=dp_model, + zbl_model=zbl_model, + sw_rmin=sw_rmin, + sw_rmax=sw_rmax, + smin_alpha=smin_alpha, + ) + + def _compute_weight( + self, + extended_coord: np.ndarray, + extended_atype: np.ndarray, + nlists_: List[np.ndarray], + ) -> List[np.ndarray]: + """ZBL weight. + + Returns + ------- + List[np.ndarray] + the atomic ZBL weight for interpolation. (nframes, nloc, 1) + """ + assert ( + self.sw_rmax > self.sw_rmin + ), "The upper boundary `sw_rmax` must be greater than the lower boundary `sw_rmin`." + + dp_nlist = nlists_[0] + zbl_nlist = nlists_[1] + + zbl_nnei = zbl_nlist.shape[-1] + dp_nnei = dp_nlist.shape[-1] + + # use the larger rr based on nlist + nlist_larger = zbl_nlist if zbl_nnei >= dp_nnei else dp_nlist + masked_nlist = np.clip(nlist_larger, 0, None) + pairwise_rr = PairTabModel._get_pairwise_dist(extended_coord, masked_nlist) + + numerator = np.sum( + pairwise_rr * np.exp(-pairwise_rr / self.smin_alpha), axis=-1 + ) # masked nnei will be zero, no need to handle + denominator = np.sum( + np.where( + nlist_larger != -1, + np.exp(-pairwise_rr / self.smin_alpha), + np.zeros_like(nlist_larger), + ), + axis=-1, + ) # handle masked nnei. + sigma = numerator / denominator + u = (sigma - self.sw_rmin) / (self.sw_rmax - self.sw_rmin) + coef = np.zeros_like(u) + left_mask = sigma < self.sw_rmin + mid_mask = (self.sw_rmin <= sigma) & (sigma < self.sw_rmax) + right_mask = sigma >= self.sw_rmax + coef[left_mask] = 1 + smooth = -6 * u**5 + 15 * u**4 - 10 * u**3 + 1 + coef[mid_mask] = smooth[mid_mask] + coef[right_mask] = 0 + self.zbl_weight = coef + return [1 - np.expand_dims(coef, -1), np.expand_dims(coef, -1)] diff --git a/deepmd/dpmodel/model/pair_tab_model.py b/deepmd/dpmodel/model/pairtab_atomic_model.py similarity index 82% rename from deepmd/dpmodel/model/pair_tab_model.py rename to deepmd/dpmodel/model/pairtab_atomic_model.py index dc658d8662..d4feb970fb 100644 --- a/deepmd/dpmodel/model/pair_tab_model.py +++ b/deepmd/dpmodel/model/pairtab_atomic_model.py @@ -82,7 +82,10 @@ def fitting_output_def(self) -> FittingOutputDef: def get_rcut(self) -> float: return self.rcut - def get_sel(self) -> int: + def get_sel(self) -> List[int]: + return [self.sel] + + def get_nsel(self) -> int: return self.sel def distinguish_types(self) -> bool: @@ -109,21 +112,20 @@ def forward_atomic( extended_atype, nlist, mapping: Optional[np.ndarray] = None, - do_atomic_virial: bool = False, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, ) -> Dict[str, np.ndarray]: - self.nframes, self.nloc, self.nnei = nlist.shape - extended_coord = extended_coord.reshape(self.nframes, -1, 3) + nframes, nloc, nnei = nlist.shape + extended_coord = extended_coord.reshape(nframes, -1, 3) # this will mask all -1 in the nlist - masked_nlist = np.clip(nlist, 0, None) - - atype = extended_atype[:, : self.nloc] # (nframes, nloc) - pairwise_dr = self._get_pairwise_dist( - extended_coord - ) # (nframes, nall, nall, 3) - pairwise_rr = np.sqrt( - np.sum(np.power(pairwise_dr, 2), axis=-1) - ) # (nframes, nall, nall) + mask = nlist >= 0 + masked_nlist = nlist * mask + + atype = extended_atype[:, :nloc] # (nframes, nloc) + pairwise_rr = self._get_pairwise_dist( + extended_coord, masked_nlist + ) # (nframes, nloc, nnei) self.tab_data = self.tab_data.reshape( self.tab.ntypes, self.tab.ntypes, self.tab.nspline, 4 ) @@ -133,13 +135,13 @@ def forward_atomic( np.arange(extended_atype.shape[0])[:, None, None], masked_nlist ] - # slice rr to get (nframes, nloc, nnei) - rr = np.take_along_axis(pairwise_rr[:, : self.nloc, :], masked_nlist, 2) - raw_atomic_energy = self._pair_tabulated_inter(nlist, atype, j_type, rr) + raw_atomic_energy = self._pair_tabulated_inter( + nlist, atype, j_type, pairwise_rr + ) atomic_energy = 0.5 * np.sum( np.where(nlist != -1, raw_atomic_energy, np.zeros_like(raw_atomic_energy)), axis=-1, - ).reshape(self.nframes, self.nloc, 1) + ).reshape(nframes, nloc, 1) return {"energy": atomic_energy} @@ -178,17 +180,18 @@ def _pair_tabulated_inter( This function is used to calculate the pairwise energy between two atoms. It uses a table containing cubic spline coefficients calculated in PairTab. """ + nframes, nloc, nnei = nlist.shape rmin = self.tab_info[0] hh = self.tab_info[1] hi = 1.0 / hh - self.nspline = int(self.tab_info[2] + 0.1) + nspline = int(self.tab_info[2] + 0.1) uu = (rr - rmin) * hi # this is broadcasted to (nframes,nloc,nnei) # if nnei of atom 0 has -1 in the nlist, uu would be 0. # this is to handle the nlist where the mask is set to 0, so that we don't raise exception for those atoms. - uu = np.where(nlist != -1, uu, self.nspline + 1) + uu = np.where(nlist != -1, uu, nspline + 1) if np.any(uu < 0): raise Exception("coord go beyond table lower boundary") @@ -197,34 +200,42 @@ def _pair_tabulated_inter( uu -= idx table_coef = self._extract_spline_coefficient( - i_type, j_type, idx, self.tab_data, self.nspline + i_type, j_type, idx, self.tab_data, nspline ) - table_coef = table_coef.reshape(self.nframes, self.nloc, self.nnei, 4) - ener = self._calcualte_ener(table_coef, uu) + table_coef = table_coef.reshape(nframes, nloc, nnei, 4) + ener = self._calculate_ener(table_coef, uu) # here we need to overwrite energy to zero at rcut and beyond. mask_beyond_rcut = rr >= self.rcut # also overwrite values beyond extrapolation to zero - extrapolation_mask = rr >= self.tab.rmin + self.nspline * self.tab.hh + extrapolation_mask = rr >= self.tab.rmin + nspline * self.tab.hh ener[mask_beyond_rcut] = 0 ener[extrapolation_mask] = 0 return ener @staticmethod - def _get_pairwise_dist(coords: np.ndarray) -> np.ndarray: + def _get_pairwise_dist(coords: np.ndarray, nlist: np.ndarray) -> np.ndarray: """Get pairwise distance `dr`. Parameters ---------- coords : np.ndarray - The coordinate of the atoms shape of (nframes, nall, 3). + The coordinate of the atoms, shape of (nframes, nall, 3). + nlist + The masked nlist, shape of (nframes, nloc, nnei). Returns ------- np.ndarray - The pairwise distance between the atoms (nframes, nall, nall, 3). + The pairwise distance between the atoms (nframes, nloc, nnei). """ - return np.expand_dims(coords, 2) - np.expand_dims(coords, 1) + batch_indices = np.arange(nlist.shape[0])[:, None, None] + neighbor_atoms = coords[batch_indices, nlist] + loc_atoms = coords[:, : nlist.shape[1], :] + pairwise_dr = loc_atoms[:, :, None, :] - neighbor_atoms + pairwise_rr = np.sqrt(np.sum(np.power(pairwise_dr, 2), axis=-1)) + + return pairwise_rr @staticmethod def _extract_spline_coefficient( @@ -279,7 +290,7 @@ def _extract_spline_coefficient( return final_coef @staticmethod - def _calcualte_ener(coef: np.ndarray, uu: np.ndarray) -> np.ndarray: + def _calculate_ener(coef: np.ndarray, uu: np.ndarray) -> np.ndarray: """Calculate energy using spline coeeficients. Parameters diff --git a/deepmd/dpmodel/utils/nlist.py b/deepmd/dpmodel/utils/nlist.py index bc6592d52b..657d6ecee2 100644 --- a/deepmd/dpmodel/utils/nlist.py +++ b/deepmd/dpmodel/utils/nlist.py @@ -169,7 +169,7 @@ def build_multiple_neighbor_list( nall = coord1.shape[1] coord0 = coord1[:, :nloc, :] nlist_mask = nlist == -1 - tnlist_0 = nlist + tnlist_0 = nlist.copy() tnlist_0[nlist_mask] = 0 index = np.tile(tnlist_0.reshape(nb, nloc * nsel, 1), [1, 1, 3]) coord2 = np.take_along_axis(coord1, index, axis=1).reshape(nb, nloc, nsel, 3) diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index c4de02ed20..6cbab5af4d 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -4,18 +4,62 @@ from deepmd.pt.model.descriptor.descriptor import ( Descriptor, ) +from deepmd.pt.model.model.dp_atomic_model import ( + DPAtomicModel, +) +from deepmd.pt.model.model.pairtab_atomic_model import ( + PairTabModel, +) from deepmd.pt.model.task import ( Fitting, ) from .ener import ( EnergyModel, + ZBLModel, ) from .model import ( BaseModel, ) +def get_zbl_model(model_params, sampled=None): + model_params = copy.deepcopy(model_params) + ntypes = len(model_params["type_map"]) + # descriptor + model_params["descriptor"]["ntypes"] = ntypes + descriptor = Descriptor(**model_params["descriptor"]) + # fitting + fitting_net = model_params.get("fitting_net", None) + fitting_net["type"] = fitting_net.get("type", "ener") + fitting_net["ntypes"] = descriptor.get_ntypes() + fitting_net["distinguish_types"] = descriptor.distinguish_types() + fitting_net["embedding_width"] = descriptor.get_dim_out() + grad_force = "direct" not in fitting_net["type"] + if not grad_force: + fitting_net["out_dim"] = descriptor.get_dim_emb() + if "ener" in fitting_net["type"]: + fitting_net["return_energy"] = True + fitting = Fitting(**fitting_net) + dp_model = DPAtomicModel( + descriptor, fitting, type_map=model_params["type_map"], resuming=True + ) + # pairtab + filepath = model_params["use_srtab"] + pt_model = PairTabModel( + filepath, model_params["descriptor"]["rcut"], model_params["descriptor"]["sel"] + ) + + rmin = model_params["sw_rmin"] + rmax = model_params["sw_rmax"] + return ZBLModel( + dp_model, + pt_model, + rmin, + rmax, + ) + + def get_model(model_params, sampled=None): model_params = copy.deepcopy(model_params) ntypes = len(model_params["type_map"]) diff --git a/deepmd/pt/model/model/ener.py b/deepmd/pt/model/model/ener.py index 9a6e60d963..ea35cf5a82 100644 --- a/deepmd/pt/model/model/ener.py +++ b/deepmd/pt/model/model/ener.py @@ -9,11 +9,84 @@ from .dp_atomic_model import ( DPAtomicModel, ) +from .linear_atomic_model import ( + DPZBLLinearAtomicModel, +) from .make_model import ( make_model, ) DPModel = make_model(DPAtomicModel) +ZBLModel_ = make_model(DPZBLLinearAtomicModel) + + +class ZBLModel(ZBLModel_): + model_type = "ener" + + def __init__( + self, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) + + def forward( + self, + coord, + atype, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + model_ret = self.forward_common( + coord, + atype, + box, + ) + + model_predict = {} + model_predict["atom_energy"] = model_ret["energy"] + model_predict["energy"] = model_ret["energy_redu"] + if self.do_grad("energy"): + model_predict["force"] = model_ret["energy_derv_r"].squeeze(-2) + if do_atomic_virial: + model_predict["atom_virial"] = model_ret["energy_derv_c"].squeeze(-3) + model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) + else: + model_predict["force"] = model_ret["dforce"] + return model_predict + + def forward_lower( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ): + model_ret = self.forward_common_lower( + extended_coord, + extended_atype, + nlist, + mapping, + ) + + model_predict = {} + model_predict["atom_energy"] = model_ret["energy"] + model_predict["energy"] = model_ret["energy_redu"] + if self.do_grad("energy"): + model_predict["extended_force"] = model_ret["energy_derv_r"].squeeze(-2) + model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) + if do_atomic_virial: + model_predict["extended_virial"] = model_ret["energy_derv_c"].squeeze( + -2 + ) + else: + assert model_ret["dforce"] is not None + model_predict["dforce"] = model_ret["dforce"] + model_predict = model_ret + return model_predict class EnergyModel(DPModel): diff --git a/deepmd/pt/model/model/linear_atomic_model.py b/deepmd/pt/model/model/linear_atomic_model.py new file mode 100644 index 0000000000..8b50f5e4f5 --- /dev/null +++ b/deepmd/pt/model/model/linear_atomic_model.py @@ -0,0 +1,315 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import sys +from abc import ( + abstractmethod, +) +from typing import ( + Dict, + List, + Optional, + Tuple, +) + +import torch + +from deepmd.dpmodel import ( + FittingOutputDef, + OutputVariableDef, +) +from deepmd.pt.utils.nlist import ( + build_multiple_neighbor_list, + get_multiple_nlist_key, + nlist_distinguish_types, +) + +from .base_atomic_model import ( + BaseAtomicModel, +) +from .dp_atomic_model import ( + DPAtomicModel, +) +from .model import ( + BaseModel, +) +from .pairtab_atomic_model import ( + PairTabModel, +) + + +class LinearAtomicModel(BaseModel, BaseAtomicModel): + """Linear model make linear combinations of several existing models. + + Parameters + ---------- + models : list[DPAtomicModel or PairTabModel] + A list of models to be combined. PairTabModel must be used together with a DPAtomicModel. + """ + + def __init__( + self, + models: List[BaseAtomicModel], + **kwargs, + ): + super().__init__() + self.models = torch.nn.ModuleList(models) + self.atomic_bias = None + self.distinguish_type_list = [ + model.distinguish_types() for model in self.models + ] + + def distinguish_types(self) -> bool: + """If distinguish different types by sorting.""" + return False + + def get_rcut(self) -> float: + """Get the cut-off radius.""" + return max(self.get_model_rcuts()) + + def get_model_rcuts(self) -> List[float]: + """Get the cut-off radius for each individual models.""" + return [model.get_rcut() for model in self.models] + + def get_sel(self) -> List[int]: + return [max([model.get_nsel() for model in self.models])] + + def get_model_nsels(self) -> List[int]: + """Get the processed sels for each individual models. Not distinguishing types.""" + return [model.get_nsel() for model in self.models] + + def get_model_sels(self) -> List[List[int]]: + """Get the sels for each individual models.""" + return [model.get_sel() for model in self.models] + + def _sort_rcuts_sels(self) -> Tuple[List[float], List[int]]: + # sort the pair of rcut and sels in ascending order, first based on sel, then on rcut. + rcuts = torch.tensor(self.get_model_rcuts(), dtype=torch.float64) + nsels = torch.tensor(self.get_model_nsels()) + zipped = torch.stack([torch.tensor(rcuts), torch.tensor(nsels)], dim=0).T + inner_sorting = torch.argsort(zipped[:, 1], dim=0) + inner_sorted = zipped[inner_sorting] + outer_sorting = torch.argsort(inner_sorted[:, 0], stable=True) + outer_sorted = inner_sorted[outer_sorting] + sorted_rcuts: List[float] = outer_sorted[:, 0].tolist() + sorted_sels: List[int] = outer_sorted[:, 1].to(torch.int64).tolist() + return sorted_rcuts, sorted_sels + + def forward_atomic( + self, + extended_coord: torch.Tensor, + extended_atype: torch.Tensor, + nlist: torch.Tensor, + mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + ) -> Dict[str, torch.Tensor]: + """Return atomic prediction. + + Parameters + ---------- + extended_coord + coodinates in extended region, (nframes, nall * 3) + extended_atype + atomic type in extended region, (nframes, nall) + nlist + neighbor list, (nframes, nloc, nsel). + mapping + mapps the extended indices to local indices. + fparam + frame parameter. (nframes, ndf) + aparam + atomic parameter. (nframes, nloc, nda) + + Returns + ------- + result_dict + the result dict, defined by the fitting net output def. + """ + nframes, nloc, nnei = nlist.shape + if self.do_grad(): + extended_coord.requires_grad_(True) + extended_coord = extended_coord.view(nframes, -1, 3) + sorted_rcuts, sorted_sels = self._sort_rcuts_sels() + nlists = build_multiple_neighbor_list( + extended_coord, + nlist, + sorted_rcuts, + sorted_sels, + ) + raw_nlists = [ + nlists[get_multiple_nlist_key(rcut, sel)] + for rcut, sel in zip(self.get_model_rcuts(), self.get_model_nsels()) + ] + nlists_ = [ + nl if not dt else nlist_distinguish_types(nl, extended_atype, sel) + for dt, nl, sel in zip( + self.distinguish_type_list, raw_nlists, self.get_model_sels() + ) + ] + ener_list = [] + + for i, model in enumerate(self.models): + ener_list.append( + model.forward_atomic( + extended_coord, + extended_atype, + nlists_[i], + mapping, + fparam, + aparam, + )["energy"] + ) + + weights = self._compute_weight(extended_coord, extended_atype, nlists_) + + if self.atomic_bias is not None: + raise NotImplementedError("Need to add bias in a future PR.") + else: + fit_ret = { + "energy": torch.sum( + torch.stack(ener_list) * torch.stack(weights), dim=0 + ), + } # (nframes, nloc, 1) + return fit_ret + + def fitting_output_def(self) -> FittingOutputDef: + return FittingOutputDef( + [ + OutputVariableDef( + name="energy", + shape=[1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ) + ] + ) + + @staticmethod + def serialize(models) -> dict: + return { + "models": [model.serialize() for model in models], + "model_name": [model.__class__.__name__ for model in models], + } + + @staticmethod + def deserialize(data) -> List[BaseAtomicModel]: + model_names = data["model_name"] + models = [ + getattr(sys.modules[__name__], name).deserialize(model) + for name, model in zip(model_names, data["models"]) + ] + return models + + @abstractmethod + def _compute_weight( + self, extended_coord, extended_atype, nlists_ + ) -> List[torch.Tensor]: + """This should be a list of user defined weights that matches the number of models to be combined.""" + raise NotImplementedError + + +class DPZBLLinearAtomicModel(LinearAtomicModel): + """Model linearly combine a list of AtomicModels. + + Parameters + ---------- + models + This linear model should take a DPAtomicModel and a PairTable model. + """ + + def __init__( + self, + dp_model: DPAtomicModel, + zbl_model: PairTabModel, + sw_rmin: float, + sw_rmax: float, + smin_alpha: Optional[float] = 0.1, + **kwargs, + ): + models = [dp_model, zbl_model] + super().__init__(models, **kwargs) + self.dp_model = dp_model + self.zbl_model = zbl_model + + self.sw_rmin = sw_rmin + self.sw_rmax = sw_rmax + self.smin_alpha = smin_alpha + + # this is a placeholder being updated in _compute_weight, to handle Jit attribute init error. + self.zbl_weight = torch.empty(0, dtype=torch.float64) + + def serialize(self) -> dict: + return { + "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), + "sw_rmin": self.sw_rmin, + "sw_rmax": self.sw_rmax, + "smin_alpha": self.smin_alpha, + } + + @classmethod + def deserialize(cls, data) -> "DPZBLLinearAtomicModel": + sw_rmin = data["sw_rmin"] + sw_rmax = data["sw_rmax"] + smin_alpha = data["smin_alpha"] + + dp_model, zbl_model = LinearAtomicModel.deserialize(data["models"]) + + return cls( + dp_model=dp_model, + zbl_model=zbl_model, + sw_rmin=sw_rmin, + sw_rmax=sw_rmax, + smin_alpha=smin_alpha, + ) + + def _compute_weight( + self, + extended_coord: torch.Tensor, + extended_atype: torch.Tensor, + nlists_: List[torch.Tensor], + ) -> List[torch.Tensor]: + """ZBL weight. + + Returns + ------- + List[torch.Tensor] + the atomic ZBL weight for interpolation. (nframes, nloc, 1) + """ + assert ( + self.sw_rmax > self.sw_rmin + ), "The upper boundary `sw_rmax` must be greater than the lower boundary `sw_rmin`." + + dp_nlist = nlists_[0] + zbl_nlist = nlists_[1] + + zbl_nnei = zbl_nlist.shape[-1] + dp_nnei = dp_nlist.shape[-1] + + # use the larger rr based on nlist + nlist_larger = zbl_nlist if zbl_nnei >= dp_nnei else dp_nlist + masked_nlist = torch.clamp(nlist_larger, 0) + pairwise_rr = PairTabModel._get_pairwise_dist(extended_coord, masked_nlist) + numerator = torch.sum( + pairwise_rr * torch.exp(-pairwise_rr / self.smin_alpha), dim=-1 + ) # masked nnei will be zero, no need to handle + denominator = torch.sum( + torch.where( + nlist_larger != -1, + torch.exp(-pairwise_rr / self.smin_alpha), + torch.zeros_like(nlist_larger), + ), + dim=-1, + ) # handle masked nnei. + + sigma = numerator / denominator # nfrmes, nloc + u = (sigma - self.sw_rmin) / (self.sw_rmax - self.sw_rmin) + coef = torch.zeros_like(u) + left_mask = sigma < self.sw_rmin + mid_mask = (self.sw_rmin <= sigma) & (sigma < self.sw_rmax) + right_mask = sigma >= self.sw_rmax + coef[left_mask] = 1 + smooth = -6 * u**5 + 15 * u**4 - 10 * u**3 + 1 + coef[mid_mask] = smooth[mid_mask] + coef[right_mask] = 0 + self.zbl_weight = coef # nframes, nloc + return [1 - coef.unsqueeze(-1), coef.unsqueeze(-1)] # to match the model order. diff --git a/deepmd/pt/model/model/pair_tab_model.py b/deepmd/pt/model/model/pairtab_atomic_model.py similarity index 79% rename from deepmd/pt/model/model/pair_tab_model.py rename to deepmd/pt/model/model/pairtab_atomic_model.py index 83089f86b4..98215191c1 100644 --- a/deepmd/pt/model/model/pair_tab_model.py +++ b/deepmd/pt/model/model/pairtab_atomic_model.py @@ -54,7 +54,7 @@ def __init__( super().__init__() self.tab_file = tab_file self.rcut = rcut - self.tab = PairTab(self.tab_file, rcut=rcut) + self.tab = self._set_pairtab(tab_file, rcut) # handle deserialization with no input file if self.tab_file is not None: @@ -78,6 +78,10 @@ def __init__( else: raise TypeError("sel must be int or list[int]") + @torch.jit.ignore + def _set_pairtab(self, tab_file: str, rcut: float) -> PairTab: + return PairTab(tab_file, rcut) + def fitting_output_def(self) -> FittingOutputDef: return FittingOutputDef( [ @@ -94,7 +98,10 @@ def fitting_output_def(self) -> FittingOutputDef: def get_rcut(self) -> float: return self.rcut - def get_sel(self) -> int: + def get_sel(self) -> List[int]: + return [self.sel] + + def get_nsel(self) -> int: return self.sel def distinguish_types(self) -> bool: @@ -117,39 +124,41 @@ def deserialize(cls, data) -> "PairTabModel": def forward_atomic( self, - extended_coord, - extended_atype, - nlist, + extended_coord: torch.Tensor, + extended_atype: torch.Tensor, + nlist: torch.Tensor, mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, do_atomic_virial: bool = False, ) -> Dict[str, torch.Tensor]: - self.nframes, self.nloc, self.nnei = nlist.shape - extended_coord = extended_coord.view(self.nframes, -1, 3) + nframes, nloc, nnei = nlist.shape + extended_coord = extended_coord.view(nframes, -1, 3) + if self.do_grad(): + extended_coord.requires_grad_(True) # this will mask all -1 in the nlist - masked_nlist = torch.clamp(nlist, 0) - - atype = extended_atype[:, : self.nloc] # (nframes, nloc) - pairwise_dr = self._get_pairwise_dist( - extended_coord - ) # (nframes, nall, nall, 3) - pairwise_rr = pairwise_dr.pow(2).sum(-1).sqrt() # (nframes, nall, nall) + mask = nlist >= 0 + masked_nlist = nlist * mask + atype = extended_atype[:, :nloc] # (nframes, nloc) + pairwise_rr = self._get_pairwise_dist( + extended_coord, masked_nlist + ) # (nframes, nloc, nnei) self.tab_data = self.tab_data.view( - self.tab.ntypes, self.tab.ntypes, self.tab.nspline, 4 + int(self.tab_info[-1]), int(self.tab_info[-1]), int(self.tab_info[2]), 4 ) - # to calculate the atomic_energy, we need 3 tensors, i_type, j_type, rr + # to calculate the atomic_energy, we need 3 tensors, i_type, j_type, pairwise_rr # i_type : (nframes, nloc), this is atype. # j_type : (nframes, nloc, nnei) j_type = extended_atype[ torch.arange(extended_atype.size(0))[:, None, None], masked_nlist ] - # slice rr to get (nframes, nloc, nnei) - rr = torch.gather(pairwise_rr[:, : self.nloc, :], 2, masked_nlist) - - raw_atomic_energy = self._pair_tabulated_inter(nlist, atype, j_type, rr) + raw_atomic_energy = self._pair_tabulated_inter( + nlist, atype, j_type, pairwise_rr + ) atomic_energy = 0.5 * torch.sum( torch.where( @@ -195,17 +204,18 @@ def _pair_tabulated_inter( This function is used to calculate the pairwise energy between two atoms. It uses a table containing cubic spline coefficients calculated in PairTab. """ + nframes, nloc, nnei = nlist.shape rmin = self.tab_info[0] hh = self.tab_info[1] hi = 1.0 / hh - self.nspline = int(self.tab_info[2] + 0.1) + nspline = int(self.tab_info[2] + 0.1) uu = (rr - rmin) * hi # this is broadcasted to (nframes,nloc,nnei) # if nnei of atom 0 has -1 in the nlist, uu would be 0. # this is to handle the nlist where the mask is set to 0, so that we don't raise exception for those atoms. - uu = torch.where(nlist != -1, uu, self.nspline + 1) + uu = torch.where(nlist != -1, uu, nspline + 1) if torch.any(uu < 0): raise Exception("coord go beyond table lower boundary") @@ -215,57 +225,44 @@ def _pair_tabulated_inter( uu -= idx table_coef = self._extract_spline_coefficient( - i_type, j_type, idx, self.tab_data, self.nspline + i_type, j_type, idx, self.tab_data, nspline ) - table_coef = table_coef.view(self.nframes, self.nloc, self.nnei, 4) - ener = self._calcualte_ener(table_coef, uu) + table_coef = table_coef.view(nframes, nloc, nnei, 4) + ener = self._calculate_ener(table_coef, uu) # here we need to overwrite energy to zero at rcut and beyond. mask_beyond_rcut = rr >= self.rcut # also overwrite values beyond extrapolation to zero - extrapolation_mask = rr >= self.tab.rmin + self.nspline * self.tab.hh + extrapolation_mask = rr >= rmin + nspline * hh ener[mask_beyond_rcut] = 0 ener[extrapolation_mask] = 0 return ener @staticmethod - def _get_pairwise_dist(coords: torch.Tensor) -> torch.Tensor: + def _get_pairwise_dist(coords: torch.Tensor, nlist: torch.Tensor) -> torch.Tensor: """Get pairwise distance `dr`. Parameters ---------- coords : torch.Tensor - The coordinate of the atoms shape of (nframes, nall, 3). + The coordinate of the atoms, shape of (nframes, nall, 3). + nlist + The masked nlist, shape of (nframes, nloc, nnei) Returns ------- torch.Tensor - The pairwise distance between the atoms (nframes, nall, nall, 3). - - Examples - -------- - coords = torch.tensor([[ - [0,0,0], - [1,3,5], - [2,4,6] - ]]) - - dist = tensor([[ - [[ 0, 0, 0], - [-1, -3, -5], - [-2, -4, -6]], - - [[ 1, 3, 5], - [ 0, 0, 0], - [-1, -1, -1]], - - [[ 2, 4, 6], - [ 1, 1, 1], - [ 0, 0, 0]] - ]]) + The pairwise distance between the atoms (nframes, nloc, nnei). """ - return coords.unsqueeze(2) - coords.unsqueeze(1) + nframes, nloc, nnei = nlist.shape + coord_l = coords[:, :nloc].view(nframes, -1, 1, 3) + index = nlist.view(nframes, -1).unsqueeze(-1).expand(-1, -1, 3) + coord_r = torch.gather(coords, 1, index) + coord_r = coord_r.view(nframes, nloc, nnei, 3) + diff = coord_r - coord_l + pairwise_rr = torch.linalg.norm(diff, dim=-1, keepdim=True).squeeze(-1) + return pairwise_rr @staticmethod def _extract_spline_coefficient( @@ -316,7 +313,7 @@ def _extract_spline_coefficient( return final_coef @staticmethod - def _calcualte_ener(coef: torch.Tensor, uu: torch.Tensor) -> torch.Tensor: + def _calculate_ener(coef: torch.Tensor, uu: torch.Tensor) -> torch.Tensor: """Calculate energy using spline coeeficients. Parameters diff --git a/source/tests/common/dpmodel/test_linear_atomic_model.py b/source/tests/common/dpmodel/test_linear_atomic_model.py new file mode 100644 index 0000000000..6dcff97d74 --- /dev/null +++ b/source/tests/common/dpmodel/test_linear_atomic_model.py @@ -0,0 +1,167 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest +from unittest.mock import ( + patch, +) + +import numpy as np + +from deepmd.dpmodel.descriptor.se_e2_a import ( + DescrptSeA, +) +from deepmd.dpmodel.fitting.invar_fitting import ( + InvarFitting, +) +from deepmd.dpmodel.model.dp_atomic_model import ( + DPAtomicModel, +) +from deepmd.dpmodel.model.linear_atomic_model import ( + DPZBLLinearAtomicModel, +) +from deepmd.dpmodel.model.pairtab_atomic_model import ( + PairTabModel, +) + + +class TestWeightCalculation(unittest.TestCase): + @patch("numpy.loadtxt") + def test_pairwise(self, mock_loadtxt): + file_path = "dummy_path" + mock_loadtxt.return_value = np.array( + [ + [0.05, 1.0, 2.0, 3.0], + [0.1, 0.8, 1.6, 2.4], + [0.15, 0.5, 1.0, 1.5], + [0.2, 0.25, 0.4, 0.75], + [0.25, 0.0, 0.0, 0.0], + ] + ) + extended_atype = np.array([[0, 0]]) + nlist = np.array([[[1], [-1]]]) + + ds = DescrptSeA( + rcut=0.3, + rcut_smth=0.4, + sel=[3], + ) + ft = InvarFitting( + "energy", + 2, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ) + + type_map = ["foo", "bar"] + zbl_model = PairTabModel(tab_file=file_path, rcut=0.3, sel=2) + dp_model = DPAtomicModel(ds, ft, type_map=type_map) + + wgt_model = DPZBLLinearAtomicModel( + dp_model, + zbl_model, + sw_rmin=0.1, + sw_rmax=0.25, + ) + wgt_res = [] + for dist in np.linspace(0.05, 0.3, 10): + extended_coord = np.array( + [ + [ + [0.0, 0.0, 0.0], + [0.0, dist, 0.0], + ], + ] + ) + + wgt_model.forward_atomic(extended_coord, extended_atype, nlist) + + wgt_res.append(wgt_model.zbl_weight) + results = np.stack(wgt_res).reshape(10, 2) + excepted_res = np.array( + [ + [1.0, 0.0], + [1.0, 0.0], + [0.9995, 0.0], + [0.9236, 0.0], + [0.6697, 0.0], + [0.3303, 0.0], + [0.0764, 0.0], + [0.0005, 0.0], + [0.0, 0.0], + [0.0, 0.0], + ], + ) + np.testing.assert_allclose(results, excepted_res, rtol=0.0001, atol=0.0001) + + +class TestIntegration(unittest.TestCase): + @patch("numpy.loadtxt") + def setUp(self, mock_loadtxt): + self.nloc = 3 + self.nall = 4 + self.nf, self.nt = 1, 2 + self.coord_ext = np.array( + [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 1], + [0, -2, 0], + ], + dtype=np.float64, + ).reshape([1, self.nall * 3]) + self.atype_ext = np.array([0, 0, 1, 0], dtype=int).reshape([1, self.nall]) + self.sel = [5, 2] + self.nlist = np.array( + [ + [1, 3, -1, -1, -1, 2, -1], + [0, -1, -1, -1, -1, 2, -1], + [0, 1, -1, -1, -1, -1, -1], + ], + dtype=int, + ).reshape([1, self.nloc, sum(self.sel)]) + self.rcut = 0.4 + self.rcut_smth = 2.2 + + file_path = "dummy_path" + mock_loadtxt.return_value = np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + ] + ) + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ) + type_map = ["foo", "bar"] + dp_model = DPAtomicModel(ds, ft, type_map=type_map) + zbl_model = PairTabModel(file_path, self.rcut, sum(self.sel)) + self.md0 = DPZBLLinearAtomicModel( + dp_model, + zbl_model, + sw_rmin=0.1, + sw_rmax=0.25, + ) + self.md1 = DPZBLLinearAtomicModel.deserialize(self.md0.serialize()) + + def test_self_consistency(self): + ret0 = self.md0.forward_atomic(self.coord_ext, self.atype_ext, self.nlist) + ret1 = self.md1.forward_atomic(self.coord_ext, self.atype_ext, self.nlist) + np.testing.assert_allclose( + ret0["energy"], + ret1["energy"], + ) + + +if __name__ == "__main__": + unittest.main(warnings="ignore") diff --git a/source/tests/common/dpmodel/test_pairtab.py b/source/tests/common/dpmodel/test_pairtab_atomic_model.py similarity index 97% rename from source/tests/common/dpmodel/test_pairtab.py rename to source/tests/common/dpmodel/test_pairtab_atomic_model.py index 3713d33510..f1e7bd257c 100644 --- a/source/tests/common/dpmodel/test_pairtab.py +++ b/source/tests/common/dpmodel/test_pairtab_atomic_model.py @@ -6,7 +6,7 @@ import numpy as np -from deepmd.dpmodel.model.pair_tab_model import ( +from deepmd.dpmodel.model.pairtab_atomic_model import ( PairTabModel, ) @@ -199,5 +199,6 @@ def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: np.testing.assert_allclose(results, expected_result, 0.0001, 0.0001) - if __name__ == "__main__": - unittest.main() + +if __name__ == "__main__": + unittest.main(warnings="ignore") diff --git a/source/tests/common/dpmodel/test_pairtab_preprocess.py b/source/tests/common/dpmodel/test_pairtab_preprocess.py index 26f96a3ca4..da3b9251f7 100644 --- a/source/tests/common/dpmodel/test_pairtab_preprocess.py +++ b/source/tests/common/dpmodel/test_pairtab_preprocess.py @@ -273,3 +273,7 @@ def test_preprocess(self): rtol=1e-03, atol=1e-03, ) + + +if __name__ == "__main__": + unittest.main(warnings="ignore") diff --git a/source/tests/pt/model/test_autodiff.py b/source/tests/pt/model/test_autodiff.py index 8840fbdd4c..24dc69458d 100644 --- a/source/tests/pt/model/test_autodiff.py +++ b/source/tests/pt/model/test_autodiff.py @@ -7,6 +7,7 @@ from deepmd.pt.model.model import ( get_model, + get_zbl_model, ) from deepmd.pt.utils import ( env, @@ -20,6 +21,7 @@ model_dpa1, model_dpa2, model_se_e2_a, + model_zbl, ) @@ -190,3 +192,19 @@ def setUp(self): model_params = copy.deepcopy(model_dpa2) self.type_split = True self.model = get_model(model_params, sampled).to(env.DEVICE) + + +class TestEnergyModelZBLForce(unittest.TestCase, ForceTest): + def setUp(self): + model_params = copy.deepcopy(model_zbl) + sampled = make_sample(model_params) + self.type_split = False + self.model = get_zbl_model(model_params, sampled).to(env.DEVICE) + + +class TestEnergyModelZBLVirial(unittest.TestCase, VirialTest): + def setUp(self): + model_params = copy.deepcopy(model_zbl) + sampled = make_sample(model_params) + self.type_split = False + self.model = get_zbl_model(model_params, sampled).to(env.DEVICE) diff --git a/source/tests/pt/model/test_linear_atomic_model.py b/source/tests/pt/model/test_linear_atomic_model.py new file mode 100644 index 0000000000..211b1f8215 --- /dev/null +++ b/source/tests/pt/model/test_linear_atomic_model.py @@ -0,0 +1,183 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest +from unittest.mock import ( + patch, +) + +import numpy as np +import torch + +from deepmd.dpmodel.model.linear_atomic_model import ( + DPZBLLinearAtomicModel as DPDPZBLLinearAtomicModel, +) +from deepmd.pt.model.descriptor.se_a import ( + DescrptSeA, +) +from deepmd.pt.model.model.dp_atomic_model import ( + DPAtomicModel, +) +from deepmd.pt.model.model.ener import ( + ZBLModel, +) +from deepmd.pt.model.model.linear_atomic_model import ( + DPZBLLinearAtomicModel, +) +from deepmd.pt.model.model.pairtab_atomic_model import ( + PairTabModel, +) +from deepmd.pt.model.task.ener import ( + InvarFitting, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, + to_torch_tensor, +) + +from .test_env_mat import ( + TestCaseSingleFrameWithNlist, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION + + +class TestWeightCalculation(unittest.TestCase): + @patch("numpy.loadtxt") + def test_pairwise(self, mock_loadtxt): + file_path = "dummy_path" + mock_loadtxt.return_value = np.array( + [ + [0.05, 1.0, 2.0, 3.0], + [0.1, 0.8, 1.6, 2.4], + [0.15, 0.5, 1.0, 1.5], + [0.2, 0.25, 0.4, 0.75], + [0.25, 0.0, 0.0, 0.0], + ] + ) + extended_atype = torch.tensor([[0, 0]]) + nlist = torch.tensor([[[1], [-1]]]) + + ds = DescrptSeA( + rcut=0.3, + rcut_smth=0.4, + sel=[3], + ).to(env.DEVICE) + ft = InvarFitting( + "energy", + 2, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ).to(env.DEVICE) + + type_map = ["foo", "bar"] + zbl_model = PairTabModel(tab_file=file_path, rcut=0.3, sel=2) + dp_model = DPAtomicModel(ds, ft, type_map=type_map, resuming=True).to( + env.DEVICE + ) + wgt_model = DPZBLLinearAtomicModel( + dp_model, + zbl_model, + sw_rmin=0.1, + sw_rmax=0.25, + ) + wgt_res = [] + for dist in np.linspace(0.05, 0.3, 10): + extended_coord = torch.tensor( + [ + [ + [0.0, 0.0, 0.0], + [0.0, dist, 0.0], + ], + ] + ) + + wgt_model.forward_atomic(extended_coord, extended_atype, nlist) + + wgt_res.append(wgt_model.zbl_weight) + results = torch.stack(wgt_res).reshape(10, 2) + excepted_res = torch.tensor( + [ + [1.0, 0.0], + [1.0, 0.0], + [0.9995, 0.0], + [0.9236, 0.0], + [0.6697, 0.0], + [0.3303, 0.0], + [0.0764, 0.0], + [0.0005, 0.0], + [0.0, 0.0], + [0.0, 0.0], + ], + dtype=torch.float64, + ) + torch.testing.assert_close(results, excepted_res, rtol=0.0001, atol=0.0001) + + +class TestIntegration(unittest.TestCase, TestCaseSingleFrameWithNlist): + @patch("numpy.loadtxt") + def setUp(self, mock_loadtxt): + TestCaseSingleFrameWithNlist.setUp(self) + file_path = "dummy_path" + mock_loadtxt.return_value = np.array( + [ + [0.005, 1.0, 2.0, 3.0], + [0.01, 0.8, 1.6, 2.4], + [0.015, 0.5, 1.0, 1.5], + [0.02, 0.25, 0.4, 0.75], + ] + ) + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ).to(env.DEVICE) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + distinguish_types=ds.distinguish_types(), + ).to(env.DEVICE) + type_map = ["foo", "bar"] + dp_model = DPAtomicModel(ds, ft, type_map=type_map, resuming=True).to( + env.DEVICE + ) + zbl_model = PairTabModel(file_path, self.rcut, sum(self.sel)) + self.md0 = DPZBLLinearAtomicModel( + dp_model, + zbl_model, + sw_rmin=0.1, + sw_rmax=0.25, + ).to(env.DEVICE) + self.md1 = DPZBLLinearAtomicModel.deserialize(self.md0.serialize()).to( + env.DEVICE + ) + self.md2 = DPDPZBLLinearAtomicModel.deserialize(self.md0.serialize()) + self.md3 = ZBLModel(dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25) + + def test_self_consistency(self): + args = [ + to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] + ] + ret0 = self.md0.forward_atomic(*args) + ret1 = self.md1.forward_atomic(*args) + ret2 = self.md2.forward_atomic(self.coord_ext, self.atype_ext, self.nlist) + np.testing.assert_allclose( + to_numpy_array(ret0["energy"]), + to_numpy_array(ret1["energy"]), + ) + + np.testing.assert_allclose( + to_numpy_array(ret0["energy"]), ret2["energy"], atol=0.001, rtol=0.001 + ) + + def test_jit(self): + torch.jit.script(self.md1) + torch.jit.script(self.md3) + + +if __name__ == "__main__": + unittest.main(warnings="ignore") diff --git a/source/tests/pt/model/test_pairtab.py b/source/tests/pt/model/test_pairtab_atomic_model.py similarity index 95% rename from source/tests/pt/model/test_pairtab.py rename to source/tests/pt/model/test_pairtab_atomic_model.py index e27e2cf2a1..23718c134a 100644 --- a/source/tests/pt/model/test_pairtab.py +++ b/source/tests/pt/model/test_pairtab_atomic_model.py @@ -7,10 +7,13 @@ import numpy as np import torch -from deepmd.dpmodel.model.pair_tab_model import PairTabModel as DPPairTabModel -from deepmd.pt.model.model.pair_tab_model import ( +from deepmd.dpmodel.model.pairtab_atomic_model import PairTabModel as DPPairTabModel +from deepmd.pt.model.model.pairtab_atomic_model import ( PairTabModel, ) +from deepmd.pt.utils.utils import ( + to_numpy_array, +) class TestPairTab(unittest.TestCase): @@ -114,9 +117,8 @@ def test_cross_deserialize(self): expected_result = self.model.forward_atomic( self.extended_coord, self.extended_atype, torch.from_numpy(self.nlist) ) - np.testing.assert_allclose( - result["energy"], expected_result["energy"], 0.0001, 0.0001 + result["energy"], to_numpy_array(expected_result["energy"]), 0.0001, 0.0001 ) @@ -235,5 +237,6 @@ def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: torch.testing.assert_close(results, expected_result, rtol=0.0001, atol=0.0001) - if __name__ == "__main__": - unittest.main() + +if __name__ == "__main__": + unittest.main(warnings="ignore") diff --git a/source/tests/pt/model/test_permutation.py b/source/tests/pt/model/test_permutation.py index b9724bb2af..2301b6ea10 100644 --- a/source/tests/pt/model/test_permutation.py +++ b/source/tests/pt/model/test_permutation.py @@ -12,6 +12,7 @@ ) from deepmd.pt.model.model import ( get_model, + get_zbl_model, ) from deepmd.pt.utils import ( env, @@ -45,6 +46,30 @@ "data_stat_nbatch": 20, } +model_zbl = { + "type_map": ["O", "H", "B"], + "use_srtab": "source/tests/pt/model/water/data/zbl_tab_potential/H2O_tab_potential.txt", + "smin_alpha": 0.1, + "sw_rmin": 0.2, + "sw_rmax": 1.0, + "descriptor": { + "type": "se_e2_a", + "sel": [46, 92, 4], + "rcut_smth": 0.50, + "rcut": 6.00, + "neuron": [25, 50, 100], + "resnet_dt": False, + "axis_neuron": 16, + "seed": 1, + }, + "fitting_net": { + "neuron": [24, 24, 24], + "resnet_dt": True, + "seed": 1, + }, + "data_stat_nbatch": 20, +} + model_dpa2 = { "type_map": ["O", "H", "B"], "descriptor": { @@ -302,6 +327,14 @@ def setUp(self): self.model = get_model(model_params, sampled).to(env.DEVICE) +class TestEnergyModelZBL(unittest.TestCase, PermutationTest): + def setUp(self): + model_params = copy.deepcopy(model_zbl) + sampled = make_sample(model_params) + self.type_split = False + self.model = get_zbl_model(model_params, sampled).to(env.DEVICE) + + # class TestEnergyFoo(unittest.TestCase): # def test(self): # model_params = model_dpau diff --git a/source/tests/pt/model/test_rot.py b/source/tests/pt/model/test_rot.py index 7222fd6f69..982753e94f 100644 --- a/source/tests/pt/model/test_rot.py +++ b/source/tests/pt/model/test_rot.py @@ -9,6 +9,7 @@ ) from deepmd.pt.model.model import ( get_model, + get_zbl_model, ) from deepmd.pt.utils import ( env, @@ -20,6 +21,7 @@ model_dpa2, model_hybrid, model_se_e2_a, + model_zbl, ) dtype = torch.float64 @@ -177,5 +179,13 @@ def setUp(self): self.model = get_model(model_params, sampled).to(env.DEVICE) +class TestEnergyModelZBL(unittest.TestCase, RotTest): + def setUp(self): + model_params = copy.deepcopy(model_zbl) + sampled = make_sample(model_params) + self.type_split = False + self.model = get_zbl_model(model_params, sampled).to(env.DEVICE) + + if __name__ == "__main__": unittest.main() diff --git a/source/tests/pt/model/test_smooth.py b/source/tests/pt/model/test_smooth.py index 2e3bf61d10..f2f45c74aa 100644 --- a/source/tests/pt/model/test_smooth.py +++ b/source/tests/pt/model/test_smooth.py @@ -9,6 +9,7 @@ ) from deepmd.pt.model.model import ( get_model, + get_zbl_model, ) from deepmd.pt.utils import ( env, @@ -20,6 +21,7 @@ model_dpa2, model_hybrid, model_se_e2_a, + model_zbl, ) dtype = torch.float64 @@ -210,6 +212,15 @@ def setUp(self): self.epsilon, self.aprec = None, None +class TestEnergyModelZBL(unittest.TestCase, SmoothTest): + def setUp(self): + model_params = copy.deepcopy(model_zbl) + sampled = make_sample(model_params) + self.type_split = False + self.model = get_zbl_model(model_params, sampled).to(env.DEVICE) + self.epsilon, self.aprec = None, None + + # class TestEnergyFoo(unittest.TestCase): # def test(self): # model_params = model_dpau diff --git a/source/tests/pt/model/test_trans.py b/source/tests/pt/model/test_trans.py index e5d379b9ff..967d505c6d 100644 --- a/source/tests/pt/model/test_trans.py +++ b/source/tests/pt/model/test_trans.py @@ -9,6 +9,7 @@ ) from deepmd.pt.model.model import ( get_model, + get_zbl_model, ) from deepmd.pt.utils import ( env, @@ -20,6 +21,7 @@ model_dpa2, model_hybrid, model_se_e2_a, + model_zbl, ) dtype = torch.float64 @@ -133,5 +135,13 @@ def setUp(self): self.model = get_model(model_params, sampled).to(env.DEVICE) +class TestEnergyModelZBL(unittest.TestCase, TransTest): + def setUp(self): + model_params = copy.deepcopy(model_zbl) + sampled = make_sample(model_params) + self.type_split = False + self.model = get_zbl_model(model_params, sampled).to(env.DEVICE) + + if __name__ == "__main__": unittest.main() diff --git a/source/tests/pt/model/water/data/zbl_tab_potential/H2O_tab_potential.txt b/source/tests/pt/model/water/data/zbl_tab_potential/H2O_tab_potential.txt new file mode 100644 index 0000000000..66fcb8e946 --- /dev/null +++ b/source/tests/pt/model/water/data/zbl_tab_potential/H2O_tab_potential.txt @@ -0,0 +1,1000 @@ +0.0010 913709.625838 114389.26607 14320.660836 25838 114389.26607 14320.660836 +0.0020 453190.075792 56822.165078 7124.559066 75792 56822.165078 7124.559066 +0.0030 299716.609389 37635.860646 4726.059712 09389 37635.860646 4726.059712 +0.0040 223004.208152 28044.724786 3526.959232 08152 28044.724786 3526.959232 +0.0050 176995.875921 22291.632310 2807.616935 75921 22291.632310 2807.616935 +0.0060 146339.286793 18457.541826 2328.152606 86793 18457.541826 2328.152606 +0.0070 124454.877677 15720.007305 1985.760451 77677 15720.007305 1985.760451 +0.0080 108052.871443 13667.805976 1729.037583 71443 13667.805976 1729.037583 +0.0090 95305.6179694 12072.480958 1529.426853 79694 12072.480958 1529.426853 +0.0100 85116.5305655 10796.958308 1369.793979 05655 10796.958308 1369.793979 +0.0110 76787.7843454 9754.0093240 1239.235334 43454 9754.0093240 1239.235334 +0.0120 69854.1654175 8885.4816862 1130.481842 54175 8885.4816862 1130.481842 +0.0130 63993.6050636 8151.1162355 1038.501071 50636 8151.1162355 1038.501071 +0.0140 58976.0564146 7522.1565542 959.6984312 64146 7522.1565542 959.6984312 +0.0150 54632.8204564 6977.5147177 891.4378965 04564 6977.5147177 891.4378965 +0.0160 50837.3747846 6501.3748881 831.7424519 47846 6501.3748881 831.7424519 +0.0170 47492.9686820 6081.6426991 779.1002669 86820 6081.6426991 779.1002669 +0.0180 44524.3531708 5708.9115119 732.3354774 31708 5708.9115119 732.3354774 +0.0190 41872.1226283 5375.7551174 690.5197734 26283 5375.7551174 690.5197734 +0.0200 39488.7539185 5076.2326272 652.9105109 39185 5076.2326272 652.9105109 +0.0210 37335.7772003 4805.5348243 618.9065049 72003 4805.5348243 618.9065049 +0.0220 35381.7183353 4559.7269643 588.0158802 83353 4559.7269643 588.0158802 +0.0230 33600.5780582 4335.5586712 559.8323083 80582 4335.5586712 559.8323083 +0.0240 31970.6913556 4130.3213594 534.0171847 13556 4130.3213594 534.0171847 +0.0250 30473.8605947 3941.7398750 510.2860845 05947 3941.7398750 510.2860845 +0.0260 29094.6886984 3767.8891424 488.3983430 86984 3767.8891424 488.3983430 +0.0270 27820.0605059 3607.1293341 468.1489521 05059 3607.1293341 468.1489521 +0.0280 26638.7352692 3458.0549325 449.3621928 52692 3458.0549325 449.3621928 +0.0290 25541.0234620 3319.4543312 431.8865855 34620 3319.4543312 431.8865855 +0.0300 24518.5282265 3190.2775152 415.5908499 82265 3190.2775152 415.5908499 +0.0310 23563.9368637 3069.6099976 400.3606472 68637 3069.6099976 400.3606472 +0.0320 22670.8514191 2956.6516420 386.0959329 14191 2956.6516420 386.0959329 +0.0330 21833.6500715 2850.6993366 372.7087910 00715 2850.6993366 372.7087910 +0.0340 21047.3729830 2751.1327248 360.1216502 29830 2751.1327248 360.1216502 +0.0350 20307.6277175 2657.4023825 348.2658062 77175 2657.4023825 348.2658062 +0.0360 19610.5104235 2569.0199661 337.0801904 04235 2569.0199661 337.0801904 +0.0370 18952.5397978 2485.5499575 326.5103374 97978 2485.5499575 326.5103374 +0.0380 18330.6014769 2406.6027127 316.5075168 14769 2406.6027127 316.5075168 +0.0390 17741.9009829 2331.8285805 307.0279975 09829 2331.8285805 307.0279975 +0.0400 17183.9237284 2260.9129024 298.0324229 37284 2260.9129024 298.0324229 +0.0410 16654.4008745 2193.5717452 289.4852776 08745 2193.5717452 289.4852776 +0.0420 16151.2800661 2129.5482423 281.3544296 00661 2129.5482423 281.3544296 +0.0430 15672.7002509 2068.6094464 273.6107373 02509 2068.6094464 273.6107373 +0.0440 15216.9699328 2010.5436107 266.2277097 99328 2010.5436107 266.2277097 +0.0450 14782.5483242 1955.1578329 259.1812115 83242 1955.1578329 259.1812115 +0.0460 14368.0289580 1902.2760069 252.4492073 89580 1902.2760069 252.4492073 +0.0470 13972.1253902 1851.7370350 246.0115383 53902 1851.7370350 246.0115383 +0.0480 13593.6586918 1803.3932646 239.8497262 86918 1803.3932646 239.8497262 +0.0490 13231.5464708 1757.1091159 233.9468027 64708 1757.1091159 233.9468027 +0.0500 12884.7932124 1712.7598740 228.2871576 32124 1712.7598740 228.2871576 +0.0510 12552.4817558 1670.2306236 222.8564060 17558 1670.2306236 222.8564060 +0.0520 12233.7657548 1629.4153064 217.6412705 57548 1629.4153064 217.6412705 +0.0530 11927.8629910 1590.2158855 212.6294767 29910 1590.2158855 212.6294767 +0.0540 11634.0494314 1552.5416017 207.8096602 94314 1552.5416017 207.8096602 +0.0550 11351.6539336 1516.3083123 203.1712838 39336 1516.3083123 203.1712838 +0.0560 11080.0535186 1481.4378999 198.7045641 35186 1481.4378999 198.7045641 +0.0570 10818.6691413 1447.8577434 194.4004048 91413 1447.8577434 194.4004048 +0.0580 10566.9618984 1415.5002442 190.2503376 18984 1415.5002442 190.2503376 +0.0590 10324.4296227 1384.3024001 186.2464693 96227 1384.3024001 186.2464693 +0.0600 10090.6038173 1354.2054222 182.3814335 38173 1354.2054222 182.3814335 +0.0610 9865.0468917 1325.1543893 178.6483475 68917 1325.1543893 178.6483475 +0.0620 9647.3496659 1297.0979357 175.0407734 96659 1297.0979357 175.0407734 +0.0630 9437.1291115 1269.9879689 171.5526826 91115 1269.9879689 171.5526826 +0.0640 9234.0263053 1243.7794136 168.1784240 63053 1243.7794136 168.1784240 +0.0650 9037.7045714 1218.4299795 164.9126949 45714 1218.4299795 164.9126949 +0.0660 8847.8477928 1193.8999501 161.7505145 77928 1193.8999501 161.7505145 +0.0670 8664.1588738 1170.1519903 158.6871999 88738 1170.1519903 158.6871999 +0.0680 8486.3583383 1147.1509714 155.7183444 83383 1147.1509714 155.7183444 +0.0690 8314.1830501 1124.8638108 152.8397972 30501 1124.8638108 152.8397972 +0.0700 8147.3850427 1103.2593259 150.0476452 50427 1103.2593259 150.0476452 +0.0710 7985.7304489 1082.3080999 147.3381963 04489 1082.3080999 147.3381963 +0.0720 7828.9985183 1061.9823592 144.7079640 85183 1061.9823592 144.7079640 +0.0730 7676.9807165 1042.2558606 142.1536534 07165 1042.2558606 142.1536534 +0.0740 7529.4798977 1023.1037878 139.6721482 98977 1023.1037878 139.6721482 +0.0750 7386.3095424 1004.5026562 137.2604986 95424 1004.5026562 137.2604986 +0.0760 7247.2930565 986.4302250 134.9159107 930565 986.4302250 134.9159107 +0.0770 7112.2631243 968.8654164 132.6357361 631243 968.8654164 132.6357361 +0.0780 6981.0611116 951.7882409 130.4174626 611116 951.7882409 130.4174626 +0.0790 6853.5365143 935.1797284 128.2587056 365143 935.1797284 128.2587056 +0.0800 6729.5464483 919.0218641 126.1572004 464483 919.0218641 126.1572004 +0.0810 6608.9551768 903.2975297 124.1107942 551768 903.2975297 124.1107942 +0.0820 6491.6336731 887.9904484 122.1174397 336731 887.9904484 122.1174397 +0.0830 6377.4592142 873.0851342 120.1751889 592142 873.0851342 120.1751889 +0.0840 6266.3150042 858.5668449 118.2821865 150042 858.5668449 118.2821865 +0.0850 6158.0898234 844.4215378 116.4366651 898234 844.4215378 116.4366651 +0.0860 6052.6777030 830.6358295 114.6369400 777030 830.6358295 114.6369400 +0.0870 5949.9776216 817.1969572 112.8814040 776216 817.1969572 112.8814040 +0.0880 5849.8932223 804.0927442 111.1685235 932223 804.0927442 111.1685235 +0.0890 5752.3325494 791.3115660 109.4968341 325494 791.3115660 109.4968341 +0.0900 5657.2078026 778.8423203 107.8649368 078026 778.8423203 107.8649368 +0.0910 5564.4351069 766.6743978 106.2714943 351069 766.6743978 106.2714943 +0.0920 5473.9342981 754.7976551 104.7152279 342981 754.7976551 104.7152279 +0.0930 5385.6287222 743.2023904 103.1949141 287222 743.2023904 103.1949141 +0.0940 5299.4450471 731.8793190 101.7093819 450471 731.8793190 101.7093819 +0.0950 5215.3130867 720.8195518 100.2575097 130867 720.8195518 100.2575097 +0.0960 5133.1656359 710.0145745 98.8382229 1656359 710.0145745 98.8382229 +0.0970 5052.9383157 699.4562281 97.4504918 9383157 699.4562281 97.4504918 +0.0980 4974.5694279 689.1366911 96.0933285 5694279 689.1366911 96.0933285 +0.0990 4897.9998188 679.0484617 94.7657857 9998188 679.0484617 94.7657857 +0.1000 4823.1727507 669.1843423 93.4669540 1727507 669.1843423 93.4669540 +0.1010 4750.0337815 659.5374244 92.1959604 0337815 659.5374244 92.1959604 +0.1020 4678.5306510 650.1010737 90.9519663 5306510 650.1010737 90.9519663 +0.1030 4608.6131741 640.8689177 89.7341659 6131741 640.8689177 89.7341659 +0.1040 4540.2331402 631.8348323 88.5417846 2331402 631.8348323 88.5417846 +0.1050 4473.3442182 622.9929301 87.3740776 3442182 622.9929301 87.3740776 +0.1060 4407.9018671 614.3375495 86.2303284 9018671 614.3375495 86.2303284 +0.1070 4343.8632512 605.8632435 85.1098475 8632512 605.8632435 85.1098475 +0.1080 4281.1871608 597.5647703 84.0119712 1871608 597.5647703 84.0119712 +0.1090 4219.8339365 589.4370834 82.9360602 8339365 589.4370834 82.9360602 +0.1100 4159.7653981 581.4753230 81.8814988 7653981 581.4753230 81.8814988 +0.1110 4100.9447770 573.6748074 80.8476936 9447770 573.6748074 80.8476936 +0.1120 4043.3366528 566.0310249 79.8340726 3366528 566.0310249 79.8340726 +0.1130 3986.9068928 558.5396262 78.8400844 9068928 558.5396262 78.8400844 +0.1140 3931.6225949 551.1964175 77.8651968 6225949 551.1964175 77.8651968 +0.1150 3877.4520333 543.9973537 76.9088966 4520333 543.9973537 76.9088966 +0.1160 3824.3646071 536.9385314 75.9706883 3646071 536.9385314 75.9706883 +0.1170 3772.3307921 530.0161836 75.0500935 3307921 530.0161836 75.0500935 +0.1180 3721.3220939 523.2266731 74.1466504 3220939 523.2266731 74.1466504 +0.1190 3671.3110047 516.5664876 73.2599126 3110047 516.5664876 73.2599126 +0.1200 3622.2709610 510.0322343 72.3894489 2709610 510.0322343 72.3894489 +0.1210 3574.1763045 503.6206346 71.5348425 1763045 503.6206346 71.5348425 +0.1220 3527.0022444 497.3285198 70.6956904 0022444 497.3285198 70.6956904 +0.1230 3480.7248214 491.1528264 69.8716028 7248214 491.1528264 69.8716028 +0.1240 3435.3208739 485.0905919 69.0622028 3208739 485.0905919 69.0622028 +0.1250 3390.7680053 479.1389505 68.2671256 7680053 479.1389505 68.2671256 +0.1260 3347.0445536 473.2951298 67.4860180 0445536 473.2951298 67.4860180 +0.1270 3304.1295618 467.5564462 66.7185382 1295618 467.5564462 66.7185382 +0.1280 3262.0027498 461.9203022 65.9643553 0027498 461.9203022 65.9643553 +0.1290 3220.6444879 456.3841826 65.2231486 6444879 456.3841826 65.2231486 +0.1300 3180.0357713 450.9456517 64.4946075 0357713 450.9456517 64.4946075 +0.1310 3140.1581958 445.6023495 63.7784310 1581958 445.6023495 63.7784310 +0.1320 3100.9939349 440.3519898 63.0743273 9939349 440.3519898 63.0743273 +0.1330 3062.5257173 435.1923563 62.3820136 5257173 435.1923563 62.3820136 +0.1340 3024.7368060 430.1213011 61.7012156 7368060 430.1213011 61.7012156 +0.1350 2987.6109783 425.1367411 61.0316673 6109783 425.1367411 61.0316673 +0.1360 2951.1325064 420.2366563 60.3731105 1325064 420.2366563 60.3731105 +0.1370 2915.2861387 415.4190873 59.7252948 2861387 415.4190873 59.7252948 +0.1380 2880.0570829 410.6821328 59.0879771 0570829 410.6821328 59.0879771 +0.1390 2845.4309885 406.0239478 58.4609214 4309885 406.0239478 58.4609214 +0.1400 2811.3939311 401.4427415 57.8438986 3939311 401.4427415 57.8438986 +0.1410 2777.9323966 396.9367752 57.2366861 9323966 396.9367752 57.2366861 +0.1420 2745.0332671 392.5043608 56.6390678 0332671 392.5043608 56.6390678 +0.1430 2712.6838060 388.1438584 56.0508336 6838060 388.1438584 56.0508336 +0.1440 2680.8716450 383.8536753 55.4717796 8716450 383.8536753 55.4717796 +0.1450 2649.5847710 379.6322639 54.9017073 5847710 379.6322639 54.9017073 +0.1460 2618.8115134 375.4781203 54.3404239 8115134 375.4781203 54.3404239 +0.1470 2588.5405327 371.3897825 53.7877420 5405327 371.3897825 53.7877420 +0.1480 2558.7608086 367.3658297 53.2434792 7608086 367.3658297 53.2434792 +0.1490 2529.4616294 363.4048800 52.7074581 4616294 363.4048800 52.7074581 +0.1500 2500.6325811 359.5055896 52.1795062 6325811 359.5055896 52.1795062 +0.1510 2472.2635377 355.6666516 51.6594557 2635377 355.6666516 51.6594557 +0.1520 2444.3446512 351.8867943 51.1471432 3446512 351.8867943 51.1471432 +0.1530 2416.8663423 348.1647806 50.6424097 8663423 348.1647806 50.6424097 +0.1540 2389.8192919 344.4994064 50.1451003 8192919 344.4994064 50.1451003 +0.1550 2363.1944319 340.8894997 49.6550644 1944319 340.8894997 49.6550644 +0.1560 2336.9829372 337.3339196 49.1721551 9829372 337.3339196 49.1721551 +0.1570 2311.1762181 333.8315552 48.6962295 1762181 333.8315552 48.6962295 +0.1580 2285.7659120 330.3813249 48.2271484 7659120 330.3813249 48.2271484 +0.1590 2260.7438767 326.9821749 47.7647759 7438767 326.9821749 47.7647759 +0.1600 2236.1021827 323.6330788 47.3089799 1021827 323.6330788 47.3089799 +0.1610 2211.8331072 320.3330368 46.8596315 8331072 320.3330368 46.8596315 +0.1620 2187.9291268 317.0810744 46.4166050 9291268 317.0810744 46.4166050 +0.1630 2164.3829117 313.8762419 45.9797781 3829117 313.8762419 45.9797781 +0.1640 2141.1873194 310.7176137 45.5490312 1873194 310.7176137 45.5490312 +0.1650 2118.3353890 307.6042874 45.1242479 3353890 307.6042874 45.1242479 +0.1660 2095.8203354 304.5353832 44.7053147 8203354 304.5353832 44.7053147 +0.1670 2073.6355442 301.5100431 44.2921206 6355442 301.5100431 44.2921206 +0.1680 2051.7745660 298.5274302 43.8845577 7745660 298.5274302 43.8845577 +0.1690 2030.2311119 295.5867284 43.4825203 2311119 295.5867284 43.4825203 +0.1700 2008.9990482 292.6871414 43.0859057 9990482 292.6871414 43.0859057 +0.1710 1988.0723922 289.8278921 42.6946132 0723922 289.8278921 42.6946132 +0.1720 1967.4453070 287.0082225 42.3085448 4453070 287.0082225 42.3085448 +0.1730 1947.1120979 284.2273926 41.9276048 1120979 284.2273926 41.9276048 +0.1740 1927.0672078 281.4846800 41.5516997 0672078 281.4846800 41.5516997 +0.1750 1907.3052129 278.7793797 41.1807380 3052129 278.7793797 41.1807380 +0.1760 1887.8208195 276.1108033 40.8146308 8208195 276.1108033 40.8146308 +0.1770 1868.6088596 273.4782784 40.4532907 6088596 273.4782784 40.4532907 +0.1780 1849.6642873 270.8811486 40.0966328 6642873 270.8811486 40.0966328 +0.1790 1830.9821758 268.3187725 39.7445739 9821758 268.3187725 39.7445739 +0.1800 1812.5577133 265.7905239 39.3970327 5577133 265.7905239 39.3970327 +0.1810 1794.3862002 263.2957906 39.0539298 3862002 263.2957906 39.0539298 +0.1820 1776.4630458 260.8339748 38.7151877 4630458 260.8339748 38.7151877 +0.1830 1758.7837651 258.4044920 38.3807303 7837651 258.4044920 38.3807303 +0.1840 1741.3439759 256.0067712 38.0504836 3439759 256.0067712 38.0504836 +0.1850 1724.1393960 253.6402542 37.7243750 1393960 253.6402542 37.7243750 +0.1860 1707.1658404 251.3043952 37.4023336 1658404 251.3043952 37.4023336 +0.1870 1690.4192185 248.9986608 37.0842900 4192185 248.9986608 37.0842900 +0.1880 1673.8955316 246.7225293 36.7701764 8955316 246.7225293 36.7701764 +0.1890 1657.5908704 244.4754905 36.4599264 5908704 244.4754905 36.4599264 +0.1900 1641.5014126 242.2570456 36.1534752 5014126 242.2570456 36.1534752 +0.1910 1625.6234204 240.0667066 35.8507590 6234204 240.0667066 35.8507590 +0.1920 1609.9532382 237.9039960 35.5517159 9532382 237.9039960 35.5517159 +0.1930 1594.4872906 235.7684470 35.2562850 4872906 235.7684470 35.2562850 +0.1940 1579.2220803 233.6596024 34.9644066 2220803 233.6596024 34.9644066 +0.1950 1564.1541856 231.5770153 34.6760226 1541856 231.5770153 34.6760226 +0.1960 1549.2802589 229.5202480 34.3910759 2802589 229.5202480 34.3910759 +0.1970 1534.5970244 227.4888722 34.1095106 5970244 227.4888722 34.1095106 +0.1980 1520.1012763 225.4824686 33.8312720 1012763 225.4824686 33.8312720 +0.1990 1505.7898772 223.5006269 33.5563066 7898772 223.5006269 33.5563066 +0.2000 1491.6597561 221.5429453 33.2845619 6597561 221.5429453 33.2845619 +0.2010 1477.7079067 219.6090303 33.0159865 7079067 219.6090303 33.0159865 +0.2020 1463.9313857 217.6984967 32.7505301 9313857 217.6984967 32.7505301 +0.2030 1450.3273114 215.8109671 32.4881435 3273114 215.8109671 32.4881435 +0.2040 1436.8928620 213.9460720 32.2287782 8928620 213.9460720 32.2287782 +0.2050 1423.6252739 212.1034495 31.9723870 6252739 212.1034495 31.9723870 +0.2060 1410.5218407 210.2827449 31.7189234 5218407 210.2827449 31.7189234 +0.2070 1397.5799113 208.4836109 31.4683421 5799113 208.4836109 31.4683421 +0.2080 1384.7968886 206.7057069 31.2205985 7968886 206.7057069 31.2205985 +0.2090 1372.1702286 204.9486995 30.9756490 1702286 204.9486995 30.9756490 +0.2100 1359.6974383 203.2122618 30.7334506 6974383 203.2122618 30.7334506 +0.2110 1347.3760753 201.4960735 30.4939616 3760753 201.4960735 30.4939616 +0.2120 1335.2037458 199.7998204 30.2571406 2037458 199.7998204 30.2571406 +0.2130 1323.1781040 198.1231947 30.0229474 1781040 198.1231947 30.0229474 +0.2140 1311.2968506 196.4658948 29.7913425 2968506 196.4658948 29.7913425 +0.2150 1299.5577318 194.8276247 29.5622869 5577318 194.8276247 29.5622869 +0.2160 1287.9585380 193.2080943 29.3357428 9585380 193.2080943 29.3357428 +0.2170 1276.4971033 191.6070192 29.1116726 4971033 191.6070192 29.1116726 +0.2180 1265.1713036 190.0241204 28.8900399 1713036 190.0241204 28.8900399 +0.2190 1253.9790564 188.4591242 28.6708087 9790564 188.4591242 28.6708087 +0.2200 1242.9183194 186.9117623 28.4539438 9183194 186.9117623 28.4539438 +0.2210 1231.9870896 185.3817715 28.2394106 9870896 185.3817715 28.2394106 +0.2220 1221.1834024 183.8688934 28.0271751 1834024 183.8688934 28.0271751 +0.2230 1210.5053309 182.3728746 27.8172040 5053309 182.3728746 27.8172040 +0.2240 1199.9509845 180.8934666 27.6094647 9509845 180.8934666 27.6094647 +0.2250 1189.5185088 179.4304255 27.4039251 5185088 179.4304255 27.4039251 +0.2260 1179.2060841 177.9835117 27.2005537 2060841 177.9835117 27.2005537 +0.2270 1169.0119251 176.5524904 26.9993196 0119251 176.5524904 26.9993196 +0.2280 1158.9342796 175.1371309 26.8001924 9342796 175.1371309 26.8001924 +0.2290 1148.9714282 173.7372069 26.6031423 9714282 173.7372069 26.6031423 +0.2300 1139.1216834 172.3524963 26.4081402 1216834 172.3524963 26.4081402 +0.2310 1129.3833889 170.9827808 26.2151571 3833889 170.9827808 26.2151571 +0.2320 1119.7549187 169.6278463 26.0241650 7549187 169.6278463 26.0241650 +0.2330 1110.2346768 168.2874825 25.8351362 2346768 168.2874825 25.8351362 +0.2340 1100.8210961 166.9614829 25.6480433 8210961 166.9614829 25.6480433 +0.2350 1091.5126382 165.6496448 25.4628597 5126382 165.6496448 25.4628597 +0.2360 1082.3077924 164.3517691 25.2795591 3077924 164.3517691 25.2795591 +0.2370 1073.2050753 163.0676601 25.0981157 2050753 163.0676601 25.0981157 +0.2380 1064.2030300 161.7971257 24.9185042 2030300 161.7971257 24.9185042 +0.2390 1055.3002259 160.5399772 24.7406996 3002259 160.5399772 24.7406996 +0.2400 1046.4952577 159.2960293 24.5646775 4952577 159.2960293 24.5646775 +0.2410 1037.7867450 158.0650998 24.3904138 7867450 158.0650998 24.3904138 +0.2420 1029.1733319 156.8470097 24.2178849 1733319 156.8470097 24.2178849 +0.2430 1020.6536864 155.6415833 24.0470676 6536864 155.6415833 24.0470676 +0.2440 1012.2264998 154.4486478 23.8779390 2264998 154.4486478 23.8779390 +0.2450 1003.8904862 153.2680334 23.7104767 8904862 153.2680334 23.7104767 +0.2460 995.6443822 152.0995732 23.5446586 6443822 152.0995732 23.5446586 +0.2470 987.4869462 150.9431032 23.3804631 4869462 150.9431032 23.3804631 +0.2480 979.4169580 149.7984622 23.2178688 4169580 149.7984622 23.2178688 +0.2490 971.4332186 148.6654918 23.0568547 4332186 148.6654918 23.0568547 +0.2500 963.5345494 147.5440362 22.8974003 5345494 147.5440362 22.8974003 +0.2510 955.7197919 146.4339423 22.7394853 7197919 146.4339423 22.7394853 +0.2520 947.9878073 145.3350596 22.5830897 9878073 145.3350596 22.5830897 +0.2530 940.3374762 144.2472399 22.4281939 3374762 144.2472399 22.4281939 +0.2540 932.7676981 143.1703377 22.2747787 7676981 143.1703377 22.2747787 +0.2550 925.2773908 142.1042099 22.1228251 2773908 142.1042099 22.1228251 +0.2560 917.8654906 141.0487157 21.9723144 8654906 141.0487157 21.9723144 +0.2570 910.5309511 140.0037168 21.8232283 5309511 140.0037168 21.8232283 +0.2580 903.2727438 138.9690768 21.6755488 2727438 138.9690768 21.6755488 +0.2590 896.0898568 137.9446619 21.5292580 0898568 137.9446619 21.5292580 +0.2600 888.9812952 136.9303404 21.3843385 9812952 136.9303404 21.3843385 +0.2610 881.9460805 135.9259827 21.2407731 9460805 135.9259827 21.2407731 +0.2620 874.9832499 134.9314613 21.0985449 9832499 134.9314613 21.0985449 +0.2630 868.0918568 133.9466506 20.9576372 0918568 133.9466506 20.9576372 +0.2640 861.2709696 132.9714274 20.8180337 2709696 132.9714274 20.8180337 +0.2650 854.5196721 132.0056702 20.6797182 5196721 132.0056702 20.6797182 +0.2660 847.8370627 131.0492595 20.5426748 8370627 131.0492595 20.5426748 +0.2670 841.2222545 130.1020778 20.4068880 2222545 130.1020778 20.4068880 +0.2680 834.6743747 129.1640092 20.2723424 6743747 129.1640092 20.2723424 +0.2690 828.1925646 128.2349399 20.1390228 1925646 128.2349399 20.1390228 +0.2700 821.7759790 127.3147578 20.0069143 7759790 127.3147578 20.0069143 +0.2710 815.4237863 126.4033526 19.8760022 4237863 126.4033526 19.8760022 +0.2720 809.1351680 125.5006157 19.7462722 1351680 125.5006157 19.7462722 +0.2730 802.9093184 124.6064402 19.6177099 9093184 124.6064402 19.6177099 +0.2740 796.7454448 123.7207207 19.4903015 7454448 123.7207207 19.4903015 +0.2750 790.6427665 122.8433537 19.3640330 6427665 122.8433537 19.3640330 +0.2760 784.6005152 121.9742372 19.2388909 6005152 121.9742372 19.2388909 +0.2770 778.6179347 121.1132707 19.1148619 6179347 121.1132707 19.1148619 +0.2780 772.6942802 120.2603553 18.9919327 6942802 120.2603553 18.9919327 +0.2790 766.8288187 119.4153936 18.8700905 8288187 119.4153936 18.8700905 +0.2800 761.0208283 118.5782897 18.7493224 0208283 118.5782897 18.7493224 +0.2810 755.2695984 117.7489490 18.6296158 2695984 117.7489490 18.6296158 +0.2820 749.5744291 116.9272787 18.5109583 5744291 116.9272787 18.5109583 +0.2830 743.9346311 116.1131870 18.3933378 9346311 116.1131870 18.3933378 +0.2840 738.3495259 115.3065837 18.2767421 3495259 115.3065837 18.2767421 +0.2850 732.8184450 114.5073800 18.1611595 8184450 114.5073800 18.1611595 +0.2860 727.3407300 113.7154881 18.0465783 3407300 113.7154881 18.0465783 +0.2870 721.9157327 112.9308219 17.9329869 9157327 112.9308219 17.9329869 +0.2880 716.5428142 112.1532963 17.8203740 5428142 112.1532963 17.8203740 +0.2890 711.2213456 111.3828276 17.7087284 2213456 111.3828276 17.7087284 +0.2900 705.9507071 110.6193334 17.5980392 9507071 110.6193334 17.5980392 +0.2910 700.7302882 109.8627322 17.4882954 7302882 109.8627322 17.4882954 +0.2920 695.5594874 109.1129441 17.3794864 5594874 109.1129441 17.3794864 +0.2930 690.4377121 108.3698900 17.2716016 4377121 108.3698900 17.2716016 +0.2940 685.3643785 107.6334922 17.1646306 3643785 107.6334922 17.1646306 +0.2950 680.3389114 106.9036741 17.0585633 3389114 106.9036741 17.0585633 +0.2960 675.3607438 106.1803600 16.9533894 3607438 106.1803600 16.9533894 +0.2970 670.4293171 105.4634755 16.8490991 4293171 105.4634755 16.8490991 +0.2980 665.5440809 104.7529472 16.7456826 5440809 104.7529472 16.7456826 +0.2990 660.7044927 104.0487027 16.6431300 7044927 104.0487027 16.6431300 +0.3000 655.9100179 103.3506708 16.5414320 9100179 103.3506708 16.5414320 +0.3010 651.1601295 102.6587812 16.4405792 1601295 102.6587812 16.4405792 +0.3020 646.4543081 101.9729644 16.3405621 4543081 101.9729644 16.3405621 +0.3030 641.7920419 101.2931523 16.2413718 7920419 101.2931523 16.2413718 +0.3040 637.1728262 100.6192774 16.1429992 1728262 100.6192774 16.1429992 +0.3050 632.5961636 99.9512734 16.0454353 .5961636 99.9512734 16.0454353 +0.3060 628.0615636 99.2890746 15.9486715 .0615636 99.2890746 15.9486715 +0.3070 623.5685430 98.6326167 15.8526991 .5685430 98.6326167 15.8526991 +0.3080 619.1166250 97.9818358 15.7575095 .1166250 97.9818358 15.7575095 +0.3090 614.7053397 97.3366692 15.6630943 .7053397 97.3366692 15.6630943 +0.3100 610.3342239 96.6970550 15.5694453 .3342239 96.6970550 15.5694453 +0.3110 606.0028208 96.0629321 15.4765543 .0028208 96.0629321 15.4765543 +0.3120 601.7106798 95.4342402 15.3844132 .7106798 95.4342402 15.3844132 +0.3130 597.4573568 94.8109200 15.2930139 .4573568 94.8109200 15.2930139 +0.3140 593.2424137 94.1929129 15.2023488 .2424137 94.1929129 15.2023488 +0.3150 589.0654185 93.5801610 15.1124099 .0654185 93.5801610 15.1124099 +0.3160 584.9259453 92.9726074 15.0231897 .9259453 92.9726074 15.0231897 +0.3170 580.8235739 92.3701958 14.9346807 .8235739 92.3701958 14.9346807 +0.3180 576.7578898 91.7728708 14.8468753 .7578898 91.7728708 14.8468753 +0.3190 572.7284843 91.1805775 14.7597662 .7284843 91.1805775 14.7597662 +0.3200 568.7349543 90.5932620 14.6733463 .7349543 90.5932620 14.6733463 +0.3210 564.7769020 90.0108711 14.5876082 .7769020 90.0108711 14.5876082 +0.3220 560.8539351 89.4333521 14.5025451 .8539351 89.4333521 14.5025451 +0.3230 556.9656667 88.8606532 14.4181498 .9656667 88.8606532 14.4181498 +0.3240 553.1117150 88.2927232 14.3344156 .1117150 88.2927232 14.3344156 +0.3250 549.2917033 87.7295116 14.2513356 .2917033 87.7295116 14.2513356 +0.3260 545.5052600 87.1709686 14.1689032 .5052600 87.1709686 14.1689032 +0.3270 541.7520185 86.6170450 14.0871117 .7520185 86.6170450 14.0871117 +0.3280 538.0316170 86.0676922 14.0059546 .0316170 86.0676922 14.0059546 +0.3290 534.3436986 85.5228624 13.9254255 .3436986 85.5228624 13.9254255 +0.3300 530.6879111 84.9825082 13.8455180 .6879111 84.9825082 13.8455180 +0.3310 527.0639069 84.4465831 13.7662258 .0639069 84.4465831 13.7662258 +0.3320 523.4713431 83.9150409 13.6875428 .4713431 83.9150409 13.6875428 +0.3330 519.9098812 83.3878361 13.6094628 .9098812 83.3878361 13.6094628 +0.3340 516.3791872 82.8649240 13.5319798 .3791872 82.8649240 13.5319798 +0.3350 512.8789315 82.3462602 13.4550878 .8789315 82.3462602 13.4550878 +0.3360 509.4087887 81.8318010 13.3787809 .4087887 81.8318010 13.3787809 +0.3370 505.9684377 81.3215032 13.3030534 .9684377 81.3215032 13.3030534 +0.3380 502.5575616 80.8153242 13.2278994 .5575616 80.8153242 13.2278994 +0.3390 499.1758475 80.3132218 13.1533134 .1758475 80.3132218 13.1533134 +0.3400 495.8229866 79.8151546 13.0792897 .8229866 79.8151546 13.0792897 +0.3410 492.4986741 79.3210816 13.0058227 .4986741 79.3210816 13.0058227 +0.3420 489.2026091 78.8309622 12.9329071 .2026091 78.8309622 12.9329071 +0.3430 485.9344945 78.3447564 12.8605374 .9344945 78.3447564 12.8605374 +0.3440 482.6940372 77.8624247 12.7887084 .6940372 77.8624247 12.7887084 +0.3450 479.4809474 77.3839282 12.7174147 .4809474 77.3839282 12.7174147 +0.3460 476.2949395 76.9092283 12.6466511 .2949395 76.9092283 12.6466511 +0.3470 473.1357312 76.4382870 12.5764126 .1357312 76.4382870 12.5764126 +0.3480 470.0030438 75.9710667 12.5066941 .0030438 75.9710667 12.5066941 +0.3490 466.8966023 75.5075304 12.4374905 .8966023 75.5075304 12.4374905 +0.3500 463.8161349 75.0476413 12.3687969 .8161349 75.0476413 12.3687969 +0.3510 460.7613734 74.5913633 12.3006084 .7613734 74.5913633 12.3006084 +0.3520 457.7320529 74.1386607 12.2329203 .7320529 74.1386607 12.2329203 +0.3530 454.7279118 73.6894981 12.1657276 .7279118 73.6894981 12.1657276 +0.3540 451.7486918 73.2438407 12.0990258 .7486918 73.2438407 12.0990258 +0.3550 448.7941378 72.8016540 12.0328101 .7941378 72.8016540 12.0328101 +0.3560 445.8639978 72.3629040 11.9670761 .8639978 72.3629040 11.9670761 +0.3570 442.9580230 71.9275570 11.9018190 .9580230 71.9275570 11.9018190 +0.3580 440.0759676 71.4955799 11.8370344 .0759676 71.4955799 11.8370344 +0.3590 437.2175888 71.0669398 11.7727180 .2175888 71.0669398 11.7727180 +0.3600 434.3826470 70.6416043 11.7088653 .3826470 70.6416043 11.7088653 +0.3610 431.5709052 70.2195415 11.6454719 .5709052 70.2195415 11.6454719 +0.3620 428.7821296 69.8007195 11.5825337 .7821296 69.8007195 11.5825337 +0.3630 426.0160891 69.3851072 11.5200463 .0160891 69.3851072 11.5200463 +0.3640 423.2725553 68.9726737 11.4580056 .2725553 68.9726737 11.4580056 +0.3650 420.5513029 68.5633884 11.3964074 .5513029 68.5633884 11.3964074 +0.3660 417.8521090 68.1572211 11.3352478 .8521090 68.1572211 11.3352478 +0.3670 415.1747536 67.7541421 11.2745225 .1747536 67.7541421 11.2745225 +0.3680 412.5190192 67.3541219 11.2142277 .5190192 67.3541219 11.2142277 +0.3690 409.8846910 66.9571314 11.1543594 .8846910 66.9571314 11.1543594 +0.3700 407.2715568 66.5631417 11.0949137 .2715568 66.5631417 11.0949137 +0.3710 404.6794069 66.1721246 11.0358867 .6794069 66.1721246 11.0358867 +0.3720 402.1080341 65.7840517 10.9772747 .1080341 65.7840517 10.9772747 +0.3730 399.5572337 65.3988954 10.9190739 .5572337 65.3988954 10.9190739 +0.3740 397.0268033 65.0166283 10.8612804 .0268033 65.0166283 10.8612804 +0.3750 394.5165432 64.6372231 10.8038908 .5165432 64.6372231 10.8038908 +0.3760 392.0262556 64.2606530 10.7469013 .0262556 64.2606530 10.7469013 +0.3770 389.5557456 63.8868916 10.6903083 .5557456 63.8868916 10.6903083 +0.3780 387.1048200 63.5159126 10.6341083 .1048200 63.5159126 10.6341083 +0.3790 384.6732884 63.1476901 10.5782978 .6732884 63.1476901 10.5782978 +0.3800 382.2609623 62.7821985 10.5228732 .2609623 62.7821985 10.5228732 +0.3810 379.8676555 62.4194124 10.4678312 .8676555 62.4194124 10.4678312 +0.3820 377.4931840 62.0593069 10.4131683 .4931840 62.0593069 10.4131683 +0.3830 375.1373659 61.7018572 10.3588812 .1373659 61.7018572 10.3588812 +0.3840 372.8000214 61.3470387 10.3049666 .8000214 61.3470387 10.3049666 +0.3850 370.4809729 60.9948274 10.2514211 .4809729 60.9948274 10.2514211 +0.3860 368.1800447 60.6451993 10.1982415 .1800447 60.6451993 10.1982415 +0.3870 365.8970633 60.2981307 10.1454247 .8970633 60.2981307 10.1454247 +0.3880 363.6318570 59.9535983 10.0929674 .6318570 59.9535983 10.0929674 +0.3890 361.3842561 59.6115790 10.0408664 .3842561 59.6115790 10.0408664 +0.3900 359.1540931 59.2720498 9.9891187 9.1540931 59.2720498 9.9891187 +0.3910 356.9412021 58.9349881 9.9377213 6.9412021 58.9349881 9.9377213 +0.3920 354.7454193 58.6003717 9.8866709 4.7454193 58.6003717 9.8866709 +0.3930 352.5665826 58.2681784 9.8359647 2.5665826 58.2681784 9.8359647 +0.3940 350.4045319 57.9383862 9.7855997 0.4045319 57.9383862 9.7855997 +0.3950 348.2591089 57.6109737 9.7355729 8.2591089 57.6109737 9.7355729 +0.3960 346.1301569 57.2859194 9.6858814 6.1301569 57.2859194 9.6858814 +0.3970 344.0175213 56.9632022 9.6365223 4.0175213 56.9632022 9.6365223 +0.3980 341.9210489 56.6428011 9.5874928 1.9210489 56.6428011 9.5874928 +0.3990 339.8405885 56.3246954 9.5387901 9.8405885 56.3246954 9.5387901 +0.4000 337.7759903 56.0088648 9.4904113 7.7759903 56.0088648 9.4904113 +0.4010 335.7271066 55.6952889 9.4423537 5.7271066 55.6952889 9.4423537 +0.4020 333.6937909 55.3839477 9.3946146 3.6937909 55.3839477 9.3946146 +0.4030 331.6758987 55.0748214 9.3471913 1.6758987 55.0748214 9.3471913 +0.4040 329.6732868 54.7678904 9.3000811 9.6732868 54.7678904 9.3000811 +0.4050 327.6858138 54.4631355 9.2532814 7.6858138 54.4631355 9.2532814 +0.4060 325.7133399 54.1605373 9.2067894 5.7133399 54.1605373 9.2067894 +0.4070 323.7557266 53.8600769 9.1606028 3.7557266 53.8600769 9.1606028 +0.4080 321.8128372 53.5617355 9.1147188 1.8128372 53.5617355 9.1147188 +0.4090 319.8845364 53.2654947 9.0691350 9.8845364 53.2654947 9.0691350 +0.4100 317.9706903 52.9713360 9.0238489 7.9706903 52.9713360 9.0238489 +0.4110 316.0711666 52.6792413 8.9788579 6.0711666 52.6792413 8.9788579 +0.4120 314.1858343 52.3891926 8.9341596 4.1858343 52.3891926 8.9341596 +0.4130 312.3145641 52.1011721 8.8897516 2.3145641 52.1011721 8.8897516 +0.4140 310.4572279 51.8151622 8.8456315 0.4572279 51.8151622 8.8456315 +0.4150 308.6136989 51.5311456 8.8017969 8.6136989 51.5311456 8.8017969 +0.4160 306.7838520 51.2491049 8.7582454 6.7838520 51.2491049 8.7582454 +0.4170 304.9675631 50.9690232 8.7149747 4.9675631 50.9690232 8.7149747 +0.4180 303.1647097 50.6908836 8.6719826 3.1647097 50.6908836 8.6719826 +0.4190 301.3751706 50.4146694 8.6292666 1.3751706 50.4146694 8.6292666 +0.4200 299.5988257 50.1403641 8.5868246 9.5988257 50.1403641 8.5868246 +0.4210 297.8355565 49.8679514 8.5446543 7.8355565 49.8679514 8.5446543 +0.4220 296.0852455 49.5974150 8.5027536 6.0852455 49.5974150 8.5027536 +0.4230 294.3477765 49.3287390 8.4611201 4.3477765 49.3287390 8.4611201 +0.4240 292.6230348 49.0619075 8.4197518 2.6230348 49.0619075 8.4197518 +0.4250 290.9109067 48.7969049 8.3786464 0.9109067 48.7969049 8.3786464 +0.4260 289.2112796 48.5337157 8.3378020 9.2112796 48.5337157 8.3378020 +0.4270 287.5240424 48.2723245 8.2972163 7.5240424 48.2723245 8.2972163 +0.4280 285.8490849 48.0127161 8.2568873 5.8490849 48.0127161 8.2568873 +0.4290 284.1862984 47.7548755 8.2168129 4.1862984 47.7548755 8.2168129 +0.4300 282.5355749 47.4987878 8.1769910 2.5355749 47.4987878 8.1769910 +0.4310 280.8968079 47.2444382 8.1374197 0.8968079 47.2444382 8.1374197 +0.4320 279.2698919 46.9918123 8.0980970 9.2698919 46.9918123 8.0980970 +0.4330 277.6547226 46.7408954 8.0590208 7.6547226 46.7408954 8.0590208 +0.4340 276.0511966 46.4916735 8.0201893 6.0511966 46.4916735 8.0201893 +0.4350 274.4592117 46.2441323 7.9816004 4.4592117 46.2441323 7.9816004 +0.4360 272.8786668 45.9982578 7.9432522 2.8786668 45.9982578 7.9432522 +0.4370 271.3094618 45.7540362 7.9051429 1.3094618 45.7540362 7.9051429 +0.4380 269.7514977 45.5114537 7.8672705 9.7514977 45.5114537 7.8672705 +0.4390 268.2046764 45.2704969 7.8296332 8.2046764 45.2704969 7.8296332 +0.4400 266.6689010 45.0311521 7.7922291 6.6689010 45.0311521 7.7922291 +0.4410 265.1440755 44.7934062 7.7550565 5.1440755 44.7934062 7.7550565 +0.4420 263.6301049 44.5572460 7.7181134 3.6301049 44.5572460 7.7181134 +0.4430 262.1268953 44.3226583 7.6813981 2.1268953 44.3226583 7.6813981 +0.4440 260.6343534 44.0896304 7.6449088 0.6343534 44.0896304 7.6449088 +0.4450 259.1523873 43.8581493 7.6086438 9.1523873 43.8581493 7.6086438 +0.4460 257.6809058 43.6282025 7.5726013 7.6809058 43.6282025 7.5726013 +0.4470 256.2198188 43.3997775 7.5367796 6.2198188 43.3997775 7.5367796 +0.4480 254.7690369 43.1728617 7.5011769 4.7690369 43.1728617 7.5011769 +0.4490 253.3284718 42.9474430 7.4657916 3.3284718 42.9474430 7.4657916 +0.4500 251.8980359 42.7235091 7.4306220 1.8980359 42.7235091 7.4306220 +0.4510 250.4776428 42.5010480 7.3956665 0.4776428 42.5010480 7.3956665 +0.4520 249.0672067 42.2800478 7.3609233 9.0672067 42.2800478 7.3609233 +0.4530 247.6666428 42.0604967 7.3263909 7.6666428 42.0604967 7.3263909 +0.4540 246.2758672 41.8423830 7.2920677 6.2758672 41.8423830 7.2920677 +0.4550 244.8947966 41.6256950 7.2579520 4.8947966 41.6256950 7.2579520 +0.4560 243.5233489 41.4104214 7.2240422 3.5233489 41.4104214 7.2240422 +0.4570 242.1614425 41.1965508 7.1903369 2.1614425 41.1965508 7.1903369 +0.4580 240.8089969 40.9840718 7.1568344 0.8089969 40.9840718 7.1568344 +0.4590 239.4659322 40.7729735 7.1235332 9.4659322 40.7729735 7.1235332 +0.4600 238.1321693 40.5632447 7.0904317 8.1321693 40.5632447 7.0904317 +0.4610 236.8076301 40.3548745 7.0575286 6.8076301 40.3548745 7.0575286 +0.4620 235.4922372 40.1478521 7.0248222 5.4922372 40.1478521 7.0248222 +0.4630 234.1859137 39.9421668 6.9923110 4.1859137 39.9421668 6.9923110 +0.4640 232.8885838 39.7378079 6.9599937 2.8885838 39.7378079 6.9599937 +0.4650 231.6001723 39.5347651 6.9278688 1.6001723 39.5347651 6.9278688 +0.4660 230.3206049 39.3330277 6.8959348 0.3206049 39.3330277 6.8959348 +0.4670 229.0498078 39.1325857 6.8641902 9.0498078 39.1325857 6.8641902 +0.4680 227.7877080 38.9334286 6.8326338 7.7877080 38.9334286 6.8326338 +0.4690 226.5342334 38.7355464 6.8012640 6.5342334 38.7355464 6.8012640 +0.4700 225.2893125 38.5389292 6.7700794 5.2893125 38.5389292 6.7700794 +0.4710 224.0528743 38.3435668 6.7390788 4.0528743 38.3435668 6.7390788 +0.4720 222.8248488 38.1494496 6.7082608 2.8248488 38.1494496 6.7082608 +0.4730 221.6051665 37.9565678 6.6776239 1.6051665 37.9565678 6.6776239 +0.4740 220.3937588 37.7649118 6.6471669 0.3937588 37.7649118 6.6471669 +0.4750 219.1905574 37.5744719 6.6168884 9.1905574 37.5744719 6.6168884 +0.4760 217.9954950 37.3852387 6.5867871 7.9954950 37.3852387 6.5867871 +0.4770 216.8085049 37.1972029 6.5568617 6.8085049 37.1972029 6.5568617 +0.4780 215.6295208 37.0103551 6.5271109 5.6295208 37.0103551 6.5271109 +0.4790 214.4584774 36.8246861 6.4975335 4.4584774 36.8246861 6.4975335 +0.4800 213.2953097 36.6401869 6.4681281 3.2953097 36.6401869 6.4681281 +0.4810 212.1399536 36.4568483 6.4388935 2.1399536 36.4568483 6.4388935 +0.4820 210.9923455 36.2746615 6.4098286 0.9923455 36.2746615 6.4098286 +0.4830 209.8524225 36.0936176 6.3809319 9.8524225 36.0936176 6.3809319 +0.4840 208.7201220 35.9137077 6.3522023 8.7201220 35.9137077 6.3522023 +0.4850 207.5953824 35.7349232 6.3236387 7.5953824 35.7349232 6.3236387 +0.4860 206.4781425 35.5572555 6.2952397 6.4781425 35.5572555 6.2952397 +0.4870 205.3683417 35.3806959 6.2670043 5.3683417 35.3806959 6.2670043 +0.4880 204.2659200 35.2052361 6.2389312 4.2659200 35.2052361 6.2389312 +0.4890 203.1708179 35.0308677 6.2110192 3.1708179 35.0308677 6.2110192 +0.4900 202.0829765 34.8575823 6.1832672 2.0829765 34.8575823 6.1832672 +0.4910 201.0023376 34.6853717 6.1556741 1.0023376 34.6853717 6.1556741 +0.4920 199.9288434 34.5142278 6.1282387 9.9288434 34.5142278 6.1282387 +0.4930 198.8624366 34.3441424 6.1009599 8.8624366 34.3441424 6.1009599 +0.4940 197.8030607 34.1751076 6.0738365 7.8030607 34.1751076 6.0738365 +0.4950 196.7506594 34.0071154 6.0468675 6.7506594 34.0071154 6.0468675 +0.4960 195.7051773 33.8401579 6.0200517 5.7051773 33.8401579 6.0200517 +0.4970 194.6665591 33.6742273 5.9933881 4.6665591 33.6742273 5.9933881 +0.4980 193.6347503 33.5093160 5.9668756 3.6347503 33.5093160 5.9668756 +0.4990 192.6096969 33.3454163 5.9405131 2.6096969 33.3454163 5.9405131 +0.5000 191.5913454 33.1825205 5.9142996 1.5913454 33.1825205 5.9142996 +0.5010 190.5796426 33.0206211 5.8882340 0.5796426 33.0206211 5.8882340 +0.5020 189.5745362 32.8597108 5.8623152 9.5745362 32.8597108 5.8623152 +0.5030 188.5759739 32.6997821 5.8365423 8.5759739 32.6997821 5.8365423 +0.5040 187.5839043 32.5408276 5.8109141 7.5839043 32.5408276 5.8109141 +0.5050 186.5982763 32.3828402 5.7854298 6.5982763 32.3828402 5.7854298 +0.5060 185.6190392 32.2258127 5.7600882 5.6190392 32.2258127 5.7600882 +0.5070 184.6461430 32.0697379 5.7348884 4.6461430 32.0697379 5.7348884 +0.5080 183.6795379 31.9146087 5.7098294 3.6795379 31.9146087 5.7098294 +0.5090 182.7191747 31.7604183 5.6849101 2.7191747 31.7604183 5.6849101 +0.5100 181.7650046 31.6071595 5.6601298 1.7650046 31.6071595 5.6601298 +0.5110 180.8169795 31.4548256 5.6354873 0.8169795 31.4548256 5.6354873 +0.5120 179.8750513 31.3034097 5.6109817 9.8750513 31.3034097 5.6109817 +0.5130 178.9391727 31.1529051 5.5866120 8.9391727 31.1529051 5.5866120 +0.5140 178.0092967 31.0033051 5.5623774 8.0092967 31.0033051 5.5623774 +0.5150 177.0853767 30.8546031 5.5382769 7.0853767 30.8546031 5.5382769 +0.5160 176.1673666 30.7067924 5.5143096 6.1673666 30.7067924 5.5143096 +0.5170 175.2552207 30.5598666 5.4904745 5.2552207 30.5598666 5.4904745 +0.5180 174.3488937 30.4138191 5.4667707 4.3488937 30.4138191 5.4667707 +0.5190 173.4483407 30.2686436 5.4431974 3.4483407 30.2686436 5.4431974 +0.5200 172.5535172 30.1243337 5.4197536 2.5535172 30.1243337 5.4197536 +0.5210 171.6643793 29.9808832 5.3964385 1.6643793 29.9808832 5.3964385 +0.5220 170.7808831 29.8382858 5.3732511 0.7808831 29.8382858 5.3732511 +0.5230 169.9029855 29.6965352 5.3501907 9.9029855 29.6965352 5.3501907 +0.5240 169.0306436 29.5556254 5.3272563 9.0306436 29.5556254 5.3272563 +0.5250 168.1638148 29.4155503 5.3044471 8.1638148 29.4155503 5.3044471 +0.5260 167.3024571 29.2763038 5.2817622 7.3024571 29.2763038 5.2817622 +0.5270 166.4465288 29.1378801 5.2592009 6.4465288 29.1378801 5.2592009 +0.5280 165.5959885 29.0002730 5.2367621 5.5959885 29.0002730 5.2367621 +0.5290 164.7507952 28.8634769 5.2144453 4.7507952 28.8634769 5.2144453 +0.5300 163.9109083 28.7274858 5.1922494 3.9109083 28.7274858 5.1922494 +0.5310 163.0762876 28.5922939 5.1701737 3.0762876 28.5922939 5.1701737 +0.5320 162.2468931 28.4578957 5.1482173 2.2468931 28.4578957 5.1482173 +0.5330 161.4226854 28.3242853 5.1263796 1.4226854 28.3242853 5.1263796 +0.5340 160.6036253 28.1914571 5.1046596 0.6036253 28.1914571 5.1046596 +0.5350 159.7896739 28.0594056 5.0830567 9.7896739 28.0594056 5.0830567 +0.5360 158.9807927 27.9281253 5.0615699 8.9807927 27.9281253 5.0615699 +0.5370 158.1769437 27.7976106 5.0401986 8.1769437 27.7976106 5.0401986 +0.5380 157.3780890 27.6678562 5.0189420 7.3780890 27.6678562 5.0189420 +0.5390 156.5841911 27.5388565 4.9977992 6.5841911 27.5388565 4.9977992 +0.5400 155.7952129 27.4106063 4.9767696 5.7952129 27.4106063 4.9767696 +0.5410 155.0111177 27.2831003 4.9558524 5.0111177 27.2831003 4.9558524 +0.5420 154.2318688 27.1563333 4.9350468 4.2318688 27.1563333 4.9350468 +0.5430 153.4574302 27.0302999 4.9143522 3.4574302 27.0302999 4.9143522 +0.5440 152.6877660 26.9049950 4.8937677 2.6877660 26.9049950 4.8937677 +0.5450 151.9228407 26.7804136 4.8732926 1.9228407 26.7804136 4.8732926 +0.5460 151.1626191 26.6565505 4.8529263 1.1626191 26.6565505 4.8529263 +0.5470 150.4070662 26.5334007 4.8326680 0.4070662 26.5334007 4.8326680 +0.5480 149.6561475 26.4109592 4.8125170 9.6561475 26.4109592 4.8125170 +0.5490 148.9098286 26.2892210 4.7924726 8.9098286 26.2892210 4.7924726 +0.5500 148.1680756 26.1681812 4.7725341 8.1680756 26.1681812 4.7725341 +0.5510 147.4308547 26.0478349 4.7527008 7.4308547 26.0478349 4.7527008 +0.5520 146.6981325 25.9281774 4.7329721 6.6981325 25.9281774 4.7329721 +0.5530 145.9698760 25.8092038 4.7133471 5.9698760 25.8092038 4.7133471 +0.5540 145.2460523 25.6909093 4.6938253 5.2460523 25.6909093 4.6938253 +0.5550 144.5266287 25.5732893 4.6744060 4.5266287 25.5732893 4.6744060 +0.5560 143.8115732 25.4563391 4.6550885 3.8115732 25.4563391 4.6550885 +0.5570 143.1008536 25.3400540 4.6358722 3.1008536 25.3400540 4.6358722 +0.5580 142.3944382 25.2244294 4.6167564 2.3944382 25.2244294 4.6167564 +0.5590 141.6922957 25.1094608 4.5977404 1.6922957 25.1094608 4.5977404 +0.5600 140.9943949 24.9951437 4.5788237 0.9943949 24.9951437 4.5788237 +0.5610 140.3007048 24.8814735 4.5600055 0.3007048 24.8814735 4.5600055 +0.5620 139.6111948 24.7684458 4.5412852 9.6111948 24.7684458 4.5412852 +0.5630 138.9258345 24.6560562 4.5226623 8.9258345 24.6560562 4.5226623 +0.5640 138.2445939 24.5443004 4.5041360 8.2445939 24.5443004 4.5041360 +0.5650 137.5674430 24.4331739 4.4857058 7.5674430 24.4331739 4.4857058 +0.5660 136.8943523 24.3226725 4.4673710 6.8943523 24.3226725 4.4673710 +0.5670 136.2252924 24.2127919 4.4491311 6.2252924 24.2127919 4.4491311 +0.5680 135.5602343 24.1035279 4.4309854 5.5602343 24.1035279 4.4309854 +0.5690 134.8991490 23.9948762 4.4129333 4.8991490 23.9948762 4.4129333 +0.5700 134.2420080 23.8868327 4.3949742 4.2420080 23.8868327 4.3949742 +0.5710 133.5887829 23.7793933 4.3771076 3.5887829 23.7793933 4.3771076 +0.5720 132.9394456 23.6725538 4.3593328 2.9394456 23.6725538 4.3593328 +0.5730 132.2939681 23.5663102 4.3416493 2.2939681 23.5663102 4.3416493 +0.5740 131.6523229 23.4606585 4.3240564 1.6523229 23.4606585 4.3240564 +0.5750 131.0144825 23.3555946 4.3065537 1.0144825 23.3555946 4.3065537 +0.5760 130.3804198 23.2511146 4.2891405 0.3804198 23.2511146 4.2891405 +0.5770 129.7501078 23.1472146 4.2718163 9.7501078 23.1472146 4.2718163 +0.5780 129.1235197 23.0438906 4.2545805 9.1235197 23.0438906 4.2545805 +0.5790 128.5006290 22.9411387 4.2374326 8.5006290 22.9411387 4.2374326 +0.5800 127.8814095 22.8389551 4.2203720 7.8814095 22.8389551 4.2203720 +0.5810 127.2658351 22.7373361 4.2033981 7.2658351 22.7373361 4.2033981 +0.5820 126.6538800 22.6362777 4.1865105 6.6538800 22.6362777 4.1865105 +0.5830 126.0455185 22.5357763 4.1697085 6.0455185 22.5357763 4.1697085 +0.5840 125.4407252 22.4358282 4.1529917 5.4407252 22.4358282 4.1529917 +0.5850 124.8394749 22.3364296 4.1363594 4.8394749 22.3364296 4.1363594 +0.5860 124.2417426 22.2375768 4.1198113 4.2417426 22.2375768 4.1198113 +0.5870 123.6475035 22.1392663 4.1033467 3.6475035 22.1392663 4.1033467 +0.5880 123.0567330 22.0414944 4.0869651 3.0567330 22.0414944 4.0869651 +0.5890 122.4694068 21.9442576 4.0706661 2.4694068 21.9442576 4.0706661 +0.5900 121.8855007 21.8475522 4.0544491 1.8855007 21.8475522 4.0544491 +0.5910 121.3049907 21.7513748 4.0383135 1.3049907 21.7513748 4.0383135 +0.5920 120.7278530 21.6557219 4.0222590 0.7278530 21.6557219 4.0222590 +0.5930 120.1540640 21.5605900 4.0062849 0.1540640 21.5605900 4.0062849 +0.5940 119.5836004 21.4659757 3.9903909 9.5836004 21.4659757 3.9903909 +0.5950 119.0164390 21.3718756 3.9745764 9.0164390 21.3718756 3.9745764 +0.5960 118.4525566 21.2782862 3.9588408 8.4525566 21.2782862 3.9588408 +0.5970 117.8919307 21.1852042 3.9431838 7.8919307 21.1852042 3.9431838 +0.5980 117.3345384 21.0926262 3.9276049 7.3345384 21.0926262 3.9276049 +0.5990 116.7803573 21.0005491 3.9121035 6.7803573 21.0005491 3.9121035 +0.6000 116.2293653 20.9089694 3.8966792 6.2293653 20.9089694 3.8966792 +0.6010 115.6815402 20.8178839 3.8813315 5.6815402 20.8178839 3.8813315 +0.6020 115.1368600 20.7272894 3.8660600 5.1368600 20.7272894 3.8660600 +0.6030 114.5953032 20.6371827 3.8508642 4.5953032 20.6371827 3.8508642 +0.6040 114.0568481 20.5475606 3.8357436 4.0568481 20.5475606 3.8357436 +0.6050 113.5214734 20.4584200 3.8206977 3.5214734 20.4584200 3.8206977 +0.6060 112.9891578 20.3697576 3.8057262 2.9891578 20.3697576 3.8057262 +0.6070 112.4598804 20.2815705 3.7908285 2.4598804 20.2815705 3.7908285 +0.6080 111.9336202 20.1938554 3.7760043 1.9336202 20.1938554 3.7760043 +0.6090 111.4103567 20.1066094 3.7612530 1.4103567 20.1066094 3.7612530 +0.6100 110.8900692 20.0198294 3.7465743 0.8900692 20.0198294 3.7465743 +0.6110 110.3727375 19.9335124 3.7319676 0.3727375 19.9335124 3.7319676 +0.6120 109.8583413 19.8476555 3.7174326 9.8583413 19.8476555 3.7174326 +0.6130 109.3468606 19.7622555 3.7029688 9.3468606 19.7622555 3.7029688 +0.6140 108.8382755 19.6773096 3.6885758 8.8382755 19.6773096 3.6885758 +0.6150 108.3325663 19.5928150 3.6742532 8.3325663 19.5928150 3.6742532 +0.6160 107.8297135 19.5087685 3.6600006 7.8297135 19.5087685 3.6600006 +0.6170 107.3296977 19.4251675 3.6458174 7.3296977 19.4251675 3.6458174 +0.6180 106.8324997 19.3420090 3.6317034 6.8324997 19.3420090 3.6317034 +0.6190 106.3381002 19.2592901 3.6176581 6.3381002 19.2592901 3.6176581 +0.6200 105.8464805 19.1770082 3.6036811 5.8464805 19.1770082 3.6036811 +0.6210 105.3576217 19.0951603 3.5897719 5.3576217 19.0951603 3.5897719 +0.6220 104.8715052 19.0137437 3.5759302 4.8715052 19.0137437 3.5759302 +0.6230 104.3881125 18.9327557 3.5621556 4.3881125 18.9327557 3.5621556 +0.6240 103.9074253 18.8521936 3.5484477 3.9074253 18.8521936 3.5484477 +0.6250 103.4294254 18.7720545 3.5348060 3.4294254 18.7720545 3.5348060 +0.6260 102.9540947 18.6923359 3.5212303 2.9540947 18.6923359 3.5212303 +0.6270 102.4814152 18.6130350 3.5077200 2.4814152 18.6130350 3.5077200 +0.6280 102.0113694 18.5341493 3.4942748 2.0113694 18.5341493 3.4942748 +0.6290 101.5439394 18.4556760 3.4808944 1.5439394 18.4556760 3.4808944 +0.6300 101.0791079 18.3776126 3.4675783 1.0791079 18.3776126 3.4675783 +0.6310 100.6168575 18.2999565 3.4543261 0.6168575 18.2999565 3.4543261 +0.6320 100.1571709 18.2227051 3.4411376 0.1571709 18.2227051 3.4411376 +0.6330 99.7000311 18.1458558 3.4280123 9.7000311 18.1458558 3.4280123 +0.6340 99.2454212 18.0694062 3.4149498 9.2454212 18.0694062 3.4149498 +0.6350 98.7933242 17.9933537 3.4019497 8.7933242 17.9933537 3.4019497 +0.6360 98.3437237 17.9176958 3.3890118 8.3437237 17.9176958 3.3890118 +0.6370 97.8966029 17.8424301 3.3761357 7.8966029 17.8424301 3.3761357 +0.6380 97.4519455 17.7675540 3.3633209 7.4519455 17.7675540 3.3633209 +0.6390 97.0097351 17.6930652 3.3505671 7.0097351 17.6930652 3.3505671 +0.6400 96.5699557 17.6189612 3.3378740 6.5699557 17.6189612 3.3378740 +0.6410 96.1325912 17.5452397 3.3252412 6.1325912 17.5452397 3.3252412 +0.6420 95.6976256 17.4718981 3.3126684 5.6976256 17.4718981 3.3126684 +0.6430 95.2650431 17.3989342 3.3001551 5.2650431 17.3989342 3.3001551 +0.6440 94.8348282 17.3263457 3.2877012 4.8348282 17.3263457 3.2877012 +0.6450 94.4069651 17.2541301 3.2753062 4.4069651 17.2541301 3.2753062 +0.6460 93.9814386 17.1822851 3.2629697 3.9814386 17.1822851 3.2629697 +0.6470 93.5582333 17.1108086 3.2506915 3.5582333 17.1108086 3.2506915 +0.6480 93.1373339 17.0396981 3.2384713 3.1373339 17.0396981 3.2384713 +0.6490 92.7187255 16.9689514 3.2263085 2.7187255 16.9689514 3.2263085 +0.6500 92.3023930 16.8985663 3.2142031 2.3023930 16.8985663 3.2142031 +0.6510 91.8883217 16.8285405 3.2021545 1.8883217 16.8285405 3.2021545 +0.6520 91.4764967 16.7588718 3.1901625 1.4764967 16.7588718 3.1901625 +0.6530 91.0669035 16.6895580 3.1782268 1.0669035 16.6895580 3.1782268 +0.6540 90.6595276 16.6205970 3.1663470 0.6595276 16.6205970 3.1663470 +0.6550 90.2543545 16.5519865 3.1545229 0.2543545 16.5519865 3.1545229 +0.6560 89.8513700 16.4837244 3.1427540 9.8513700 16.4837244 3.1427540 +0.6570 89.4505599 16.4158086 3.1310401 9.4505599 16.4158086 3.1310401 +0.6580 89.0519101 16.3482369 3.1193809 9.0519101 16.3482369 3.1193809 +0.6590 88.6554066 16.2810073 3.1077761 8.6554066 16.2810073 3.1077761 +0.6600 88.2610357 16.2141176 3.0962252 8.2610357 16.2141176 3.0962252 +0.6610 87.8687835 16.1475658 3.0847282 7.8687835 16.1475658 3.0847282 +0.6620 87.4786365 16.0813498 3.0732845 7.4786365 16.0813498 3.0732845 +0.6630 87.0905810 16.0154675 3.0618940 7.0905810 16.0154675 3.0618940 +0.6640 86.7046036 15.9499170 3.0505563 6.7046036 15.9499170 3.0505563 +0.6650 86.3206910 15.8846962 3.0392712 6.3206910 15.8846962 3.0392712 +0.6660 85.9388300 15.8198031 3.0280382 5.9388300 15.8198031 3.0280382 +0.6670 85.5590074 15.7552357 3.0168572 5.5590074 15.7552357 3.0168572 +0.6680 85.1812101 15.6909920 3.0057279 5.1812101 15.6909920 3.0057279 +0.6690 84.8054253 15.6270702 2.9946499 4.8054253 15.6270702 2.9946499 +0.6700 84.4316400 15.5634682 2.9836230 4.4316400 15.5634682 2.9836230 +0.6710 84.0598416 15.5001840 2.9726468 4.0598416 15.5001840 2.9726468 +0.6720 83.6900173 15.4372159 2.9617211 3.6900173 15.4372159 2.9617211 +0.6730 83.3221547 15.3745619 2.9508456 3.3221547 15.3745619 2.9508456 +0.6740 82.9562412 15.3122200 2.9400201 2.9562412 15.3122200 2.9400201 +0.6750 82.5922645 15.2501885 2.9292442 2.5922645 15.2501885 2.9292442 +0.6760 82.2302123 15.1884654 2.9185176 2.2302123 15.1884654 2.9185176 +0.6770 81.8700724 15.1270489 2.9078402 1.8700724 15.1270489 2.9078402 +0.6780 81.5118328 15.0659372 2.8972116 1.5118328 15.0659372 2.8972116 +0.6790 81.1554813 15.0051284 2.8866315 1.1554813 15.0051284 2.8866315 +0.6800 80.8010062 14.9446207 2.8760997 0.8010062 14.9446207 2.8760997 +0.6810 80.4483955 14.8844124 2.8656158 0.4483955 14.8844124 2.8656158 +0.6820 80.0976376 14.8245015 2.8551798 0.0976376 14.8245015 2.8551798 +0.6830 79.7487207 14.7648865 2.8447912 9.7487207 14.7648865 2.8447912 +0.6840 79.4016333 14.7055654 2.8344498 9.4016333 14.7055654 2.8344498 +0.6850 79.0563639 14.6465366 2.8241553 9.0563639 14.6465366 2.8241553 +0.6860 78.7129012 14.5877983 2.8139075 8.7129012 14.5877983 2.8139075 +0.6870 78.3712338 14.5293488 2.8037062 8.3712338 14.5293488 2.8037062 +0.6880 78.0313504 14.4711864 2.7935510 8.0313504 14.4711864 2.7935510 +0.6890 77.6932400 14.4133093 2.7834418 7.6932400 14.4133093 2.7834418 +0.6900 77.3568914 14.3557160 2.7733782 7.3568914 14.3557160 2.7733782 +0.6910 77.0222938 14.2984046 2.7633600 7.0222938 14.2984046 2.7633600 +0.6920 76.6894360 14.2413736 2.7533870 6.6894360 14.2413736 2.7533870 +0.6930 76.3583075 14.1846212 2.7434589 6.3583075 14.1846212 2.7434589 +0.6940 76.0288973 14.1281459 2.7335755 6.0288973 14.1281459 2.7335755 +0.6950 75.7011949 14.0719461 2.7237365 5.7011949 14.0719461 2.7237365 +0.6960 75.3751896 14.0160200 2.7139416 5.3751896 14.0160200 2.7139416 +0.6970 75.0508709 13.9603661 2.7041907 5.0508709 13.9603661 2.7041907 +0.6980 74.7282285 13.9049827 2.6944835 4.7282285 13.9049827 2.6944835 +0.6990 74.4072519 13.8498684 2.6848198 4.4072519 13.8498684 2.6848198 +0.7000 74.0879308 13.7950215 2.6751993 4.0879308 13.7950215 2.6751993 +0.7010 73.7702551 13.7404405 2.6656217 3.7702551 13.7404405 2.6656217 +0.7020 73.4542145 13.6861238 2.6560870 3.4542145 13.6861238 2.6560870 +0.7030 73.1397992 13.6320698 2.6465947 3.1397992 13.6320698 2.6465947 +0.7040 72.8269989 13.5782771 2.6371447 2.8269989 13.5782771 2.6371447 +0.7050 72.5158039 13.5247441 2.6277368 2.5158039 13.5247441 2.6277368 +0.7060 72.2062043 13.4714693 2.6183707 2.2062043 13.4714693 2.6183707 +0.7070 71.8981904 13.4184512 2.6090463 1.8981904 13.4184512 2.6090463 +0.7080 71.5917523 13.3656882 2.5997632 1.5917523 13.3656882 2.5997632 +0.7090 71.2868805 13.3131791 2.5905212 1.2868805 13.3131791 2.5905212 +0.7100 70.9835654 13.2609221 2.5813202 0.9835654 13.2609221 2.5813202 +0.7110 70.6817975 13.2089160 2.5721599 0.6817975 13.2089160 2.5721599 +0.7120 70.3815674 13.1571592 2.5630401 0.3815674 13.1571592 2.5630401 +0.7130 70.0828658 13.1056503 2.5539606 0.0828658 13.1056503 2.5539606 +0.7140 69.7856832 13.0543879 2.5449211 9.7856832 13.0543879 2.5449211 +0.7150 69.4900106 13.0033705 2.5359215 9.4900106 13.0033705 2.5359215 +0.7160 69.1958387 12.9525968 2.5269615 9.1958387 12.9525968 2.5269615 +0.7170 68.9031585 12.9020653 2.5180409 8.9031585 12.9020653 2.5180409 +0.7180 68.6119608 12.8517746 2.5091596 8.6119608 12.8517746 2.5091596 +0.7190 68.3222368 12.8017234 2.5003172 8.3222368 12.8017234 2.5003172 +0.7200 68.0339775 12.7519103 2.4915136 8.0339775 12.7519103 2.4915136 +0.7210 67.7471742 12.7023340 2.4827486 7.7471742 12.7023340 2.4827486 +0.7220 67.4618179 12.6529930 2.4740220 7.4618179 12.6529930 2.4740220 +0.7230 67.1779001 12.6038860 2.4653335 7.1779001 12.6038860 2.4653335 +0.7240 66.8954120 12.5550117 2.4566830 6.8954120 12.5550117 2.4566830 +0.7250 66.6143450 12.5063688 2.4480703 6.6143450 12.5063688 2.4480703 +0.7260 66.3346907 12.4579559 2.4394952 6.3346907 12.4579559 2.4394952 +0.7270 66.0564406 12.4097717 2.4309574 6.0564406 12.4097717 2.4309574 +0.7280 65.7795861 12.3618149 2.4224568 5.7795861 12.3618149 2.4224568 +0.7290 65.5041191 12.3140843 2.4139931 5.5041191 12.3140843 2.4139931 +0.7300 65.2300311 12.2665786 2.4055663 5.2300311 12.2665786 2.4055663 +0.7310 64.9573141 12.2192964 2.3971760 4.9573141 12.2192964 2.3971760 +0.7320 64.6859596 12.1722365 2.3888221 4.6859596 12.1722365 2.3888221 +0.7330 64.4159598 12.1253977 2.3805044 4.4159598 12.1253977 2.3805044 +0.7340 64.1473064 12.0787787 2.3722227 4.1473064 12.0787787 2.3722227 +0.7350 63.8799915 12.0323782 2.3639768 3.8799915 12.0323782 2.3639768 +0.7360 63.6140072 11.9861951 2.3557666 3.6140072 11.9861951 2.3557666 +0.7370 63.3493455 11.9402280 2.3475917 3.3493455 11.9402280 2.3475917 +0.7380 63.0859986 11.8944759 2.3394522 3.0859986 11.8944759 2.3394522 +0.7390 62.8239587 11.8489374 2.3313477 2.8239587 11.8489374 2.3313477 +0.7400 62.5632181 11.8036114 2.3232781 2.5632181 11.8036114 2.3232781 +0.7410 62.3037690 11.7584966 2.3152432 2.3037690 11.7584966 2.3152432 +0.7420 62.0456040 11.7135920 2.3072429 2.0456040 11.7135920 2.3072429 +0.7430 61.7887153 11.6688963 2.2992769 1.7887153 11.6688963 2.2992769 +0.7440 61.5330955 11.6244083 2.2913450 1.5330955 11.6244083 2.2913450 +0.7450 61.2787372 11.5801269 2.2834471 1.2787372 11.5801269 2.2834471 +0.7460 61.0256328 11.5360509 2.2755831 1.0256328 11.5360509 2.2755831 +0.7470 60.7737751 11.4921792 2.2677526 0.7737751 11.4921792 2.2677526 +0.7480 60.5231566 11.4485107 2.2599557 0.5231566 11.4485107 2.2599557 +0.7490 60.2737703 11.4050442 2.2521920 0.2737703 11.4050442 2.2521920 +0.7500 60.0256087 11.3617785 2.2444614 0.0256087 11.3617785 2.2444614 +0.7510 59.7786649 11.3187127 2.2367638 9.7786649 11.3187127 2.2367638 +0.7520 59.5329316 11.2758455 2.2290989 9.5329316 11.2758455 2.2290989 +0.7530 59.2884018 11.2331758 2.2214666 9.2884018 11.2331758 2.2214666 +0.7540 59.0450684 11.1907026 2.2138668 9.0450684 11.1907026 2.2138668 +0.7550 58.8029246 11.1484248 2.2062993 8.8029246 11.1484248 2.2062993 +0.7560 58.5619634 11.1063413 2.1987638 8.5619634 11.1063413 2.1987638 +0.7570 58.3221779 11.0644510 2.1912603 8.3221779 11.0644510 2.1912603 +0.7580 58.0835612 11.0227529 2.1837885 8.0835612 11.0227529 2.1837885 +0.7590 57.8461067 10.9812459 2.1763483 7.8461067 10.9812459 2.1763483 +0.7600 57.6098075 10.9399289 2.1689396 7.6098075 10.9399289 2.1689396 +0.7610 57.3746570 10.8988008 2.1615622 7.3746570 10.8988008 2.1615622 +0.7620 57.1406485 10.8578608 2.1542159 7.1406485 10.8578608 2.1542159 +0.7630 56.9077755 10.8171077 2.1469005 6.9077755 10.8171077 2.1469005 +0.7640 56.6760313 10.7765404 2.1396160 6.6760313 10.7765404 2.1396160 +0.7650 56.4454095 10.7361581 2.1323621 6.4454095 10.7361581 2.1323621 +0.7660 56.2159036 10.6959596 2.1251387 6.2159036 10.6959596 2.1251387 +0.7670 55.9875072 10.6559440 2.1179456 5.9875072 10.6559440 2.1179456 +0.7680 55.7602140 10.6161102 2.1107827 5.7602140 10.6161102 2.1107827 +0.7690 55.5340174 10.5764573 2.1036499 5.5340174 10.5764573 2.1036499 +0.7700 55.3089114 10.5369842 2.0965469 5.3089114 10.5369842 2.0965469 +0.7710 55.0848896 10.4976901 2.0894736 5.0848896 10.4976901 2.0894736 +0.7720 54.8619458 10.4585739 2.0824299 4.8619458 10.4585739 2.0824299 +0.7730 54.6400739 10.4196346 2.0754157 4.6400739 10.4196346 2.0754157 +0.7740 54.4192677 10.3808713 2.0684307 4.4192677 10.3808713 2.0684307 +0.7750 54.1995211 10.3422831 2.0614748 4.1995211 10.3422831 2.0614748 +0.7760 53.9808282 10.3038690 2.0545479 3.9808282 10.3038690 2.0545479 +0.7770 53.7631829 10.2656280 2.0476498 3.7631829 10.2656280 2.0476498 +0.7780 53.5465792 10.2275592 2.0407804 3.5465792 10.2275592 2.0407804 +0.7790 53.3310112 10.1896616 2.0339396 3.3310112 10.1896616 2.0339396 +0.7800 53.1164730 10.1519345 2.0271272 3.1164730 10.1519345 2.0271272 +0.7810 52.9029589 10.1143767 2.0203430 2.9029589 10.1143767 2.0203430 +0.7820 52.6904629 10.0769875 2.0135869 2.6904629 10.0769875 2.0135869 +0.7830 52.4789794 10.0397659 2.0068588 2.4789794 10.0397659 2.0068588 +0.7840 52.2685025 10.0027109 2.0001585 2.2685025 10.0027109 2.0001585 +0.7850 52.0590267 9.9658218 1.9934859 52.0590267 9.9658218 1.9934859 +0.7860 51.8505462 9.9290976 1.9868409 51.8505462 9.9290976 1.9868409 +0.7870 51.6430554 9.8925374 1.9802232 51.6430554 9.8925374 1.9802232 +0.7880 51.4365489 9.8561404 1.9736329 51.4365489 9.8561404 1.9736329 +0.7890 51.2310209 9.8199057 1.9670697 51.2310209 9.8199057 1.9670697 +0.7900 51.0264660 9.7838323 1.9605335 51.0264660 9.7838323 1.9605335 +0.7910 50.8228788 9.7479195 1.9540241 50.8228788 9.7479195 1.9540241 +0.7920 50.6202538 9.7121664 1.9475415 50.6202538 9.7121664 1.9475415 +0.7930 50.4185857 9.6765721 1.9410855 50.4185857 9.6765721 1.9410855 +0.7940 50.2178690 9.6411358 1.9346560 50.2178690 9.6411358 1.9346560 +0.7950 50.0180984 9.6058567 1.9282528 50.0180984 9.6058567 1.9282528 +0.7960 49.8192687 9.5707338 1.9218759 49.8192687 9.5707338 1.9218759 +0.7970 49.6213746 9.5357665 1.9155250 49.6213746 9.5357665 1.9155250 +0.7980 49.4244109 9.5009538 1.9092000 49.4244109 9.5009538 1.9092000 +0.7990 49.2283723 9.4662949 1.9029009 49.2283723 9.4662949 1.9029009 +0.8000 49.0332538 9.4317890 1.8966275 49.0332538 9.4317890 1.8966275 +0.8010 48.8390502 9.3974354 1.8903796 48.8390502 9.3974354 1.8903796 +0.8020 48.6457564 9.3632331 1.8841572 48.6457564 9.3632331 1.8841572 +0.8030 48.4533674 9.3291814 1.8779600 48.4533674 9.3291814 1.8779600 +0.8040 48.2618782 9.2952795 1.8717881 48.2618782 9.2952795 1.8717881 +0.8050 48.0712838 9.2615267 1.8656413 48.0712838 9.2615267 1.8656413 +0.8060 47.8815791 9.2279220 1.8595194 47.8815791 9.2279220 1.8595194 +0.8070 47.6927594 9.1944649 1.8534223 47.6927594 9.1944649 1.8534223 +0.8080 47.5048196 9.1611543 1.8473499 47.5048196 9.1611543 1.8473499 +0.8090 47.3177550 9.1279897 1.8413020 47.3177550 9.1279897 1.8413020 +0.8100 47.1315608 9.0949702 1.8352787 47.1315608 9.0949702 1.8352787 +0.8110 46.9462320 9.0620951 1.8292797 46.9462320 9.0620951 1.8292797 +0.8120 46.7617641 9.0293636 1.8233049 46.7617641 9.0293636 1.8233049 +0.8130 46.5781521 8.9967749 1.8173541 46.5781521 8.9967749 1.8173541 +0.8140 46.3953915 8.9643283 1.8114274 46.3953915 8.9643283 1.8114274 +0.8150 46.2134775 8.9320232 1.8055246 46.2134775 8.9320232 1.8055246 +0.8160 46.0324056 8.8998586 1.7996454 46.0324056 8.8998586 1.7996454 +0.8170 45.8521710 8.8678339 1.7937900 45.8521710 8.8678339 1.7937900 +0.8180 45.6727692 8.8359484 1.7879580 45.6727692 8.8359484 1.7879580 +0.8190 45.4941957 8.8042014 1.7821494 45.4941957 8.8042014 1.7821494 +0.8200 45.3164460 8.7725920 1.7763642 45.3164460 8.7725920 1.7763642 +0.8210 45.1395155 8.7411197 1.7706021 45.1395155 8.7411197 1.7706021 +0.8220 44.9633997 8.7097837 1.7648630 44.9633997 8.7097837 1.7648630 +0.8230 44.7880943 8.6785832 1.7591470 44.7880943 8.6785832 1.7591470 +0.8240 44.6135948 8.6475176 1.7534537 44.6135948 8.6475176 1.7534537 +0.8250 44.4398968 8.6165862 1.7477832 44.4398968 8.6165862 1.7477832 +0.8260 44.2669961 8.5857883 1.7421353 44.2669961 8.5857883 1.7421353 +0.8270 44.0948882 8.5551232 1.7365100 44.0948882 8.5551232 1.7365100 +0.8280 43.9235689 8.5245902 1.7309070 43.9235689 8.5245902 1.7309070 +0.8290 43.7530338 8.4941886 1.7253263 43.7530338 8.4941886 1.7253263 +0.8300 43.5832788 8.4639178 1.7197678 43.5832788 8.4639178 1.7197678 +0.8310 43.4142997 8.4337771 1.7142314 43.4142997 8.4337771 1.7142314 +0.8320 43.2460921 8.4037657 1.7087170 43.2460921 8.4037657 1.7087170 +0.8330 43.0786521 8.3738831 1.7032245 43.0786521 8.3738831 1.7032245 +0.8340 42.9119754 8.3441286 1.6977537 42.9119754 8.3441286 1.6977537 +0.8350 42.7460579 8.3145015 1.6923045 42.7460579 8.3145015 1.6923045 +0.8360 42.5808956 8.2850012 1.6868770 42.5808956 8.2850012 1.6868770 +0.8370 42.4164843 8.2556269 1.6814708 42.4164843 8.2556269 1.6814708 +0.8380 42.2528201 8.2263782 1.6760861 42.2528201 8.2263782 1.6760861 +0.8390 42.0898989 8.1972543 1.6707225 42.0898989 8.1972543 1.6707225 +0.8400 41.9277168 8.1682546 1.6653802 41.9277168 8.1682546 1.6653802 +0.8410 41.7662698 8.1393784 1.6600588 41.7662698 8.1393784 1.6600588 +0.8420 41.6055540 8.1106252 1.6547585 41.6055540 8.1106252 1.6547585 +0.8430 41.4455654 8.0819943 1.6494789 41.4455654 8.0819943 1.6494789 +0.8440 41.2863002 8.0534850 1.6442201 41.2863002 8.0534850 1.6442201 +0.8450 41.1277545 8.0250968 1.6389820 41.1277545 8.0250968 1.6389820 +0.8460 40.9699245 7.9968291 1.6337644 40.9699245 7.9968291 1.6337644 +0.8470 40.8128063 7.9686812 1.6285673 40.8128063 7.9686812 1.6285673 +0.8480 40.6563962 7.9406525 1.6233905 40.6563962 7.9406525 1.6233905 +0.8490 40.5006904 7.9127424 1.6182340 40.5006904 7.9127424 1.6182340 +0.8500 40.3456852 7.8849503 1.6130976 40.3456852 7.8849503 1.6130976 +0.8510 40.1913769 7.8572757 1.6079813 40.1913769 7.8572757 1.6079813 +0.8520 40.0377617 7.8297179 1.6028850 40.0377617 7.8297179 1.6028850 +0.8530 39.8848360 7.8022763 1.5978086 39.8848360 7.8022763 1.5978086 +0.8540 39.7325961 7.7749504 1.5927520 39.7325961 7.7749504 1.5927520 +0.8550 39.5810385 7.7477396 1.5877150 39.5810385 7.7477396 1.5877150 +0.8560 39.4301594 7.7206432 1.5826977 39.4301594 7.7206432 1.5826977 +0.8570 39.2799554 7.6936608 1.5776998 39.2799554 7.6936608 1.5776998 +0.8580 39.1304228 7.6667916 1.5727214 39.1304228 7.6667916 1.5727214 +0.8590 38.9815582 7.6400353 1.5677623 38.9815582 7.6400353 1.5677623 +0.8600 38.8333580 7.6133912 1.5628224 38.8333580 7.6133912 1.5628224 +0.8610 38.6858187 7.5868587 1.5579017 38.6858187 7.5868587 1.5579017 +0.8620 38.5389369 7.5604372 1.5530001 38.5389369 7.5604372 1.5530001 +0.8630 38.3927091 7.5341263 1.5481174 38.3927091 7.5341263 1.5481174 +0.8640 38.2471318 7.5079254 1.5432535 38.2471318 7.5079254 1.5432535 +0.8650 38.1022017 7.4818339 1.5384085 38.1022017 7.4818339 1.5384085 +0.8660 37.9579153 7.4558513 1.5335822 37.9579153 7.4558513 1.5335822 +0.8670 37.8142694 7.4299770 1.5287744 37.8142694 7.4299770 1.5287744 +0.8680 37.6712605 7.4042105 1.5239853 37.6712605 7.4042105 1.5239853 +0.8690 37.5288854 7.3785512 1.5192145 37.5288854 7.3785512 1.5192145 +0.8700 37.3871406 7.3529987 1.5144621 37.3871406 7.3529987 1.5144621 +0.8710 37.2460231 7.3275524 1.5097280 37.2460231 7.3275524 1.5097280 +0.8720 37.1055294 7.3022117 1.5050120 37.1055294 7.3022117 1.5050120 +0.8730 36.9656563 7.2769761 1.5003142 36.9656563 7.2769761 1.5003142 +0.8740 36.8264006 7.2518452 1.4956344 36.8264006 7.2518452 1.4956344 +0.8750 36.6877592 7.2268184 1.4909725 36.6877592 7.2268184 1.4909725 +0.8760 36.5497287 7.2018952 1.4863284 36.5497287 7.2018952 1.4863284 +0.8770 36.4123061 7.1770750 1.4817022 36.4123061 7.1770750 1.4817022 +0.8780 36.2754882 7.1523574 1.4770936 36.2754882 7.1523574 1.4770936 +0.8790 36.1392719 7.1277418 1.4725026 36.1392719 7.1277418 1.4725026 +0.8800 36.0036540 7.1032278 1.4679291 36.0036540 7.1032278 1.4679291 +0.8810 35.8686315 7.0788149 1.4633731 35.8686315 7.0788149 1.4633731 +0.8820 35.7342013 7.0545025 1.4588345 35.7342013 7.0545025 1.4588345 +0.8830 35.6003603 7.0302902 1.4543131 35.6003603 7.0302902 1.4543131 +0.8840 35.4671056 7.0061774 1.4498089 35.4671056 7.0061774 1.4498089 +0.8850 35.3344340 6.9821637 1.4453219 35.3344340 6.9821637 1.4453219 +0.8860 35.2023426 6.9582486 1.4408519 35.2023426 6.9582486 1.4408519 +0.8870 35.0708284 6.9344316 1.4363988 35.0708284 6.9344316 1.4363988 +0.8880 34.9398885 6.9107122 1.4319627 34.9398885 6.9107122 1.4319627 +0.8890 34.8095199 6.8870900 1.4275434 34.8095199 6.8870900 1.4275434 +0.8900 34.6797197 6.8635645 1.4231408 34.6797197 6.8635645 1.4231408 +0.8910 34.5504849 6.8401351 1.4187548 34.5504849 6.8401351 1.4187548 +0.8920 34.4218127 6.8168015 1.4143854 34.4218127 6.8168015 1.4143854 +0.8930 34.2937002 6.7935632 1.4100326 34.2937002 6.7935632 1.4100326 +0.8940 34.1661445 6.7704197 1.4056962 34.1661445 6.7704197 1.4056962 +0.8950 34.0391429 6.7473705 1.4013761 34.0391429 6.7473705 1.4013761 +0.8960 33.9126924 6.7244151 1.3970723 33.9126924 6.7244151 1.3970723 +0.8970 33.7867903 6.7015532 1.3927847 33.7867903 6.7015532 1.3927847 +0.8980 33.6614338 6.6787843 1.3885133 33.6614338 6.6787843 1.3885133 +0.8990 33.5366200 6.6561079 1.3842579 33.5366200 6.6561079 1.3842579 +0.9000 33.4123463 6.6335236 1.3800185 33.4123463 6.6335236 1.3800185 +0.9010 33.2886100 6.6110309 1.3757950 33.2886100 6.6110309 1.3757950 +0.9020 33.1654082 6.5886293 1.3715874 33.1654082 6.5886293 1.3715874 +0.9030 33.0427382 6.5663185 1.3673955 33.0427382 6.5663185 1.3673955 +0.9040 32.9205975 6.5440981 1.3632194 32.9205975 6.5440981 1.3632194 +0.9050 32.7989833 6.5219674 1.3590588 32.7989833 6.5219674 1.3590588 +0.9060 32.6778929 6.4999262 1.3549139 32.6778929 6.4999262 1.3549139 +0.9070 32.5573237 6.4779741 1.3507844 32.5573237 6.4779741 1.3507844 +0.9080 32.4372731 6.4561104 1.3466703 32.4372731 6.4561104 1.3466703 +0.9090 32.3177384 6.4343350 1.3425716 32.3177384 6.4343350 1.3425716 +0.9100 32.1987171 6.4126472 1.3384881 32.1987171 6.4126472 1.3384881 +0.9110 32.0802066 6.3910468 1.3344199 32.0802066 6.3910468 1.3344199 +0.9120 31.9622042 6.3695332 1.3303668 31.9622042 6.3695332 1.3303668 +0.9130 31.8447076 6.3481061 1.3263288 31.8447076 6.3481061 1.3263288 +0.9140 31.7277140 6.3267651 1.3223058 31.7277140 6.3267651 1.3223058 +0.9150 31.6112211 6.3055096 1.3182978 31.6112211 6.3055096 1.3182978 +0.9160 31.4952262 6.2843395 1.3143046 31.4952262 6.2843395 1.3143046 +0.9170 31.3797269 6.2632541 1.3103262 31.3797269 6.2632541 1.3103262 +0.9180 31.2647207 6.2422532 1.3063625 31.2647207 6.2422532 1.3063625 +0.9190 31.1502052 6.2213362 1.3024136 31.1502052 6.2213362 1.3024136 +0.9200 31.0361779 6.2005029 1.2984792 31.0361779 6.2005029 1.2984792 +0.9210 30.9226363 6.1797528 1.2945594 30.9226363 6.1797528 1.2945594 +0.9220 30.8095781 6.1590855 1.2906541 30.8095781 6.1590855 1.2906541 +0.9230 30.6970008 6.1385007 1.2867632 30.6970008 6.1385007 1.2867632 +0.9240 30.5849021 6.1179979 1.2828866 30.5849021 6.1179979 1.2828866 +0.9250 30.4732795 6.0975767 1.2790243 30.4732795 6.0975767 1.2790243 +0.9260 30.3621308 6.0772368 1.2751763 30.3621308 6.0772368 1.2751763 +0.9270 30.2514534 6.0569777 1.2713424 30.2514534 6.0569777 1.2713424 +0.9280 30.1412452 6.0367991 1.2675226 30.1412452 6.0367991 1.2675226 +0.9290 30.0315037 6.0167007 1.2637168 30.0315037 6.0167007 1.2637168 +0.9300 29.9222268 5.9966820 1.2599250 29.9222268 5.9966820 1.2599250 +0.9310 29.8134120 5.9767426 1.2561471 29.8134120 5.9767426 1.2561471 +0.9320 29.7050570 5.9568822 1.2523831 29.7050570 5.9568822 1.2523831 +0.9330 29.5971597 5.9371004 1.2486328 29.5971597 5.9371004 1.2486328 +0.9340 29.4897178 5.9173969 1.2448963 29.4897178 5.9173969 1.2448963 +0.9350 29.3827290 5.8977712 1.2411735 29.3827290 5.8977712 1.2411735 +0.9360 29.2761910 5.8782230 1.2374642 29.2761910 5.8782230 1.2374642 +0.9370 29.1701017 5.8587520 1.2337685 29.1701017 5.8587520 1.2337685 +0.9380 29.0644589 5.8393577 1.2300863 29.0644589 5.8393577 1.2300863 +0.9390 28.9592603 5.8200398 1.2264175 28.9592603 5.8200398 1.2264175 +0.9400 28.8545038 5.8007980 1.2227621 28.8545038 5.8007980 1.2227621 +0.9410 28.7501872 5.7816319 1.2191200 28.7501872 5.7816319 1.2191200 +0.9420 28.6463084 5.7625412 1.2154912 28.6463084 5.7625412 1.2154912 +0.9430 28.5428651 5.7435254 1.2118755 28.5428651 5.7435254 1.2118755 +0.9440 28.4398554 5.7245843 1.2082730 28.4398554 5.7245843 1.2082730 +0.9450 28.3372770 5.7057175 1.2046836 28.3372770 5.7057175 1.2046836 +0.9460 28.2351278 5.6869246 1.2011071 28.2351278 5.6869246 1.2011071 +0.9470 28.1334058 5.6682053 1.1975437 28.1334058 5.6682053 1.1975437 +0.9480 28.0321089 5.6495593 1.1939931 28.0321089 5.6495593 1.1939931 +0.9490 27.9312350 5.6309862 1.1904554 27.9312350 5.6309862 1.1904554 +0.9500 27.8307820 5.6124856 1.1869305 27.8307820 5.6124856 1.1869305 +0.9510 27.7307479 5.5940574 1.1834183 27.7307479 5.5940574 1.1834183 +0.9520 27.6311306 5.5757010 1.1799189 27.6311306 5.5757010 1.1799189 +0.9530 27.5319282 5.5574162 1.1764320 27.5319282 5.5574162 1.1764320 +0.9540 27.4331386 5.5392026 1.1729577 27.4331386 5.5392026 1.1729577 +0.9550 27.3347598 5.5210600 1.1694960 27.3347598 5.5210600 1.1694960 +0.9560 27.2367898 5.5029879 1.1660467 27.2367898 5.5029879 1.1660467 +0.9570 27.1392266 5.4849861 1.1626098 27.1392266 5.4849861 1.1626098 +0.9580 27.0420683 5.4670542 1.1591853 27.0420683 5.4670542 1.1591853 +0.9590 26.9453130 5.4491919 1.1557730 26.9453130 5.4491919 1.1557730 +0.9600 26.8489586 5.4313990 1.1523731 26.8489586 5.4313990 1.1523731 +0.9610 26.7530032 5.4136749 1.1489853 26.7530032 5.4136749 1.1489853 +0.9620 26.6574449 5.3960196 1.1456096 26.6574449 5.3960196 1.1456096 +0.9630 26.5622819 5.3784326 1.1422461 26.5622819 5.3784326 1.1422461 +0.9640 26.4675121 5.3609136 1.1388946 26.4675121 5.3609136 1.1388946 +0.9650 26.3731337 5.3434623 1.1355551 26.3731337 5.3434623 1.1355551 +0.9660 26.2791448 5.3260784 1.1322275 26.2791448 5.3260784 1.1322275 +0.9670 26.1855436 5.3087616 1.1289118 26.1855436 5.3087616 1.1289118 +0.9680 26.0923281 5.2915115 1.1256080 26.0923281 5.2915115 1.1256080 +0.9690 25.9994966 5.2743280 1.1223159 25.9994966 5.2743280 1.1223159 +0.9700 25.9070472 5.2572106 1.1190356 25.9070472 5.2572106 1.1190356 +0.9710 25.8149780 5.2401591 1.1157669 25.8149780 5.2401591 1.1157669 +0.9720 25.7232873 5.2231731 1.1125099 25.7232873 5.2231731 1.1125099 +0.9730 25.6319732 5.2062525 1.1092644 25.6319732 5.2062525 1.1092644 +0.9740 25.5410340 5.1893967 1.1060305 25.5410340 5.1893967 1.1060305 +0.9750 25.4504678 5.1726057 1.1028081 25.4504678 5.1726057 1.1028081 +0.9760 25.3602728 5.1558791 1.0995971 25.3602728 5.1558791 1.0995971 +0.9770 25.2704474 5.1392165 1.0963975 25.2704474 5.1392165 1.0963975 +0.9780 25.1809897 5.1226177 1.0932092 25.1809897 5.1226177 1.0932092 +0.9790 25.0918980 5.1060825 1.0900323 25.0918980 5.1060825 1.0900323 +0.9800 25.0031705 5.0896104 1.0868665 25.0031705 5.0896104 1.0868665 +0.9810 24.9148055 5.0732013 1.0837120 24.9148055 5.0732013 1.0837120 +0.9820 24.8268013 5.0568548 1.0805686 24.8268013 5.0568548 1.0805686 +0.9830 24.7391563 5.0405707 1.0774363 24.7391563 5.0405707 1.0774363 +0.9840 24.6518686 5.0243487 1.0743150 24.6518686 5.0243487 1.0743150 +0.9850 24.5649365 5.0081885 1.0712048 24.5649365 5.0081885 1.0712048 +0.9860 24.4783585 4.9920898 1.0681055 24.4783585 4.9920898 1.0681055 +0.9870 24.3921328 4.9760523 1.0650171 24.3921328 4.9760523 1.0650171 +0.9880 24.3062578 4.9600758 1.0619396 24.3062578 4.9600758 1.0619396 +0.9890 24.2207318 4.9441600 1.0588729 24.2207318 4.9441600 1.0588729 +0.9900 24.1355532 4.9283046 1.0558169 24.1355532 4.9283046 1.0558169 +0.9910 24.0507203 4.9125093 1.0527717 24.0507203 4.9125093 1.0527717 +0.9920 23.9662314 4.8967740 1.0497372 23.9662314 4.8967740 1.0497372 +0.9930 23.8820851 4.8810982 1.0467134 23.8820851 4.8810982 1.0467134 +0.9940 23.7982796 4.8654818 1.0437001 23.7982796 4.8654818 1.0437001 +0.9950 23.7148134 4.8499244 1.0406973 23.7148134 4.8499244 1.0406973 +0.9960 23.6316848 4.8344259 1.0377051 23.6316848 4.8344259 1.0377051 +0.9970 23.5488924 4.8189859 1.0347233 23.5488924 4.8189859 1.0347233 +0.9980 23.4664344 4.8036042 1.0317519 23.4664344 4.8036042 1.0317519 +0.9990 23.3843094 4.7882805 1.0287909 23.3843094 4.7882805 1.0287909 +1.0000 23.3025158 4.7730145 1.0258403 23.3025158 4.7730145 1.0258403 From 5ad3d9622ece4a0ca4692c39611ca0191f4fbcdc Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 8 Feb 2024 01:50:36 -0500 Subject: [PATCH 062/686] refactor print summary (#3243) Signed-off-by: Jinzhe Zeng --- deepmd/env.py | 46 ++++++++++++ deepmd/pt/entrypoints/main.py | 33 +++++++++ deepmd/tf/entrypoints/train.py | 6 -- deepmd/tf/env.py | 33 ++------- deepmd/tf/train/run_options.py | 84 +++++++++------------- deepmd/utils/hostlist.py | 2 +- deepmd/utils/summary.py | 127 +++++++++++++++++++++++++++++++++ source/config/run_config.ini | 2 +- 8 files changed, 245 insertions(+), 88 deletions(-) create mode 100644 deepmd/utils/summary.py diff --git a/deepmd/env.py b/deepmd/env.py index 451b79d94f..8215de39ac 100644 --- a/deepmd/env.py +++ b/deepmd/env.py @@ -1,20 +1,38 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging import os +from configparser import ( + ConfigParser, +) +from pathlib import ( + Path, +) from typing import ( + Dict, Tuple, ) import numpy as np +import deepmd.lib + __all__ = [ "GLOBAL_NP_FLOAT_PRECISION", "GLOBAL_ENER_FLOAT_PRECISION", "global_float_prec", + "GLOBAL_CONFIG", + "SHARED_LIB_MODULE", + "SHARED_LIB_DIR", ] log = logging.getLogger(__name__) + +SHARED_LIB_MODULE = "lib" +SHARED_LIB_DIR = Path(deepmd.lib.__path__[0]) +CONFIG_FILE = SHARED_LIB_DIR / "run_config.ini" + + # FLOAT_PREC dp_float_prec = os.environ.get("DP_INTERFACE_PREC", "high").lower() if dp_float_prec in ("high", ""): @@ -111,3 +129,31 @@ def get_default_nthreads() -> Tuple[int, int]: os.environ.get("TF_INTRA_OP_PARALLELISM_THREADS", "0"), ) ) + + +def _get_package_constants( + config_file: Path = CONFIG_FILE, +) -> Dict[str, str]: + """Read package constants set at compile time by CMake to dictionary. + + Parameters + ---------- + config_file : str, optional + path to CONFIG file, by default "run_config.ini" + + Returns + ------- + Dict[str, str] + dictionary with package constants + """ + if not config_file.is_file(): + raise FileNotFoundError( + f"CONFIG file not found at {config_file}. " + "Please check if the package is installed correctly." + ) + config = ConfigParser() + config.read(config_file) + return dict(config.items("CONFIG")) + + +GLOBAL_CONFIG = _get_package_constants() diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 680d3313a6..d73cd316cb 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -14,6 +14,7 @@ import torch import torch.distributed as dist +import torch.version from torch.distributed.elastic.multiprocessing.errors import ( record, ) @@ -48,6 +49,9 @@ from deepmd.pt.utils.dataloader import ( DpLoaderSet, ) +from deepmd.pt.utils.env import ( + DEVICE, +) from deepmd.pt.utils.finetune import ( change_finetune_model_params, ) @@ -57,6 +61,7 @@ from deepmd.pt.utils.stat import ( make_stat_input, ) +from deepmd.utils.summary import SummaryPrinter as BaseSummaryPrinter log = logging.getLogger(__name__) @@ -238,8 +243,36 @@ def prepare_trainer_input_single( return trainer +class SummaryPrinter(BaseSummaryPrinter): + """Summary printer for PyTorch.""" + + def is_built_with_cuda(self) -> bool: + """Check if the backend is built with CUDA.""" + return torch.version.cuda is not None + + def is_built_with_rocm(self) -> bool: + """Check if the backend is built with ROCm.""" + return torch.version.hip is not None + + def get_compute_device(self) -> str: + """Get Compute device.""" + return str(DEVICE) + + def get_ngpus(self) -> int: + """Get the number of GPUs.""" + return torch.cuda.device_count() + + def get_backend_info(self) -> dict: + """Get backend information.""" + return { + "Backend": "PyTorch", + "PT ver": f"v{torch.__version__}-g{torch.version.git_version[:11]}", + } + + def train(FLAGS): log.info("Configuration path: %s", FLAGS.INPUT) + SummaryPrinter()() with open(FLAGS.INPUT) as fin: config = json.load(fin) trainer = get_trainer( diff --git a/deepmd/tf/entrypoints/train.py b/deepmd/tf/entrypoints/train.py index 17063d2bac..f0b9481d99 100755 --- a/deepmd/tf/entrypoints/train.py +++ b/deepmd/tf/entrypoints/train.py @@ -31,9 +31,6 @@ Model, ) from deepmd.tf.train.run_options import ( - BUILD, - CITATION, - WELCOME, RunOptions, ) from deepmd.tf.train.trainer import ( @@ -159,9 +156,6 @@ def train( dtype=tf.string, ) - for message in WELCOME + CITATION + BUILD: - log.info(message) - run_opt.print_resource_summary() if origin_type_map is not None: jdata["model"]["origin_type_map"] = origin_type_map diff --git a/deepmd/tf/env.py b/deepmd/tf/env.py index 6bc89664c7..e94c052f55 100644 --- a/deepmd/tf/env.py +++ b/deepmd/tf/env.py @@ -4,9 +4,6 @@ import ctypes import os import platform -from configparser import ( - ConfigParser, -) from importlib import ( import_module, reload, @@ -17,7 +14,6 @@ from typing import ( TYPE_CHECKING, Any, - Dict, ) import numpy as np @@ -25,10 +21,12 @@ Version, ) -import deepmd.lib from deepmd.env import ( + GLOBAL_CONFIG, GLOBAL_ENER_FLOAT_PRECISION, GLOBAL_NP_FLOAT_PRECISION, + SHARED_LIB_DIR, + SHARED_LIB_MODULE, ) from deepmd.env import get_default_nthreads as get_tf_default_nthreads from deepmd.env import ( @@ -112,11 +110,9 @@ def dlopen_library(module: str, filename: str): "ATTENTION_LAYER_PATTERN", "REMOVE_SUFFIX_DICT", "TF_VERSION", + "tf_py_version", ] -SHARED_LIB_MODULE = "lib" -SHARED_LIB_DIR = Path(deepmd.lib.__path__[0]) -CONFIG_FILE = SHARED_LIB_DIR / "run_config.ini" # Python library version try: @@ -398,27 +394,6 @@ def get_module(module_name: str) -> "ModuleType": return module -def _get_package_constants( - config_file: Path = CONFIG_FILE, -) -> Dict[str, str]: - """Read package constants set at compile time by CMake to dictionary. - - Parameters - ---------- - config_file : str, optional - path to CONFIG file, by default "run_config.ini" - - Returns - ------- - Dict[str, str] - dictionary with package constants - """ - config = ConfigParser() - config.read(config_file) - return dict(config.items("CONFIG")) - - -GLOBAL_CONFIG = _get_package_constants() if GLOBAL_CONFIG["enable_tensorflow"] == "0": raise RuntimeError( "TensorFlow backend is not built. To enable it, " diff --git a/deepmd/tf/train/run_options.py b/deepmd/tf/train/run_options.py index fb9d8beecb..b835d63852 100644 --- a/deepmd/tf/train/run_options.py +++ b/deepmd/tf/train/run_options.py @@ -22,57 +22,57 @@ from deepmd.tf.env import ( GLOBAL_CONFIG, TF_VERSION, - get_tf_default_nthreads, - global_float_prec, tf, ) from deepmd.tf.loggers import ( set_log_handles, ) +from deepmd.utils.summary import SummaryPrinter as BaseSummaryPrinter if TYPE_CHECKING: import horovod.tensorflow as HVD __all__ = [ - "WELCOME", - "CITATION", - "BUILD", "RunOptions", ] log = logging.getLogger(__name__) -# http://patorjk.com/software/taag. Font:Big" -WELCOME = ( - r" _____ _____ __ __ _____ _ _ _ ", - r"| __ \ | __ \ | \/ || __ \ | | (_)| | ", - r"| | | | ___ ___ | |__) || \ / || | | | ______ | | __ _ | |_ ", - r"| | | | / _ \ / _ \| ___/ | |\/| || | | ||______|| |/ /| || __|", - r"| |__| || __/| __/| | | | | || |__| | | < | || |_ ", - r"|_____/ \___| \___||_| |_| |_||_____/ |_|\_\|_| \__|", -) +class SummaryPrinter(BaseSummaryPrinter): + """Summary printer for TensorFlow.""" -CITATION = ( - "Please read and cite:", - "Wang, Zhang, Han and E, Comput.Phys.Comm. 228, 178-184 (2018)", - "Zeng et al, J. Chem. Phys., 159, 054801 (2023)", - "See https://deepmd.rtfd.io/credits/ for details.", -) + def __init__(self, compute_device: str, ngpus: int) -> None: + super().__init__() + self.compute_device = compute_device + self.ngpus = ngpus -_sep = "\n " -BUILD = ( - f"installed to: {GLOBAL_CONFIG['install_prefix']}", - f"source : {GLOBAL_CONFIG['git_summ']}", - f"source brach: {GLOBAL_CONFIG['git_branch']}", - f"source commit: {GLOBAL_CONFIG['git_hash']}", - f"source commit at: {GLOBAL_CONFIG['git_date']}", - f"build float prec: {global_float_prec}", - f"build variant: {GLOBAL_CONFIG['dp_variant']}", - f"build with tf inc: {GLOBAL_CONFIG['tf_include_dir']}", - f"build with tf lib: {GLOBAL_CONFIG['tf_libs'].replace(';', _sep)}", -) + def is_built_with_cuda(self) -> bool: + """Check if the backend is built with CUDA.""" + return tf.test.is_built_with_cuda() + + def is_built_with_rocm(self) -> bool: + """Check if the backend is built with ROCm.""" + return tf.test.is_built_with_rocm() + + def get_compute_device(self) -> str: + """Get Compute device.""" + return self.compute_device + + def get_ngpus(self) -> int: + """Get the number of GPUs.""" + return self.ngpus + + def get_backend_info(self) -> dict: + """Get backend information.""" + return { + "Backend": "TensorFlow", + "TF ver": tf.version.GIT_VERSION, + "build with TF ver": TF_VERSION, + "build with TF inc": GLOBAL_CONFIG["tf_include_dir"].replace(";", "\n"), + "build with TF lib": GLOBAL_CONFIG["tf_libs"].replace(";", "\n"), + } class RunOptions: @@ -148,25 +148,7 @@ def is_chief(self): def print_resource_summary(self): """Print build and current running cluster configuration summary.""" - log.info("---Summary of the training---------------------------------------") - if self.is_distrib: - log.info("distributed") - log.info(f"world size: {self.world_size}") - log.info(f"my rank: {self.my_rank}") - log.info(f"node list: {self.nodelist}") - log.info(f"running on: {self.nodename}") - log.info(f"computing device: {self.my_device}") - if tf.test.is_built_with_cuda(): - env_value = os.environ.get("CUDA_VISIBLE_DEVICES", "unset") - log.info(f"CUDA_VISIBLE_DEVICES: {env_value}") - if hasattr(tf.test, "is_built_with_rocm") and tf.test.is_built_with_rocm(): - env_value = os.environ.get("HIP_VISIBLE_DEVICES", "unset") - log.info(f"HIP_VISIBLE_DEVICES: {env_value}") - log.info(f"Count of visible GPU: {len(self.gpus or [])}") - intra, inter = get_tf_default_nthreads() - log.info(f"num_intra_threads: {intra:d}") - log.info(f"num_inter_threads: {inter:d}") - log.info("-----------------------------------------------------------------") + SummaryPrinter(self.my_device, len(self.gpus or []))() def _setup_logger( self, diff --git a/deepmd/utils/hostlist.py b/deepmd/utils/hostlist.py index d09a8d8bf1..c184b04031 100644 --- a/deepmd/utils/hostlist.py +++ b/deepmd/utils/hostlist.py @@ -31,4 +31,4 @@ def get_host_names() -> Tuple[str, List[str]]: if comm.Get_size() == 1: return host_name, [host_name] host_names = comm.allgather(host_name) - return host_name, list(set(host_names)) + return host_name, host_names diff --git a/deepmd/utils/summary.py b/deepmd/utils/summary.py new file mode 100644 index 0000000000..e2118bf7e0 --- /dev/null +++ b/deepmd/utils/summary.py @@ -0,0 +1,127 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging +import os +from abc import ( + ABC, + abstractmethod, +) +from typing import ( + ClassVar, +) + +import deepmd +from deepmd.env import ( + GLOBAL_CONFIG, + get_default_nthreads, + global_float_prec, +) +from deepmd.utils.hostlist import ( + get_host_names, +) + +log = logging.getLogger(__name__) + + +class SummaryPrinter(ABC): + """Base summary printer. + + Backends should inherit from this class and implement the abstract methods. + """ + + # http://patorjk.com/software/taag. Font:Big" + WELCOME = ( + r" _____ _____ __ __ _____ _ _ _ ", + r"| __ \ | __ \ | \/ || __ \ | | (_)| | ", + r"| | | | ___ ___ | |__) || \ / || | | | ______ | | __ _ | |_ ", + r"| | | | / _ \ / _ \| ___/ | |\/| || | | ||______|| |/ /| || __|", + r"| |__| || __/| __/| | | | | || |__| | | < | || |_ ", + r"|_____/ \___| \___||_| |_| |_||_____/ |_|\_\|_| \__|", + ) + + CITATION = ( + "Please read and cite:", + "Wang, Zhang, Han and E, Comput.Phys.Comm. 228, 178-184 (2018)", + "Zeng et al, J. Chem. Phys., 159, 054801 (2023)", + "See https://deepmd.rtfd.io/credits/ for details.", + ) + + BUILD: ClassVar = { + "installed to": "\n".join(deepmd.__path__), + "source": GLOBAL_CONFIG["git_summ"], + "source brach": GLOBAL_CONFIG["git_branch"], + "source commit": GLOBAL_CONFIG["git_hash"], + "source commit at": GLOBAL_CONFIG["git_date"], + "use float prec": global_float_prec, + "build variant": GLOBAL_CONFIG["dp_variant"], + } + + def __call__(self): + """Print build and current running cluster configuration summary.""" + nodename, nodelist = get_host_names() + build_info = self.BUILD.copy() + build_info.update(self.get_backend_info()) + if len(nodelist) > 1: + build_info.update( + { + "world size": str(len(nodelist)), + "node list": ", ".join(set(nodelist)), + } + ) + build_info.update( + { + "running on": nodename, + "computing device": self.get_compute_device(), + } + ) + if self.is_built_with_cuda(): + env_value = os.environ.get("CUDA_VISIBLE_DEVICES", "unset") + build_info["CUDA_VISIBLE_DEVICES"] = env_value + if self.is_built_with_rocm(): + env_value = os.environ.get("HIP_VISIBLE_DEVICES", "unset") + build_info["HIP_VISIBLE_DEVICES"] = env_value + if self.is_built_with_cuda() or self.is_built_with_rocm(): + build_info["Count of visible GPUs"] = str(self.get_ngpus()) + + intra, inter = get_default_nthreads() + build_info.update( + { + "num_intra_threads": str(intra), + "num_inter_threads": str(inter), + } + ) + # count the maximum characters in the keys and values + max_key_len = max(len(k) for k in build_info) + 2 + max_val_len = max( + len(x) for v in build_info.values() for x in str(v).split("\n") + ) + # print the summary + for line in self.WELCOME + self.CITATION: + log.info(line) + log.info("-" * (max_key_len + max_val_len)) + for kk, vv in build_info.items(): + for iline, vline in enumerate(str(vv).split("\n")): + if iline == 0: + log.info(f"{kk + ': ':<{max_key_len}}{vline}") + else: + log.info(f"{'':<{max_key_len}}{vline}") + log.info("-" * (max_key_len + max_val_len)) + + @abstractmethod + def is_built_with_cuda(self) -> bool: + """Check if the backend is built with CUDA.""" + + @abstractmethod + def is_built_with_rocm(self) -> bool: + """Check if the backend is built with ROCm.""" + + @abstractmethod + def get_compute_device(self) -> str: + """Get Compute device.""" + + @abstractmethod + def get_ngpus(self) -> int: + """Get the number of GPUs.""" + + def get_backend_info(self) -> dict: + """Get backend information.""" + return {} diff --git a/source/config/run_config.ini b/source/config/run_config.ini index 11f4100e61..5cdaa35317 100644 --- a/source/config/run_config.ini +++ b/source/config/run_config.ini @@ -7,7 +7,7 @@ GIT_BRANCH = @GIT_BRANCH@ ENABLE_TENSORFLOW = @ENABLE_TENSORFLOW@ ENABLE_PYTORCH = @ENABLE_PYTORCH@ TF_INCLUDE_DIR = @TensorFlow_INCLUDE_DIRS@ -TF_LIBS = @TensorFlow_LIBRARY@ +TF_LIBS = @TensorFlow_LIBRARY_PATH@ TF_VERSION = @TENSORFLOW_VERSION@ TF_CXX11_ABI_FLAG = @OP_CXX_ABI@ MODEL_VERSION=@MODEL_VERSION@ From cfdda1d79cafd2bbc5f61bf3d56157af7a3eea3c Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Thu, 8 Feb 2024 14:51:21 +0800 Subject: [PATCH 063/686] add hessian support in output def. (#3246) hessian not implemented in neither tf nor pt. Co-authored-by: Han Wang --- deepmd/dpmodel/output_def.py | 24 +- .../tests/common/dpmodel/test_output_def.py | 239 +++++++++++++----- 2 files changed, 201 insertions(+), 62 deletions(-) diff --git a/deepmd/dpmodel/output_def.py b/deepmd/dpmodel/output_def.py index 8b190ed5de..fac24534eb 100644 --- a/deepmd/dpmodel/output_def.py +++ b/deepmd/dpmodel/output_def.py @@ -172,8 +172,12 @@ class OutputVariableDef: are differentiable. Virial, the transposed negative gradient with cell tensor times cell tensor, will be calculated, see eq 40 JCP 159, 054801 (2023). + atomic : bool + If the variable is defined for each atom. category : int The category of the output variable. + hessian : bool + If hessian is requred """ def __init__( @@ -185,6 +189,7 @@ def __init__( c_differentiable: bool = False, atomic: bool = True, category: int = OutputVariableCategory.OUT.value, + r_hessian: bool = False, ): self.name = name self.shape = list(shape) @@ -194,13 +199,15 @@ def __init__( self.c_differentiable = c_differentiable if self.c_differentiable and not self.r_differentiable: raise ValueError("c differentiable requires r_differentiable") - if not self.reduciable and self.r_differentiable: - raise ValueError("only reduciable variable are r differentiable") - if not self.reduciable and self.c_differentiable: - raise ValueError("only reduciable variable are c differentiable") if self.reduciable and not self.atomic: raise ValueError("a reduciable variable should be atomic") self.category = category + self.r_hessian = r_hessian + if self.r_hessian: + if not self.reduciable: + raise ValueError("only reduciable variable can calculate hessian") + if not self.r_differentiable: + raise ValueError("only r_differentiable variable can calculate hessian") class FittingOutputDef: @@ -257,6 +264,7 @@ def __init__( self.def_outp = fit_defs self.def_redu = do_reduce(self.def_outp.get_data()) self.def_derv_r, self.def_derv_c = do_derivative(self.def_outp.get_data()) + self.def_hess_r, _ = do_derivative(self.def_derv_r) self.def_derv_c_redu = do_reduce(self.def_derv_c) self.var_defs: Dict[str, OutputVariableDef] = {} for ii in [ @@ -265,6 +273,7 @@ def __init__( self.def_derv_c, self.def_derv_r, self.def_derv_c_redu, + self.def_hess_r, ]: self.var_defs.update(ii) @@ -292,6 +301,9 @@ def keys_redu(self): def keys_derv_r(self): return self.def_derv_r.keys() + def keys_hess_r(self): + return self.def_hess_r.keys() + def keys_derv_c(self): return self.def_derv_c.keys() @@ -392,7 +404,9 @@ def do_derivative( rkr, vv.shape + [3], # noqa: RUF005 reduciable=False, - r_differentiable=False, + r_differentiable=( + vv.r_hessian and vv.category == OutputVariableCategory.OUT.value + ), c_differentiable=False, atomic=True, category=apply_operation(vv, OutputVariableOperation.DERV_R), diff --git a/source/tests/common/dpmodel/test_output_def.py b/source/tests/common/dpmodel/test_output_def.py index 9e794e0f32..272082513c 100644 --- a/source/tests/common/dpmodel/test_output_def.py +++ b/source/tests/common/dpmodel/test_output_def.py @@ -44,6 +44,16 @@ def test_model_output_def(self): r_differentiable=True, c_differentiable=True, atomic=True, + r_hessian=False, + ), + OutputVariableDef( + "energy2", + [1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + atomic=True, + r_hessian=True, ), OutputVariableDef( "dos", @@ -64,26 +74,33 @@ def test_model_output_def(self): ] # fitting definition fd = FittingOutputDef(defs) - expected_keys = ["energy", "dos", "foo"] + expected_keys = ["energy", "energy2", "dos", "foo"] self.assertEqual( set(expected_keys), set(fd.keys()), ) # shape self.assertEqual(fd["energy"].shape, [1]) + self.assertEqual(fd["energy2"].shape, [1]) self.assertEqual(fd["dos"].shape, [10]) self.assertEqual(fd["foo"].shape, [3]) # atomic self.assertEqual(fd["energy"].atomic, True) + self.assertEqual(fd["energy2"].atomic, True) self.assertEqual(fd["dos"].atomic, True) self.assertEqual(fd["foo"].atomic, True) # reduce self.assertEqual(fd["energy"].reduciable, True) + self.assertEqual(fd["energy2"].reduciable, True) self.assertEqual(fd["dos"].reduciable, True) self.assertEqual(fd["foo"].reduciable, False) # derivative self.assertEqual(fd["energy"].r_differentiable, True) self.assertEqual(fd["energy"].c_differentiable, True) + self.assertEqual(fd["energy"].r_hessian, False) + self.assertEqual(fd["energy2"].r_differentiable, True) + self.assertEqual(fd["energy2"].c_differentiable, True) + self.assertEqual(fd["energy2"].r_hessian, True) self.assertEqual(fd["dos"].r_differentiable, False) self.assertEqual(fd["foo"].r_differentiable, False) self.assertEqual(fd["dos"].c_differentiable, False) @@ -92,12 +109,18 @@ def test_model_output_def(self): md = ModelOutputDef(fd) expected_keys = [ "energy", + "energy2", "dos", "foo", "energy_redu", "energy_derv_r", "energy_derv_c", "energy_derv_c_redu", + "energy2_redu", + "energy2_derv_r", + "energy2_derv_r_derv_r", + "energy2_derv_c", + "energy2_derv_c_redu", "dos_redu", ] self.assertEqual( @@ -108,33 +131,51 @@ def test_model_output_def(self): self.assertEqual(md[kk].name, kk) # reduce self.assertEqual(md["energy"].reduciable, True) + self.assertEqual(md["energy2"].reduciable, True) self.assertEqual(md["dos"].reduciable, True) self.assertEqual(md["foo"].reduciable, False) # derivative self.assertEqual(md["energy"].r_differentiable, True) self.assertEqual(md["energy"].c_differentiable, True) + self.assertEqual(md["energy"].r_hessian, False) + self.assertEqual(md["energy2"].r_differentiable, True) + self.assertEqual(md["energy2"].c_differentiable, True) + self.assertEqual(md["energy2"].r_hessian, True) self.assertEqual(md["dos"].r_differentiable, False) self.assertEqual(md["foo"].r_differentiable, False) self.assertEqual(md["dos"].c_differentiable, False) self.assertEqual(md["foo"].c_differentiable, False) # shape self.assertEqual(md["energy"].shape, [1]) + self.assertEqual(md["energy2"].shape, [1]) self.assertEqual(md["dos"].shape, [10]) self.assertEqual(md["foo"].shape, [3]) self.assertEqual(md["energy_redu"].shape, [1]) self.assertEqual(md["energy_derv_r"].shape, [1, 3]) self.assertEqual(md["energy_derv_c"].shape, [1, 9]) self.assertEqual(md["energy_derv_c_redu"].shape, [1, 9]) + self.assertEqual(md["energy2_redu"].shape, [1]) + self.assertEqual(md["energy2_derv_r"].shape, [1, 3]) + self.assertEqual(md["energy2_derv_c"].shape, [1, 9]) + self.assertEqual(md["energy2_derv_c_redu"].shape, [1, 9]) + self.assertEqual(md["energy2_derv_r_derv_r"].shape, [1, 3, 3]) # atomic self.assertEqual(md["energy"].atomic, True) + self.assertEqual(md["energy2"].atomic, True) self.assertEqual(md["dos"].atomic, True) self.assertEqual(md["foo"].atomic, True) self.assertEqual(md["energy_redu"].atomic, False) self.assertEqual(md["energy_derv_r"].atomic, True) self.assertEqual(md["energy_derv_c"].atomic, True) self.assertEqual(md["energy_derv_c_redu"].atomic, False) + self.assertEqual(md["energy2_redu"].atomic, False) + self.assertEqual(md["energy2_derv_r"].atomic, True) + self.assertEqual(md["energy2_derv_c"].atomic, True) + self.assertEqual(md["energy2_derv_c_redu"].atomic, False) + self.assertEqual(md["energy2_derv_r_derv_r"].atomic, True) # category self.assertEqual(md["energy"].category, OutputVariableCategory.OUT) + self.assertEqual(md["energy2"].category, OutputVariableCategory.OUT) self.assertEqual(md["dos"].category, OutputVariableCategory.OUT) self.assertEqual(md["foo"].category, OutputVariableCategory.OUT) self.assertEqual(md["energy_redu"].category, OutputVariableCategory.REDU) @@ -143,101 +184,159 @@ def test_model_output_def(self): self.assertEqual( md["energy_derv_c_redu"].category, OutputVariableCategory.DERV_C_REDU ) - # flag - self.assertEqual(md["energy"].category & OutputVariableOperation.REDU, 0) - self.assertEqual(md["energy"].category & OutputVariableOperation.DERV_R, 0) - self.assertEqual(md["energy"].category & OutputVariableOperation.DERV_C, 0) - self.assertEqual(md["dos"].category & OutputVariableOperation.REDU, 0) - self.assertEqual(md["dos"].category & OutputVariableOperation.DERV_R, 0) - self.assertEqual(md["dos"].category & OutputVariableOperation.DERV_C, 0) - self.assertEqual(md["foo"].category & OutputVariableOperation.REDU, 0) - self.assertEqual(md["foo"].category & OutputVariableOperation.DERV_R, 0) - self.assertEqual(md["foo"].category & OutputVariableOperation.DERV_C, 0) - self.assertEqual( - md["energy_redu"].category & OutputVariableOperation.REDU, - OutputVariableOperation.REDU, - ) - self.assertEqual(md["energy_redu"].category & OutputVariableOperation.DERV_R, 0) - self.assertEqual(md["energy_redu"].category & OutputVariableOperation.DERV_C, 0) - self.assertEqual(md["energy_derv_r"].category & OutputVariableOperation.REDU, 0) + self.assertEqual(md["energy2_redu"].category, OutputVariableCategory.REDU) + self.assertEqual(md["energy2_derv_r"].category, OutputVariableCategory.DERV_R) + self.assertEqual(md["energy2_derv_c"].category, OutputVariableCategory.DERV_C) self.assertEqual( - md["energy_derv_r"].category & OutputVariableOperation.DERV_R, - OutputVariableOperation.DERV_R, + md["energy2_derv_c_redu"].category, OutputVariableCategory.DERV_C_REDU ) self.assertEqual( - md["energy_derv_r"].category & OutputVariableOperation.DERV_C, 0 + md["energy2_derv_r_derv_r"].category, OutputVariableCategory.DERV_R_DERV_R ) - self.assertEqual(md["energy_derv_c"].category & OutputVariableOperation.REDU, 0) + # flag + OVO = OutputVariableOperation + self.assertEqual(md["energy"].category & OVO.REDU, 0) + self.assertEqual(md["energy"].category & OVO.DERV_R, 0) + self.assertEqual(md["energy"].category & OVO.DERV_C, 0) + self.assertEqual(md["energy2"].category & OVO.REDU, 0) + self.assertEqual(md["energy2"].category & OVO.DERV_R, 0) + self.assertEqual(md["energy2"].category & OVO.DERV_C, 0) + self.assertEqual(md["dos"].category & OVO.REDU, 0) + self.assertEqual(md["dos"].category & OVO.DERV_R, 0) + self.assertEqual(md["dos"].category & OVO.DERV_C, 0) + self.assertEqual(md["foo"].category & OVO.REDU, 0) + self.assertEqual(md["foo"].category & OVO.DERV_R, 0) + self.assertEqual(md["foo"].category & OVO.DERV_C, 0) + # flag: energy self.assertEqual( - md["energy_derv_c"].category & OutputVariableOperation.DERV_R, 0 + md["energy_redu"].category & OVO.REDU, + OVO.REDU, ) + self.assertEqual(md["energy_redu"].category & OVO.DERV_R, 0) + self.assertEqual(md["energy_redu"].category & OVO.DERV_C, 0) + self.assertEqual(md["energy_derv_r"].category & OVO.REDU, 0) self.assertEqual( - md["energy_derv_c"].category & OutputVariableOperation.DERV_C, - OutputVariableOperation.DERV_C, + md["energy_derv_r"].category & OVO.DERV_R, + OVO.DERV_R, ) + self.assertEqual(md["energy_derv_r"].category & OVO.DERV_C, 0) + self.assertEqual(md["energy_derv_c"].category & OVO.REDU, 0) + self.assertEqual(md["energy_derv_c"].category & OVO.DERV_R, 0) self.assertEqual( - md["energy_derv_c_redu"].category & OutputVariableOperation.REDU, - OutputVariableOperation.REDU, + md["energy_derv_c"].category & OVO.DERV_C, + OVO.DERV_C, ) self.assertEqual( - md["energy_derv_c_redu"].category & OutputVariableOperation.DERV_R, 0 + md["energy_derv_c_redu"].category & OVO.REDU, + OVO.REDU, ) + self.assertEqual(md["energy_derv_c_redu"].category & OVO.DERV_R, 0) self.assertEqual( - md["energy_derv_c_redu"].category & OutputVariableOperation.DERV_C, - OutputVariableOperation.DERV_C, + md["energy_derv_c_redu"].category & OVO.DERV_C, + OVO.DERV_C, ) - - # apply_operation + # flag: energy2 + kk = "energy2_redu" + self.assertEqual(md[kk].category & OVO.REDU, OVO.REDU) + self.assertEqual(md[kk].category & OVO.DERV_R, 0) + self.assertEqual(md[kk].category & OVO.DERV_C, 0) + self.assertEqual(md[kk].category & OVO._SEC_DERV_R, 0) + kk = "energy2_derv_r" + self.assertEqual(md[kk].category & OVO.REDU, 0) + self.assertEqual(md[kk].category & OVO.DERV_R, OVO.DERV_R) + self.assertEqual(md[kk].category & OVO.DERV_C, 0) + self.assertEqual(md[kk].category & OVO._SEC_DERV_R, 0) + kk = "energy2_derv_c" + self.assertEqual(md[kk].category & OVO.REDU, 0) + self.assertEqual(md[kk].category & OVO.DERV_R, 0) + self.assertEqual(md[kk].category & OVO.DERV_C, OVO.DERV_C) + self.assertEqual(md[kk].category & OVO._SEC_DERV_R, 0) + kk = "energy2_derv_c_redu" + self.assertEqual(md[kk].category & OVO.REDU, OVO.REDU) + self.assertEqual(md[kk].category & OVO.DERV_R, 0) + self.assertEqual(md[kk].category & OVO.DERV_C, OVO.DERV_C) + self.assertEqual(md[kk].category & OVO._SEC_DERV_R, 0) + kk = "energy2_derv_r_derv_r" + self.assertEqual(md[kk].category & OVO.REDU, 0) + self.assertEqual(md[kk].category & OVO.DERV_R, OVO.DERV_R) + self.assertEqual(md[kk].category & OVO.DERV_C, 0) + self.assertEqual(md[kk].category & OVO._SEC_DERV_R, OVO._SEC_DERV_R) + # apply_operation: energy self.assertEqual( - apply_operation(md["energy"], OutputVariableOperation.REDU), + apply_operation(md["energy"], OVO.REDU), md["energy_redu"].category, ) self.assertEqual( - apply_operation(md["energy"], OutputVariableOperation.DERV_R), + apply_operation(md["energy"], OVO.DERV_R), md["energy_derv_r"].category, ) self.assertEqual( - apply_operation(md["energy"], OutputVariableOperation.DERV_C), + apply_operation(md["energy"], OVO.DERV_C), md["energy_derv_c"].category, ) self.assertEqual( - apply_operation(md["energy_derv_c"], OutputVariableOperation.REDU), + apply_operation(md["energy_derv_c"], OVO.REDU), md["energy_derv_c_redu"].category, ) + # apply_operation: energy2 + self.assertEqual( + apply_operation(md["energy2"], OVO.REDU), + md["energy2_redu"].category, + ) + self.assertEqual( + apply_operation(md["energy2"], OVO.DERV_R), + md["energy2_derv_r"].category, + ) + self.assertEqual( + apply_operation(md["energy2"], OVO.DERV_C), + md["energy2_derv_c"].category, + ) + self.assertEqual( + apply_operation(md["energy2_derv_c"], OVO.REDU), + md["energy2_derv_c_redu"].category, + ) + self.assertEqual( + apply_operation(md["energy2_derv_r"], OVO.DERV_R), + md["energy2_derv_r_derv_r"].category, + ) + # raise ValueError + with self.assertRaises(ValueError): + apply_operation(md["energy_redu"], OVO.REDU) + with self.assertRaises(ValueError): + apply_operation(md["energy_derv_c"], OVO.DERV_C) + with self.assertRaises(ValueError): + apply_operation(md["energy_derv_c_redu"], OVO.REDU) # raise ValueError with self.assertRaises(ValueError): - apply_operation(md["energy_redu"], OutputVariableOperation.REDU) + apply_operation(md["energy2_redu"], OVO.REDU) with self.assertRaises(ValueError): - apply_operation(md["energy_derv_c"], OutputVariableOperation.DERV_C) + apply_operation(md["energy2_derv_c"], OVO.DERV_C) with self.assertRaises(ValueError): - apply_operation(md["energy_derv_c_redu"], OutputVariableOperation.REDU) + apply_operation(md["energy2_derv_c_redu"], OVO.REDU) + with self.assertRaises(ValueError): + apply_operation(md["energy2_derv_r_derv_r"], OVO.DERV_R) # hession - hession_cat = apply_operation( - md["energy_derv_r"], OutputVariableOperation.DERV_R - ) - self.assertEqual( - hession_cat & OutputVariableOperation.DERV_R, OutputVariableOperation.DERV_R - ) + hession_cat = apply_operation(md["energy_derv_r"], OVO.DERV_R) + self.assertEqual(hession_cat & OVO.DERV_R, OVO.DERV_R) self.assertEqual( - hession_cat & OutputVariableOperation._SEC_DERV_R, - OutputVariableOperation._SEC_DERV_R, + hession_cat & OVO._SEC_DERV_R, + OVO._SEC_DERV_R, ) self.assertEqual(hession_cat, OutputVariableCategory.DERV_R_DERV_R) hession_vardef = OutputVariableDef( "energy_derv_r_derv_r", [1], False, False, category=hession_cat ) with self.assertRaises(ValueError): - apply_operation(hession_vardef, OutputVariableOperation.DERV_R) + apply_operation(hession_vardef, OVO.DERV_R) - def test_raise_no_redu_deriv(self): - with self.assertRaises(ValueError) as context: - OutputVariableDef( - "energy", - [1], - reduciable=False, - r_differentiable=True, - c_differentiable=False, - ) + def test_no_raise_no_redu_deriv(self): + OutputVariableDef( + "energy", + [1], + reduciable=False, + r_differentiable=True, + c_differentiable=False, + ) def test_raise_requires_r_deriv(self): with self.assertRaises(ValueError) as context: @@ -253,6 +352,32 @@ def test_raise_redu_not_atomic(self): with self.assertRaises(ValueError) as context: (OutputVariableDef("energy", [1], reduciable=True, atomic=False),) + def test_hessian_not_reducible(self): + with self.assertRaises(ValueError) as context: + ( + OutputVariableDef( + "energy", + [1], + reduciable=False, + atomic=False, + r_differentiable=True, + r_hessian=True, + ), + ) + + def test_hessian_not_r_differentiable(self): + with self.assertRaises(ValueError) as context: + ( + OutputVariableDef( + "energy", + [1], + reduciable=True, + atomic=False, + r_differentiable=False, + r_hessian=True, + ), + ) + def test_model_decorator(self): nf = 2 nloc = 3 From c2350993939c5ed59aeda0ff04ab66d04bdb7633 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 8 Feb 2024 07:35:03 -0500 Subject: [PATCH 064/686] refactor DeepEval (#3213) Implement high-level `DeepEval` and low-level `DeepEvalBackend`. Ugly things: (1) `DipoleChargeModifier` is not updated in this PR. Thus, it still inherits from the old `DeepEval`. (It should not inherit from `DeepEval`!!!) (2) There are no unit tests or testing models for DeepGlobarPolar or DeepWFC. (3) The shape of the atomic tensor looks different from forces and atomic virials. TODO: - [x] Add docs --------- Signed-off-by: Jinzhe Zeng Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- deepmd/__init__.py | 25 + deepmd/infer/backend.py | 1 + deepmd/infer/deep_dipole.py | 28 + deepmd/infer/deep_dos.py | 144 ++++ deepmd/infer/deep_eval.py | 507 ++++++++++++++ deepmd/infer/deep_polar.py | 97 +++ deepmd/infer/deep_pot.py | 156 +++-- deepmd/infer/deep_tensor.py | 238 +++++++ deepmd/infer/deep_wfc.py | 28 + deepmd/pt/infer/deep_eval.py | 291 +++++--- deepmd/tf/entrypoints/test.py | 40 +- deepmd/tf/infer/__init__.py | 106 +-- deepmd/tf/infer/data_modifier.py | 4 +- deepmd/tf/infer/deep_dipole.py | 17 +- deepmd/tf/infer/deep_dos.py | 508 +------------- deepmd/tf/infer/deep_eval.py | 1071 +++++++++++++++++++++++++++++- deepmd/tf/infer/deep_polar.py | 169 +---- deepmd/tf/infer/deep_pot.py | 692 +------------------ deepmd/tf/infer/deep_tensor.py | 4 +- deepmd/tf/infer/deep_wfc.py | 70 +- deepmd/tf/model/frozen.py | 32 +- 21 files changed, 2543 insertions(+), 1685 deletions(-) create mode 100644 deepmd/infer/deep_dipole.py create mode 100644 deepmd/infer/deep_dos.py create mode 100644 deepmd/infer/deep_eval.py create mode 100644 deepmd/infer/deep_polar.py create mode 100644 deepmd/infer/deep_tensor.py create mode 100644 deepmd/infer/deep_wfc.py diff --git a/deepmd/__init__.py b/deepmd/__init__.py index f95536db50..5664c3edc6 100644 --- a/deepmd/__init__.py +++ b/deepmd/__init__.py @@ -14,6 +14,31 @@ __version__, ) + +def DeepPotential(*args, **kwargs): + """Factory function that forwards to DeepEval (for compatbility + and performance). + + Parameters + ---------- + *args + positional arguments + **kwargs + keyword arguments + + Returns + ------- + DeepEval + potentials + """ + from deepmd.infer import ( + DeepPotential, + ) + + return DeepPotential(*args, **kwargs) + + __all__ = [ "__version__", + "DeepPotential", ] diff --git a/deepmd/infer/backend.py b/deepmd/infer/backend.py index 809e19466b..26eef22eb4 100644 --- a/deepmd/infer/backend.py +++ b/deepmd/infer/backend.py @@ -21,6 +21,7 @@ def detect_backend(filename: str) -> DPBackend: filename : str The model file name """ + filename = str(filename).lower() if filename.endswith(".pb"): return DPBackend.TensorFlow elif filename.endswith(".pth") or filename.endswith(".pt"): diff --git a/deepmd/infer/deep_dipole.py b/deepmd/infer/deep_dipole.py new file mode 100644 index 0000000000..b443b54417 --- /dev/null +++ b/deepmd/infer/deep_dipole.py @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from deepmd.infer.deep_tensor import ( + DeepTensor, +) + + +class DeepDipole(DeepTensor): + """Deep dipole model. + + Parameters + ---------- + model_file : Path + The name of the frozen model file. + *args : list + Positional arguments. + auto_batch_size : bool or int or AutoBatchSize, default: True + If True, automatic batch size will be used. If int, it will be used + as the initial batch size. + neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional + The ASE neighbor list class to produce the neighbor list. If None, the + neighbor list will be built natively in the model. + **kwargs : dict + Keyword arguments. + """ + + @property + def output_tensor_name(self) -> str: + return "dipole" diff --git a/deepmd/infer/deep_dos.py b/deepmd/infer/deep_dos.py new file mode 100644 index 0000000000..d95d2a119f --- /dev/null +++ b/deepmd/infer/deep_dos.py @@ -0,0 +1,144 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Any, + Dict, + List, + Optional, + Tuple, + Union, +) + +import numpy as np + +from deepmd.dpmodel.output_def import ( + FittingOutputDef, + ModelOutputDef, + OutputVariableDef, +) + +from .deep_eval import ( + DeepEval, +) + + +class DeepDOS(DeepEval): + """Deep density of states model. + + Parameters + ---------- + model_file : Path + The name of the frozen model file. + *args : list + Positional arguments. + auto_batch_size : bool or int or AutoBatchSize, default: True + If True, automatic batch size will be used. If int, it will be used + as the initial batch size. + neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional + The ASE neighbor list class to produce the neighbor list. If None, the + neighbor list will be built natively in the model. + **kwargs : dict + Keyword arguments. + """ + + @property + def output_def(self) -> ModelOutputDef: + """Get the output definition of this model.""" + return ModelOutputDef( + FittingOutputDef( + [ + OutputVariableDef( + "dos", + shape=[-1], + reduciable=True, + atomic=True, + ), + ] + ) + ) + + def eval( + self, + coords: np.ndarray, + cells: Optional[np.ndarray], + atom_types: Union[List[int], np.ndarray], + atomic: bool = False, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + mixed_type: bool = False, + **kwargs: Dict[str, Any], + ) -> Tuple[np.ndarray, ...]: + """Evaluate energy, force, and virial. If atomic is True, + also return atomic energy and atomic virial. + + Parameters + ---------- + coords : np.ndarray + The coordinates of the atoms, in shape (nframes, natoms, 3). + cells : np.ndarray + The cell vectors of the system, in shape (nframes, 9). If the system + is not periodic, set it to None. + atom_types : List[int] or np.ndarray + The types of the atoms. If mixed_type is False, the shape is (natoms,); + otherwise, the shape is (nframes, natoms). + atomic : bool, optional + Whether to return atomic energy and atomic virial, by default False. + fparam : np.ndarray, optional + The frame parameters, by default None. + aparam : np.ndarray, optional + The atomic parameters, by default None. + mixed_type : bool, optional + Whether the atom_types is mixed type, by default False. + **kwargs : Dict[str, Any] + Keyword arguments. + + Returns + ------- + energy + The energy of the system, in shape (nframes,). + force + The force of the system, in shape (nframes, natoms, 3). + virial + The virial of the system, in shape (nframes, 9). + atomic_energy + The atomic energy of the system, in shape (nframes, natoms). Only returned + when atomic is True. + atomic_virial + The atomic virial of the system, in shape (nframes, natoms, 9). Only returned + when atomic is True. + """ + ( + coords, + cells, + atom_types, + fparam, + aparam, + nframes, + natoms, + ) = self._standard_input(coords, cells, atom_types, fparam, aparam, mixed_type) + results = self.deep_eval.eval( + coords, + cells, + atom_types, + atomic, + fparam=fparam, + aparam=aparam, + **kwargs, + ) + # energy = results["dos_redu"].reshape(nframes, self.get_numb_dos()) + atomic_energy = results["dos"].reshape(nframes, natoms, self.get_numb_dos()) + # not same as dos_redu... why? + energy = np.sum(atomic_energy, axis=1) + + if atomic: + return ( + energy, + atomic_energy, + ) + else: + return (energy,) + + def get_numb_dos(self) -> int: + return self.deep_eval.get_numb_dos() + + +__all__ = ["DeepDOS"] diff --git a/deepmd/infer/deep_eval.py b/deepmd/infer/deep_eval.py new file mode 100644 index 0000000000..b1d17afc8b --- /dev/null +++ b/deepmd/infer/deep_eval.py @@ -0,0 +1,507 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from abc import ( + ABC, + abstractmethod, +) +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Dict, + List, + Optional, + Tuple, + Union, +) + +import numpy as np + +from deepmd.dpmodel.output_def import ( + FittingOutputDef, + ModelOutputDef, +) +from deepmd.utils.batch_size import ( + AutoBatchSize, +) + +from .backend import ( + DPBackend, + detect_backend, +) + +if TYPE_CHECKING: + import ase.neighborlist + + +class DeepEvalBackend(ABC): + """Low-level Deep Evaluator interface. + + Backends should inherbit implement this interface. High-level interface + will be built on top of this. + + Parameters + ---------- + model_file : Path + The name of the frozen model file. + *args : list + Positional arguments. + auto_batch_size : bool or int or AutoBatchSize, default: True + If True, automatic batch size will be used. If int, it will be used + as the initial batch size. + neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional + The ASE neighbor list class to produce the neighbor list. If None, the + neighbor list will be built natively in the model. + **kwargs : dict + Keyword arguments. + """ + + _OUTDEF_DP2BACKEND: ClassVar[dict] = { + "energy": "atom_energy", + "energy_redu": "energy", + "energy_derv_r": "force", + "energy_derv_c": "atom_virial", + "energy_derv_c_redu": "virial", + "polar": "polar", + "polar_redu": "global_polar", + "polar_derv_r": "force", + "polar_derv_c": "atom_virial", + "polar_derv_c_redu": "virial", + "dipole": "dipole", + "dipole_redu": "global_dipole", + "dipole_derv_r": "force", + "dipole_derv_c": "atom_virial", + "dipole_derv_c_redu": "virial", + "dos": "atom_dos", + "dos_redu": "dos", + } + + @abstractmethod + def __init__( + self, + model_file: str, + output_def: ModelOutputDef, + *args: List[Any], + auto_batch_size: Union[bool, int, AutoBatchSize] = True, + neighbor_list: Optional["ase.neighborlist.NewPrimitiveNeighborList"] = None, + **kwargs: Dict[str, Any], + ) -> None: + pass + + def __new__(cls, model_file: str, *args, **kwargs): + if cls is DeepEvalBackend: + backend = detect_backend(model_file) + if backend == DPBackend.TensorFlow: + from deepmd.tf.infer.deep_eval import DeepEval as DeepEvalTF + + return super().__new__(DeepEvalTF) + elif backend == DPBackend.PyTorch: + from deepmd.pt.infer.deep_eval import DeepEval as DeepEvalPT + + return super().__new__(DeepEvalPT) + else: + raise NotImplementedError("Unsupported backend: " + str(backend)) + return super().__new__(cls) + + @abstractmethod + def eval( + self, + coords: np.ndarray, + cells: np.ndarray, + atom_types: np.ndarray, + atomic: bool = False, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + **kwargs: Dict[str, Any], + ) -> Dict[str, np.ndarray]: + """Evaluate the energy, force and virial by using this DP. + + Parameters + ---------- + coords + The coordinates of atoms. + The array should be of size nframes x natoms x 3 + cells + The cell of the region. + If None then non-PBC is assumed, otherwise using PBC. + The array should be of size nframes x 9 + atom_types + The atom types + The list should contain natoms ints + atomic + Calculate the atomic energy and virial + fparam + The frame parameter. + The array can be of size : + - nframes x dim_fparam. + - dim_fparam. Then all frames are assumed to be provided with the same fparam. + aparam + The atomic parameter + The array can be of size : + - nframes x natoms x dim_aparam. + - natoms x dim_aparam. Then all frames are assumed to be provided with the same aparam. + - dim_aparam. Then all frames and atoms are provided with the same aparam. + **kwargs + Other parameters + + Returns + ------- + output_dict : dict + The output of the evaluation. The keys are the names of the output + variables, and the values are the corresponding output arrays. + """ + + @abstractmethod + def get_rcut(self) -> float: + """Get the cutoff radius of this model.""" + + @abstractmethod + def get_ntypes(self) -> int: + """Get the number of atom types of this model.""" + + @abstractmethod + def get_type_map(self) -> List[str]: + """Get the type map (element name of the atom types) of this model.""" + + @abstractmethod + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this DP.""" + + @abstractmethod + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this DP.""" + + def eval_descriptor( + self, + coords: np.ndarray, + cells: np.ndarray, + atom_types: np.ndarray, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + efield: Optional[np.ndarray] = None, + mixed_type: bool = False, + **kwargs: Dict[str, Any], + ) -> np.ndarray: + """Evaluate descriptors by using this DP. + + Parameters + ---------- + coords + The coordinates of atoms. + The array should be of size nframes x natoms x 3 + cells + The cell of the region. + If None then non-PBC is assumed, otherwise using PBC. + The array should be of size nframes x 9 + atom_types + The atom types + The list should contain natoms ints + fparam + The frame parameter. + The array can be of size : + - nframes x dim_fparam. + - dim_fparam. Then all frames are assumed to be provided with the same fparam. + aparam + The atomic parameter + The array can be of size : + - nframes x natoms x dim_aparam. + - natoms x dim_aparam. Then all frames are assumed to be provided with the same aparam. + - dim_aparam. Then all frames and atoms are provided with the same aparam. + efield + The external field on atoms. + The array should be of size nframes x natoms x 3 + mixed_type + Whether to perform the mixed_type mode. + If True, the input data has the mixed_type format (see doc/model/train_se_atten.md), + in which frames in a system may have different natoms_vec(s), with the same nloc. + + Returns + ------- + descriptor + Descriptors. + """ + raise NotImplementedError + + def eval_typeebd(self) -> np.ndarray: + """Evaluate output of type embedding network by using this model. + + Returns + ------- + np.ndarray + The output of type embedding network. The shape is [ntypes, o_size], + where ntypes is the number of types, and o_size is the number of nodes + in the output layer. + + Raises + ------ + KeyError + If the model does not enable type embedding. + """ + raise NotImplementedError + + def _check_mixed_types(self, atom_types: np.ndarray) -> bool: + """Check if atom types of all frames are the same. + + Traditional descriptors like se_e2_a requires all the frames to + have the same atom types. + + Parameters + ---------- + atom_types : np.ndarray + The atom types of all frames, in shape nframes * natoms. + """ + return np.all(np.equal(atom_types, atom_types[0])) + + @property + @abstractmethod + def model_type(self) -> "DeepEval": + """The the evaluator of the model type.""" + + @abstractmethod + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + + def get_numb_dos(self) -> int: + """Get the number of DOS.""" + raise NotImplementedError + + def get_has_efield(self): + """Check if the model has efield.""" + return False + + @abstractmethod + def get_ntypes_spin(self) -> int: + """Get the number of spin atom types of this model.""" + + +class DeepEval(ABC): + """High-level Deep Evaluator interface. + + The specific DeepEval, such as DeepPot and DeepTensor, should inherit + from this class. This class provides a high-level interface on the top + of the low-level interface. + + Parameters + ---------- + model_file : Path + The name of the frozen model file. + *args : list + Positional arguments. + auto_batch_size : bool or int or AutoBatchSize, default: True + If True, automatic batch size will be used. If int, it will be used + as the initial batch size. + neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional + The ASE neighbor list class to produce the neighbor list. If None, the + neighbor list will be built natively in the model. + **kwargs : dict + Keyword arguments. + """ + + def __new__(cls, model_file: str, *args, **kwargs): + if cls is DeepEval: + deep_eval = DeepEvalBackend( + model_file, + ModelOutputDef(FittingOutputDef([])), + *args, + **kwargs, + ) + return super().__new__(deep_eval.model_type) + return super().__new__(cls) + + def __init__( + self, + model_file: str, + *args: List[Any], + auto_batch_size: Union[bool, int, AutoBatchSize] = True, + neighbor_list: Optional["ase.neighborlist.NewPrimitiveNeighborList"] = None, + **kwargs: Dict[str, Any], + ) -> None: + self.deep_eval = DeepEvalBackend( + model_file, + self.output_def, + *args, + auto_batch_size=auto_batch_size, + neighbor_list=neighbor_list, + **kwargs, + ) + + @property + @abstractmethod + def output_def(self) -> ModelOutputDef: + """Returns the output variable definitions.""" + + def get_rcut(self) -> float: + """Get the cutoff radius of this model.""" + return self.deep_eval.get_rcut() + + def get_ntypes(self) -> int: + """Get the number of atom types of this model.""" + return self.deep_eval.get_ntypes() + + def get_type_map(self) -> List[str]: + """Get the type map (element name of the atom types) of this model.""" + return self.deep_eval.get_type_map() + + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this DP.""" + return self.deep_eval.get_dim_fparam() + + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this DP.""" + return self.deep_eval.get_dim_aparam() + + def _get_natoms_and_nframes( + self, + coords: np.ndarray, + atom_types: np.ndarray, + mixed_type: bool = False, + ) -> Tuple[int, int]: + if mixed_type or atom_types.ndim > 1: + natoms = len(atom_types[0]) + else: + natoms = len(atom_types) + if natoms == 0: + assert coords.size == 0 + else: + coords = np.reshape(np.array(coords), [-1, natoms * 3]) + nframes = coords.shape[0] + return natoms, nframes + + def _expande_atype(self, atype: np.ndarray, nframes: int, mixed_type: bool): + if not mixed_type: + atype = np.tile(atype.reshape(1, -1), (nframes, 1)) + return atype + + def eval_descriptor( + self, + coords: np.ndarray, + cells: Optional[np.ndarray], + atom_types: np.ndarray, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + mixed_type: bool = False, + **kwargs: Dict[str, Any], + ) -> np.ndarray: + """Evaluate descriptors by using this DP. + + Parameters + ---------- + coords + The coordinates of atoms. + The array should be of size nframes x natoms x 3 + cells + The cell of the region. + If None then non-PBC is assumed, otherwise using PBC. + The array should be of size nframes x 9 + atom_types + The atom types + The list should contain natoms ints + fparam + The frame parameter. + The array can be of size : + - nframes x dim_fparam. + - dim_fparam. Then all frames are assumed to be provided with the same fparam. + aparam + The atomic parameter + The array can be of size : + - nframes x natoms x dim_aparam. + - natoms x dim_aparam. Then all frames are assumed to be provided with the same aparam. + - dim_aparam. Then all frames and atoms are provided with the same aparam. + efield + The external field on atoms. + The array should be of size nframes x natoms x 3 + mixed_type + Whether to perform the mixed_type mode. + If True, the input data has the mixed_type format (see doc/model/train_se_atten.md), + in which frames in a system may have different natoms_vec(s), with the same nloc. + + Returns + ------- + descriptor + Descriptors. + """ + ( + coords, + cells, + atom_types, + fparam, + aparam, + nframes, + natoms, + ) = self._standard_input(coords, cells, atom_types, fparam, aparam, mixed_type) + descriptor = self.deep_eval.eval_descriptor( + coords, + cells, + atom_types, + fparam=fparam, + aparam=aparam, + **kwargs, + ) + return descriptor + + def eval_typeebd(self) -> np.ndarray: + """Evaluate output of type embedding network by using this model. + + Returns + ------- + np.ndarray + The output of type embedding network. The shape is [ntypes, o_size], + where ntypes is the number of types, and o_size is the number of nodes + in the output layer. + + Raises + ------ + KeyError + If the model does not enable type embedding. + + See Also + -------- + deepmd.tf.utils.type_embed.TypeEmbedNet : The type embedding network. + + Examples + -------- + Get the output of type embedding network of `graph.pb`: + + >>> from deepmd.infer import DeepPotential + >>> dp = DeepPotential('graph.pb') + >>> dp.eval_typeebd() + """ + return self.deep_eval.eval_typeebd() + + def _standard_input(self, coords, cells, atom_types, fparam, aparam, mixed_type): + coords = np.array(coords) + if cells is not None: + cells = np.array(cells) + atom_types = np.array(atom_types, dtype=np.int32) + if fparam is not None: + fparam = np.array(fparam) + if aparam is not None: + aparam = np.array(aparam) + natoms, nframes = self._get_natoms_and_nframes(coords, atom_types, mixed_type) + atom_types = self._expande_atype(atom_types, nframes, mixed_type) + return coords, cells, atom_types, fparam, aparam, nframes, natoms + + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return self.deep_eval.get_sel_type() + + def _get_sel_natoms(self, atype) -> int: + return np.sum(np.isin(atype, self.get_sel_type()).astype(int)) + + @property + def has_efield(self) -> bool: + """Check if the model has efield.""" + return self.deep_eval.get_has_efield() + + def get_ntypes_spin(self) -> int: + """Get the number of spin atom types of this model.""" + return self.deep_eval.get_ntypes_spin() diff --git a/deepmd/infer/deep_polar.py b/deepmd/infer/deep_polar.py new file mode 100644 index 0000000000..f857619871 --- /dev/null +++ b/deepmd/infer/deep_polar.py @@ -0,0 +1,97 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + List, + Optional, + Union, +) + +import numpy as np + +from deepmd.infer.deep_tensor import ( + DeepTensor, +) + + +class DeepPolar(DeepTensor): + """Deep polar model. + + Parameters + ---------- + model_file : Path + The name of the frozen model file. + *args : list + Positional arguments. + auto_batch_size : bool or int or AutoBatchSize, default: True + If True, automatic batch size will be used. If int, it will be used + as the initial batch size. + neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional + The ASE neighbor list class to produce the neighbor list. If None, the + neighbor list will be built natively in the model. + **kwargs : dict + Keyword arguments. + """ + + @property + def output_tensor_name(self) -> str: + return "polar" + + +class DeepGlobalPolar(DeepTensor): + @property + def output_tensor_name(self) -> str: + return "global_polar" + + def eval( + self, + coords: np.ndarray, + cells: Optional[np.ndarray], + atom_types: Union[List[int], np.ndarray], + atomic: bool = False, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + mixed_type: bool = False, + **kwargs: dict, + ) -> np.ndarray: + """Evaluate the model. + + Parameters + ---------- + coords + The coordinates of atoms. + The array should be of size nframes x natoms x 3 + cells + The cell of the region. + If None then non-PBC is assumed, otherwise using PBC. + The array should be of size nframes x 9 + atom_types : list[int] or np.ndarray + The atom types + The list should contain natoms ints + atomic + If True (default), return the atomic tensor + Otherwise return the global tensor + fparam + Not used in this model + aparam + Not used in this model + mixed_type + Whether to perform the mixed_type mode. + If True, the input data has the mixed_type format (see doc/model/train_se_atten.md), + in which frames in a system may have different natoms_vec(s), with the same nloc. + + Returns + ------- + tensor + The returned tensor + If atomic == False then of size nframes x output_dim + else of size nframes x natoms x output_dim + """ + return super().eval( + coords, + cells, + atom_types, + atomic=atomic, + fparam=fparam, + aparam=aparam, + mixed_type=mixed_type, + **kwargs, + ) diff --git a/deepmd/infer/deep_pot.py b/deepmd/infer/deep_pot.py index 546c0f3c7e..463a07115c 100644 --- a/deepmd/infer/deep_pot.py +++ b/deepmd/infer/deep_pot.py @@ -1,9 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from abc import ( - ABC, - abstractmethod, -) from typing import ( + Any, + Dict, List, Optional, Tuple, @@ -12,68 +10,76 @@ import numpy as np -from deepmd.utils.batch_size import ( - AutoBatchSize, +from deepmd.dpmodel.output_def import ( + FittingOutputDef, + ModelOutputDef, + OutputVariableDef, ) -from .backend import ( - DPBackend, - detect_backend, +from .deep_eval import ( + DeepEval, ) -class DeepPot(ABC): +class DeepPot(DeepEval): """Potential energy model. Parameters ---------- model_file : Path The name of the frozen model file. + *args : list + Positional arguments. auto_batch_size : bool or int or AutoBatchSize, default: True If True, automatic batch size will be used. If int, it will be used as the initial batch size. neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional The ASE neighbor list class to produce the neighbor list. If None, the neighbor list will be built natively in the model. + **kwargs : dict + Keyword arguments. + + Examples + -------- + >>> from deepmd.infer import DeepPot + >>> import numpy as np + >>> dp = DeepPot('graph.pb') + >>> coord = np.array([[1,0,0], [0,0,1.5], [1,0,3]]).reshape([1, -1]) + >>> cell = np.diag(10 * np.ones(3)).reshape([1, -1]) + >>> atype = [1,0,1] + >>> e, f, v = dp.eval(coord, cell, atype) + + where `e`, `f` and `v` are predicted energy, force and virial of the system, respectively. """ - @abstractmethod - def __init__( - self, - model_file, - *args, - auto_batch_size: Union[bool, int, AutoBatchSize] = True, - neighbor_list=None, - **kwargs, - ) -> None: - pass - - def __new__(cls, model_file: str, *args, **kwargs): - if cls is DeepPot: - backend = detect_backend(model_file) - if backend == DPBackend.TensorFlow: - from deepmd.tf.infer.deep_pot import DeepPot as DeepPotTF - - return super().__new__(DeepPotTF) - elif backend == DPBackend.PyTorch: - from deepmd.pt.infer.deep_eval import DeepPot as DeepPotPT - - return super().__new__(DeepPotPT) - else: - raise NotImplementedError("Unsupported backend: " + str(backend)) - return super().__new__(cls) + @property + def output_def(self) -> ModelOutputDef: + """Get the output definition of this model.""" + return ModelOutputDef( + FittingOutputDef( + [ + OutputVariableDef( + "energy", + shape=[1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + atomic=True, + ), + ] + ) + ) - @abstractmethod def eval( self, coords: np.ndarray, - cells: np.ndarray, - atom_types: List[int], + cells: Optional[np.ndarray], + atom_types: Union[List[int], np.ndarray], atomic: bool = False, fparam: Optional[np.ndarray] = None, aparam: Optional[np.ndarray] = None, - efield: Optional[np.ndarray] = None, mixed_type: bool = False, + **kwargs: Dict[str, Any], ) -> Tuple[np.ndarray, ...]: """Evaluate energy, force, and virial. If atomic is True, also return atomic energy and atomic virial. @@ -85,7 +91,7 @@ def eval( cells : np.ndarray The cell vectors of the system, in shape (nframes, 9). If the system is not periodic, set it to None. - atom_types : List[int] + atom_types : List[int] or np.ndarray The types of the atoms. If mixed_type is False, the shape is (natoms,); otherwise, the shape is (nframes, natoms). atomic : bool, optional @@ -94,10 +100,10 @@ def eval( The frame parameters, by default None. aparam : np.ndarray, optional The atomic parameters, by default None. - efield : np.ndarray, optional - The electric field, by default None. mixed_type : bool, optional - Whether the system contains mixed atom types, by default False. + Whether the atom_types is mixed type, by default False. + **kwargs : Dict[str, Any] + Keyword arguments. Returns ------- @@ -121,22 +127,54 @@ def eval( # finetune: +mixed_type # dpdata # ase - - @abstractmethod - def get_ntypes(self) -> int: - """Get the number of atom types of this model.""" - - @abstractmethod - def get_type_map(self) -> List[str]: - """Get the type map (element name of the atom types) of this model.""" - - @abstractmethod - def get_dim_fparam(self) -> int: - """Get the number (dimension) of frame parameters of this DP.""" - - @abstractmethod - def get_dim_aparam(self) -> int: - """Get the number (dimension) of atomic parameters of this DP.""" + ( + coords, + cells, + atom_types, + fparam, + aparam, + nframes, + natoms, + ) = self._standard_input(coords, cells, atom_types, fparam, aparam, mixed_type) + results = self.deep_eval.eval( + coords, + cells, + atom_types, + atomic, + fparam=fparam, + aparam=aparam, + **kwargs, + ) + energy = results["energy_redu"].reshape(nframes, 1) + force = results["energy_derv_r"].reshape(nframes, natoms, 3) + virial = results["energy_derv_c_redu"].reshape(nframes, 9) + + if atomic: + if self.get_ntypes_spin() > 0: + ntypes_real = self.get_ntypes() - self.get_ntypes_spin() + natoms_real = sum( + [ + np.count_nonzero(np.array(atom_types[0]) == ii) + for ii in range(ntypes_real) + ] + ) + else: + natoms_real = natoms + atomic_energy = results["energy"].reshape(nframes, natoms_real, 1) + atomic_virial = results["energy_derv_c"].reshape(nframes, natoms, 9) + return ( + energy, + force, + virial, + atomic_energy, + atomic_virial, + ) + else: + return ( + energy, + force, + virial, + ) __all__ = ["DeepPot"] diff --git a/deepmd/infer/deep_tensor.py b/deepmd/infer/deep_tensor.py new file mode 100644 index 0000000000..a6cefa63c1 --- /dev/null +++ b/deepmd/infer/deep_tensor.py @@ -0,0 +1,238 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from abc import ( + abstractmethod, +) +from typing import ( + List, + Optional, + Tuple, + Union, +) + +import numpy as np + +from deepmd.dpmodel.output_def import ( + FittingOutputDef, + ModelOutputDef, + OutputVariableDef, +) +from deepmd.infer.deep_eval import ( + DeepEval, +) + + +class DeepTensor(DeepEval): + """Deep Tensor Model. + + Parameters + ---------- + model_file : Path + The name of the frozen model file. + *args : list + Positional arguments. + auto_batch_size : bool or int or AutoBatchSize, default: True + If True, automatic batch size will be used. If int, it will be used + as the initial batch size. + neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional + The ASE neighbor list class to produce the neighbor list. If None, the + neighbor list will be built natively in the model. + **kwargs : dict + Keyword arguments. + """ + + def eval( + self, + coords: np.ndarray, + cells: Optional[np.ndarray], + atom_types: Union[List[int], np.ndarray], + atomic: bool = True, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + mixed_type: bool = False, + **kwargs: dict, + ) -> np.ndarray: + """Evaluate the model. + + Parameters + ---------- + coords + The coordinates of atoms. + The array should be of size nframes x natoms x 3 + cells + The cell of the region. + If None then non-PBC is assumed, otherwise using PBC. + The array should be of size nframes x 9 + atom_types : list[int] or np.ndarray + The atom types + The list should contain natoms ints + atomic + If True (default), return the atomic tensor + Otherwise return the global tensor + fparam + Not used in this model + aparam + Not used in this model + efield + Not used in this model + mixed_type + Whether to perform the mixed_type mode. + If True, the input data has the mixed_type format (see doc/model/train_se_atten.md), + in which frames in a system may have different natoms_vec(s), with the same nloc. + + Returns + ------- + tensor + The returned tensor + If atomic == False then of size nframes x output_dim + else of size nframes x natoms x output_dim + """ + ( + coords, + cells, + atom_types, + fparam, + aparam, + nframes, + natoms, + ) = self._standard_input(coords, cells, atom_types, fparam, aparam, mixed_type) + results = self.deep_eval.eval( + coords, + cells, + atom_types, + atomic, + fparam=fparam, + aparam=aparam, + **kwargs, + ) + sel_natoms = self._get_sel_natoms(atom_types[0]) + if atomic: + return results[self.output_tensor_name].reshape(nframes, sel_natoms, -1) + else: + return results[f"{self.output_tensor_name}_redu"].reshape(nframes, -1) + + def eval_full( + self, + coords: np.ndarray, + cells: Optional[np.ndarray], + atom_types: np.ndarray, + atomic: bool = False, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + mixed_type: bool = False, + **kwargs: dict, + ) -> Tuple[np.ndarray, ...]: + """Evaluate the model with interface similar to the energy model. + Will return global tensor, component-wise force and virial + and optionally atomic tensor and atomic virial. + + Parameters + ---------- + coords + The coordinates of atoms. + The array should be of size nframes x natoms x 3 + cells + The cell of the region. + If None then non-PBC is assumed, otherwise using PBC. + The array should be of size nframes x 9 + atom_types + The atom types + The list should contain natoms ints + atomic + Whether to calculate atomic tensor and virial + fparam + Not used in this model + aparam + Not used in this model + mixed_type + Whether to perform the mixed_type mode. + If True, the input data has the mixed_type format (see doc/model/train_se_atten.md), + in which frames in a system may have different natoms_vec(s), with the same nloc. + + Returns + ------- + tensor + The global tensor. + shape: [nframes x nout] + force + The component-wise force (negative derivative) on each atom. + shape: [nframes x nout x natoms x 3] + virial + The component-wise virial of the tensor. + shape: [nframes x nout x 9] + atom_tensor + The atomic tensor. Only returned when atomic == True + shape: [nframes x natoms x nout] + atom_virial + The atomic virial. Only returned when atomic == True + shape: [nframes x nout x natoms x 9] + """ + ( + coords, + cells, + atom_types, + fparam, + aparam, + nframes, + natoms, + ) = self._standard_input(coords, cells, atom_types, fparam, aparam, mixed_type) + results = self.deep_eval.eval( + coords, + cells, + atom_types, + atomic, + fparam=fparam, + aparam=aparam, + **kwargs, + ) + sel_natoms = self._get_sel_natoms(atom_types[0]) + energy = results[f"{self.output_tensor_name}_redu"].reshape(nframes, -1) + force = results[f"{self.output_tensor_name}_derv_r"].reshape( + nframes, -1, natoms, 3 + ) + virial = results[f"{self.output_tensor_name}_derv_c_redu"].reshape( + nframes, -1, 9 + ) + atomic_energy = results[self.output_tensor_name].reshape( + nframes, sel_natoms, -1 + ) + atomic_virial = results[f"{self.output_tensor_name}_derv_c"].reshape( + nframes, -1, natoms, 9 + ) + + if atomic: + return ( + energy, + force, + virial, + atomic_energy, + atomic_virial, + ) + else: + return ( + energy, + force, + virial, + ) + + @property + @abstractmethod + def output_tensor_name(self) -> str: + """The name of the tensor.""" + + @property + def output_def(self) -> ModelOutputDef: + """Get the output definition of this model.""" + return ModelOutputDef( + FittingOutputDef( + [ + OutputVariableDef( + self.output_tensor_name, + shape=[-1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + atomic=True, + ), + ] + ) + ) diff --git a/deepmd/infer/deep_wfc.py b/deepmd/infer/deep_wfc.py new file mode 100644 index 0000000000..deed938e04 --- /dev/null +++ b/deepmd/infer/deep_wfc.py @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from deepmd.infer.deep_tensor import ( + DeepTensor, +) + + +class DeepWFC(DeepTensor): + """Deep WFC model. + + Parameters + ---------- + model_file : Path + The name of the frozen model file. + *args : list + Positional arguments. + auto_batch_size : bool or int or AutoBatchSize, default: True + If True, automatic batch size will be used. If int, it will be used + as the initial batch size. + neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional + The ASE neighbor list class to produce the neighbor list. If None, the + neighbor list will be built natively in the model. + **kwargs : dict + Keyword arguments. + """ + + @property + def output_tensor_name(self) -> str: + return "wfc" diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index 4ba9e17b52..b42bee1dbe 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -1,9 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from pathlib import ( - Path, -) from typing import ( + TYPE_CHECKING, + Any, Callable, + Dict, List, Optional, Tuple, @@ -13,7 +13,17 @@ import numpy as np import torch -from deepmd.infer.deep_pot import DeepPot as DeepPotBase +from deepmd.dpmodel.output_def import ( + ModelOutputDef, + OutputVariableCategory, + OutputVariableDef, +) +from deepmd.infer.deep_eval import ( + DeepEvalBackend, +) +from deepmd.infer.deep_pot import ( + DeepPot, +) from deepmd.pt.model.model import ( get_model, ) @@ -31,13 +41,43 @@ GLOBAL_PT_FLOAT_PRECISION, ) +if TYPE_CHECKING: + import ase.neighborlist + + from deepmd.infer.deep_eval import DeepEval as DeepEvalWrapper + + +class DeepEval(DeepEvalBackend): + """PyTorch backend implementaion of DeepEval. + + Parameters + ---------- + model_file : Path + The name of the frozen model file. + output_def : ModelOutputDef + The output definition of the model. + *args : list + Positional arguments. + auto_batch_size : bool or int or AutomaticBatchSize, default: False + If True, automatic batch size will be used. If int, it will be used + as the initial batch size. + neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional + The ASE neighbor list class to produce the neighbor list. If None, the + neighbor list will be built natively in the model. + **kwargs : dict + Keyword arguments. + """ -class DeepEval: def __init__( self, - model_file: "Path", + model_file: str, + output_def: ModelOutputDef, + *args: List[Any], auto_batch_size: Union[bool, int, AutoBatchSize] = True, + neighbor_list: Optional["ase.neighborlist.NewPrimitiveNeighborList"] = None, + **kwargs: Dict[str, Any], ): + self.output_def = output_def self.model_path = model_file state_dict = torch.load(model_file, map_location=env.DEVICE) if "model" in state_dict: @@ -64,42 +104,99 @@ def __init__( else: raise TypeError("auto_batch_size should be bool, int, or AutoBatchSize") - def eval( - self, - coords: Union[np.ndarray, torch.Tensor], - cells: Optional[Union[np.ndarray, torch.Tensor]], - atom_types: Union[np.ndarray, torch.Tensor, List[int]], - atomic: bool = False, - ): - raise NotImplementedError + def get_rcut(self) -> float: + """Get the cutoff radius of this model.""" + return self.rcut + def get_ntypes(self) -> int: + """Get the number of atom types of this model.""" + return len(self.type_map) -class DeepPot(DeepEval, DeepPotBase): - def __init__( - self, - model_file: "Path", - auto_batch_size: Union[bool, int, AutoBatchSize] = True, - neighbor_list=None, - ): - if neighbor_list is not None: - raise NotImplementedError - super().__init__( - model_file, - auto_batch_size=auto_batch_size, - ) + def get_type_map(self) -> List[str]: + """Get the type map (element name of the atom types) of this model.""" + return self.type_map + + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this DP.""" + return 0 + + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this DP.""" + return 0 + + @property + def model_type(self) -> "DeepEvalWrapper": + """The the evaluator of the model type.""" + return DeepPot + + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return [] + + def get_numb_dos(self) -> int: + """Get the number of DOS.""" + return 0 + + def get_has_efield(self): + """Check if the model has efield.""" + return False + + def get_ntypes_spin(self): + """Get the number of spin atom types of this model.""" + return 0 def eval( self, coords: np.ndarray, cells: np.ndarray, - atom_types: List[int], + atom_types: np.ndarray, atomic: bool = False, fparam: Optional[np.ndarray] = None, aparam: Optional[np.ndarray] = None, - efield: Optional[np.ndarray] = None, - mixed_type: bool = False, - ): - if fparam is not None or aparam is not None or efield is not None: + **kwargs: Dict[str, Any], + ) -> Dict[str, np.ndarray]: + """Evaluate the energy, force and virial by using this DP. + + Parameters + ---------- + coords + The coordinates of atoms. + The array should be of size nframes x natoms x 3 + cells + The cell of the region. + If None then non-PBC is assumed, otherwise using PBC. + The array should be of size nframes x 9 + atom_types + The atom types + The list should contain natoms ints + atomic + Calculate the atomic energy and virial + fparam + The frame parameter. + The array can be of size : + - nframes x dim_fparam. + - dim_fparam. Then all frames are assumed to be provided with the same fparam. + aparam + The atomic parameter + The array can be of size : + - nframes x natoms x dim_aparam. + - natoms x dim_aparam. Then all frames are assumed to be provided with the same aparam. + - dim_aparam. Then all frames and atoms are provided with the same aparam. + **kwargs + Other parameters + + Returns + ------- + output_dict : dict + The output of the evaluation. The keys are the names of the output + variables, and the values are the corresponding output arrays. + """ + if fparam is not None or aparam is not None: raise NotImplementedError # convert all of the input to numpy array atom_types = np.array(atom_types, dtype=np.int32) @@ -109,10 +206,48 @@ def eval( natoms, numb_test = self._get_natoms_and_nframes( coords, atom_types, len(atom_types.shape) > 1 ) - return self._eval_func(self._eval_model, numb_test, natoms)( - coords, cells, atom_types, atomic + request_defs = self._get_request_defs(atomic) + out = self._eval_func(self._eval_model, numb_test, natoms)( + coords, cells, atom_types, request_defs + ) + return dict( + zip( + [x.name for x in request_defs], + out, + ) ) + def _get_request_defs(self, atomic: bool) -> List[OutputVariableDef]: + """Get the requested output definitions. + + When atomic is True, all output_def are requested. + When atomic is False, only energy (tensor), force, and virial + are requested. + + Parameters + ---------- + atomic : bool + Whether to request the atomic output. + + Returns + ------- + list[OutputVariableDef] + The requested output definitions. + """ + if atomic: + return list(self.output_def.var_defs.values()) + else: + return [ + x + for x in self.output_def.var_defs.values() + if x.category + in ( + OutputVariableCategory.REDU, + OutputVariableCategory.DERV_R, + OutputVariableCategory.DERV_C_REDU, + ) + ] + def _eval_func(self, inner_func: Callable, numb_test: int, natoms: int) -> Callable: """Wrapper method with auto batch size. @@ -144,7 +279,7 @@ def eval_func(*args, **kwargs): def _get_natoms_and_nframes( self, coords: np.ndarray, - atom_types: Union[List[int], np.ndarray], + atom_types: np.ndarray, mixed_type: bool = False, ) -> Tuple[int, int]: if mixed_type: @@ -163,14 +298,9 @@ def _eval_model( coords: np.ndarray, cells: Optional[np.ndarray], atom_types: np.ndarray, - atomic: bool = False, + request_defs: List[OutputVariableDef], ): model = self.dp.to(DEVICE) - energy_out = None - atomic_energy_out = None - force_out = None - virial_out = None - atomic_virial_out = None nframes = coords.shape[0] if len(atom_types.shape) == 1: @@ -190,59 +320,44 @@ def _eval_model( else: box_input = None + do_atomic_virial = any( + x.category == OutputVariableCategory.DERV_C_REDU for x in request_defs + ) batch_output = model( - coord_input, type_input, box=box_input, do_atomic_virial=atomic + coord_input, type_input, box=box_input, do_atomic_virial=do_atomic_virial ) if isinstance(batch_output, tuple): batch_output = batch_output[0] - energy_out = batch_output["energy"].reshape(nframes, 1).detach().cpu().numpy() - if "atom_energy" in batch_output: - atomic_energy_out = ( - batch_output["atom_energy"] - .reshape(nframes, natoms, 1) - .detach() - .cpu() - .numpy() - ) - force_out = ( - batch_output["force"].reshape(nframes, natoms, 3).detach().cpu().numpy() - ) - virial_out = batch_output["virial"].reshape(nframes, 9).detach().cpu().numpy() - if "atom_virial" in batch_output: - atomic_virial_out = ( - batch_output["atom_virial"] - .reshape(nframes, natoms, 9) - .detach() - .cpu() - .numpy() - ) - - if not atomic: - return energy_out, force_out, virial_out - else: - return ( - energy_out, - force_out, - virial_out, - atomic_energy_out, - atomic_virial_out, - ) - - def get_ntypes(self) -> int: - """Get the number of atom types of this model.""" - return len(self.type_map) - - def get_type_map(self) -> List[str]: - """Get the type map (element name of the atom types) of this model.""" - return self.type_map - def get_dim_fparam(self) -> int: - """Get the number (dimension) of frame parameters of this DP.""" - return 0 + results = [] + for odef in request_defs: + pt_name = self._OUTDEF_DP2BACKEND[odef.name] + if pt_name in batch_output: + shape = self._get_output_shape(odef, nframes, natoms) + out = batch_output[pt_name].reshape(shape).detach().cpu().numpy() + results.append(out) + return tuple(results) - def get_dim_aparam(self) -> int: - """Get the number (dimension) of atomic parameters of this DP.""" - return 0 + def _get_output_shape(self, odef, nframes, natoms): + if odef.category == OutputVariableCategory.DERV_C_REDU: + # virial + return [nframes, *odef.shape[:-1], 9] + elif odef.category == OutputVariableCategory.REDU: + # energy + return [nframes, *odef.shape, 1] + elif odef.category == OutputVariableCategory.DERV_C: + # atom_virial + return [nframes, *odef.shape[:-1], natoms, 9] + elif odef.category == OutputVariableCategory.DERV_R: + # force + return [nframes, *odef.shape[:-1], natoms, 3] + elif odef.category == OutputVariableCategory.OUT: + # atom_energy, atom_tensor + # Something wrong here? + # return [nframes, *shape, natoms, 1] + return [nframes, natoms, *odef.shape, 1] + else: + raise RuntimeError("unknown category") # For tests only diff --git a/deepmd/tf/entrypoints/test.py b/deepmd/tf/entrypoints/test.py index 1b917bc276..e1a2e3d46b 100644 --- a/deepmd/tf/entrypoints/test.py +++ b/deepmd/tf/entrypoints/test.py @@ -14,6 +14,22 @@ import numpy as np +from deepmd.infer.deep_dipole import ( + DeepDipole, +) +from deepmd.infer.deep_dos import ( + DeepDOS, +) +from deepmd.infer.deep_polar import ( + DeepGlobalPolar, + DeepPolar, +) +from deepmd.infer.deep_pot import ( + DeepPot, +) +from deepmd.infer.deep_wfc import ( + DeepWFC, +) from deepmd.tf import ( DeepPotential, ) @@ -115,7 +131,7 @@ def test( log.info(f"# testing system : {system}") # create data class - tmap = dp.get_type_map() if dp.model_type == "ener" else None + tmap = dp.get_type_map() if isinstance(dp, DeepPot) else None data = DeepmdData( system, set_prefix, @@ -124,7 +140,7 @@ def test( sort_atoms=False, ) - if dp.model_type == "ener": + if isinstance(dp, DeepPot): err = test_ener( dp, data, @@ -134,7 +150,7 @@ def test( atomic, append_detail=(cc != 0), ) - elif dp.model_type == "dos": + elif isinstance(dp, DeepDOS): err = test_dos( dp, data, @@ -144,11 +160,11 @@ def test( atomic, append_detail=(cc != 0), ) - elif dp.model_type == "dipole": + elif isinstance(dp, DeepDipole): err = test_dipole(dp, data, numb_test, detail_file, atomic) - elif dp.model_type == "polar": + elif isinstance(dp, DeepPolar): err = test_polar(dp, data, numb_test, detail_file, atomic=atomic) - elif dp.model_type == "global_polar": # should not appear in this new version + elif isinstance(dp, DeepGlobalPolar): # should not appear in this new version log.warning( "Global polar model is not currently supported. Please directly use the polar mode and change loss parameters." ) @@ -166,17 +182,17 @@ def test( if len(all_sys) > 1: log.info("# ----------weighted average of errors----------- ") log.info(f"# number of systems : {len(all_sys)}") - if dp.model_type == "ener": + if dp == "ener": print_ener_sys_avg(avg_err) - elif dp.model_type == "dos": + elif dp == "dos": print_dos_sys_avg(avg_err) - elif dp.model_type == "dipole": + elif dp == "dipole": print_dipole_sys_avg(avg_err) - elif dp.model_type == "polar": + elif dp == "polar": print_polar_sys_avg(avg_err) - elif dp.model_type == "global_polar": + elif dp == "global_polar": print_polar_sys_avg(avg_err) - elif dp.model_type == "wfc": + elif dp == "wfc": print_wfc_sys_avg(avg_err) log.info("# ----------------------------------------------- ") diff --git a/deepmd/tf/infer/__init__.py b/deepmd/tf/infer/__init__.py index c1071af35c..e0168198ff 100644 --- a/deepmd/tf/infer/__init__.py +++ b/deepmd/tf/infer/__init__.py @@ -1,12 +1,8 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Submodule containing all the implemented potentials.""" -from pathlib import ( - Path, -) from typing import ( - Optional, - Union, + TYPE_CHECKING, ) from .data_modifier import ( @@ -38,6 +34,11 @@ calc_model_devi, ) +if TYPE_CHECKING: + from deepmd.infer.deep_eval import ( + DeepEval, + ) + __all__ = [ "DeepPotential", "DeepDipole", @@ -53,94 +54,23 @@ ] -def DeepPotential( - model_file: Union[str, Path], - load_prefix: str = "load", - default_tf_graph: bool = False, - input_map: Optional[dict] = None, - neighbor_list=None, -) -> Union[DeepDipole, DeepGlobalPolar, DeepPolar, DeepPot, DeepDOS, DeepWFC]: - """Factory function that will inialize appropriate potential read from `model_file`. +def DeepPotential(*args, **kwargs) -> "DeepEval": + """Factory function that forwards to DeepEval (for compatbility). Parameters ---------- - model_file : str - The name of the frozen model file. - load_prefix : str - The prefix in the load computational graph - default_tf_graph : bool - If uses the default tf graph, otherwise build a new tf graph for evaluation - input_map : dict, optional - The input map for tf.import_graph_def. Only work with default tf graph - neighbor_list : ase.neighborlist.NeighborList, optional - The neighbor list object. If None, then build the native neighbor list. + *args + positional arguments + **kwargs + keyword arguments Returns ------- - Union[DeepDipole, DeepGlobalPolar, DeepPolar, DeepPot, DeepWFC] - one of the available potentials - - Raises - ------ - RuntimeError - if model file does not correspond to any implementd potential + DeepEval + potentials """ - mf = Path(model_file) - - model_type = DeepEval( - mf, - load_prefix=load_prefix, - default_tf_graph=default_tf_graph, - input_map=input_map, - ).model_type - - if model_type == "ener": - dp = DeepPot( - mf, - load_prefix=load_prefix, - default_tf_graph=default_tf_graph, - input_map=input_map, - neighbor_list=neighbor_list, - ) - elif model_type == "dos": - dp = DeepDOS( - mf, - load_prefix=load_prefix, - default_tf_graph=default_tf_graph, - input_map=input_map, - ) - elif model_type == "dipole": - dp = DeepDipole( - mf, - load_prefix=load_prefix, - default_tf_graph=default_tf_graph, - input_map=input_map, - neighbor_list=neighbor_list, - ) - elif model_type == "polar": - dp = DeepPolar( - mf, - load_prefix=load_prefix, - default_tf_graph=default_tf_graph, - input_map=input_map, - neighbor_list=neighbor_list, - ) - elif model_type == "global_polar": - dp = DeepGlobalPolar( - mf, - load_prefix=load_prefix, - default_tf_graph=default_tf_graph, - input_map=input_map, - neighbor_list=neighbor_list, - ) - elif model_type == "wfc": - dp = DeepWFC( - mf, - load_prefix=load_prefix, - default_tf_graph=default_tf_graph, - input_map=input_map, - ) - else: - raise RuntimeError(f"unknown model type {model_type}") + from deepmd.infer.deep_eval import ( + DeepEval, + ) - return dp + return DeepEval(*args, **kwargs) diff --git a/deepmd/tf/infer/data_modifier.py b/deepmd/tf/infer/data_modifier.py index e53151d80a..ccd072673d 100644 --- a/deepmd/tf/infer/data_modifier.py +++ b/deepmd/tf/infer/data_modifier.py @@ -17,9 +17,7 @@ op_module, tf, ) -from deepmd.tf.infer.deep_dipole import ( - DeepDipole, -) +from deepmd.tf.infer.deep_dipole import DeepDipoleOld as DeepDipole from deepmd.tf.infer.ewald_recp import ( EwaldRecp, ) diff --git a/deepmd/tf/infer/deep_dipole.py b/deepmd/tf/infer/deep_dipole.py index a0a4a0f78e..e10d09564d 100644 --- a/deepmd/tf/infer/deep_dipole.py +++ b/deepmd/tf/infer/deep_dipole.py @@ -1,20 +1,25 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from pathlib import ( + Path, +) from typing import ( - TYPE_CHECKING, Optional, ) +from deepmd.infer.deep_dipole import ( + DeepDipole, +) from deepmd.tf.infer.deep_tensor import ( DeepTensor, ) -if TYPE_CHECKING: - from pathlib import ( - Path, - ) +__all__ = [ + "DeepDipole", +] -class DeepDipole(DeepTensor): +class DeepDipoleOld(DeepTensor): + # used for DipoleChargeModifier only """Constructor. Parameters diff --git a/deepmd/tf/infer/deep_dos.py b/deepmd/tf/infer/deep_dos.py index 9b2830161d..7a9f9b781c 100644 --- a/deepmd/tf/infer/deep_dos.py +++ b/deepmd/tf/infer/deep_dos.py @@ -1,506 +1,8 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import logging -from typing import ( - TYPE_CHECKING, - Callable, - List, - Optional, - Tuple, - Union, +from deepmd.infer.deep_dos import ( + DeepDOS, ) -import numpy as np - -from deepmd.tf.common import ( - make_default_mesh, -) -from deepmd.tf.infer.deep_eval import ( - DeepEval, -) -from deepmd.tf.utils.batch_size import ( - AutoBatchSize, -) -from deepmd.tf.utils.sess import ( - run_sess, -) - -if TYPE_CHECKING: - from pathlib import ( - Path, - ) - -log = logging.getLogger(__name__) - - -class DeepDOS(DeepEval): - """Constructor. - - Parameters - ---------- - model_file : Path - The name of the frozen model file. - load_prefix: str - The prefix in the load computational graph - default_tf_graph : bool - If uses the default tf graph, otherwise build a new tf graph for evaluation - auto_batch_size : bool or int or AutomaticBatchSize, default: True - If True, automatic batch size will be used. If int, it will be used - as the initial batch size. - input_map : dict, optional - The input map for tf.import_graph_def. Only work with default tf graph - - Warnings - -------- - For developers: `DeepTensor` initializer must be called at the end after - `self.tensors` are modified because it uses the data in `self.tensors` dict. - Do not chanage the order! - """ - - def __init__( - self, - model_file: "Path", - load_prefix: str = "load", - default_tf_graph: bool = False, - auto_batch_size: Union[bool, int, AutoBatchSize] = True, - input_map: Optional[dict] = None, - ) -> None: - # add these tensors on top of what is defined by DeepTensor Class - # use this in favor of dict update to move attribute from class to - # instance namespace - self.tensors = { - # descrpt attrs - "t_ntypes": "descrpt_attr/ntypes:0", - "t_rcut": "descrpt_attr/rcut:0", - # fitting attrs - "t_dfparam": "fitting_attr/dfparam:0", - "t_daparam": "fitting_attr/daparam:0", - "t_numb_dos": "fitting_attr/numb_dos:0", - # model attrs - "t_tmap": "model_attr/tmap:0", - # inputs - "t_coord": "t_coord:0", - "t_type": "t_type:0", - "t_natoms": "t_natoms:0", - "t_box": "t_box:0", - "t_mesh": "t_mesh:0", - # add output tensors - "t_dos": "o_dos:0", - "t_atom_dos": "o_atom_dos:0", - "t_descriptor": "o_descriptor:0", - } - DeepEval.__init__( - self, - model_file, - load_prefix=load_prefix, - default_tf_graph=default_tf_graph, - auto_batch_size=auto_batch_size, - input_map=input_map, - ) - - # load optional tensors - operations = [op.name for op in self.graph.get_operations()] - # check if the graph has these operations: - # if yes add them - if "load/t_fparam" in operations: - self.tensors.update({"t_fparam": "t_fparam:0"}) - self.has_fparam = True - else: - log.debug("Could not get tensor 't_fparam:0'") - self.t_fparam = None - self.has_fparam = False - - if "load/t_aparam" in operations: - self.tensors.update({"t_aparam": "t_aparam:0"}) - self.has_aparam = True - else: - log.debug("Could not get tensor 't_aparam:0'") - self.t_aparam = None - self.has_aparam = False - - # now load tensors to object attributes - for attr_name, tensor_name in self.tensors.items(): - try: - self._get_tensor(tensor_name, attr_name) - except KeyError: - if attr_name != "t_descriptor": - raise - - self._run_default_sess() - self.tmap = self.tmap.decode("UTF-8").split() - - # setup modifier - try: - t_modifier_type = self._get_tensor("modifier_attr/type:0") - self.modifier_type = run_sess(self.sess, t_modifier_type).decode("UTF-8") - except (ValueError, KeyError): - self.modifier_type = None - - def _run_default_sess(self): - [ - self.ntypes, - self.rcut, - self.numb_dos, - self.dfparam, - self.daparam, - self.tmap, - ] = run_sess( - self.sess, - [ - self.t_ntypes, - self.t_rcut, - self.t_numb_dos, - self.t_dfparam, - self.t_daparam, - self.t_tmap, - ], - ) - - def get_ntypes(self) -> int: - """Get the number of atom types of this model.""" - return self.ntypes - - def get_rcut(self) -> float: - """Get the cut-off radius of this model.""" - return self.rcut - - def get_numb_dos(self) -> int: - """Get the length of DOS output of this DP model.""" - return self.numb_dos - - def get_type_map(self) -> List[str]: - """Get the type map (element name of the atom types) of this model.""" - return self.tmap - - def get_sel_type(self) -> List[int]: - """Unsupported in this model.""" - raise NotImplementedError("This model type does not support this attribute") - - def get_dim_fparam(self) -> int: - """Get the number (dimension) of frame parameters of this DP.""" - return self.dfparam - - def get_dim_aparam(self) -> int: - """Get the number (dimension) of atomic parameters of this DP.""" - return self.daparam - - def _eval_func(self, inner_func: Callable, numb_test: int, natoms: int) -> Callable: - """Wrapper method with auto batch size. - - Parameters - ---------- - inner_func : Callable - the method to be wrapped - numb_test : int - number of tests - natoms : int - number of atoms - - Returns - ------- - Callable - the wrapper - """ - if self.auto_batch_size is not None: - - def eval_func(*args, **kwargs): - return self.auto_batch_size.execute_all( - inner_func, numb_test, natoms, *args, **kwargs - ) - - else: - eval_func = inner_func - return eval_func - - def _get_natoms_and_nframes( - self, - coords: np.ndarray, - atom_types: Union[List[int], np.ndarray], - mixed_type: bool = False, - ) -> Tuple[int, int]: - if mixed_type: - natoms = len(atom_types[0]) - else: - natoms = len(atom_types) - coords = np.reshape(np.array(coords), [-1, natoms * 3]) - nframes = coords.shape[0] - return natoms, nframes - - def eval( - self, - coords: np.ndarray, - cells: np.ndarray, - atom_types: List[int], - atomic: bool = False, - fparam: Optional[np.ndarray] = None, - aparam: Optional[np.ndarray] = None, - mixed_type: bool = False, - ) -> Tuple[np.ndarray, ...]: - """Evaluate the dos, atom_dos by using this model. - - Parameters - ---------- - coords - The coordinates of atoms. - The array should be of size nframes x natoms x 3 - cells - The cell of the region. - If None then non-PBC is assumed, otherwise using PBC. - The array should be of size nframes x 9 - atom_types - The atom types - The list should contain natoms ints - atomic - Calculate the atomic energy and virial - fparam - The frame parameter. - The array can be of size : - - nframes x dim_fparam. - - dim_fparam. Then all frames are assumed to be provided with the same fparam. - aparam - The atomic parameter - The array can be of size : - - nframes x natoms x dim_aparam. - - natoms x dim_aparam. Then all frames are assumed to be provided with the same aparam. - - dim_aparam. Then all frames and atoms are provided with the same aparam. - mixed_type - Whether to perform the mixed_type mode. - If True, the input data has the mixed_type format (see doc/model/train_se_atten.md), - in which frames in a system may have different natoms_vec(s), with the same nloc. - - Returns - ------- - dos - The electron density of state. - atom_dos - The atom-sited density of state. Only returned when atomic == True - """ - # reshape coords before getting shape - natoms, numb_test = self._get_natoms_and_nframes( - coords, atom_types, mixed_type=mixed_type - ) - output = self._eval_func(self._eval_inner, numb_test, natoms)( - coords, - cells, - atom_types, - fparam=fparam, - aparam=aparam, - atomic=atomic, - mixed_type=mixed_type, - ) - - return output - - def _prepare_feed_dict( - self, - coords, - cells, - atom_types, - fparam=None, - aparam=None, - atomic=False, - mixed_type=False, - ): - # standarize the shape of inputs - natoms, nframes = self._get_natoms_and_nframes( - coords, atom_types, mixed_type=mixed_type - ) - if mixed_type: - atom_types = np.array(atom_types, dtype=int).reshape([-1, natoms]) - else: - atom_types = np.array(atom_types, dtype=int).reshape([-1]) - coords = np.reshape(np.array(coords), [-1, natoms * 3]) - if cells is None: - pbc = False - # make cells to work around the requirement of pbc - cells = np.tile(np.eye(3), [nframes, 1]).reshape([nframes, 9]) - else: - pbc = True - cells = np.array(cells).reshape([nframes, 9]) - - if self.has_fparam: - assert fparam is not None - fparam = np.array(fparam) - if self.has_aparam: - assert aparam is not None - aparam = np.array(aparam) - - # reshape the inputs - if self.has_fparam: - fdim = self.get_dim_fparam() - if fparam.size == nframes * fdim: - fparam = np.reshape(fparam, [nframes, fdim]) - elif fparam.size == fdim: - fparam = np.tile(fparam.reshape([-1]), [nframes, 1]) - else: - raise RuntimeError( - "got wrong size of frame param, should be either %d x %d or %d" - % (nframes, fdim, fdim) - ) - if self.has_aparam: - fdim = self.get_dim_aparam() - if aparam.size == nframes * natoms * fdim: - aparam = np.reshape(aparam, [nframes, natoms * fdim]) - elif aparam.size == natoms * fdim: - aparam = np.tile(aparam.reshape([-1]), [nframes, 1]) - elif aparam.size == fdim: - aparam = np.tile(aparam.reshape([-1]), [nframes, natoms]) - else: - raise RuntimeError( - "got wrong size of frame param, should be either %d x %d x %d or %d x %d or %d" - % (nframes, natoms, fdim, natoms, fdim, fdim) - ) - - # sort inputs - coords, atom_types, imap = self.sort_input( - coords, atom_types, mixed_type=mixed_type - ) - - # make natoms_vec and default_mesh - natoms_vec = self.make_natoms_vec(atom_types, mixed_type=mixed_type) - assert natoms_vec[0] == natoms - - # evaluate - feed_dict_test = {} - feed_dict_test[self.t_natoms] = natoms_vec - if mixed_type: - feed_dict_test[self.t_type] = atom_types.reshape([-1]) - else: - feed_dict_test[self.t_type] = np.tile(atom_types, [nframes, 1]).reshape( - [-1] - ) - feed_dict_test[self.t_coord] = np.reshape(coords, [-1]) - - if len(self.t_box.shape) == 1: - feed_dict_test[self.t_box] = np.reshape(cells, [-1]) - elif len(self.t_box.shape) == 2: - feed_dict_test[self.t_box] = cells - else: - raise RuntimeError - feed_dict_test[self.t_mesh] = make_default_mesh(pbc, mixed_type) - if self.has_fparam: - feed_dict_test[self.t_fparam] = np.reshape(fparam, [-1]) - if self.has_aparam: - feed_dict_test[self.t_aparam] = np.reshape(aparam, [-1]) - return feed_dict_test, imap - - def _eval_inner( - self, - coords, - cells, - atom_types, - fparam=None, - aparam=None, - atomic=False, - mixed_type=False, - ): - natoms, nframes = self._get_natoms_and_nframes( - coords, atom_types, mixed_type=mixed_type - ) - feed_dict_test, imap = self._prepare_feed_dict( - coords, cells, atom_types, fparam, aparam, mixed_type=mixed_type - ) - t_out = [self.t_dos] - if atomic: - t_out += [self.t_atom_dos] - - v_out = run_sess(self.sess, t_out, feed_dict=feed_dict_test) - dos = v_out[0] - if atomic: - atom_dos = v_out[1] - - # reverse map of the outputs - if atomic: - atom_dos = self.reverse_map( - np.reshape(atom_dos, [nframes, -1, self.numb_dos]), imap - ) - dos = np.sum(atom_dos, axis=1) - - dos = np.reshape(dos, [nframes, self.numb_dos]) - if atomic: - atom_dos = np.reshape(atom_dos, [nframes, natoms, self.numb_dos]) - return dos, atom_dos - else: - return dos - - def eval_descriptor( - self, - coords: np.ndarray, - cells: np.ndarray, - atom_types: List[int], - fparam: Optional[np.ndarray] = None, - aparam: Optional[np.ndarray] = None, - efield: Optional[np.ndarray] = None, - mixed_type: bool = False, - ) -> np.array: - """Evaluate descriptors by using this DP. - - Parameters - ---------- - coords - The coordinates of atoms. - The array should be of size nframes x natoms x 3 - cells - The cell of the region. - If None then non-PBC is assumed, otherwise using PBC. - The array should be of size nframes x 9 - atom_types - The atom types - The list should contain natoms ints - fparam - The frame parameter. - The array can be of size : - - nframes x dim_fparam. - - dim_fparam. Then all frames are assumed to be provided with the same fparam. - aparam - The atomic parameter - The array can be of size : - - nframes x natoms x dim_aparam. - - natoms x dim_aparam. Then all frames are assumed to be provided with the same aparam. - - dim_aparam. Then all frames and atoms are provided with the same aparam. - efield - The external field on atoms. - The array should be of size nframes x natoms x 3 - mixed_type - Whether to perform the mixed_type mode. - If True, the input data has the mixed_type format (see doc/model/train_se_atten.md), - in which frames in a system may have different natoms_vec(s), with the same nloc. - - Returns - ------- - descriptor - Descriptors. - """ - natoms, numb_test = self._get_natoms_and_nframes( - coords, atom_types, mixed_type=mixed_type - ) - descriptor = self._eval_func(self._eval_descriptor_inner, numb_test, natoms)( - coords, - cells, - atom_types, - fparam=fparam, - aparam=aparam, - efield=efield, - mixed_type=mixed_type, - ) - return descriptor - - def _eval_descriptor_inner( - self, - coords: np.ndarray, - cells: np.ndarray, - atom_types: List[int], - fparam: Optional[np.ndarray] = None, - aparam: Optional[np.ndarray] = None, - efield: Optional[np.ndarray] = None, - mixed_type: bool = False, - ) -> np.array: - natoms, nframes = self._get_natoms_and_nframes( - coords, atom_types, mixed_type=mixed_type - ) - feed_dict_test, imap = self._prepare_feed_dict( - coords, cells, atom_types, fparam, aparam, efield, mixed_type=mixed_type - ) - (descriptor,) = run_sess( - self.sess, [self.t_descriptor], feed_dict=feed_dict_test - ) - return self.reverse_map(np.reshape(descriptor, [nframes, natoms, -1]), imap) +__all__ = [ + "DeepDOS", +] diff --git a/deepmd/tf/infer/deep_eval.py b/deepmd/tf/infer/deep_eval.py index 9e3106f4ad..c87a888c37 100644 --- a/deepmd/tf/infer/deep_eval.py +++ b/deepmd/tf/infer/deep_eval.py @@ -4,13 +4,42 @@ ) from typing import ( TYPE_CHECKING, + Callable, + Dict, List, Optional, + Tuple, Union, ) import numpy as np +from deepmd.common import ( + make_default_mesh, +) +from deepmd.dpmodel.output_def import ( + ModelOutputDef, + OutputVariableCategory, +) +from deepmd.infer.deep_dipole import ( + DeepDipole, +) +from deepmd.infer.deep_dos import ( + DeepDOS, +) +from deepmd.infer.deep_eval import ( + DeepEvalBackend, +) +from deepmd.infer.deep_polar import ( + DeepGlobalPolar, + DeepPolar, +) +from deepmd.infer.deep_pot import ( + DeepPot, +) +from deepmd.infer.deep_wfc import ( + DeepWFC, +) from deepmd.tf.env import ( MODEL_VERSION, default_tf_session_config, @@ -28,8 +57,1048 @@ Path, ) + from deepmd.infer.deep_eval import DeepEval as DeepEvalWrapper + + +class DeepEval(DeepEvalBackend): + """TensorFlow backend implementation for DeepEval. + + Parameters + ---------- + model_file : Path + The name of the frozen model file. + output_def : ModelOutputDef + The output definition of the model. + *args : list + Positional arguments. + load_prefix: str + The prefix in the load computational graph + default_tf_graph : bool + If uses the default tf graph, otherwise build a new tf graph for evaluation + auto_batch_size : bool or int or AutomaticBatchSize, default: False + If True, automatic batch size will be used. If int, it will be used + as the initial batch size. + input_map : dict, optional + The input map for tf.import_graph_def. Only work with default tf graph + neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional + The ASE neighbor list class to produce the neighbor list. If None, the + neighbor list will be built natively in the model. + **kwargs : dict + Keyword arguments. + """ + + def __init__( + self, + model_file: "Path", + output_def: ModelOutputDef, + *args: list, + load_prefix: str = "load", + default_tf_graph: bool = False, + auto_batch_size: Union[bool, int, AutoBatchSize] = False, + input_map: Optional[dict] = None, + neighbor_list=None, + **kwargs: dict, + ): + self.graph = self._load_graph( + model_file, + prefix=load_prefix, + default_tf_graph=default_tf_graph, + input_map=input_map, + ) + self.load_prefix = load_prefix + + # graph_compatable should be called after graph and prefix are set + if not self._graph_compatable(): + raise RuntimeError( + f"model in graph (version {self.model_version}) is incompatible" + f"with the model (version {MODEL_VERSION}) supported by the current code." + "See https://deepmd.rtfd.io/compatability/ for details." + ) + + # set default to False, as subclasses may not support + if isinstance(auto_batch_size, bool): + if auto_batch_size: + self.auto_batch_size = AutoBatchSize() + else: + self.auto_batch_size = None + elif isinstance(auto_batch_size, int): + self.auto_batch_size = AutoBatchSize(auto_batch_size) + elif isinstance(auto_batch_size, AutoBatchSize): + self.auto_batch_size = auto_batch_size + else: + raise TypeError("auto_batch_size should be bool, int, or AutoBatchSize") + + self.neighbor_list = neighbor_list + + self.output_def = output_def + self._init_tensors() + self._init_attr() + self.has_efield = self.tensors["efield"] is not None + self.has_fparam = self.tensors["fparam"] is not None + self.has_aparam = self.tensors["aparam"] is not None + self.has_spin = self.ntypes_spin > 0 + self.modifier_type = None + + # looks ugly... + if self.modifier_type == "dipole_charge": + from deepmd.tf.infer.data_modifier import ( + DipoleChargeModifier, + ) + + t_mdl_name = self._get_tensor("modifier_attr/mdl_name:0") + t_mdl_charge_map = self._get_tensor("modifier_attr/mdl_charge_map:0") + t_sys_charge_map = self._get_tensor("modifier_attr/sys_charge_map:0") + t_ewald_h = self._get_tensor("modifier_attr/ewald_h:0") + t_ewald_beta = self._get_tensor("modifier_attr/ewald_beta:0") + [mdl_name, mdl_charge_map, sys_charge_map, ewald_h, ewald_beta] = run_sess( + self.sess, + [ + t_mdl_name, + t_mdl_charge_map, + t_sys_charge_map, + t_ewald_h, + t_ewald_beta, + ], + ) + mdl_name = mdl_name.decode("UTF-8") + mdl_charge_map = [int(ii) for ii in mdl_charge_map.decode("UTF-8").split()] + sys_charge_map = [int(ii) for ii in sys_charge_map.decode("UTF-8").split()] + self.dm = DipoleChargeModifier( + mdl_name, + mdl_charge_map, + sys_charge_map, + ewald_h=ewald_h, + ewald_beta=ewald_beta, + ) + + def _init_tensors(self): + tensor_names = { + # descrpt attrs + "ntypes": "descrpt_attr/ntypes:0", + "rcut": "descrpt_attr/rcut:0", + # model attrs + "tmap": "model_attr/tmap:0", + # inputs + "coord": "t_coord:0", + "type": "t_type:0", + "natoms": "t_natoms:0", + "box": "t_box:0", + "mesh": "t_mesh:0", + } + optional_tensor_names = { + # fitting attrs + "dfparam": "fitting_attr/dfparam:0", + "daparam": "fitting_attr/daparam:0", + "numb_dos": "fitting_attr/numb_dos:0", + # model attrs + "sel_type": "model_attr/sel_type:0", + # additonal inputs + "efield": "t_efield:0", + "fparam": "t_fparam:0", + "aparam": "t_aparam:0", + "ntypes_spin": "spin_attr/ntypes_spin:0", + # descriptor + "descriptor": "o_descriptor:0", + } + # output tensors + output_tensor_names = {} + for vv in self.output_def.var_defs: + output_tensor_names[vv] = f"o_{self._OUTDEF_DP2BACKEND[vv]}:0" + + self.tensors = {} + for tensor_key, tensor_name in tensor_names.items(): + self.tensors[tensor_key] = self._get_tensor(tensor_name) + for tensor_key, tensor_name in optional_tensor_names.items(): + try: + self.tensors[tensor_key] = self._get_tensor(tensor_name) + except KeyError: + self.tensors[tensor_key] = None + self.output_tensors = {} + removed_defs = [] + for ii, (tensor_key, tensor_name) in enumerate(output_tensor_names.items()): + try: + self.output_tensors[tensor_key] = self._get_tensor(tensor_name) + except KeyError: + # do not output + removed_defs.append(ii) + for ii in sorted(removed_defs, reverse=True): + del self.output_def.var_defs[list(self.output_def.var_defs.keys())[ii]] + + def _init_attr(self): + [ + self.ntypes, + self.rcut, + tmap, + ] = run_sess( + self.sess, + [ + self.tensors["ntypes"], + self.tensors["rcut"], + self.tensors["tmap"], + ], + ) + if self.tensors["ntypes_spin"] is not None: + self.ntypes_spin = run_sess(self.sess, [self.tensors["ntypes_spin"]])[0] + else: + self.ntypes_spin = 0 + if self.tensors["dfparam"] is not None: + self.dfparam = run_sess(self.sess, [self.tensors["dfparam"]])[0] + else: + self.dfparam = 0 + if self.tensors["daparam"] is not None: + self.daparam = run_sess(self.sess, [self.tensors["daparam"]])[0] + else: + self.daparam = 0 + if self.tensors["sel_type"] is not None: + self.sel_type = run_sess(self.sess, [self.tensors["sel_type"]])[0] + else: + self.sel_type = None + if self.tensors["numb_dos"] is not None: + self.numb_dos = run_sess(self.sess, [self.tensors["numb_dos"]])[0] + else: + self.numb_dos = 0 + self.tmap = tmap.decode("utf-8").split() + + @property + @lru_cache(maxsize=None) + def model_type(self) -> "DeepEvalWrapper": + """Get type of model. + + :type:str + """ + t_mt = self._get_tensor("model_attr/model_type:0") + [mt] = run_sess(self.sess, [t_mt], feed_dict={}) + model_type = mt.decode("utf-8") + if model_type == "ener": + return DeepPot + elif model_type == "dos": + return DeepDOS + elif model_type == "dipole": + return DeepDipole + elif model_type == "polar": + return DeepPolar + elif model_type == "global_polar": + return DeepGlobalPolar + elif model_type == "wfc": + return DeepWFC + else: + raise RuntimeError(f"unknown model type {model_type}") + + @property + @lru_cache(maxsize=None) + def model_version(self) -> str: + """Get version of model. + + Returns + ------- + str + version of model + """ + try: + t_mt = self._get_tensor("model_attr/model_version:0") + except KeyError: + # For deepmd-kit version 0.x - 1.x, set model version to 0.0 + return "0.0" + else: + [mt] = run_sess(self.sess, [t_mt], feed_dict={}) + return mt.decode("utf-8") + + @property + @lru_cache(maxsize=None) + def sess(self) -> tf.Session: + """Get TF session.""" + # start a tf session associated to the graph + return tf.Session(graph=self.graph, config=default_tf_session_config) + + def _graph_compatable(self) -> bool: + """Check the model compatability. + + Returns + ------- + bool + If the model stored in the graph file is compatable with the current code + """ + model_version_major = int(self.model_version.split(".")[0]) + model_version_minor = int(self.model_version.split(".")[1]) + MODEL_VERSION_MAJOR = int(MODEL_VERSION.split(".")[0]) + MODEL_VERSION_MINOR = int(MODEL_VERSION.split(".")[1]) + if (model_version_major != MODEL_VERSION_MAJOR) or ( + model_version_minor > MODEL_VERSION_MINOR + ): + return False + else: + return True + + def _get_tensor( + self, + tensor_name: str, + ) -> tf.Tensor: + """Get TF graph tensor. + + Parameters + ---------- + tensor_name : str + name of tensor to get + + Returns + ------- + tf.Tensor + loaded tensor + """ + # do not use os.path.join as it doesn't work on Windows + tensor_path = "/".join((self.load_prefix, tensor_name)) + tensor = self.graph.get_tensor_by_name(tensor_path) + return tensor + + @staticmethod + def _load_graph( + frozen_graph_filename: "Path", + prefix: str = "load", + default_tf_graph: bool = False, + input_map: Optional[dict] = None, + ): + # We load the protobuf file from the disk and parse it to retrieve the + # unserialized graph_def + with tf.gfile.GFile(str(frozen_graph_filename), "rb") as f: + graph_def = tf.GraphDef() + graph_def.ParseFromString(f.read()) + + if default_tf_graph: + tf.import_graph_def( + graph_def, + input_map=input_map, + return_elements=None, + name=prefix, + producer_op_list=None, + ) + graph = tf.get_default_graph() + else: + # Then, we can use again a convenient built-in function to import + # a graph_def into the current default Graph + with tf.Graph().as_default() as graph: + tf.import_graph_def( + graph_def, + input_map=None, + return_elements=None, + name=prefix, + producer_op_list=None, + ) + + return graph + + @staticmethod + def sort_input( + coord: np.ndarray, + atom_type: np.ndarray, + sel_atoms: Optional[List[int]] = None, + ): + """Sort atoms in the system according their types. + + Parameters + ---------- + coord + The coordinates of atoms. + Should be of shape [nframes, natoms, 3] + atom_type + The type of atoms + Should be of shape [natoms] + sel_atoms + The selected atoms by type + + Returns + ------- + coord_out + The coordinates after sorting + atom_type_out + The atom types after sorting + idx_map + The index mapping from the input to the output. + For example coord_out = coord[:,idx_map,:] + sel_atom_type + Only output if sel_atoms is not None + The sorted selected atom types + sel_idx_map + Only output if sel_atoms is not None + The index mapping from the selected atoms to sorted selected atoms. + """ + natoms = atom_type.shape[1] + if sel_atoms is not None: + selection = np.array([False] * natoms, dtype=bool) + for ii in sel_atoms: + selection += atom_type[0] == ii + sel_atom_type = atom_type[:, selection] + idx = np.arange(natoms) + idx_map = np.lexsort((idx, atom_type[0])) + nframes = coord.shape[0] + coord = coord.reshape([nframes, -1, 3]) + coord = np.reshape(coord[:, idx_map, :], [nframes, -1]) + atom_type = atom_type[:, idx_map] + if sel_atoms is not None: + sel_natoms = sel_atom_type.shape[1] + sel_idx = np.arange(sel_natoms) + sel_idx_map = np.lexsort((sel_idx, sel_atom_type[0])) + sel_atom_type = sel_atom_type[:, sel_idx_map] + return coord, atom_type, idx_map, sel_atom_type, sel_idx_map + else: + return coord, atom_type, idx_map, atom_type, idx_map + + @staticmethod + def reverse_map(vec: np.ndarray, imap: List[int]) -> np.ndarray: + """Reverse mapping of a vector according to the index map. + + Parameters + ---------- + vec + Input vector. Be of shape [nframes, natoms, -1] + imap + Index map. Be of shape [natoms] + + Returns + ------- + vec_out + Reverse mapped vector. + """ + ret = np.zeros(vec.shape) + ret[:, imap, :] = vec + return ret + + def make_natoms_vec( + self, + atom_types: np.ndarray, + ) -> np.ndarray: + """Make the natom vector used by deepmd-kit. + + Parameters + ---------- + atom_types + The type of atoms + + Returns + ------- + natoms + The number of atoms. This tensor has the length of Ntypes + 2 + natoms[0]: number of local atoms + natoms[1]: total number of atoms held by this processor + natoms[i]: 2 <= i < Ntypes+2, number of type i atoms + + """ + natoms_vec = np.zeros(self.ntypes + 2).astype(int) + natoms = atom_types[0].size + natoms_vec[0] = natoms + natoms_vec[1] = natoms + for ii in range(self.ntypes): + natoms_vec[ii + 2] = np.count_nonzero(atom_types[0] == ii) + return natoms_vec + + def eval_typeebd(self) -> np.ndarray: + """Evaluate output of type embedding network by using this model. + + Returns + ------- + np.ndarray + The output of type embedding network. The shape is [ntypes, o_size], + where ntypes is the number of types, and o_size is the number of nodes + in the output layer. + + Raises + ------ + KeyError + If the model does not enable type embedding. + + See Also + -------- + deepmd.tf.utils.type_embed.TypeEmbedNet : The type embedding network. + + Examples + -------- + Get the output of type embedding network of `graph.pb`: + + >>> from deepmd.tf.infer import DeepPotential + >>> dp = DeepPotential('graph.pb') + >>> dp.eval_typeebd() + """ + t_typeebd = self._get_tensor("t_typeebd:0") + [typeebd] = run_sess(self.sess, [t_typeebd], feed_dict={}) + return typeebd + + def build_neighbor_list( + self, + coords: np.ndarray, + cell: Optional[np.ndarray], + atype: np.ndarray, + imap: np.ndarray, + neighbor_list, + ): + """Make the mesh with neighbor list for a single frame. + + Parameters + ---------- + coords : np.ndarray + The coordinates of atoms. Should be of shape [natoms, 3] + cell : Optional[np.ndarray] + The cell of the system. Should be of shape [3, 3] + atype : np.ndarray + The type of atoms. Should be of shape [natoms] + imap : np.ndarray + The index map of atoms. Should be of shape [natoms] + neighbor_list : ase.neighborlist.NewPrimitiveNeighborList + ASE neighbor list. The following method or attribute will be + used/set: bothways, self_interaction, update, build, first_neigh, + pair_second, offset_vec. + + Returns + ------- + natoms_vec : np.ndarray + The number of atoms. This tensor has the length of Ntypes + 2 + natoms[0]: nloc + natoms[1]: nall + natoms[i]: 2 <= i < Ntypes+2, number of type i atoms for nloc + coords : np.ndarray + The coordinates of atoms, including ghost atoms. Should be of + shape [nframes, nall, 3] + atype : np.ndarray + The type of atoms, including ghost atoms. Should be of shape [nall] + mesh : np.ndarray + The mesh in nei_mode=4. + imap : np.ndarray + The index map of atoms. Should be of shape [nall] + ghost_map : np.ndarray + The index map of ghost atoms. Should be of shape [nghost] + """ + pbc = np.repeat(cell is not None, 3) + cell = cell.reshape(3, 3) + positions = coords.reshape(-1, 3) + neighbor_list.bothways = True + neighbor_list.self_interaction = False + if neighbor_list.update(pbc, cell, positions): + neighbor_list.build(pbc, cell, positions) + first_neigh = neighbor_list.first_neigh.copy() + pair_second = neighbor_list.pair_second.copy() + offset_vec = neighbor_list.offset_vec.copy() + # get out-of-box neighbors + out_mask = np.any(offset_vec != 0, axis=1) + out_idx = pair_second[out_mask] + out_offset = offset_vec[out_mask] + out_coords = positions[out_idx] + out_offset.dot(cell) + atype = np.array(atype, dtype=int).reshape(-1) + out_atype = atype[out_idx] + + nloc = positions.shape[0] + nghost = out_idx.size + all_coords = np.concatenate((positions, out_coords), axis=0) + all_atype = np.concatenate((atype, out_atype), axis=0) + # convert neighbor indexes + ghost_map = pair_second[out_mask] + pair_second[out_mask] = np.arange(nloc, nloc + nghost) + # get the mesh + mesh = np.zeros(16 + nloc * 2 + pair_second.size, dtype=int) + mesh[0] = nloc + # ilist + mesh[16 : 16 + nloc] = np.arange(nloc) + # numnei + mesh[16 + nloc : 16 + nloc * 2] = first_neigh[1:] - first_neigh[:-1] + # jlist + mesh[16 + nloc * 2 :] = pair_second + + # natoms_vec + natoms_vec = np.zeros(self.ntypes + 2).astype(int) + natoms_vec[0] = nloc + natoms_vec[1] = nloc + nghost + for ii in range(self.ntypes): + natoms_vec[ii + 2] = np.count_nonzero(atype == ii) + # imap append ghost atoms + imap = np.concatenate((imap, np.arange(nloc, nloc + nghost))) + return natoms_vec, all_coords, all_atype, mesh, imap, ghost_map + + def get_ntypes(self) -> int: + """Get the number of atom types of this model.""" + return self.ntypes + + def get_ntypes_spin(self) -> int: + """Get the number of spin atom types of this model.""" + return self.ntypes_spin + + def get_rcut(self) -> float: + """Get the cut-off radius of this model.""" + return self.rcut + + def get_type_map(self) -> List[str]: + """Get the type map (element name of the atom types) of this model.""" + return self.tmap + + def get_sel_type(self) -> Optional[np.ndarray]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return np.array(self.sel_type).ravel() + + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this DP.""" + return self.dfparam + + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this DP.""" + return self.daparam + + def _eval_func(self, inner_func: Callable, numb_test: int, natoms: int) -> Callable: + """Wrapper method with auto batch size. + + Parameters + ---------- + inner_func : Callable + the method to be wrapped + numb_test : int + number of tests + natoms : int + number of atoms + + Returns + ------- + Callable + the wrapper + """ + if self.auto_batch_size is not None: + + def eval_func(*args, **kwargs): + return self.auto_batch_size.execute_all( + inner_func, numb_test, natoms, *args, **kwargs + ) + + else: + eval_func = inner_func + return eval_func + + def _get_natoms_and_nframes( + self, + coords: np.ndarray, + atom_types: Union[List[int], np.ndarray], + ) -> Tuple[int, int]: + natoms = len(atom_types[0]) + if natoms == 0: + assert coords.size == 0 + else: + coords = np.reshape(np.array(coords), [-1, natoms * 3]) + nframes = coords.shape[0] + return natoms, nframes + + def eval( + self, + coords: np.ndarray, + cells: np.ndarray, + atom_types: np.ndarray, + atomic: bool = False, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + efield: Optional[np.ndarray] = None, + ) -> Dict[str, np.ndarray]: + """Evaluate the energy, force and virial by using this DP. + + Parameters + ---------- + coords + The coordinates of atoms. + The array should be of size nframes x natoms x 3 + cells + The cell of the region. + If None then non-PBC is assumed, otherwise using PBC. + The array should be of size nframes x 9 + atom_types + The atom types + The list should contain natoms ints + atomic + Calculate the atomic energy and virial + fparam + The frame parameter. + The array can be of size : + - nframes x dim_fparam. + - dim_fparam. Then all frames are assumed to be provided with the same fparam. + aparam + The atomic parameter + The array can be of size : + - nframes x natoms x dim_aparam. + - natoms x dim_aparam. Then all frames are assumed to be provided with the same aparam. + - dim_aparam. Then all frames and atoms are provided with the same aparam. + efield + The external field on atoms. + The array should be of size nframes x natoms x 3 + + Returns + ------- + output_dict : dict + The output of the evaluation. The keys are the names of the output + variables, and the values are the corresponding output arrays. + """ + # reshape coords before getting shape + natoms, numb_test = self._get_natoms_and_nframes( + coords, + atom_types, + ) + output = self._eval_func(self._eval_inner, numb_test, natoms)( + coords, + cells, + atom_types, + fparam=fparam, + aparam=aparam, + atomic=atomic, + efield=efield, + ) + if not isinstance(output, tuple): + output = (output,) + + output_dict = { + odef.name: oo for oo, odef in zip(output, self.output_def.var_defs.values()) + } + # ugly!! + if self.modifier_type is not None and isinstance(self.model_type, DeepPot): + if atomic: + raise RuntimeError("modifier does not support atomic modification") + me, mf, mv = self.dm.eval(coords, cells, atom_types) + output = list(output) # tuple to list + e, f, v = output[:3] + output_dict["energy_redu"] += me.reshape(e.shape) + output_dict["energy_deri_r"] += mf.reshape(f.shape) + output_dict["energy_deri_c_redu"] += mv.reshape(v.shape) + return output_dict + + def _prepare_feed_dict( + self, + coords, + cells, + atom_types, + fparam=None, + aparam=None, + efield=None, + ): + # standarize the shape of inputs + natoms, nframes = self._get_natoms_and_nframes( + coords, + atom_types, + ) + atom_types = np.array(atom_types, dtype=int).reshape([nframes, natoms]) + coords = np.reshape(np.array(coords), [nframes, natoms * 3]) + if cells is None: + pbc = False + # make cells to work around the requirement of pbc + cells = np.tile(np.eye(3), [nframes, 1]).reshape([nframes, 9]) + else: + pbc = True + cells = np.array(cells).reshape([nframes, 9]) + + if self.has_fparam: + assert fparam is not None + fparam = np.array(fparam) + if self.has_aparam: + assert aparam is not None + aparam = np.array(aparam) + if self.has_efield: + assert ( + efield is not None + ), "you are using a model with external field, parameter efield should be provided" + efield = np.array(efield) + + # reshape the inputs + if self.has_fparam: + fdim = self.get_dim_fparam() + if fparam.size == nframes * fdim: + fparam = np.reshape(fparam, [nframes, fdim]) + elif fparam.size == fdim: + fparam = np.tile(fparam.reshape([-1]), [nframes, 1]) + else: + raise RuntimeError( + "got wrong size of frame param, should be either %d x %d or %d" + % (nframes, fdim, fdim) + ) + if self.has_aparam: + fdim = self.get_dim_aparam() + if aparam.size == nframes * natoms * fdim: + aparam = np.reshape(aparam, [nframes, natoms * fdim]) + elif aparam.size == natoms * fdim: + aparam = np.tile(aparam.reshape([-1]), [nframes, 1]) + elif aparam.size == fdim: + aparam = np.tile(aparam.reshape([-1]), [nframes, natoms]) + else: + raise RuntimeError( + "got wrong size of frame param, should be either %d x %d x %d or %d x %d or %d" + % (nframes, natoms, fdim, natoms, fdim, fdim) + ) + + # sort inputs + coords, atom_types, imap, sel_at, sel_imap = self.sort_input( + coords, atom_types, sel_atoms=self.get_sel_type() + ) + if self.has_efield: + efield = np.reshape(efield, [nframes, natoms, 3]) + efield = efield[:, imap, :] + efield = np.reshape(efield, [nframes, natoms * 3]) + if self.has_aparam: + aparam = np.reshape(aparam, [nframes, natoms, fdim]) + aparam = aparam[:, imap, :] + aparam = np.reshape(aparam, [nframes, natoms * fdim]) + + # make natoms_vec and default_mesh + if self.neighbor_list is None: + natoms_vec = self.make_natoms_vec(atom_types) + assert natoms_vec[0] == natoms + mesh = make_default_mesh(pbc, not self._check_mixed_types(atom_types)) + ghost_map = None + else: + if nframes > 1: + raise NotImplementedError( + "neighbor_list does not support multiple frames" + ) + ( + natoms_vec, + coords, + atom_types, + mesh, + imap, + ghost_map, + ) = self.build_neighbor_list( + coords, + cells if cells is not None else None, + atom_types, + imap, + self.neighbor_list, + ) + + # evaluate + feed_dict_test = {} + feed_dict_test[self.tensors["natoms"]] = natoms_vec + feed_dict_test[self.tensors["type"]] = atom_types.reshape([-1]) + feed_dict_test[self.tensors["coord"]] = np.reshape(coords, [-1]) + + if len(self.tensors["box"].shape) == 1: + feed_dict_test[self.tensors["box"]] = np.reshape(cells, [-1]) + elif len(self.tensors["box"].shape) == 2: + feed_dict_test[self.tensors["box"]] = cells + else: + raise RuntimeError + if self.has_efield: + feed_dict_test[self.tensors["efield"]] = np.reshape(efield, [-1]) + feed_dict_test[self.tensors["mesh"]] = mesh + if self.has_fparam: + feed_dict_test[self.tensors["fparam"]] = np.reshape(fparam, [-1]) + if self.has_aparam: + feed_dict_test[self.tensors["aparam"]] = np.reshape(aparam, [-1]) + return feed_dict_test, imap, natoms_vec, ghost_map, sel_at, sel_imap + + def _eval_inner( + self, + coords, + cells, + atom_types, + fparam=None, + aparam=None, + efield=None, + **kwargs, + ): + natoms, nframes = self._get_natoms_and_nframes( + coords, + atom_types, + ) + ( + feed_dict_test, + imap, + natoms_vec, + ghost_map, + sel_at, + sel_imap, + ) = self._prepare_feed_dict( + coords, + cells, + atom_types, + fparam, + aparam, + efield, + ) + + nloc = natoms_vec[0] + nloc_sel = sel_at.shape[1] + nall = natoms_vec[1] + + t_out = list(self.output_tensors.values()) + + v_out = run_sess(self.sess, t_out, feed_dict=feed_dict_test) + + if nloc_sel == 0: + nloc_sel = nloc + sel_imap = imap + if self.has_spin: + ntypes_real = self.ntypes - self.ntypes_spin + natoms_real = sum( + [ + np.count_nonzero(np.array(atom_types[0]) == ii) + for ii in range(ntypes_real) + ] + ) + else: + natoms_real = nloc_sel + if ghost_map is not None: + # add the value of ghost atoms to real atoms + for ii, odef in enumerate(self.output_def.var_defs.values()): + # when the shape is nall + if odef.category in ( + OutputVariableCategory.DERV_R, + OutputVariableCategory.DERV_C, + ): + odef_shape = self._get_output_shape(odef, nframes, nall) + tmp_shape = [np.prod(odef_shape[:-2]), *odef_shape[-2:]] + v_out[ii] = np.reshape(v_out[ii], tmp_shape) + for jj in range(v_out[ii].shape[0]): + np.add.at(v_out[ii][jj], ghost_map, v_out[ii][jj, nloc:]) + + for ii, odef in enumerate(self.output_def.var_defs.values()): + if odef.category in ( + OutputVariableCategory.DERV_R, + OutputVariableCategory.DERV_C, + ): + odef_shape = self._get_output_shape(odef, nframes, nall) + tmp_shape = [np.prod(odef_shape[:-2]), *odef_shape[-2:]] + # reverse map of the outputs + v_out[ii] = self.reverse_map(np.reshape(v_out[ii], tmp_shape), imap) + v_out[ii] = np.reshape(v_out[ii], odef_shape) + if nloc < nall: + v_out[ii] = v_out[ii][:, :, :nloc] + elif odef.category == OutputVariableCategory.OUT: + odef_shape = self._get_output_shape(odef, nframes, natoms_real) + v_out[ii] = self.reverse_map( + np.reshape(v_out[ii], odef_shape), sel_imap[:natoms_real] + ) + v_out[ii] = np.reshape(v_out[ii], odef_shape) + elif odef.category in ( + OutputVariableCategory.REDU, + OutputVariableCategory.DERV_C_REDU, + ): + odef_shape = self._get_output_shape(odef, nframes, 0) + v_out[ii] = np.reshape(v_out[ii], odef_shape) + else: + raise RuntimeError("unknown category") + return tuple(v_out) + + def _get_output_shape(self, odef, nframes, natoms): + if odef.category == OutputVariableCategory.DERV_C_REDU: + # virial + return [nframes, *odef.shape[:-1], 9] + elif odef.category == OutputVariableCategory.REDU: + # energy + return [nframes, *odef.shape, 1] + elif odef.category == OutputVariableCategory.DERV_C: + # atom_virial + return [nframes, *odef.shape[:-1], natoms, 9] + elif odef.category == OutputVariableCategory.DERV_R: + # force + return [nframes, *odef.shape[:-1], natoms, 3] + elif odef.category == OutputVariableCategory.OUT: + # atom_energy, atom_tensor + # Something wrong here? + # return [nframes, *shape, natoms, 1] + return [nframes, natoms, *odef.shape, 1] + else: + raise RuntimeError("unknown category") + + def eval_descriptor( + self, + coords: np.ndarray, + cells: np.ndarray, + atom_types: np.ndarray, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + efield: Optional[np.ndarray] = None, + ) -> np.ndarray: + """Evaluate descriptors by using this DP. + + Parameters + ---------- + coords + The coordinates of atoms. + The array should be of size nframes x natoms x 3 + cells + The cell of the region. + If None then non-PBC is assumed, otherwise using PBC. + The array should be of size nframes x 9 + atom_types + The atom types + The list should contain natoms ints + fparam + The frame parameter. + The array can be of size : + - nframes x dim_fparam. + - dim_fparam. Then all frames are assumed to be provided with the same fparam. + aparam + The atomic parameter + The array can be of size : + - nframes x natoms x dim_aparam. + - natoms x dim_aparam. Then all frames are assumed to be provided with the same aparam. + - dim_aparam. Then all frames and atoms are provided with the same aparam. + efield + The external field on atoms. + The array should be of size nframes x natoms x 3 + + Returns + ------- + descriptor + Descriptors. + """ + natoms, numb_test = self._get_natoms_and_nframes( + coords, + atom_types, + ) + descriptor = self._eval_func(self._eval_descriptor_inner, numb_test, natoms)( + coords, + cells, + atom_types, + fparam=fparam, + aparam=aparam, + efield=efield, + ) + return descriptor + + def _eval_descriptor_inner( + self, + coords: np.ndarray, + cells: np.ndarray, + atom_types: np.ndarray, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + efield: Optional[np.ndarray] = None, + ) -> np.ndarray: + natoms, nframes = self._get_natoms_and_nframes( + coords, + atom_types, + ) + ( + feed_dict_test, + imap, + natoms_vec, + ghost_map, + sel_at, + sel_imap, + ) = self._prepare_feed_dict( + coords, + cells, + atom_types, + fparam, + aparam, + efield, + ) + (descriptor,) = run_sess( + self.sess, [self.tensors["descriptor"]], feed_dict=feed_dict_test + ) + imap = imap[:natoms] + return self.reverse_map(np.reshape(descriptor, [nframes, natoms, -1]), imap) + + def get_numb_dos(self) -> int: + return self.numb_dos + + def get_has_efield(self) -> bool: + return self.has_efield + -class DeepEval: +class DeepEvalOld: + # old class for DipoleChargeModifier only """Common methods for DeepPot, DeepWFC, DeepPolar, ... Parameters diff --git a/deepmd/tf/infer/deep_polar.py b/deepmd/tf/infer/deep_polar.py index e0b73da2a2..c3d42fd537 100644 --- a/deepmd/tf/infer/deep_polar.py +++ b/deepmd/tf/infer/deep_polar.py @@ -1,165 +1,10 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from typing import ( - TYPE_CHECKING, - List, - Optional, +from deepmd.infer.deep_polar import ( + DeepGlobalPolar, + DeepPolar, ) -import numpy as np - -from deepmd.tf.infer.deep_tensor import ( - DeepTensor, -) - -if TYPE_CHECKING: - from pathlib import ( - Path, - ) - - -class DeepPolar(DeepTensor): - """Constructor. - - Parameters - ---------- - model_file : Path - The name of the frozen model file. - load_prefix: str - The prefix in the load computational graph - default_tf_graph : bool - If uses the default tf graph, otherwise build a new tf graph for evaluation - input_map : dict, optional - The input map for tf.import_graph_def. Only work with default tf graph - neighbor_list : ase.neighborlist.NeighborList, optional - The neighbor list object. If None, then build the native neighbor list. - - Warnings - -------- - For developers: `DeepTensor` initializer must be called at the end after - `self.tensors` are modified because it uses the data in `self.tensors` dict. - Do not chanage the order! - """ - - def __init__( - self, - model_file: "Path", - load_prefix: str = "load", - default_tf_graph: bool = False, - input_map: Optional[dict] = None, - neighbor_list=None, - ) -> None: - # use this in favor of dict update to move attribute from class to - # instance namespace - self.tensors = dict( - { - # output tensor - "t_tensor": "o_polar:0", - }, - **self.tensors, - ) - - DeepTensor.__init__( - self, - model_file, - load_prefix=load_prefix, - default_tf_graph=default_tf_graph, - input_map=input_map, - neighbor_list=neighbor_list, - ) - - def get_dim_fparam(self) -> int: - """Unsupported in this model.""" - raise NotImplementedError("This model type does not support this attribute") - - def get_dim_aparam(self) -> int: - """Unsupported in this model.""" - raise NotImplementedError("This model type does not support this attribute") - - -class DeepGlobalPolar(DeepTensor): - """Constructor. - - Parameters - ---------- - model_file : str - The name of the frozen model file. - load_prefix: str - The prefix in the load computational graph - default_tf_graph : bool - If uses the default tf graph, otherwise build a new tf graph for evaluation - neighbor_list : ase.neighborlist.NeighborList, optional - The neighbor list object. If None, then build the native neighbor list. - """ - - def __init__( - self, - model_file: str, - load_prefix: str = "load", - default_tf_graph: bool = False, - neighbor_list=None, - ) -> None: - self.tensors.update( - { - "t_sel_type": "model_attr/sel_type:0", - # output tensor - "t_tensor": "o_global_polar:0", - } - ) - - DeepTensor.__init__( - self, - model_file, - load_prefix=load_prefix, - default_tf_graph=default_tf_graph, - neighbor_list=None, - ) - - def eval( - self, - coords: np.ndarray, - cells: np.ndarray, - atom_types: List[int], - atomic: bool = False, - fparam: Optional[np.ndarray] = None, - aparam: Optional[np.ndarray] = None, - efield: Optional[np.ndarray] = None, - ) -> np.ndarray: - """Evaluate the model. - - Parameters - ---------- - coords - The coordinates of atoms. - The array should be of size nframes x natoms x 3 - cells - The cell of the region. - If None then non-PBC is assumed, otherwise using PBC. - The array should be of size nframes x 9 - atom_types - The atom types - The list should contain natoms ints - atomic - Not used in this model - fparam - Not used in this model - aparam - Not used in this model - efield - Not used in this model - - Returns - ------- - tensor - The returned tensor - If atomic == False then of size nframes x variable_dof - else of size nframes x natoms x variable_dof - """ - return DeepTensor.eval(self, coords, cells, atom_types, atomic=False) - - def get_dim_fparam(self) -> int: - """Unsupported in this model.""" - raise NotImplementedError("This model type does not support this attribute") - - def get_dim_aparam(self) -> int: - """Unsupported in this model.""" - raise NotImplementedError("This model type does not support this attribute") +__all__ = [ + "DeepPolar", + "DeepGlobalPolar", +] diff --git a/deepmd/tf/infer/deep_pot.py b/deepmd/tf/infer/deep_pot.py index 0663d2daee..587a13996a 100644 --- a/deepmd/tf/infer/deep_pot.py +++ b/deepmd/tf/infer/deep_pot.py @@ -1,692 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import logging -from typing import ( - TYPE_CHECKING, - Callable, - List, - Optional, - Tuple, - Union, +from deepmd.infer import ( + DeepPot, ) -import numpy as np - -from deepmd.infer.deep_pot import DeepPot as DeepPotBase -from deepmd.tf.common import ( - make_default_mesh, -) -from deepmd.tf.infer.data_modifier import ( - DipoleChargeModifier, -) -from deepmd.tf.infer.deep_eval import ( - DeepEval, -) -from deepmd.tf.utils.batch_size import ( - AutoBatchSize, -) -from deepmd.tf.utils.sess import ( - run_sess, -) - -if TYPE_CHECKING: - from pathlib import ( - Path, - ) - -log = logging.getLogger(__name__) - - -class DeepPot(DeepEval, DeepPotBase): - """Constructor. - - Parameters - ---------- - model_file : Path - The name of the frozen model file. - load_prefix: str - The prefix in the load computational graph - default_tf_graph : bool - If uses the default tf graph, otherwise build a new tf graph for evaluation - auto_batch_size : bool or int or AutomaticBatchSize, default: True - If True, automatic batch size will be used. If int, it will be used - as the initial batch size. - input_map : dict, optional - The input map for tf.import_graph_def. Only work with default tf graph - neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional - The ASE neighbor list class to produce the neighbor list. If None, the - neighbor list will be built natively in the model. - - Examples - -------- - >>> from deepmd.tf.infer import DeepPot - >>> import numpy as np - >>> dp = DeepPot('graph.pb') - >>> coord = np.array([[1,0,0], [0,0,1.5], [1,0,3]]).reshape([1, -1]) - >>> cell = np.diag(10 * np.ones(3)).reshape([1, -1]) - >>> atype = [1,0,1] - >>> e, f, v = dp.eval(coord, cell, atype) - - where `e`, `f` and `v` are predicted energy, force and virial of the system, respectively. - - Warnings - -------- - For developers: `DeepTensor` initializer must be called at the end after - `self.tensors` are modified because it uses the data in `self.tensors` dict. - Do not chanage the order! - """ - - def __init__( - self, - model_file: "Path", - load_prefix: str = "load", - default_tf_graph: bool = False, - auto_batch_size: Union[bool, int, AutoBatchSize] = True, - input_map: Optional[dict] = None, - neighbor_list=None, - ) -> None: - # add these tensors on top of what is defined by DeepTensor Class - # use this in favor of dict update to move attribute from class to - # instance namespace - self.tensors = { - # descrpt attrs - "t_ntypes": "descrpt_attr/ntypes:0", - "t_rcut": "descrpt_attr/rcut:0", - # fitting attrs - "t_dfparam": "fitting_attr/dfparam:0", - "t_daparam": "fitting_attr/daparam:0", - # model attrs - "t_tmap": "model_attr/tmap:0", - # inputs - "t_coord": "t_coord:0", - "t_type": "t_type:0", - "t_natoms": "t_natoms:0", - "t_box": "t_box:0", - "t_mesh": "t_mesh:0", - # add output tensors - "t_energy": "o_energy:0", - "t_force": "o_force:0", - "t_virial": "o_virial:0", - "t_ae": "o_atom_energy:0", - "t_av": "o_atom_virial:0", - "t_descriptor": "o_descriptor:0", - } - DeepEval.__init__( - self, - model_file, - load_prefix=load_prefix, - default_tf_graph=default_tf_graph, - auto_batch_size=auto_batch_size, - input_map=input_map, - neighbor_list=neighbor_list, - ) - - # load optional tensors - operations = [op.name for op in self.graph.get_operations()] - # check if the graph has these operations: - # if yes add them - - if ("%s/t_efield" % load_prefix) in operations: - self.tensors.update({"t_efield": "t_efield:0"}) - self.has_efield = True - else: - log.debug("Could not get tensor 't_efield:0'") - self.t_efield = None - self.has_efield = False - - if ("%s/t_fparam" % load_prefix) in operations: - self.tensors.update({"t_fparam": "t_fparam:0"}) - self.has_fparam = True - else: - log.debug("Could not get tensor 't_fparam:0'") - self.t_fparam = None - self.has_fparam = False - - if ("%s/t_aparam" % load_prefix) in operations: - self.tensors.update({"t_aparam": "t_aparam:0"}) - self.has_aparam = True - else: - log.debug("Could not get tensor 't_aparam:0'") - self.t_aparam = None - self.has_aparam = False - - if ("%s/spin_attr/ntypes_spin" % load_prefix) in operations: - self.tensors.update({"t_ntypes_spin": "spin_attr/ntypes_spin:0"}) - self.has_spin = True - else: - self.ntypes_spin = 0 - self.has_spin = False - - # now load tensors to object attributes - for attr_name, tensor_name in self.tensors.items(): - try: - self._get_tensor(tensor_name, attr_name) - except KeyError: - if attr_name != "t_descriptor": - raise - - self._run_default_sess() - self.tmap = self.tmap.decode("UTF-8").split() - - # setup modifier - try: - t_modifier_type = self._get_tensor("modifier_attr/type:0") - self.modifier_type = run_sess(self.sess, t_modifier_type).decode("UTF-8") - except (ValueError, KeyError): - self.modifier_type = None - - try: - t_jdata = self._get_tensor("train_attr/training_script:0") - jdata = run_sess(self.sess, t_jdata).decode("UTF-8") - import json - - jdata = json.loads(jdata) - self.descriptor_type = jdata["model"]["descriptor"]["type"] - except (ValueError, KeyError): - self.descriptor_type = None - - if self.modifier_type == "dipole_charge": - t_mdl_name = self._get_tensor("modifier_attr/mdl_name:0") - t_mdl_charge_map = self._get_tensor("modifier_attr/mdl_charge_map:0") - t_sys_charge_map = self._get_tensor("modifier_attr/sys_charge_map:0") - t_ewald_h = self._get_tensor("modifier_attr/ewald_h:0") - t_ewald_beta = self._get_tensor("modifier_attr/ewald_beta:0") - [mdl_name, mdl_charge_map, sys_charge_map, ewald_h, ewald_beta] = run_sess( - self.sess, - [ - t_mdl_name, - t_mdl_charge_map, - t_sys_charge_map, - t_ewald_h, - t_ewald_beta, - ], - ) - mdl_name = mdl_name.decode("UTF-8") - mdl_charge_map = [int(ii) for ii in mdl_charge_map.decode("UTF-8").split()] - sys_charge_map = [int(ii) for ii in sys_charge_map.decode("UTF-8").split()] - self.dm = DipoleChargeModifier( - mdl_name, - mdl_charge_map, - sys_charge_map, - ewald_h=ewald_h, - ewald_beta=ewald_beta, - ) - - def _run_default_sess(self): - if self.has_spin is True: - [ - self.ntypes, - self.ntypes_spin, - self.rcut, - self.dfparam, - self.daparam, - self.tmap, - ] = run_sess( - self.sess, - [ - self.t_ntypes, - self.t_ntypes_spin, - self.t_rcut, - self.t_dfparam, - self.t_daparam, - self.t_tmap, - ], - ) - else: - [self.ntypes, self.rcut, self.dfparam, self.daparam, self.tmap] = run_sess( - self.sess, - [ - self.t_ntypes, - self.t_rcut, - self.t_dfparam, - self.t_daparam, - self.t_tmap, - ], - ) - - def get_ntypes(self) -> int: - """Get the number of atom types of this model.""" - return self.ntypes - - def get_ntypes_spin(self): - """Get the number of spin atom types of this model.""" - return self.ntypes_spin - - def get_rcut(self) -> float: - """Get the cut-off radius of this model.""" - return self.rcut - - def get_type_map(self) -> List[str]: - """Get the type map (element name of the atom types) of this model.""" - return self.tmap - - def get_sel_type(self) -> List[int]: - """Unsupported in this model.""" - raise NotImplementedError("This model type does not support this attribute") - - def get_descriptor_type(self) -> List[int]: - """Get the descriptor type of this model.""" - return self.descriptor_type - - def get_dim_fparam(self) -> int: - """Get the number (dimension) of frame parameters of this DP.""" - return self.dfparam - - def get_dim_aparam(self) -> int: - """Get the number (dimension) of atomic parameters of this DP.""" - return self.daparam - - def _eval_func(self, inner_func: Callable, numb_test: int, natoms: int) -> Callable: - """Wrapper method with auto batch size. - - Parameters - ---------- - inner_func : Callable - the method to be wrapped - numb_test : int - number of tests - natoms : int - number of atoms - - Returns - ------- - Callable - the wrapper - """ - if self.auto_batch_size is not None: - - def eval_func(*args, **kwargs): - return self.auto_batch_size.execute_all( - inner_func, numb_test, natoms, *args, **kwargs - ) - - else: - eval_func = inner_func - return eval_func - - def _get_natoms_and_nframes( - self, - coords: np.ndarray, - atom_types: Union[List[int], np.ndarray], - mixed_type: bool = False, - ) -> Tuple[int, int]: - if mixed_type: - natoms = len(atom_types[0]) - else: - natoms = len(atom_types) - if natoms == 0: - assert coords.size == 0 - else: - coords = np.reshape(np.array(coords), [-1, natoms * 3]) - nframes = coords.shape[0] - return natoms, nframes - - def eval( - self, - coords: np.ndarray, - cells: np.ndarray, - atom_types: List[int], - atomic: bool = False, - fparam: Optional[np.ndarray] = None, - aparam: Optional[np.ndarray] = None, - efield: Optional[np.ndarray] = None, - mixed_type: bool = False, - ) -> Tuple[np.ndarray, ...]: - """Evaluate the energy, force and virial by using this DP. - - Parameters - ---------- - coords - The coordinates of atoms. - The array should be of size nframes x natoms x 3 - cells - The cell of the region. - If None then non-PBC is assumed, otherwise using PBC. - The array should be of size nframes x 9 - atom_types - The atom types - The list should contain natoms ints - atomic - Calculate the atomic energy and virial - fparam - The frame parameter. - The array can be of size : - - nframes x dim_fparam. - - dim_fparam. Then all frames are assumed to be provided with the same fparam. - aparam - The atomic parameter - The array can be of size : - - nframes x natoms x dim_aparam. - - natoms x dim_aparam. Then all frames are assumed to be provided with the same aparam. - - dim_aparam. Then all frames and atoms are provided with the same aparam. - efield - The external field on atoms. - The array should be of size nframes x natoms x 3 - mixed_type - Whether to perform the mixed_type mode. - If True, the input data has the mixed_type format (see doc/model/train_se_atten.md), - in which frames in a system may have different natoms_vec(s), with the same nloc. - - Returns - ------- - energy - The system energy. - force - The force on each atom - virial - The virial - atom_energy - The atomic energy. Only returned when atomic == True - atom_virial - The atomic virial. Only returned when atomic == True - """ - # reshape coords before getting shape - natoms, numb_test = self._get_natoms_and_nframes( - coords, atom_types, mixed_type=mixed_type - ) - output = self._eval_func(self._eval_inner, numb_test, natoms)( - coords, - cells, - atom_types, - fparam=fparam, - aparam=aparam, - atomic=atomic, - efield=efield, - mixed_type=mixed_type, - ) - - if self.modifier_type is not None: - if atomic: - raise RuntimeError("modifier does not support atomic modification") - me, mf, mv = self.dm.eval(coords, cells, atom_types) - output = list(output) # tuple to list - e, f, v = output[:3] - output[0] += me.reshape(e.shape) - output[1] += mf.reshape(f.shape) - output[2] += mv.reshape(v.shape) - output = tuple(output) - return output - - def _prepare_feed_dict( - self, - coords, - cells, - atom_types, - fparam=None, - aparam=None, - efield=None, - mixed_type=False, - ): - # standarize the shape of inputs - natoms, nframes = self._get_natoms_and_nframes( - coords, atom_types, mixed_type=mixed_type - ) - if mixed_type: - atom_types = np.array(atom_types, dtype=int).reshape([-1, natoms]) - else: - atom_types = np.array(atom_types, dtype=int).reshape([-1]) - coords = np.reshape(np.array(coords), [nframes, natoms * 3]) - if cells is None: - pbc = False - # make cells to work around the requirement of pbc - cells = np.tile(np.eye(3), [nframes, 1]).reshape([nframes, 9]) - else: - pbc = True - cells = np.array(cells).reshape([nframes, 9]) - - if self.has_fparam: - assert fparam is not None - fparam = np.array(fparam) - if self.has_aparam: - assert aparam is not None - aparam = np.array(aparam) - if self.has_efield: - assert ( - efield is not None - ), "you are using a model with external field, parameter efield should be provided" - efield = np.array(efield) - - # reshape the inputs - if self.has_fparam: - fdim = self.get_dim_fparam() - if fparam.size == nframes * fdim: - fparam = np.reshape(fparam, [nframes, fdim]) - elif fparam.size == fdim: - fparam = np.tile(fparam.reshape([-1]), [nframes, 1]) - else: - raise RuntimeError( - "got wrong size of frame param, should be either %d x %d or %d" - % (nframes, fdim, fdim) - ) - if self.has_aparam: - fdim = self.get_dim_aparam() - if aparam.size == nframes * natoms * fdim: - aparam = np.reshape(aparam, [nframes, natoms * fdim]) - elif aparam.size == natoms * fdim: - aparam = np.tile(aparam.reshape([-1]), [nframes, 1]) - elif aparam.size == fdim: - aparam = np.tile(aparam.reshape([-1]), [nframes, natoms]) - else: - raise RuntimeError( - "got wrong size of frame param, should be either %d x %d x %d or %d x %d or %d" - % (nframes, natoms, fdim, natoms, fdim, fdim) - ) - - # sort inputs - coords, atom_types, imap = self.sort_input( - coords, atom_types, mixed_type=mixed_type - ) - if self.has_efield: - efield = np.reshape(efield, [nframes, natoms, 3]) - efield = efield[:, imap, :] - efield = np.reshape(efield, [nframes, natoms * 3]) - if self.has_aparam: - aparam = np.reshape(aparam, [nframes, natoms, fdim]) - aparam = aparam[:, imap, :] - aparam = np.reshape(aparam, [nframes, natoms * fdim]) - - # make natoms_vec and default_mesh - if self.neighbor_list is None: - natoms_vec = self.make_natoms_vec(atom_types, mixed_type=mixed_type) - assert natoms_vec[0] == natoms - mesh = make_default_mesh(pbc, mixed_type) - ghost_map = None - else: - if nframes > 1: - raise NotImplementedError( - "neighbor_list does not support multiple frames" - ) - ( - natoms_vec, - coords, - atom_types, - mesh, - imap, - ghost_map, - ) = self.build_neighbor_list( - coords, - cells if cells is not None else None, - atom_types, - imap, - self.neighbor_list, - ) - - # evaluate - feed_dict_test = {} - feed_dict_test[self.t_natoms] = natoms_vec - if mixed_type: - feed_dict_test[self.t_type] = atom_types.reshape([-1]) - else: - feed_dict_test[self.t_type] = np.tile(atom_types, [nframes, 1]).reshape( - [-1] - ) - feed_dict_test[self.t_coord] = np.reshape(coords, [-1]) - - if len(self.t_box.shape) == 1: - feed_dict_test[self.t_box] = np.reshape(cells, [-1]) - elif len(self.t_box.shape) == 2: - feed_dict_test[self.t_box] = cells - else: - raise RuntimeError - if self.has_efield: - feed_dict_test[self.t_efield] = np.reshape(efield, [-1]) - feed_dict_test[self.t_mesh] = mesh - if self.has_fparam: - feed_dict_test[self.t_fparam] = np.reshape(fparam, [-1]) - if self.has_aparam: - feed_dict_test[self.t_aparam] = np.reshape(aparam, [-1]) - return feed_dict_test, imap, natoms_vec, ghost_map - - def _eval_inner( - self, - coords, - cells, - atom_types, - fparam=None, - aparam=None, - atomic=False, - efield=None, - mixed_type=False, - ): - natoms, nframes = self._get_natoms_and_nframes( - coords, atom_types, mixed_type=mixed_type - ) - feed_dict_test, imap, natoms_vec, ghost_map = self._prepare_feed_dict( - coords, cells, atom_types, fparam, aparam, efield, mixed_type=mixed_type - ) - - nloc = natoms_vec[0] - nall = natoms_vec[1] - - t_out = [self.t_energy, self.t_force, self.t_virial] - if atomic: - t_out += [self.t_ae, self.t_av] - - v_out = run_sess(self.sess, t_out, feed_dict=feed_dict_test) - energy = v_out[0] - force = v_out[1] - virial = v_out[2] - if atomic: - ae = v_out[3] - av = v_out[4] - - if self.has_spin: - ntypes_real = self.ntypes - self.ntypes_spin - natoms_real = sum( - [ - np.count_nonzero(np.array(atom_types) == ii) - for ii in range(ntypes_real) - ] - ) - else: - natoms_real = natoms - if ghost_map is not None: - # add the value of ghost atoms to real atoms - force = np.reshape(force, [nframes, -1, 3]) - np.add.at(force[0], ghost_map, force[0, nloc:]) - if atomic: - av = np.reshape(av, [nframes, -1, 9]) - np.add.at(av[0], ghost_map, av[0, nloc:]) - - # reverse map of the outputs - force = self.reverse_map(np.reshape(force, [nframes, -1, 3]), imap) - if atomic: - ae = self.reverse_map(np.reshape(ae, [nframes, -1, 1]), imap[:natoms_real]) - av = self.reverse_map(np.reshape(av, [nframes, -1, 9]), imap) - - energy = np.reshape(energy, [nframes, 1]) - force = np.reshape(force, [nframes, nall, 3]) - if nloc < nall: - force = force[:, :nloc, :] - virial = np.reshape(virial, [nframes, 9]) - if atomic: - ae = np.reshape(ae, [nframes, natoms_real, 1]) - av = np.reshape(av, [nframes, nall, 9]) - if nloc < nall: - av = av[:, :nloc, :] - return energy, force, virial, ae, av - else: - return energy, force, virial - - def eval_descriptor( - self, - coords: np.ndarray, - cells: np.ndarray, - atom_types: List[int], - fparam: Optional[np.ndarray] = None, - aparam: Optional[np.ndarray] = None, - efield: Optional[np.ndarray] = None, - mixed_type: bool = False, - ) -> np.array: - """Evaluate descriptors by using this DP. - - Parameters - ---------- - coords - The coordinates of atoms. - The array should be of size nframes x natoms x 3 - cells - The cell of the region. - If None then non-PBC is assumed, otherwise using PBC. - The array should be of size nframes x 9 - atom_types - The atom types - The list should contain natoms ints - fparam - The frame parameter. - The array can be of size : - - nframes x dim_fparam. - - dim_fparam. Then all frames are assumed to be provided with the same fparam. - aparam - The atomic parameter - The array can be of size : - - nframes x natoms x dim_aparam. - - natoms x dim_aparam. Then all frames are assumed to be provided with the same aparam. - - dim_aparam. Then all frames and atoms are provided with the same aparam. - efield - The external field on atoms. - The array should be of size nframes x natoms x 3 - mixed_type - Whether to perform the mixed_type mode. - If True, the input data has the mixed_type format (see doc/model/train_se_atten.md), - in which frames in a system may have different natoms_vec(s), with the same nloc. - - Returns - ------- - descriptor - Descriptors. - """ - natoms, numb_test = self._get_natoms_and_nframes( - coords, atom_types, mixed_type=mixed_type - ) - descriptor = self._eval_func(self._eval_descriptor_inner, numb_test, natoms)( - coords, - cells, - atom_types, - fparam=fparam, - aparam=aparam, - efield=efield, - mixed_type=mixed_type, - ) - return descriptor - - def _eval_descriptor_inner( - self, - coords: np.ndarray, - cells: np.ndarray, - atom_types: List[int], - fparam: Optional[np.ndarray] = None, - aparam: Optional[np.ndarray] = None, - efield: Optional[np.ndarray] = None, - mixed_type: bool = False, - ) -> np.array: - natoms, nframes = self._get_natoms_and_nframes( - coords, atom_types, mixed_type=mixed_type - ) - feed_dict_test, imap, natoms_vec, ghost_map = self._prepare_feed_dict( - coords, cells, atom_types, fparam, aparam, efield, mixed_type=mixed_type - ) - (descriptor,) = run_sess( - self.sess, [self.t_descriptor], feed_dict=feed_dict_test - ) - imap = imap[:natoms] - return self.reverse_map(np.reshape(descriptor, [nframes, natoms, -1]), imap) +__all__ = ["DeepPot"] diff --git a/deepmd/tf/infer/deep_tensor.py b/deepmd/tf/infer/deep_tensor.py index 9b064114b4..9e8acf8241 100644 --- a/deepmd/tf/infer/deep_tensor.py +++ b/deepmd/tf/infer/deep_tensor.py @@ -13,9 +13,7 @@ from deepmd.tf.common import ( make_default_mesh, ) -from deepmd.tf.infer.deep_eval import ( - DeepEval, -) +from deepmd.tf.infer.deep_eval import DeepEvalOld as DeepEval from deepmd.tf.utils.sess import ( run_sess, ) diff --git a/deepmd/tf/infer/deep_wfc.py b/deepmd/tf/infer/deep_wfc.py index aa0dff6f38..f7674bdde7 100644 --- a/deepmd/tf/infer/deep_wfc.py +++ b/deepmd/tf/infer/deep_wfc.py @@ -1,68 +1,8 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from typing import ( - TYPE_CHECKING, - Optional, +from deepmd.infer.deep_wfc import ( + DeepWFC, ) -from deepmd.tf.infer.deep_tensor import ( - DeepTensor, -) - -if TYPE_CHECKING: - from pathlib import ( - Path, - ) - - -class DeepWFC(DeepTensor): - """Constructor. - - Parameters - ---------- - model_file : Path - The name of the frozen model file. - load_prefix: str - The prefix in the load computational graph - default_tf_graph : bool - If uses the default tf graph, otherwise build a new tf graph for evaluation - input_map : dict, optional - The input map for tf.import_graph_def. Only work with default tf graph - - Warnings - -------- - For developers: `DeepTensor` initializer must be called at the end after - `self.tensors` are modified because it uses the data in `self.tensors` dict. - Do not chanage the order! - """ - - def __init__( - self, - model_file: "Path", - load_prefix: str = "load", - default_tf_graph: bool = False, - input_map: Optional[dict] = None, - ) -> None: - # use this in favor of dict update to move attribute from class to - # instance namespace - self.tensors = dict( - { - # output tensor - "t_tensor": "o_wfc:0", - }, - **self.tensors, - ) - DeepTensor.__init__( - self, - model_file, - load_prefix=load_prefix, - default_tf_graph=default_tf_graph, - input_map=input_map, - ) - - def get_dim_fparam(self) -> int: - """Unsupported in this model.""" - raise NotImplementedError("This model type does not support this attribute") - - def get_dim_aparam(self) -> int: - """Unsupported in this model.""" - raise NotImplementedError("This model type does not support this attribute") +__all__ = [ + "DeepWFC", +] diff --git a/deepmd/tf/model/frozen.py b/deepmd/tf/model/frozen.py index 8732fec8f4..f06ae954d1 100644 --- a/deepmd/tf/model/frozen.py +++ b/deepmd/tf/model/frozen.py @@ -7,6 +7,9 @@ Union, ) +from deepmd.infer.deep_pot import ( + DeepPot, +) from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, MODEL_VERSION, @@ -40,7 +43,12 @@ def __init__(self, model_file: str, **kwargs): super().__init__(**kwargs) self.model_file = model_file self.model = DeepPotential(model_file) - self.model_type = self.model.model_type + if isinstance(self.model, DeepPot): + self.model_type = "ener" + else: + raise NotImplementedError( + "This model type has not been implemented. " "Contribution is welcome!" + ) def build( self, @@ -122,14 +130,26 @@ def build( ) if self.model_type == "ener": return { - "energy": tf.identity(self.model.t_energy, name="o_energy" + suffix), - "force": tf.identity(self.model.t_force, name="o_force" + suffix), - "virial": tf.identity(self.model.t_virial, name="o_virial" + suffix), + # must visit the backend class + "energy": tf.identity( + self.model.deep_eval.output_tensors["energy_redu"], + name="o_energy" + suffix, + ), + "force": tf.identity( + self.model.deep_eval.output_tensors["energy_derv_r"], + name="o_force" + suffix, + ), + "virial": tf.identity( + self.model.deep_eval.output_tensors["energy_derv_c_redu"], + name="o_virial" + suffix, + ), "atom_ener": tf.identity( - self.model.t_ae, name="o_atom_energy" + suffix + self.model.deep_eval.output_tensors["energy"], + name="o_atom_energy" + suffix, ), "atom_virial": tf.identity( - self.model.t_av, name="o_atom_virial" + suffix + self.model.deep_eval.output_tensors["energy_derv_c"], + name="o_atom_virial" + suffix, ), "coord": coord_, "atype": atype_, From 9181a02944eb90da06d02a973bc5b87e80f5ac78 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Fri, 9 Feb 2024 22:02:09 +0800 Subject: [PATCH 065/686] fix: cuda tests of linear and pair atomic model (#3248) This PR is to fix LinearAtomicModel GPU compatibility. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/pt/model/model/pairtab_atomic_model.py | 20 +++++++++---------- .../pt/model/test_linear_atomic_model.py | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/deepmd/pt/model/model/pairtab_atomic_model.py b/deepmd/pt/model/model/pairtab_atomic_model.py index 98215191c1..2837aaffe7 100644 --- a/deepmd/pt/model/model/pairtab_atomic_model.py +++ b/deepmd/pt/model/model/pairtab_atomic_model.py @@ -7,9 +7,6 @@ ) import torch -from torch import ( - nn, -) from deepmd.dpmodel import ( FittingOutputDef, @@ -22,9 +19,12 @@ from .base_atomic_model import ( BaseAtomicModel, ) +from .model import ( + BaseModel, +) -class PairTabModel(nn.Module, BaseAtomicModel): +class PairTabModel(BaseModel, BaseAtomicModel): """Pairwise tabulation energy model. This model can be used to tabulate the pairwise energy between atoms for either @@ -62,11 +62,11 @@ def __init__( tab_info, tab_data, ) = self.tab.get() # this returns -> Tuple[np.array, np.array] - self.tab_info = torch.from_numpy(tab_info) - self.tab_data = torch.from_numpy(tab_data) + self.register_buffer("tab_info", torch.from_numpy(tab_info)) + self.register_buffer("tab_data", torch.from_numpy(tab_data)) else: - self.tab_info = None - self.tab_data = None + self.register_buffer("tab_info", None) + self.register_buffer("tab_data", None) # self.model_type = "ener" # self.model_version = MODEL_VERSION ## this shoud be in the parent class @@ -118,8 +118,8 @@ def deserialize(cls, data) -> "PairTabModel": tab = PairTab.deserialize(data["tab"]) tab_model = cls(None, rcut, sel) tab_model.tab = tab - tab_model.tab_info = torch.from_numpy(tab_model.tab.tab_info) - tab_model.tab_data = torch.from_numpy(tab_model.tab.tab_data) + tab_model.register_buffer("tab_info", torch.from_numpy(tab_model.tab.tab_info)) + tab_model.register_buffer("tab_data", torch.from_numpy(tab_model.tab.tab_data)) return tab_model def forward_atomic( diff --git a/source/tests/pt/model/test_linear_atomic_model.py b/source/tests/pt/model/test_linear_atomic_model.py index 211b1f8215..e9090de86a 100644 --- a/source/tests/pt/model/test_linear_atomic_model.py +++ b/source/tests/pt/model/test_linear_atomic_model.py @@ -56,8 +56,8 @@ def test_pairwise(self, mock_loadtxt): [0.25, 0.0, 0.0, 0.0], ] ) - extended_atype = torch.tensor([[0, 0]]) - nlist = torch.tensor([[[1], [-1]]]) + extended_atype = torch.tensor([[0, 0]]).to(env.DEVICE) + nlist = torch.tensor([[[1], [-1]]]).to(env.DEVICE) ds = DescrptSeA( rcut=0.3, @@ -82,7 +82,7 @@ def test_pairwise(self, mock_loadtxt): zbl_model, sw_rmin=0.1, sw_rmax=0.25, - ) + ).to(env.DEVICE) wgt_res = [] for dist in np.linspace(0.05, 0.3, 10): extended_coord = torch.tensor( @@ -92,7 +92,7 @@ def test_pairwise(self, mock_loadtxt): [0.0, dist, 0.0], ], ] - ) + ).to(env.DEVICE) wgt_model.forward_atomic(extended_coord, extended_atype, nlist) @@ -112,7 +112,7 @@ def test_pairwise(self, mock_loadtxt): [0.0, 0.0], ], dtype=torch.float64, - ) + ).to(env.DEVICE) torch.testing.assert_close(results, excepted_res, rtol=0.0001, atol=0.0001) From 9a8f712c81fbe88ab47a5aaf14b6e8b9b74bdf7f Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 9 Feb 2024 09:03:43 -0500 Subject: [PATCH 066/686] backend-indepedent dp test (#3249) Fix #3118. Note: head is not supported yet in DeepEval --------- Signed-off-by: Jinzhe Zeng --- deepmd/entrypoints/test.py | 1056 +++++++++++++++++++++++++++++++++ deepmd/pt/entrypoints/main.py | 21 +- deepmd/tf/entrypoints/test.py | 1052 +------------------------------- 3 files changed, 1063 insertions(+), 1066 deletions(-) create mode 100644 deepmd/entrypoints/test.py diff --git a/deepmd/entrypoints/test.py b/deepmd/entrypoints/test.py new file mode 100644 index 0000000000..b9421bb198 --- /dev/null +++ b/deepmd/entrypoints/test.py @@ -0,0 +1,1056 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Test trained DeePMD model.""" +import logging +from pathlib import ( + Path, +) +from typing import ( + TYPE_CHECKING, + Dict, + List, + Optional, + Tuple, +) + +import numpy as np + +from deepmd.common import ( + expand_sys_str, +) +from deepmd.infer.deep_dipole import ( + DeepDipole, +) +from deepmd.infer.deep_dos import ( + DeepDOS, +) +from deepmd.infer.deep_eval import ( + DeepEval, +) +from deepmd.infer.deep_polar import ( + DeepGlobalPolar, + DeepPolar, +) +from deepmd.infer.deep_pot import ( + DeepPot, +) +from deepmd.infer.deep_wfc import ( + DeepWFC, +) +from deepmd.utils import random as dp_random +from deepmd.utils.data import ( + DeepmdData, +) +from deepmd.utils.weight_avg import ( + weighted_average, +) + +if TYPE_CHECKING: + from deepmd.tf.infer import ( + DeepDipole, + DeepDOS, + DeepPolar, + DeepPot, + DeepWFC, + ) + from deepmd.tf.infer.deep_tensor import ( + DeepTensor, + ) + +__all__ = ["test"] + +log = logging.getLogger(__name__) + + +def test( + *, + model: str, + system: str, + datafile: str, + set_prefix: str, + numb_test: int, + rand_seed: Optional[int], + shuffle_test: bool, + detail_file: str, + atomic: bool, + head: Optional[str] = None, + **kwargs, +): + """Test model predictions. + + Parameters + ---------- + model : str + path where model is stored + system : str + system directory + datafile : str + the path to the list of systems to test + set_prefix : str + string prefix of set + numb_test : int + munber of tests to do. 0 means all data. + rand_seed : Optional[int] + seed for random generator + shuffle_test : bool + whether to shuffle tests + detail_file : Optional[str] + file where test details will be output + atomic : bool + whether per atom quantities should be computed + head : Optional[str], optional + (Supported backend: PyTorch) Task head to test if in multi-task mode. + **kwargs + additional arguments + + Raises + ------ + RuntimeError + if no valid system was found + """ + if numb_test == 0: + # only float has inf, but should work for min + numb_test = float("inf") + if datafile is not None: + with open(datafile) as datalist: + all_sys = datalist.read().splitlines() + else: + all_sys = expand_sys_str(system) + + if len(all_sys) == 0: + raise RuntimeError("Did not find valid system") + err_coll = [] + siz_coll = [] + + # init random seed + if rand_seed is not None: + dp_random.seed(rand_seed % (2**32)) + + # init model + dp = DeepEval(model, head=head) + + for cc, system in enumerate(all_sys): + log.info("# ---------------output of dp test--------------- ") + log.info(f"# testing system : {system}") + + # create data class + tmap = dp.get_type_map() if isinstance(dp, DeepPot) else None + data = DeepmdData( + system, + set_prefix, + shuffle_test=shuffle_test, + type_map=tmap, + sort_atoms=False, + ) + + if isinstance(dp, DeepPot): + err = test_ener( + dp, + data, + system, + numb_test, + detail_file, + atomic, + append_detail=(cc != 0), + ) + elif isinstance(dp, DeepDOS): + err = test_dos( + dp, + data, + system, + numb_test, + detail_file, + atomic, + append_detail=(cc != 0), + ) + elif isinstance(dp, DeepDipole): + err = test_dipole(dp, data, numb_test, detail_file, atomic) + elif isinstance(dp, DeepPolar): + err = test_polar(dp, data, numb_test, detail_file, atomic=atomic) + elif isinstance(dp, DeepGlobalPolar): # should not appear in this new version + log.warning( + "Global polar model is not currently supported. Please directly use the polar mode and change loss parameters." + ) + err = test_polar( + dp, data, numb_test, detail_file, atomic=False + ) # YWolfeee: downward compatibility + log.info("# ----------------------------------------------- ") + err_coll.append(err) + + avg_err = weighted_average(err_coll) + + if len(all_sys) != len(err_coll): + log.warning("Not all systems are tested! Check if the systems are valid") + + if len(all_sys) > 1: + log.info("# ----------weighted average of errors----------- ") + log.info(f"# number of systems : {len(all_sys)}") + if isinstance(dp, DeepPot): + print_ener_sys_avg(avg_err) + elif isinstance(dp, DeepDOS): + print_dos_sys_avg(avg_err) + elif isinstance(dp, DeepDipole): + print_dipole_sys_avg(avg_err) + elif isinstance(dp, DeepPolar): + print_polar_sys_avg(avg_err) + elif isinstance(dp, DeepGlobalPolar): + print_polar_sys_avg(avg_err) + elif isinstance(dp, DeepGlobalPolar): + print_wfc_sys_avg(avg_err) + log.info("# ----------------------------------------------- ") + + +def mae(diff: np.ndarray) -> float: + """Calcalte mean absulote error. + + Parameters + ---------- + diff : np.ndarray + difference + + Returns + ------- + float + mean absulote error + """ + return np.mean(np.abs(diff)) + + +def rmse(diff: np.ndarray) -> float: + """Calculate root mean square error. + + Parameters + ---------- + diff : np.ndarray + difference + + Returns + ------- + float + root mean square error + """ + return np.sqrt(np.average(diff * diff)) + + +def save_txt_file( + fname: Path, data: np.ndarray, header: str = "", append: bool = False +): + """Save numpy array to test file. + + Parameters + ---------- + fname : str + filename + data : np.ndarray + data to save to disk + header : str, optional + header string to use in file, by default "" + append : bool, optional + if true file will be appended insted of overwriting, by default False + """ + flags = "ab" if append else "w" + with fname.open(flags) as fp: + np.savetxt(fp, data, header=header) + + +def test_ener( + dp: "DeepPot", + data: DeepmdData, + system: str, + numb_test: int, + detail_file: Optional[str], + has_atom_ener: bool, + append_detail: bool = False, +) -> Tuple[List[np.ndarray], List[int]]: + """Test energy type model. + + Parameters + ---------- + dp : DeepPot + instance of deep potential + data : DeepmdData + data container object + system : str + system directory + numb_test : int + munber of tests to do + detail_file : Optional[str] + file where test details will be output + has_atom_ener : bool + whether per atom quantities should be computed + append_detail : bool, optional + if true append output detail file, by default False + + Returns + ------- + Tuple[List[np.ndarray], List[int]] + arrays with results and their shapes + """ + data.add("energy", 1, atomic=False, must=False, high_prec=True) + data.add("force", 3, atomic=True, must=False, high_prec=False) + data.add("virial", 9, atomic=False, must=False, high_prec=False) + if dp.has_efield: + data.add("efield", 3, atomic=True, must=True, high_prec=False) + if has_atom_ener: + data.add("atom_ener", 1, atomic=True, must=True, high_prec=False) + if dp.get_dim_fparam() > 0: + data.add( + "fparam", dp.get_dim_fparam(), atomic=False, must=True, high_prec=False + ) + if dp.get_dim_aparam() > 0: + data.add("aparam", dp.get_dim_aparam(), atomic=True, must=True, high_prec=False) + + test_data = data.get_test() + mixed_type = data.mixed_type + natoms = len(test_data["type"][0]) + nframes = test_data["box"].shape[0] + numb_test = min(nframes, numb_test) + + coord = test_data["coord"][:numb_test].reshape([numb_test, -1]) + box = test_data["box"][:numb_test] + if dp.has_efield: + efield = test_data["efield"][:numb_test].reshape([numb_test, -1]) + else: + efield = None + if not data.pbc: + box = None + if mixed_type: + atype = test_data["type"][:numb_test].reshape([numb_test, -1]) + else: + atype = test_data["type"][0] + if dp.get_dim_fparam() > 0: + fparam = test_data["fparam"][:numb_test] + else: + fparam = None + if dp.get_dim_aparam() > 0: + aparam = test_data["aparam"][:numb_test] + else: + aparam = None + + ret = dp.eval( + coord, + box, + atype, + fparam=fparam, + aparam=aparam, + atomic=has_atom_ener, + efield=efield, + mixed_type=mixed_type, + ) + energy = ret[0] + force = ret[1] + virial = ret[2] + energy = energy.reshape([numb_test, 1]) + force = force.reshape([numb_test, -1]) + virial = virial.reshape([numb_test, 9]) + if has_atom_ener: + ae = ret[3] + av = ret[4] + ae = ae.reshape([numb_test, -1]) + av = av.reshape([numb_test, -1]) + if dp.get_ntypes_spin() != 0: + ntypes_real = dp.get_ntypes() - dp.get_ntypes_spin() + nloc = natoms + nloc_real = sum([np.count_nonzero(atype == ii) for ii in range(ntypes_real)]) + force_r = np.split( + force, indices_or_sections=[nloc_real * 3, nloc * 3], axis=1 + )[0] + force_m = np.split( + force, indices_or_sections=[nloc_real * 3, nloc * 3], axis=1 + )[1] + test_force_r = np.split( + test_data["force"][:numb_test], + indices_or_sections=[nloc_real * 3, nloc * 3], + axis=1, + )[0] + test_force_m = np.split( + test_data["force"][:numb_test], + indices_or_sections=[nloc_real * 3, nloc * 3], + axis=1, + )[1] + + diff_e = energy - test_data["energy"][:numb_test].reshape([-1, 1]) + mae_e = mae(diff_e) + rmse_e = rmse(diff_e) + diff_f = force - test_data["force"][:numb_test] + mae_f = mae(diff_f) + rmse_f = rmse(diff_f) + diff_v = virial - test_data["virial"][:numb_test] + mae_v = mae(diff_v) + rmse_v = rmse(diff_v) + mae_ea = mae_e / natoms + rmse_ea = rmse_e / natoms + mae_va = mae_v / natoms + rmse_va = rmse_v / natoms + if has_atom_ener: + diff_ae = test_data["atom_ener"][:numb_test].reshape([-1]) - ae.reshape([-1]) + mae_ae = mae(diff_ae) + rmse_ae = rmse(diff_ae) + if dp.get_ntypes_spin() != 0: + mae_fr = mae(force_r - test_force_r) + mae_fm = mae(force_m - test_force_m) + rmse_fr = rmse(force_r - test_force_r) + rmse_fm = rmse(force_m - test_force_m) + + log.info(f"# number of test data : {numb_test:d} ") + log.info(f"Energy MAE : {mae_e:e} eV") + log.info(f"Energy RMSE : {rmse_e:e} eV") + log.info(f"Energy MAE/Natoms : {mae_ea:e} eV") + log.info(f"Energy RMSE/Natoms : {rmse_ea:e} eV") + if dp.get_ntypes_spin() == 0: + log.info(f"Force MAE : {mae_f:e} eV/A") + log.info(f"Force RMSE : {rmse_f:e} eV/A") + else: + log.info(f"Force atom MAE : {mae_fr:e} eV/A") + log.info(f"Force spin MAE : {mae_fm:e} eV/uB") + log.info(f"Force atom RMSE : {rmse_fr:e} eV/A") + log.info(f"Force spin RMSE : {rmse_fm:e} eV/uB") + + if data.pbc: + log.info(f"Virial MAE : {mae_v:e} eV") + log.info(f"Virial RMSE : {rmse_v:e} eV") + log.info(f"Virial MAE/Natoms : {mae_va:e} eV") + log.info(f"Virial RMSE/Natoms : {rmse_va:e} eV") + if has_atom_ener: + log.info(f"Atomic ener MAE : {mae_ae:e} eV") + log.info(f"Atomic ener RMSE : {rmse_ae:e} eV") + + if detail_file is not None: + detail_path = Path(detail_file) + + pe = np.concatenate( + ( + np.reshape(test_data["energy"][:numb_test], [-1, 1]), + np.reshape(energy, [-1, 1]), + ), + axis=1, + ) + save_txt_file( + detail_path.with_suffix(".e.out"), + pe, + header="%s: data_e pred_e" % system, + append=append_detail, + ) + pe_atom = pe / natoms + save_txt_file( + detail_path.with_suffix(".e_peratom.out"), + pe_atom, + header="%s: data_e pred_e" % system, + append=append_detail, + ) + if dp.get_ntypes_spin() == 0: + pf = np.concatenate( + ( + np.reshape(test_data["force"][:numb_test], [-1, 3]), + np.reshape(force, [-1, 3]), + ), + axis=1, + ) + save_txt_file( + detail_path.with_suffix(".f.out"), + pf, + header="%s: data_fx data_fy data_fz pred_fx pred_fy pred_fz" % system, + append=append_detail, + ) + else: + pf_real = np.concatenate( + (np.reshape(test_force_r, [-1, 3]), np.reshape(force_r, [-1, 3])), + axis=1, + ) + pf_mag = np.concatenate( + (np.reshape(test_force_m, [-1, 3]), np.reshape(force_m, [-1, 3])), + axis=1, + ) + save_txt_file( + detail_path.with_suffix(".fr.out"), + pf_real, + header="%s: data_fx data_fy data_fz pred_fx pred_fy pred_fz" % system, + append=append_detail, + ) + save_txt_file( + detail_path.with_suffix(".fm.out"), + pf_mag, + header="%s: data_fmx data_fmy data_fmz pred_fmx pred_fmy pred_fmz" + % system, + append=append_detail, + ) + pv = np.concatenate( + ( + np.reshape(test_data["virial"][:numb_test], [-1, 9]), + np.reshape(virial, [-1, 9]), + ), + axis=1, + ) + save_txt_file( + detail_path.with_suffix(".v.out"), + pv, + header=f"{system}: data_vxx data_vxy data_vxz data_vyx data_vyy " + "data_vyz data_vzx data_vzy data_vzz pred_vxx pred_vxy pred_vxz pred_vyx " + "pred_vyy pred_vyz pred_vzx pred_vzy pred_vzz", + append=append_detail, + ) + pv_atom = pv / natoms + save_txt_file( + detail_path.with_suffix(".v_peratom.out"), + pv_atom, + header=f"{system}: data_vxx data_vxy data_vxz data_vyx data_vyy " + "data_vyz data_vzx data_vzy data_vzz pred_vxx pred_vxy pred_vxz pred_vyx " + "pred_vyy pred_vyz pred_vzx pred_vzy pred_vzz", + append=append_detail, + ) + if dp.get_ntypes_spin() == 0: + return { + "mae_e": (mae_e, energy.size), + "mae_ea": (mae_ea, energy.size), + "mae_f": (mae_f, force.size), + "mae_v": (mae_v, virial.size), + "mae_va": (mae_va, virial.size), + "rmse_e": (rmse_e, energy.size), + "rmse_ea": (rmse_ea, energy.size), + "rmse_f": (rmse_f, force.size), + "rmse_v": (rmse_v, virial.size), + "rmse_va": (rmse_va, virial.size), + } + else: + return { + "mae_e": (mae_e, energy.size), + "mae_ea": (mae_ea, energy.size), + "mae_fr": (mae_fr, force_r.size), + "mae_fm": (mae_fm, force_m.size), + "mae_v": (mae_v, virial.size), + "mae_va": (mae_va, virial.size), + "rmse_e": (rmse_e, energy.size), + "rmse_ea": (rmse_ea, energy.size), + "rmse_fr": (rmse_fr, force_r.size), + "rmse_fm": (rmse_fm, force_m.size), + "rmse_v": (rmse_v, virial.size), + "rmse_va": (rmse_va, virial.size), + } + + +def print_ener_sys_avg(avg: Dict[str, float]): + """Print errors summary for energy type potential. + + Parameters + ---------- + avg : np.ndarray + array with summaries + """ + log.info(f"Energy MAE : {avg['mae_e']:e} eV") + log.info(f"Energy RMSE : {avg['rmse_e']:e} eV") + log.info(f"Energy MAE/Natoms : {avg['mae_ea']:e} eV") + log.info(f"Energy RMSE/Natoms : {avg['rmse_ea']:e} eV") + if "rmse_f" in avg.keys(): + log.info(f"Force MAE : {avg['mae_f']:e} eV/A") + log.info(f"Force RMSE : {avg['rmse_f']:e} eV/A") + else: + log.info(f"Force atom MAE : {avg['mae_fr']:e} eV/A") + log.info(f"Force spin MAE : {avg['mae_fm']:e} eV/uB") + log.info(f"Force atom RMSE : {avg['rmse_fr']:e} eV/A") + log.info(f"Force spin RMSE : {avg['rmse_fm']:e} eV/uB") + log.info(f"Virial MAE : {avg['mae_v']:e} eV") + log.info(f"Virial RMSE : {avg['rmse_v']:e} eV") + log.info(f"Virial MAE/Natoms : {avg['mae_va']:e} eV") + log.info(f"Virial RMSE/Natoms : {avg['rmse_va']:e} eV") + + +def test_dos( + dp: "DeepDOS", + data: DeepmdData, + system: str, + numb_test: int, + detail_file: Optional[str], + has_atom_dos: bool, + append_detail: bool = False, +) -> Tuple[List[np.ndarray], List[int]]: + """Test DOS type model. + + Parameters + ---------- + dp : DeepDOS + instance of deep potential + data : DeepmdData + data container object + system : str + system directory + numb_test : int + munber of tests to do + detail_file : Optional[str] + file where test details will be output + has_atom_dos : bool + whether per atom quantities should be computed + append_detail : bool, optional + if true append output detail file, by default False + + Returns + ------- + Tuple[List[np.ndarray], List[int]] + arrays with results and their shapes + """ + data.add("dos", dp.numb_dos, atomic=False, must=True, high_prec=True) + if has_atom_dos: + data.add("atom_dos", dp.numb_dos, atomic=True, must=False, high_prec=True) + + if dp.get_dim_fparam() > 0: + data.add( + "fparam", dp.get_dim_fparam(), atomic=False, must=True, high_prec=False + ) + if dp.get_dim_aparam() > 0: + data.add("aparam", dp.get_dim_aparam(), atomic=True, must=True, high_prec=False) + + test_data = data.get_test() + mixed_type = data.mixed_type + natoms = len(test_data["type"][0]) + nframes = test_data["box"].shape[0] + numb_test = min(nframes, numb_test) + + coord = test_data["coord"][:numb_test].reshape([numb_test, -1]) + box = test_data["box"][:numb_test] + + if not data.pbc: + box = None + if mixed_type: + atype = test_data["type"][:numb_test].reshape([numb_test, -1]) + else: + atype = test_data["type"][0] + if dp.get_dim_fparam() > 0: + fparam = test_data["fparam"][:numb_test] + else: + fparam = None + if dp.get_dim_aparam() > 0: + aparam = test_data["aparam"][:numb_test] + else: + aparam = None + + ret = dp.eval( + coord, + box, + atype, + fparam=fparam, + aparam=aparam, + atomic=has_atom_dos, + mixed_type=mixed_type, + ) + dos = ret[0] + + dos = dos.reshape([numb_test, dp.numb_dos]) + + if has_atom_dos: + ados = ret[1] + ados = ados.reshape([numb_test, natoms * dp.numb_dos]) + + diff_dos = dos - test_data["dos"][:numb_test] + mae_dos = mae(diff_dos) + rmse_dos = rmse(diff_dos) + + mae_dosa = mae_dos / natoms + rmse_dosa = rmse_dos / natoms + + if has_atom_dos: + diff_ados = ados - test_data["atom_dos"][:numb_test] + mae_ados = mae(diff_ados) + rmse_ados = rmse(diff_ados) + + log.info(f"# number of test data : {numb_test:d} ") + + log.info(f"DOS MAE : {mae_dos:e} Occupation/eV") + log.info(f"DOS RMSE : {rmse_dos:e} Occupation/eV") + log.info(f"DOS MAE/Natoms : {mae_dosa:e} Occupation/eV") + log.info(f"DOS RMSE/Natoms : {rmse_dosa:e} Occupation/eV") + + if has_atom_dos: + log.info(f"Atomic DOS MAE : {mae_ados:e} Occupation/eV") + log.info(f"Atomic DOS RMSE : {rmse_ados:e} Occupation/eV") + + if detail_file is not None: + detail_path = Path(detail_file) + + for ii in range(numb_test): + test_out = test_data["dos"][ii].reshape(-1, 1) + pred_out = dos[ii].reshape(-1, 1) + + frame_output = np.hstack((test_out, pred_out)) + + save_txt_file( + detail_path.with_suffix(".dos.out.%.d" % ii), + frame_output, + header="%s - %.d: data_dos pred_dos" % (system, ii), + append=append_detail, + ) + + if has_atom_dos: + for ii in range(numb_test): + test_out = test_data["atom_dos"][ii].reshape(-1, 1) + pred_out = ados[ii].reshape(-1, 1) + + frame_output = np.hstack((test_out, pred_out)) + + save_txt_file( + detail_path.with_suffix(".ados.out.%.d" % ii), + frame_output, + header="%s - %.d: data_ados pred_ados" % (system, ii), + append=append_detail, + ) + + return { + "mae_dos": (mae_dos, dos.size), + "mae_dosa": (mae_dosa, dos.size), + "rmse_dos": (rmse_dos, dos.size), + "rmse_dosa": (rmse_dosa, dos.size), + } + + +def print_dos_sys_avg(avg: Dict[str, float]): + """Print errors summary for DOS type potential. + + Parameters + ---------- + avg : np.ndarray + array with summaries + """ + log.info(f"DOS MAE : {avg['mae_dos']:e} Occupation/eV") + log.info(f"DOS RMSE : {avg['rmse_dos']:e} Occupation/eV") + log.info(f"DOS MAE/Natoms : {avg['mae_dosa']:e} Occupation/eV") + log.info(f"DOS RMSE/Natoms : {avg['rmse_dosa']:e} Occupation/eV") + + +def run_test(dp: "DeepTensor", test_data: dict, numb_test: int, test_sys: DeepmdData): + """Run tests. + + Parameters + ---------- + dp : DeepTensor + instance of deep potential + test_data : dict + dictionary with test data + numb_test : int + munber of tests to do + test_sys : DeepmdData + test system + + Returns + ------- + [type] + [description] + """ + nframes = test_data["box"].shape[0] + numb_test = min(nframes, numb_test) + + coord = test_data["coord"][:numb_test].reshape([numb_test, -1]) + if test_sys.pbc: + box = test_data["box"][:numb_test] + else: + box = None + atype = test_data["type"][0] + prediction = dp.eval(coord, box, atype) + + return prediction.reshape([numb_test, -1]), numb_test, atype + + +def test_wfc( + dp: "DeepWFC", + data: DeepmdData, + numb_test: int, + detail_file: Optional[str], +) -> Tuple[List[np.ndarray], List[int]]: + """Test energy type model. + + Parameters + ---------- + dp : DeepPot + instance of deep potential + data : DeepmdData + data container object + numb_test : int + munber of tests to do + detail_file : Optional[str] + file where test details will be output + + Returns + ------- + Tuple[List[np.ndarray], List[int]] + arrays with results and their shapes + """ + data.add( + "wfc", 12, atomic=True, must=True, high_prec=False, type_sel=dp.get_sel_type() + ) + test_data = data.get_test() + wfc, numb_test, _ = run_test(dp, test_data, numb_test, data) + rmse_f = rmse(wfc - test_data["wfc"][:numb_test]) + + log.info("# number of test data : {numb_test:d} ") + log.info("WFC RMSE : {rmse_f:e} eV/A") + + if detail_file is not None: + detail_path = Path(detail_file) + pe = np.concatenate( + ( + np.reshape(test_data["wfc"][:numb_test], [-1, 12]), + np.reshape(wfc, [-1, 12]), + ), + axis=1, + ) + np.savetxt( + detail_path.with_suffix(".out"), + pe, + header="ref_wfc(12 dofs) predicted_wfc(12 dofs)", + ) + return {"rmse": (rmse_f, wfc.size)} + + +def print_wfc_sys_avg(avg): + """Print errors summary for wfc type potential. + + Parameters + ---------- + avg : np.ndarray + array with summaries + """ + log.info(f"WFC RMSE : {avg['rmse']:e} eV/A") + + +def test_polar( + dp: "DeepPolar", + data: DeepmdData, + numb_test: int, + detail_file: Optional[str], + *, + atomic: bool, +) -> Tuple[List[np.ndarray], List[int]]: + """Test energy type model. + + Parameters + ---------- + dp : DeepPot + instance of deep potential + data : DeepmdData + data container object + numb_test : int + munber of tests to do + detail_file : Optional[str] + file where test details will be output + atomic : bool + wheter to use glovbal version of polar potential + + Returns + ------- + Tuple[List[np.ndarray], List[int]] + arrays with results and their shapes + """ + data.add( + "polarizability" if not atomic else "atomic_polarizability", + 9, + atomic=atomic, + must=True, + high_prec=False, + type_sel=dp.get_sel_type(), + ) + + test_data = data.get_test() + polar, numb_test, atype = run_test(dp, test_data, numb_test, data) + + sel_type = dp.get_sel_type() + sel_natoms = 0 + for ii in sel_type: + sel_natoms += sum(atype == ii) + + # YWolfeee: do summation in global polar mode + if not atomic: + polar = np.sum(polar.reshape((polar.shape[0], -1, 9)), axis=1) + rmse_f = rmse(polar - test_data["polarizability"][:numb_test]) + rmse_fs = rmse_f / np.sqrt(sel_natoms) + rmse_fa = rmse_f / sel_natoms + else: + rmse_f = rmse(polar - test_data["atomic_polarizability"][:numb_test]) + + log.info(f"# number of test data : {numb_test:d} ") + log.info(f"Polarizability RMSE : {rmse_f:e}") + if not atomic: + log.info(f"Polarizability RMSE/sqrtN : {rmse_fs:e}") + log.info(f"Polarizability RMSE/N : {rmse_fa:e}") + log.info("The unit of error is the same as the unit of provided label.") + + if detail_file is not None: + detail_path = Path(detail_file) + + if not atomic: + pe = np.concatenate( + ( + np.reshape(test_data["polarizability"][:numb_test], [-1, 9]), + np.reshape(polar, [-1, 9]), + ), + axis=1, + ) + header_text = ( + "data_pxx data_pxy data_pxz data_pyx data_pyy data_pyz data_pzx " + "data_pzy data_pzz pred_pxx pred_pxy pred_pxz pred_pyx pred_pyy " + "pred_pyz pred_pzx pred_pzy pred_pzz" + ) + else: + pe = np.concatenate( + ( + np.reshape( + test_data["atomic_polarizability"][:numb_test], + [-1, 9 * sel_natoms], + ), + np.reshape(polar, [-1, 9 * sel_natoms]), + ), + axis=1, + ) + header_text = [ + f"{letter}{number}" + for number in range(1, sel_natoms + 1) + for letter in [ + "data_pxx", + "data_pxy", + "data_pxz", + "data_pyx", + "data_pyy", + "data_pyz", + "data_pzx", + "data_pzy", + "data_pzz", + ] + ] + [ + f"{letter}{number}" + for number in range(1, sel_natoms + 1) + for letter in [ + "pred_pxx", + "pred_pxy", + "pred_pxz", + "pred_pyx", + "pred_pyy", + "pred_pyz", + "pred_pzx", + "pred_pzy", + "pred_pzz", + ] + ] + header_text = " ".join(header_text) + + np.savetxt( + detail_path.with_suffix(".out"), + pe, + header=header_text, + ) + return {"rmse": (rmse_f, polar.size)} + + +def print_polar_sys_avg(avg): + """Print errors summary for polar type potential. + + Parameters + ---------- + avg : np.ndarray + array with summaries + """ + log.info(f"Polarizability RMSE : {avg['rmse']:e} eV/A") + + +def test_dipole( + dp: "DeepDipole", + data: DeepmdData, + numb_test: int, + detail_file: Optional[str], + atomic: bool, +) -> Tuple[List[np.ndarray], List[int]]: + """Test energy type model. + + Parameters + ---------- + dp : DeepPot + instance of deep potential + data : DeepmdData + data container object + numb_test : int + munber of tests to do + detail_file : Optional[str] + file where test details will be output + atomic : bool + whether atomic dipole is provided + + Returns + ------- + Tuple[List[np.ndarray], List[int]] + arrays with results and their shapes + """ + data.add( + "dipole" if not atomic else "atomic_dipole", + 3, + atomic=atomic, + must=True, + high_prec=False, + type_sel=dp.get_sel_type(), + ) + test_data = data.get_test() + dipole, numb_test, atype = run_test(dp, test_data, numb_test, data) + + sel_type = dp.get_sel_type() + sel_natoms = 0 + for ii in sel_type: + sel_natoms += sum(atype == ii) + + # do summation in atom dimension + if not atomic: + dipole = np.sum(dipole.reshape((dipole.shape[0], -1, 3)), axis=1) + rmse_f = rmse(dipole - test_data["dipole"][:numb_test]) + rmse_fs = rmse_f / np.sqrt(sel_natoms) + rmse_fa = rmse_f / sel_natoms + else: + rmse_f = rmse(dipole - test_data["atomic_dipole"][:numb_test]) + + log.info(f"# number of test data : {numb_test:d}") + log.info(f"Dipole RMSE : {rmse_f:e}") + if not atomic: + log.info(f"Dipole RMSE/sqrtN : {rmse_fs:e}") + log.info(f"Dipole RMSE/N : {rmse_fa:e}") + log.info("The unit of error is the same as the unit of provided label.") + + if detail_file is not None: + detail_path = Path(detail_file) + if not atomic: + pe = np.concatenate( + ( + np.reshape(test_data["dipole"][:numb_test], [-1, 3]), + np.reshape(dipole, [-1, 3]), + ), + axis=1, + ) + header_text = "data_x data_y data_z pred_x pred_y pred_z" + else: + pe = np.concatenate( + ( + np.reshape( + test_data["atomic_dipole"][:numb_test], [-1, 3 * sel_natoms] + ), + np.reshape(dipole, [-1, 3 * sel_natoms]), + ), + axis=1, + ) + header_text = [ + f"{letter}{number}" + for number in range(1, sel_natoms + 1) + for letter in ["data_x", "data_y", "data_z"] + ] + [ + f"{letter}{number}" + for number in range(1, sel_natoms + 1) + for letter in ["pred_x", "pred_y", "pred_z"] + ] + header_text = " ".join(header_text) + + np.savetxt( + detail_path.with_suffix(".out"), + pe, + header=header_text, + ) + return {"rmse": (rmse_f, dipole.size)} + + +def print_dipole_sys_avg(avg): + """Print errors summary for dipole type potential. + + Parameters + ---------- + avg : np.ndarray + array with summaries + """ + log.info(f"Dipole RMSE : {avg['rmse']:e} eV/A") diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index d73cd316cb..04bb2c2d7e 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -28,6 +28,9 @@ from deepmd.entrypoints.gui import ( start_dpgui, ) +from deepmd.entrypoints.test import ( + test, +) from deepmd.infer.model_devi import ( make_model_devi, ) @@ -286,20 +289,6 @@ def train(FLAGS): trainer.run() -def test(FLAGS): - trainer = inference.Tester( - FLAGS.model, - input_script=FLAGS.input_script, - system=FLAGS.system, - datafile=FLAGS.datafile, - numb_test=FLAGS.numb_test, - detail_file=FLAGS.detail_file, - shuffle_test=FLAGS.shuffle_test, - head=FLAGS.head, - ) - trainer.run() - - def freeze(FLAGS): model = torch.jit.script( inference.Tester(FLAGS.model, numb_test=1, head=FLAGS.head).model @@ -329,8 +318,8 @@ def main(args: Optional[Union[List[str], argparse.Namespace]] = None): if FLAGS.command == "train": train(FLAGS) elif FLAGS.command == "test": - FLAGS.output = str(Path(FLAGS.model).with_suffix(".pt")) - test(FLAGS) + dict_args["output"] = str(Path(FLAGS.model).with_suffix(".pt")) + test(**dict_args) elif FLAGS.command == "freeze": if Path(FLAGS.checkpoint_folder).is_dir(): checkpoint_path = Path(FLAGS.checkpoint_folder) diff --git a/deepmd/tf/entrypoints/test.py b/deepmd/tf/entrypoints/test.py index e1a2e3d46b..8b4ca64179 100644 --- a/deepmd/tf/entrypoints/test.py +++ b/deepmd/tf/entrypoints/test.py @@ -1,1054 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -"""Test trained DeePMD model.""" -import logging -from pathlib import ( - Path, -) -from typing import ( - TYPE_CHECKING, - Dict, - List, - Optional, - Tuple, -) - -import numpy as np - -from deepmd.infer.deep_dipole import ( - DeepDipole, -) -from deepmd.infer.deep_dos import ( - DeepDOS, -) -from deepmd.infer.deep_polar import ( - DeepGlobalPolar, - DeepPolar, -) -from deepmd.infer.deep_pot import ( - DeepPot, -) -from deepmd.infer.deep_wfc import ( - DeepWFC, -) -from deepmd.tf import ( - DeepPotential, -) -from deepmd.tf.common import ( - expand_sys_str, -) -from deepmd.tf.utils import random as dp_random -from deepmd.tf.utils.data import ( - DeepmdData, -) -from deepmd.tf.utils.weight_avg import ( - weighted_average, +from deepmd.entrypoints.test import ( + test, ) -if TYPE_CHECKING: - from deepmd.tf.infer import ( - DeepDipole, - DeepDOS, - DeepPolar, - DeepPot, - DeepWFC, - ) - from deepmd.tf.infer.deep_tensor import ( - DeepTensor, - ) - __all__ = ["test"] - -log = logging.getLogger(__name__) - - -def test( - *, - model: str, - system: str, - datafile: str, - set_prefix: str, - numb_test: int, - rand_seed: Optional[int], - shuffle_test: bool, - detail_file: str, - atomic: bool, - **kwargs, -): - """Test model predictions. - - Parameters - ---------- - model : str - path where model is stored - system : str - system directory - datafile : str - the path to the list of systems to test - set_prefix : str - string prefix of set - numb_test : int - munber of tests to do. 0 means all data. - rand_seed : Optional[int] - seed for random generator - shuffle_test : bool - whether to shuffle tests - detail_file : Optional[str] - file where test details will be output - atomic : bool - whether per atom quantities should be computed - **kwargs - additional arguments - - Raises - ------ - RuntimeError - if no valid system was found - """ - if numb_test == 0: - # only float has inf, but should work for min - numb_test = float("inf") - if datafile is not None: - datalist = open(datafile) - all_sys = datalist.read().splitlines() - datalist.close() - else: - all_sys = expand_sys_str(system) - - if len(all_sys) == 0: - raise RuntimeError("Did not find valid system") - err_coll = [] - siz_coll = [] - - # init random seed - if rand_seed is not None: - dp_random.seed(rand_seed % (2**32)) - - # init model - dp = DeepPotential(model) - - for cc, system in enumerate(all_sys): - log.info("# ---------------output of dp test--------------- ") - log.info(f"# testing system : {system}") - - # create data class - tmap = dp.get_type_map() if isinstance(dp, DeepPot) else None - data = DeepmdData( - system, - set_prefix, - shuffle_test=shuffle_test, - type_map=tmap, - sort_atoms=False, - ) - - if isinstance(dp, DeepPot): - err = test_ener( - dp, - data, - system, - numb_test, - detail_file, - atomic, - append_detail=(cc != 0), - ) - elif isinstance(dp, DeepDOS): - err = test_dos( - dp, - data, - system, - numb_test, - detail_file, - atomic, - append_detail=(cc != 0), - ) - elif isinstance(dp, DeepDipole): - err = test_dipole(dp, data, numb_test, detail_file, atomic) - elif isinstance(dp, DeepPolar): - err = test_polar(dp, data, numb_test, detail_file, atomic=atomic) - elif isinstance(dp, DeepGlobalPolar): # should not appear in this new version - log.warning( - "Global polar model is not currently supported. Please directly use the polar mode and change loss parameters." - ) - err = test_polar( - dp, data, numb_test, detail_file, atomic=False - ) # YWolfeee: downward compatibility - log.info("# ----------------------------------------------- ") - err_coll.append(err) - - avg_err = weighted_average(err_coll) - - if len(all_sys) != len(err_coll): - log.warning("Not all systems are tested! Check if the systems are valid") - - if len(all_sys) > 1: - log.info("# ----------weighted average of errors----------- ") - log.info(f"# number of systems : {len(all_sys)}") - if dp == "ener": - print_ener_sys_avg(avg_err) - elif dp == "dos": - print_dos_sys_avg(avg_err) - elif dp == "dipole": - print_dipole_sys_avg(avg_err) - elif dp == "polar": - print_polar_sys_avg(avg_err) - elif dp == "global_polar": - print_polar_sys_avg(avg_err) - elif dp == "wfc": - print_wfc_sys_avg(avg_err) - log.info("# ----------------------------------------------- ") - - -def mae(diff: np.ndarray) -> float: - """Calcalte mean absulote error. - - Parameters - ---------- - diff : np.ndarray - difference - - Returns - ------- - float - mean absulote error - """ - return np.mean(np.abs(diff)) - - -def rmse(diff: np.ndarray) -> float: - """Calculate root mean square error. - - Parameters - ---------- - diff : np.ndarray - difference - - Returns - ------- - float - root mean square error - """ - return np.sqrt(np.average(diff * diff)) - - -def save_txt_file( - fname: Path, data: np.ndarray, header: str = "", append: bool = False -): - """Save numpy array to test file. - - Parameters - ---------- - fname : str - filename - data : np.ndarray - data to save to disk - header : str, optional - header string to use in file, by default "" - append : bool, optional - if true file will be appended insted of overwriting, by default False - """ - flags = "ab" if append else "w" - with fname.open(flags) as fp: - np.savetxt(fp, data, header=header) - - -def test_ener( - dp: "DeepPot", - data: DeepmdData, - system: str, - numb_test: int, - detail_file: Optional[str], - has_atom_ener: bool, - append_detail: bool = False, -) -> Tuple[List[np.ndarray], List[int]]: - """Test energy type model. - - Parameters - ---------- - dp : DeepPot - instance of deep potential - data : DeepmdData - data container object - system : str - system directory - numb_test : int - munber of tests to do - detail_file : Optional[str] - file where test details will be output - has_atom_ener : bool - whether per atom quantities should be computed - append_detail : bool, optional - if true append output detail file, by default False - - Returns - ------- - Tuple[List[np.ndarray], List[int]] - arrays with results and their shapes - """ - data.add("energy", 1, atomic=False, must=False, high_prec=True) - data.add("force", 3, atomic=True, must=False, high_prec=False) - data.add("virial", 9, atomic=False, must=False, high_prec=False) - if dp.has_efield: - data.add("efield", 3, atomic=True, must=True, high_prec=False) - if has_atom_ener: - data.add("atom_ener", 1, atomic=True, must=True, high_prec=False) - if dp.get_dim_fparam() > 0: - data.add( - "fparam", dp.get_dim_fparam(), atomic=False, must=True, high_prec=False - ) - if dp.get_dim_aparam() > 0: - data.add("aparam", dp.get_dim_aparam(), atomic=True, must=True, high_prec=False) - - test_data = data.get_test() - mixed_type = data.mixed_type - natoms = len(test_data["type"][0]) - nframes = test_data["box"].shape[0] - numb_test = min(nframes, numb_test) - - coord = test_data["coord"][:numb_test].reshape([numb_test, -1]) - box = test_data["box"][:numb_test] - if dp.has_efield: - efield = test_data["efield"][:numb_test].reshape([numb_test, -1]) - else: - efield = None - if not data.pbc: - box = None - if mixed_type: - atype = test_data["type"][:numb_test].reshape([numb_test, -1]) - else: - atype = test_data["type"][0] - if dp.get_dim_fparam() > 0: - fparam = test_data["fparam"][:numb_test] - else: - fparam = None - if dp.get_dim_aparam() > 0: - aparam = test_data["aparam"][:numb_test] - else: - aparam = None - - ret = dp.eval( - coord, - box, - atype, - fparam=fparam, - aparam=aparam, - atomic=has_atom_ener, - efield=efield, - mixed_type=mixed_type, - ) - energy = ret[0] - force = ret[1] - virial = ret[2] - energy = energy.reshape([numb_test, 1]) - force = force.reshape([numb_test, -1]) - virial = virial.reshape([numb_test, 9]) - if has_atom_ener: - ae = ret[3] - av = ret[4] - ae = ae.reshape([numb_test, -1]) - av = av.reshape([numb_test, -1]) - if dp.get_ntypes_spin() != 0: - ntypes_real = dp.get_ntypes() - dp.get_ntypes_spin() - nloc = natoms - nloc_real = sum([np.count_nonzero(atype == ii) for ii in range(ntypes_real)]) - force_r = np.split( - force, indices_or_sections=[nloc_real * 3, nloc * 3], axis=1 - )[0] - force_m = np.split( - force, indices_or_sections=[nloc_real * 3, nloc * 3], axis=1 - )[1] - test_force_r = np.split( - test_data["force"][:numb_test], - indices_or_sections=[nloc_real * 3, nloc * 3], - axis=1, - )[0] - test_force_m = np.split( - test_data["force"][:numb_test], - indices_or_sections=[nloc_real * 3, nloc * 3], - axis=1, - )[1] - - diff_e = energy - test_data["energy"][:numb_test].reshape([-1, 1]) - mae_e = mae(diff_e) - rmse_e = rmse(diff_e) - diff_f = force - test_data["force"][:numb_test] - mae_f = mae(diff_f) - rmse_f = rmse(diff_f) - diff_v = virial - test_data["virial"][:numb_test] - mae_v = mae(diff_v) - rmse_v = rmse(diff_v) - mae_ea = mae_e / natoms - rmse_ea = rmse_e / natoms - mae_va = mae_v / natoms - rmse_va = rmse_v / natoms - if has_atom_ener: - diff_ae = test_data["atom_ener"][:numb_test].reshape([-1]) - ae.reshape([-1]) - mae_ae = mae(diff_ae) - rmse_ae = rmse(diff_ae) - if dp.get_ntypes_spin() != 0: - mae_fr = mae(force_r - test_force_r) - mae_fm = mae(force_m - test_force_m) - rmse_fr = rmse(force_r - test_force_r) - rmse_fm = rmse(force_m - test_force_m) - - log.info(f"# number of test data : {numb_test:d} ") - log.info(f"Energy MAE : {mae_e:e} eV") - log.info(f"Energy RMSE : {rmse_e:e} eV") - log.info(f"Energy MAE/Natoms : {mae_ea:e} eV") - log.info(f"Energy RMSE/Natoms : {rmse_ea:e} eV") - if dp.get_ntypes_spin() == 0: - log.info(f"Force MAE : {mae_f:e} eV/A") - log.info(f"Force RMSE : {rmse_f:e} eV/A") - else: - log.info(f"Force atom MAE : {mae_fr:e} eV/A") - log.info(f"Force spin MAE : {mae_fm:e} eV/uB") - log.info(f"Force atom RMSE : {rmse_fr:e} eV/A") - log.info(f"Force spin RMSE : {rmse_fm:e} eV/uB") - - if data.pbc: - log.info(f"Virial MAE : {mae_v:e} eV") - log.info(f"Virial RMSE : {rmse_v:e} eV") - log.info(f"Virial MAE/Natoms : {mae_va:e} eV") - log.info(f"Virial RMSE/Natoms : {rmse_va:e} eV") - if has_atom_ener: - log.info(f"Atomic ener MAE : {mae_ae:e} eV") - log.info(f"Atomic ener RMSE : {rmse_ae:e} eV") - - if detail_file is not None: - detail_path = Path(detail_file) - - pe = np.concatenate( - ( - np.reshape(test_data["energy"][:numb_test], [-1, 1]), - np.reshape(energy, [-1, 1]), - ), - axis=1, - ) - save_txt_file( - detail_path.with_suffix(".e.out"), - pe, - header="%s: data_e pred_e" % system, - append=append_detail, - ) - pe_atom = pe / natoms - save_txt_file( - detail_path.with_suffix(".e_peratom.out"), - pe_atom, - header="%s: data_e pred_e" % system, - append=append_detail, - ) - if dp.get_ntypes_spin() == 0: - pf = np.concatenate( - ( - np.reshape(test_data["force"][:numb_test], [-1, 3]), - np.reshape(force, [-1, 3]), - ), - axis=1, - ) - save_txt_file( - detail_path.with_suffix(".f.out"), - pf, - header="%s: data_fx data_fy data_fz pred_fx pred_fy pred_fz" % system, - append=append_detail, - ) - else: - pf_real = np.concatenate( - (np.reshape(test_force_r, [-1, 3]), np.reshape(force_r, [-1, 3])), - axis=1, - ) - pf_mag = np.concatenate( - (np.reshape(test_force_m, [-1, 3]), np.reshape(force_m, [-1, 3])), - axis=1, - ) - save_txt_file( - detail_path.with_suffix(".fr.out"), - pf_real, - header="%s: data_fx data_fy data_fz pred_fx pred_fy pred_fz" % system, - append=append_detail, - ) - save_txt_file( - detail_path.with_suffix(".fm.out"), - pf_mag, - header="%s: data_fmx data_fmy data_fmz pred_fmx pred_fmy pred_fmz" - % system, - append=append_detail, - ) - pv = np.concatenate( - ( - np.reshape(test_data["virial"][:numb_test], [-1, 9]), - np.reshape(virial, [-1, 9]), - ), - axis=1, - ) - save_txt_file( - detail_path.with_suffix(".v.out"), - pv, - header=f"{system}: data_vxx data_vxy data_vxz data_vyx data_vyy " - "data_vyz data_vzx data_vzy data_vzz pred_vxx pred_vxy pred_vxz pred_vyx " - "pred_vyy pred_vyz pred_vzx pred_vzy pred_vzz", - append=append_detail, - ) - pv_atom = pv / natoms - save_txt_file( - detail_path.with_suffix(".v_peratom.out"), - pv_atom, - header=f"{system}: data_vxx data_vxy data_vxz data_vyx data_vyy " - "data_vyz data_vzx data_vzy data_vzz pred_vxx pred_vxy pred_vxz pred_vyx " - "pred_vyy pred_vyz pred_vzx pred_vzy pred_vzz", - append=append_detail, - ) - if dp.get_ntypes_spin() == 0: - return { - "mae_e": (mae_e, energy.size), - "mae_ea": (mae_ea, energy.size), - "mae_f": (mae_f, force.size), - "mae_v": (mae_v, virial.size), - "mae_va": (mae_va, virial.size), - "rmse_e": (rmse_e, energy.size), - "rmse_ea": (rmse_ea, energy.size), - "rmse_f": (rmse_f, force.size), - "rmse_v": (rmse_v, virial.size), - "rmse_va": (rmse_va, virial.size), - } - else: - return { - "mae_e": (mae_e, energy.size), - "mae_ea": (mae_ea, energy.size), - "mae_fr": (mae_fr, force_r.size), - "mae_fm": (mae_fm, force_m.size), - "mae_v": (mae_v, virial.size), - "mae_va": (mae_va, virial.size), - "rmse_e": (rmse_e, energy.size), - "rmse_ea": (rmse_ea, energy.size), - "rmse_fr": (rmse_fr, force_r.size), - "rmse_fm": (rmse_fm, force_m.size), - "rmse_v": (rmse_v, virial.size), - "rmse_va": (rmse_va, virial.size), - } - - -def print_ener_sys_avg(avg: Dict[str, float]): - """Print errors summary for energy type potential. - - Parameters - ---------- - avg : np.ndarray - array with summaries - """ - log.info(f"Energy MAE : {avg['mae_e']:e} eV") - log.info(f"Energy RMSE : {avg['rmse_e']:e} eV") - log.info(f"Energy MAE/Natoms : {avg['mae_ea']:e} eV") - log.info(f"Energy RMSE/Natoms : {avg['rmse_ea']:e} eV") - if "rmse_f" in avg.keys(): - log.info(f"Force MAE : {avg['mae_f']:e} eV/A") - log.info(f"Force RMSE : {avg['rmse_f']:e} eV/A") - else: - log.info(f"Force atom MAE : {avg['mae_fr']:e} eV/A") - log.info(f"Force spin MAE : {avg['mae_fm']:e} eV/uB") - log.info(f"Force atom RMSE : {avg['rmse_fr']:e} eV/A") - log.info(f"Force spin RMSE : {avg['rmse_fm']:e} eV/uB") - log.info(f"Virial MAE : {avg['mae_v']:e} eV") - log.info(f"Virial RMSE : {avg['rmse_v']:e} eV") - log.info(f"Virial MAE/Natoms : {avg['mae_va']:e} eV") - log.info(f"Virial RMSE/Natoms : {avg['rmse_va']:e} eV") - - -def test_dos( - dp: "DeepDOS", - data: DeepmdData, - system: str, - numb_test: int, - detail_file: Optional[str], - has_atom_dos: bool, - append_detail: bool = False, -) -> Tuple[List[np.ndarray], List[int]]: - """Test DOS type model. - - Parameters - ---------- - dp : DeepDOS - instance of deep potential - data : DeepmdData - data container object - system : str - system directory - numb_test : int - munber of tests to do - detail_file : Optional[str] - file where test details will be output - has_atom_dos : bool - whether per atom quantities should be computed - append_detail : bool, optional - if true append output detail file, by default False - - Returns - ------- - Tuple[List[np.ndarray], List[int]] - arrays with results and their shapes - """ - data.add("dos", dp.numb_dos, atomic=False, must=True, high_prec=True) - if has_atom_dos: - data.add("atom_dos", dp.numb_dos, atomic=True, must=False, high_prec=True) - - if dp.get_dim_fparam() > 0: - data.add( - "fparam", dp.get_dim_fparam(), atomic=False, must=True, high_prec=False - ) - if dp.get_dim_aparam() > 0: - data.add("aparam", dp.get_dim_aparam(), atomic=True, must=True, high_prec=False) - - test_data = data.get_test() - mixed_type = data.mixed_type - natoms = len(test_data["type"][0]) - nframes = test_data["box"].shape[0] - numb_test = min(nframes, numb_test) - - coord = test_data["coord"][:numb_test].reshape([numb_test, -1]) - box = test_data["box"][:numb_test] - - if not data.pbc: - box = None - if mixed_type: - atype = test_data["type"][:numb_test].reshape([numb_test, -1]) - else: - atype = test_data["type"][0] - if dp.get_dim_fparam() > 0: - fparam = test_data["fparam"][:numb_test] - else: - fparam = None - if dp.get_dim_aparam() > 0: - aparam = test_data["aparam"][:numb_test] - else: - aparam = None - - ret = dp.eval( - coord, - box, - atype, - fparam=fparam, - aparam=aparam, - atomic=has_atom_dos, - mixed_type=mixed_type, - ) - dos = ret[0] - - dos = dos.reshape([numb_test, dp.numb_dos]) - - if has_atom_dos: - ados = ret[1] - ados = ados.reshape([numb_test, natoms * dp.numb_dos]) - - diff_dos = dos - test_data["dos"][:numb_test] - mae_dos = mae(diff_dos) - rmse_dos = rmse(diff_dos) - - mae_dosa = mae_dos / natoms - rmse_dosa = rmse_dos / natoms - - if has_atom_dos: - diff_ados = ados - test_data["atom_dos"][:numb_test] - mae_ados = mae(diff_ados) - rmse_ados = rmse(diff_ados) - - log.info(f"# number of test data : {numb_test:d} ") - - log.info(f"DOS MAE : {mae_dos:e} Occupation/eV") - log.info(f"DOS RMSE : {rmse_dos:e} Occupation/eV") - log.info(f"DOS MAE/Natoms : {mae_dosa:e} Occupation/eV") - log.info(f"DOS RMSE/Natoms : {rmse_dosa:e} Occupation/eV") - - if has_atom_dos: - log.info(f"Atomic DOS MAE : {mae_ados:e} Occupation/eV") - log.info(f"Atomic DOS RMSE : {rmse_ados:e} Occupation/eV") - - if detail_file is not None: - detail_path = Path(detail_file) - - for ii in range(numb_test): - test_out = test_data["dos"][ii].reshape(-1, 1) - pred_out = dos[ii].reshape(-1, 1) - - frame_output = np.hstack((test_out, pred_out)) - - save_txt_file( - detail_path.with_suffix(".dos.out.%.d" % ii), - frame_output, - header="%s - %.d: data_dos pred_dos" % (system, ii), - append=append_detail, - ) - - if has_atom_dos: - for ii in range(numb_test): - test_out = test_data["atom_dos"][ii].reshape(-1, 1) - pred_out = ados[ii].reshape(-1, 1) - - frame_output = np.hstack((test_out, pred_out)) - - save_txt_file( - detail_path.with_suffix(".ados.out.%.d" % ii), - frame_output, - header="%s - %.d: data_ados pred_ados" % (system, ii), - append=append_detail, - ) - - return { - "mae_dos": (mae_dos, dos.size), - "mae_dosa": (mae_dosa, dos.size), - "rmse_dos": (rmse_dos, dos.size), - "rmse_dosa": (rmse_dosa, dos.size), - } - - -def print_dos_sys_avg(avg: Dict[str, float]): - """Print errors summary for DOS type potential. - - Parameters - ---------- - avg : np.ndarray - array with summaries - """ - log.info(f"DOS MAE : {avg['mae_dos']:e} Occupation/eV") - log.info(f"DOS RMSE : {avg['rmse_dos']:e} Occupation/eV") - log.info(f"DOS MAE/Natoms : {avg['mae_dosa']:e} Occupation/eV") - log.info(f"DOS RMSE/Natoms : {avg['rmse_dosa']:e} Occupation/eV") - - -def run_test(dp: "DeepTensor", test_data: dict, numb_test: int, test_sys: DeepmdData): - """Run tests. - - Parameters - ---------- - dp : DeepTensor - instance of deep potential - test_data : dict - dictionary with test data - numb_test : int - munber of tests to do - test_sys : DeepmdData - test system - - Returns - ------- - [type] - [description] - """ - nframes = test_data["box"].shape[0] - numb_test = min(nframes, numb_test) - - coord = test_data["coord"][:numb_test].reshape([numb_test, -1]) - if test_sys.pbc: - box = test_data["box"][:numb_test] - else: - box = None - atype = test_data["type"][0] - prediction = dp.eval(coord, box, atype) - - return prediction.reshape([numb_test, -1]), numb_test, atype - - -def test_wfc( - dp: "DeepWFC", - data: DeepmdData, - numb_test: int, - detail_file: Optional[str], -) -> Tuple[List[np.ndarray], List[int]]: - """Test energy type model. - - Parameters - ---------- - dp : DeepPot - instance of deep potential - data : DeepmdData - data container object - numb_test : int - munber of tests to do - detail_file : Optional[str] - file where test details will be output - - Returns - ------- - Tuple[List[np.ndarray], List[int]] - arrays with results and their shapes - """ - data.add( - "wfc", 12, atomic=True, must=True, high_prec=False, type_sel=dp.get_sel_type() - ) - test_data = data.get_test() - wfc, numb_test, _ = run_test(dp, test_data, numb_test, data) - rmse_f = rmse(wfc - test_data["wfc"][:numb_test]) - - log.info("# number of test data : {numb_test:d} ") - log.info("WFC RMSE : {rmse_f:e} eV/A") - - if detail_file is not None: - detail_path = Path(detail_file) - pe = np.concatenate( - ( - np.reshape(test_data["wfc"][:numb_test], [-1, 12]), - np.reshape(wfc, [-1, 12]), - ), - axis=1, - ) - np.savetxt( - detail_path.with_suffix(".out"), - pe, - header="ref_wfc(12 dofs) predicted_wfc(12 dofs)", - ) - return {"rmse": (rmse_f, wfc.size)} - - -def print_wfc_sys_avg(avg): - """Print errors summary for wfc type potential. - - Parameters - ---------- - avg : np.ndarray - array with summaries - """ - log.info(f"WFC RMSE : {avg['rmse']:e} eV/A") - - -def test_polar( - dp: "DeepPolar", - data: DeepmdData, - numb_test: int, - detail_file: Optional[str], - *, - atomic: bool, -) -> Tuple[List[np.ndarray], List[int]]: - """Test energy type model. - - Parameters - ---------- - dp : DeepPot - instance of deep potential - data : DeepmdData - data container object - numb_test : int - munber of tests to do - detail_file : Optional[str] - file where test details will be output - atomic : bool - wheter to use glovbal version of polar potential - - Returns - ------- - Tuple[List[np.ndarray], List[int]] - arrays with results and their shapes - """ - data.add( - "polarizability" if not atomic else "atomic_polarizability", - 9, - atomic=atomic, - must=True, - high_prec=False, - type_sel=dp.get_sel_type(), - ) - - test_data = data.get_test() - polar, numb_test, atype = run_test(dp, test_data, numb_test, data) - - sel_type = dp.get_sel_type() - sel_natoms = 0 - for ii in sel_type: - sel_natoms += sum(atype == ii) - - # YWolfeee: do summation in global polar mode - if not atomic: - polar = np.sum(polar.reshape((polar.shape[0], -1, 9)), axis=1) - rmse_f = rmse(polar - test_data["polarizability"][:numb_test]) - rmse_fs = rmse_f / np.sqrt(sel_natoms) - rmse_fa = rmse_f / sel_natoms - else: - rmse_f = rmse(polar - test_data["atomic_polarizability"][:numb_test]) - - log.info(f"# number of test data : {numb_test:d} ") - log.info(f"Polarizability RMSE : {rmse_f:e}") - if not atomic: - log.info(f"Polarizability RMSE/sqrtN : {rmse_fs:e}") - log.info(f"Polarizability RMSE/N : {rmse_fa:e}") - log.info("The unit of error is the same as the unit of provided label.") - - if detail_file is not None: - detail_path = Path(detail_file) - - if not atomic: - pe = np.concatenate( - ( - np.reshape(test_data["polarizability"][:numb_test], [-1, 9]), - np.reshape(polar, [-1, 9]), - ), - axis=1, - ) - header_text = ( - "data_pxx data_pxy data_pxz data_pyx data_pyy data_pyz data_pzx " - "data_pzy data_pzz pred_pxx pred_pxy pred_pxz pred_pyx pred_pyy " - "pred_pyz pred_pzx pred_pzy pred_pzz" - ) - else: - pe = np.concatenate( - ( - np.reshape( - test_data["atomic_polarizability"][:numb_test], - [-1, 9 * sel_natoms], - ), - np.reshape(polar, [-1, 9 * sel_natoms]), - ), - axis=1, - ) - header_text = [ - f"{letter}{number}" - for number in range(1, sel_natoms + 1) - for letter in [ - "data_pxx", - "data_pxy", - "data_pxz", - "data_pyx", - "data_pyy", - "data_pyz", - "data_pzx", - "data_pzy", - "data_pzz", - ] - ] + [ - f"{letter}{number}" - for number in range(1, sel_natoms + 1) - for letter in [ - "pred_pxx", - "pred_pxy", - "pred_pxz", - "pred_pyx", - "pred_pyy", - "pred_pyz", - "pred_pzx", - "pred_pzy", - "pred_pzz", - ] - ] - header_text = " ".join(header_text) - - np.savetxt( - detail_path.with_suffix(".out"), - pe, - header=header_text, - ) - return {"rmse": (rmse_f, polar.size)} - - -def print_polar_sys_avg(avg): - """Print errors summary for polar type potential. - - Parameters - ---------- - avg : np.ndarray - array with summaries - """ - log.info(f"Polarizability RMSE : {avg['rmse']:e} eV/A") - - -def test_dipole( - dp: "DeepDipole", - data: DeepmdData, - numb_test: int, - detail_file: Optional[str], - atomic: bool, -) -> Tuple[List[np.ndarray], List[int]]: - """Test energy type model. - - Parameters - ---------- - dp : DeepPot - instance of deep potential - data : DeepmdData - data container object - numb_test : int - munber of tests to do - detail_file : Optional[str] - file where test details will be output - atomic : bool - whether atomic dipole is provided - - Returns - ------- - Tuple[List[np.ndarray], List[int]] - arrays with results and their shapes - """ - data.add( - "dipole" if not atomic else "atomic_dipole", - 3, - atomic=atomic, - must=True, - high_prec=False, - type_sel=dp.get_sel_type(), - ) - test_data = data.get_test() - dipole, numb_test, atype = run_test(dp, test_data, numb_test, data) - - sel_type = dp.get_sel_type() - sel_natoms = 0 - for ii in sel_type: - sel_natoms += sum(atype == ii) - - # do summation in atom dimension - if not atomic: - dipole = np.sum(dipole.reshape((dipole.shape[0], -1, 3)), axis=1) - rmse_f = rmse(dipole - test_data["dipole"][:numb_test]) - rmse_fs = rmse_f / np.sqrt(sel_natoms) - rmse_fa = rmse_f / sel_natoms - else: - rmse_f = rmse(dipole - test_data["atomic_dipole"][:numb_test]) - - log.info(f"# number of test data : {numb_test:d}") - log.info(f"Dipole RMSE : {rmse_f:e}") - if not atomic: - log.info(f"Dipole RMSE/sqrtN : {rmse_fs:e}") - log.info(f"Dipole RMSE/N : {rmse_fa:e}") - log.info("The unit of error is the same as the unit of provided label.") - - if detail_file is not None: - detail_path = Path(detail_file) - if not atomic: - pe = np.concatenate( - ( - np.reshape(test_data["dipole"][:numb_test], [-1, 3]), - np.reshape(dipole, [-1, 3]), - ), - axis=1, - ) - header_text = "data_x data_y data_z pred_x pred_y pred_z" - else: - pe = np.concatenate( - ( - np.reshape( - test_data["atomic_dipole"][:numb_test], [-1, 3 * sel_natoms] - ), - np.reshape(dipole, [-1, 3 * sel_natoms]), - ), - axis=1, - ) - header_text = [ - f"{letter}{number}" - for number in range(1, sel_natoms + 1) - for letter in ["data_x", "data_y", "data_z"] - ] + [ - f"{letter}{number}" - for number in range(1, sel_natoms + 1) - for letter in ["pred_x", "pred_y", "pred_z"] - ] - header_text = " ".join(header_text) - - np.savetxt( - detail_path.with_suffix(".out"), - pe, - header=header_text, - ) - return {"rmse": (rmse_f, dipole.size)} - - -def print_dipole_sys_avg(avg): - """Print errors summary for dipole type potential. - - Parameters - ---------- - avg : np.ndarray - array with summaries - """ - log.info(f"Dipole RMSE : {avg['rmse']:e} eV/A") From d1d78813210346945b46732f39eb4895c9fb0622 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 9 Feb 2024 09:05:18 -0500 Subject: [PATCH 067/686] pt: infer model type from ModelOutputDef (#3250) Signed-off-by: Jinzhe Zeng --- deepmd/pt/infer/deep_eval.py | 33 ++++++++++++++++++++++++++--- deepmd/pt/model/model/make_model.py | 1 + 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index b42bee1dbe..cb602dd172 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -18,12 +18,26 @@ OutputVariableCategory, OutputVariableDef, ) +from deepmd.infer.deep_dipole import ( + DeepDipole, +) +from deepmd.infer.deep_dos import ( + DeepDOS, +) +from deepmd.infer.deep_eval import DeepEval as DeepEvalWrapper from deepmd.infer.deep_eval import ( DeepEvalBackend, ) +from deepmd.infer.deep_polar import ( + DeepGlobalPolar, + DeepPolar, +) from deepmd.infer.deep_pot import ( DeepPot, ) +from deepmd.infer.deep_wfc import ( + DeepWFC, +) from deepmd.pt.model.model import ( get_model, ) @@ -44,8 +58,6 @@ if TYPE_CHECKING: import ase.neighborlist - from deepmd.infer.deep_eval import DeepEval as DeepEvalWrapper - class DeepEval(DeepEvalBackend): """PyTorch backend implementaion of DeepEval. @@ -127,7 +139,22 @@ def get_dim_aparam(self) -> int: @property def model_type(self) -> "DeepEvalWrapper": """The the evaluator of the model type.""" - return DeepPot + output_def = self.dp.model["Default"].model_output_def() + var_defs = output_def.var_defs + if "energy" in var_defs: + return DeepPot + elif "dos" in var_defs: + return DeepDOS + elif "dipole" in var_defs: + return DeepDipole + elif "polar" in var_defs: + return DeepPolar + elif "global_polar" in var_defs: + return DeepGlobalPolar + elif "wfc" in var_defs: + return DeepWFC + else: + raise RuntimeError("Unknown model type") def get_sel_type(self) -> List[int]: """Get the selected atom types of this model. diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 1e76c6a468..8a863b8cdc 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -57,6 +57,7 @@ def __init__( **kwargs, ) + @torch.jit.export def model_output_def(self): """Get the output def for the model.""" return ModelOutputDef(self.fitting_output_def()) From 0e2304fe9ab6e904d5842d8f77320b6db57fbdff Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Sat, 10 Feb 2024 08:29:04 +0800 Subject: [PATCH 068/686] breaking: pt: remove data stat from model init (#3245) Restore #3233 with resolved conflicts and conversations. This PR clean up the data stat process from model init. Please note that this code PR is just an initial cleanup and refinement of the data stat, and a more detailed design of the data stat will be completed in the next PR: - independent data stat from dataloader - data stat support for hybrid descriptors --------- Signed-off-by: Duo <50307526+iProzd@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../descriptor/make_base_descriptor.py | 8 +- deepmd/dpmodel/fitting/invar_fitting.py | 8 + deepmd/dpmodel/fitting/make_base_fitting.py | 8 + deepmd/pt/entrypoints/main.py | 75 ++++----- deepmd/pt/infer/deep_eval.py | 2 +- deepmd/pt/model/descriptor/descriptor.py | 150 +++++++++++++++-- deepmd/pt/model/descriptor/dpa1.py | 33 +++- deepmd/pt/model/descriptor/dpa2.py | 90 ++++++++--- deepmd/pt/model/descriptor/gaussian_lcc.py | 2 +- deepmd/pt/model/descriptor/hybrid.py | 40 +++-- deepmd/pt/model/descriptor/repformers.py | 10 +- deepmd/pt/model/descriptor/se_a.py | 44 ++++- deepmd/pt/model/descriptor/se_atten.py | 10 +- deepmd/pt/model/model/__init__.py | 19 +-- deepmd/pt/model/model/dp_atomic_model.py | 93 ++++++----- deepmd/pt/model/model/model.py | 152 +++--------------- deepmd/pt/model/task/ener.py | 52 +++++- deepmd/pt/model/task/fitting.py | 104 ++++++++++++ deepmd/pt/train/training.py | 21 ++- deepmd/pt/utils/stat.py | 34 +++- deepmd/pt/utils/utils.py | 9 ++ source/tests/pt/model/test_autodiff.py | 25 +-- source/tests/pt/model/test_dp_atomic_model.py | 6 +- source/tests/pt/model/test_dp_model.py | 21 +-- source/tests/pt/model/test_ener_fitting.py | 4 +- source/tests/pt/model/test_force_grad.py | 19 +-- .../pt/model/test_linear_atomic_model.py | 8 +- source/tests/pt/model/test_model.py | 7 - source/tests/pt/model/test_permutation.py | 49 +----- .../pt/model/test_permutation_denoise.py | 12 +- source/tests/pt/model/test_rot.py | 22 +-- source/tests/pt/model/test_rot_denoise.py | 10 +- source/tests/pt/model/test_rotation.py | 19 +-- source/tests/pt/model/test_saveload_dpa1.py | 3 +- .../tests/pt/model/test_saveload_se_e2_a.py | 3 +- source/tests/pt/model/test_smooth.py | 25 +-- source/tests/pt/model/test_smooth_denoise.py | 10 +- source/tests/pt/model/test_trans.py | 22 +-- source/tests/pt/model/test_trans_denoise.py | 10 +- source/tests/pt/model/test_unused_params.py | 4 +- source/tests/pt/test_stat.py | 8 +- 41 files changed, 725 insertions(+), 526 deletions(-) diff --git a/deepmd/dpmodel/descriptor/make_base_descriptor.py b/deepmd/dpmodel/descriptor/make_base_descriptor.py index 2b0025af07..29d3ad6d92 100644 --- a/deepmd/dpmodel/descriptor/make_base_descriptor.py +++ b/deepmd/dpmodel/descriptor/make_base_descriptor.py @@ -69,15 +69,13 @@ def distinguish_types(self) -> bool: """ pass - @abstractmethod def compute_input_stats(self, merged): """Update mean and stddev for descriptor elements.""" - pass + raise NotImplementedError - @abstractmethod - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + def init_desc_stat(self, **kwargs): """Initialize the model bias by the statistics.""" - pass + raise NotImplementedError @abstractmethod def fwd( diff --git a/deepmd/dpmodel/fitting/invar_fitting.py b/deepmd/dpmodel/fitting/invar_fitting.py index 820f422ef0..58607a9f26 100644 --- a/deepmd/dpmodel/fitting/invar_fitting.py +++ b/deepmd/dpmodel/fitting/invar_fitting.py @@ -236,6 +236,14 @@ def __getitem__(self, key): else: raise KeyError(key) + def compute_output_stats(self, merged): + """Update the output bias for fitting net.""" + raise NotImplementedError + + def init_fitting_stat(self, result_dict): + """Initialize the model bias by the statistics.""" + raise NotImplementedError + def serialize(self) -> dict: """Serialize the fitting to dict.""" return { diff --git a/deepmd/dpmodel/fitting/make_base_fitting.py b/deepmd/dpmodel/fitting/make_base_fitting.py index 719ac6169e..620ff316f1 100644 --- a/deepmd/dpmodel/fitting/make_base_fitting.py +++ b/deepmd/dpmodel/fitting/make_base_fitting.py @@ -52,6 +52,14 @@ def fwd( """Calculate fitting.""" pass + def compute_output_stats(self, merged): + """Update the output bias for fitting net.""" + raise NotImplementedError + + def init_fitting_stat(self, **kwargs): + """Initialize the model bias by the statistics.""" + raise NotImplementedError + @abstractmethod def serialize(self) -> dict: """Serialize the obj to dict.""" diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 04bb2c2d7e..702fc6f317 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -46,6 +46,9 @@ from deepmd.pt.model.descriptor import ( Descriptor, ) +from deepmd.pt.model.task import ( + Fitting, +) from deepmd.pt.train import ( training, ) @@ -63,6 +66,7 @@ ) from deepmd.pt.utils.stat import ( make_stat_input, + process_stat_path, ) from deepmd.utils.summary import SummaryPrinter as BaseSummaryPrinter @@ -128,51 +132,18 @@ def prepare_trainer_input_single( # stat files hybrid_descrpt = model_params_single["descriptor"]["type"] == "hybrid" - has_stat_file_path = True if not hybrid_descrpt: - ### this design requires "rcut", "rcut_smth" and "sel" in the descriptor - ### VERY BAD DESIGN!!!! - ### not all descriptors provides these parameter in their constructor - default_stat_file_name = Descriptor.get_stat_name( - model_params_single["descriptor"] - ) - model_params_single["stat_file_dir"] = data_dict_single.get( - "stat_file_dir", f"stat_files{suffix}" - ) - model_params_single["stat_file"] = data_dict_single.get( - "stat_file", default_stat_file_name - ) - model_params_single["stat_file_path"] = os.path.join( - model_params_single["stat_file_dir"], model_params_single["stat_file"] - ) - if not os.path.exists(model_params_single["stat_file_path"]): - has_stat_file_path = False - else: ### need to remove this - default_stat_file_name = [] - for descrpt in model_params_single["descriptor"]["list"]: - default_stat_file_name.append( - f'stat_file_rcut{descrpt["rcut"]:.2f}_' - f'smth{descrpt["rcut_smth"]:.2f}_' - f'sel{descrpt["sel"]}_{descrpt["type"]}.npz' - ) - model_params_single["stat_file_dir"] = data_dict_single.get( - "stat_file_dir", f"stat_files{suffix}" + stat_file_path_single, has_stat_file_path = process_stat_path( + data_dict_single.get("stat_file", None), + data_dict_single.get("stat_file_dir", f"stat_files{suffix}"), + model_params_single, + Descriptor, + Fitting, ) - model_params_single["stat_file"] = data_dict_single.get( - "stat_file", default_stat_file_name + else: ### TODO hybrid descriptor not implemented + raise NotImplementedError( + "data stat for hybrid descriptor is not implemented!" ) - assert isinstance( - model_params_single["stat_file"], list - ), "Stat file of hybrid descriptor must be a list!" - stat_file_path = [] - for stat_file_path_item in model_params_single["stat_file"]: - single_file_path = os.path.join( - model_params_single["stat_file_dir"], stat_file_path_item - ) - stat_file_path.append(single_file_path) - if not os.path.exists(single_file_path): - has_stat_file_path = False - model_params_single["stat_file_path"] = stat_file_path # validation and training data validation_data_single = DpLoaderSet( @@ -212,19 +183,30 @@ def prepare_trainer_input_single( type_split=type_split, noise_settings=noise_settings, ) - return train_data_single, validation_data_single, sampled_single + return ( + train_data_single, + validation_data_single, + sampled_single, + stat_file_path_single, + ) if not multi_task: - train_data, validation_data, sampled = prepare_trainer_input_single( + ( + train_data, + validation_data, + sampled, + stat_file_path, + ) = prepare_trainer_input_single( config["model"], config["training"], config["loss"] ) else: - train_data, validation_data, sampled = {}, {}, {} + train_data, validation_data, sampled, stat_file_path = {}, {}, {}, {} for model_key in config["model"]["model_dict"]: ( train_data[model_key], validation_data[model_key], sampled[model_key], + stat_file_path[model_key], ) = prepare_trainer_input_single( config["model"]["model_dict"][model_key], config["training"]["data_dict"][model_key], @@ -235,7 +217,8 @@ def prepare_trainer_input_single( trainer = training.Trainer( config, train_data, - sampled, + sampled=sampled, + stat_file_path=stat_file_path, validation_data=validation_data, init_model=init_model, restart_model=restart_model, diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index cb602dd172..894d307d04 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -100,7 +100,7 @@ def __init__( assert not self.multi_task, "multitask mode currently not supported!" self.type_split = self.input_param["descriptor"]["type"] in ["se_e2_a"] self.type_map = self.input_param["type_map"] - self.dp = ModelWrapper(get_model(self.input_param, None).to(DEVICE)) + self.dp = ModelWrapper(get_model(self.input_param).to(DEVICE)) self.dp.load_state_dict(state_dict) self.rcut = self.dp.model["Default"].descriptor.get_rcut() self.sec = np.cumsum(self.dp.model["Default"].descriptor.get_sel()) diff --git a/deepmd/pt/model/descriptor/descriptor.py b/deepmd/pt/model/descriptor/descriptor.py index b4e866bb11..177f30d241 100644 --- a/deepmd/pt/model/descriptor/descriptor.py +++ b/deepmd/pt/model/descriptor/descriptor.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import logging from abc import ( ABC, abstractmethod, @@ -7,6 +8,7 @@ Callable, List, Optional, + Union, ) import numpy as np @@ -23,6 +25,8 @@ BaseDescriptor, ) +log = logging.getLogger(__name__) + class Descriptor(torch.nn.Module, BaseDescriptor): """The descriptor. @@ -56,15 +60,130 @@ class SomeDescript(Descriptor): return Descriptor.__plugins.register(key) @classmethod - def get_stat_name(cls, config): - descrpt_type = config["type"] - return Descriptor.__plugins.plugins[descrpt_type].get_stat_name(config) + def get_stat_name(cls, ntypes, type_name, **kwargs): + """ + Get the name for the statistic file of the descriptor. + Usually use the combination of descriptor name, rcut, rcut_smth and sel as the statistic file name. + """ + if cls is not Descriptor: + raise NotImplementedError("get_stat_name is not implemented!") + descrpt_type = type_name + return Descriptor.__plugins.plugins[descrpt_type].get_stat_name( + ntypes, type_name, **kwargs + ) @classmethod def get_data_process_key(cls, config): + """ + Get the keys for the data preprocess. + Usually need the information of rcut and sel. + TODO Need to be deprecated when the dataloader has been cleaned up. + """ + if cls is not Descriptor: + raise NotImplementedError("get_data_process_key is not implemented!") descrpt_type = config["type"] return Descriptor.__plugins.plugins[descrpt_type].get_data_process_key(config) + @property + def data_stat_key(self): + """ + Get the keys for the data statistic of the descriptor. + Return a list of statistic names needed, such as "sumr", "suma" or "sumn". + """ + raise NotImplementedError("data_stat_key is not implemented!") + + def compute_or_load_stat( + self, + type_map: List[str], + sampled=None, + stat_file_path: Optional[Union[str, List[str]]] = None, + ): + """ + Compute or load the statistics parameters of the descriptor. + Calculate and save the mean and standard deviation of the descriptor to `stat_file_path` + if `sampled` is not None, otherwise load them from `stat_file_path`. + + Parameters + ---------- + type_map + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. + sampled + The sampled data frames from different data systems. + stat_file_path + The path to the statistics files. + """ + # TODO support hybrid descriptor + descrpt_stat_key = self.data_stat_key + if sampled is not None: # compute the statistics results + tmp_dict = self.compute_input_stats(sampled) + result_dict = {key: tmp_dict[key] for key in descrpt_stat_key} + result_dict["type_map"] = type_map + if stat_file_path is not None: + self.save_stats(result_dict, stat_file_path) + else: # load the statistics results + assert stat_file_path is not None, "No stat file to load!" + result_dict = self.load_stats(type_map, stat_file_path) + self.init_desc_stat(**result_dict) + + def save_stats(self, result_dict, stat_file_path: Union[str, List[str]]): + """ + Save the statistics results to `stat_file_path`. + + Parameters + ---------- + result_dict + The dictionary of statistics results. + stat_file_path + The path to the statistics file(s). + """ + if not isinstance(stat_file_path, list): + log.info(f"Saving stat file to {stat_file_path}") + np.savez_compressed(stat_file_path, **result_dict) + else: # TODO hybrid descriptor not implemented + raise NotImplementedError( + "save_stats for hybrid descriptor is not implemented!" + ) + + def load_stats(self, type_map, stat_file_path: Union[str, List[str]]): + """ + Load the statistics results to `stat_file_path`. + + Parameters + ---------- + type_map + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. + stat_file_path + The path to the statistics file(s). + + Returns + ------- + result_dict + The dictionary of statistics results. + """ + descrpt_stat_key = self.data_stat_key + target_type_map = type_map + if not isinstance(stat_file_path, list): + log.info(f"Loading stat file from {stat_file_path}") + stats = np.load(stat_file_path) + stat_type_map = list(stats["type_map"]) + missing_type = [i for i in target_type_map if i not in stat_type_map] + assert not missing_type, ( + f"These type are not in stat file {stat_file_path}: {missing_type}! " + f"Please change the stat file path!" + ) + idx_map = [stat_type_map.index(i) for i in target_type_map] + if stats[descrpt_stat_key[0]].size: # not empty + result_dict = {key: stats[key][idx_map] for key in descrpt_stat_key} + else: + result_dict = {key: [] for key in descrpt_stat_key} + else: # TODO hybrid descriptor not implemented + raise NotImplementedError( + "load_stats for hybrid descriptor is not implemented!" + ) + return result_dict + def __new__(cls, *args, **kwargs): if cls is Descriptor: try: @@ -156,15 +275,13 @@ def get_dim_emb(self) -> int: """Returns the embedding dimension.""" pass - @abstractmethod def compute_input_stats(self, merged): """Update mean and stddev for DescriptorBlock elements.""" - pass + raise NotImplementedError - @abstractmethod - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): - """Initialize the model bias by the statistics.""" - pass + def init_desc_stat(self, **kwargs): + """Initialize mean and stddev by the statistics.""" + raise NotImplementedError def share_params(self, base_class, shared_level, resume=False): assert ( @@ -188,13 +305,14 @@ def share_params(self, base_class, shared_level, resume=False): self.sumr2, self.suma2, ) - base_class.init_desc_stat( - sumr_base + sumr, - suma_base + suma, - sumn_base + sumn, - sumr2_base + sumr2, - suma2_base + suma2, - ) + stat_dict = { + "sumr": sumr_base + sumr, + "suma": suma_base + suma, + "sumn": sumn_base + sumn, + "sumr2": sumr2_base + sumr2, + "suma2": suma2_base + suma2, + } + base_class.init_desc_stat(**stat_dict) self.mean = base_class.mean self.stddev = base_class.stddev # self.load_state_dict(base_class.state_dict()) # this does not work, because it only inits the model diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index 914c37ed51..6c1331ec1d 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import logging from typing import ( List, Optional, @@ -17,6 +18,8 @@ DescrptBlockSeAtten, ) +log = logging.getLogger(__name__) + @Descriptor.register("dpa1") @Descriptor.register("se_atten") @@ -122,21 +125,43 @@ def dim_emb(self): def compute_input_stats(self, merged): return self.se_atten.compute_input_stats(merged) - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + def init_desc_stat( + self, sumr=None, suma=None, sumn=None, sumr2=None, suma2=None, **kwargs + ): + assert True not in [x is None for x in [sumr, suma, sumn, sumr2, suma2]] self.se_atten.init_desc_stat(sumr, suma, sumn, sumr2, suma2) @classmethod - def get_stat_name(cls, config): - descrpt_type = config["type"] + def get_stat_name( + cls, ntypes, type_name, rcut=None, rcut_smth=None, sel=None, **kwargs + ): + """ + Get the name for the statistic file of the descriptor. + Usually use the combination of descriptor name, rcut, rcut_smth and sel as the statistic file name. + """ + descrpt_type = type_name assert descrpt_type in ["dpa1", "se_atten"] - return f'stat_file_dpa1_rcut{config["rcut"]:.2f}_smth{config["rcut_smth"]:.2f}_sel{config["sel"]}.npz' + return f"stat_file_descrpt_dpa1_rcut{rcut:.2f}_smth{rcut_smth:.2f}_sel{sel}_ntypes{ntypes}.npz" @classmethod def get_data_process_key(cls, config): + """ + Get the keys for the data preprocess. + Usually need the information of rcut and sel. + TODO Need to be deprecated when the dataloader has been cleaned up. + """ descrpt_type = config["type"] assert descrpt_type in ["dpa1", "se_atten"] return {"sel": config["sel"], "rcut": config["rcut"]} + @property + def data_stat_key(self): + """ + Get the keys for the data statistic of the descriptor. + Return a list of statistic names needed, such as "sumr", "suma" or "sumn". + """ + return ["sumr", "suma", "sumn", "sumr2", "suma2"] + def serialize(self) -> dict: """Serialize the obj to dict.""" raise NotImplementedError diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index b40e466ed4..05e7cec658 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import logging from typing import ( List, Optional, @@ -26,6 +27,8 @@ DescrptBlockSeAtten, ) +log = logging.getLogger(__name__) + @Descriptor.register("dpa2") class DescrptDPA2(Descriptor): @@ -296,35 +299,76 @@ def compute_input_stats(self, merged): } for item in merged ] - ( - sumr_tmp, - suma_tmp, - sumn_tmp, - sumr2_tmp, - suma2_tmp, - ) = descrpt.compute_input_stats(merged_tmp) - sumr.append(sumr_tmp) - suma.append(suma_tmp) - sumn.append(sumn_tmp) - sumr2.append(sumr2_tmp) - suma2.append(suma2_tmp) - return sumr, suma, sumn, sumr2, suma2 + tmp_stat_dict = descrpt.compute_input_stats(merged_tmp) + sumr.append(tmp_stat_dict["sumr"]) + suma.append(tmp_stat_dict["suma"]) + sumn.append(tmp_stat_dict["sumn"]) + sumr2.append(tmp_stat_dict["sumr2"]) + suma2.append(tmp_stat_dict["suma2"]) + return { + "sumr": sumr, + "suma": suma, + "sumn": sumn, + "sumr2": sumr2, + "suma2": suma2, + } - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + def init_desc_stat( + self, sumr=None, suma=None, sumn=None, sumr2=None, suma2=None, **kwargs + ): + assert True not in [x is None for x in [sumr, suma, sumn, sumr2, suma2]] for ii, descrpt in enumerate([self.repinit, self.repformers]): - descrpt.init_desc_stat(sumr[ii], suma[ii], sumn[ii], sumr2[ii], suma2[ii]) + stat_dict_ii = { + "sumr": sumr[ii], + "suma": suma[ii], + "sumn": sumn[ii], + "sumr2": sumr2[ii], + "suma2": suma2[ii], + } + descrpt.init_desc_stat(**stat_dict_ii) @classmethod - def get_stat_name(cls, config): - descrpt_type = config["type"] + def get_stat_name( + cls, + ntypes, + type_name, + repinit_rcut=None, + repinit_rcut_smth=None, + repinit_nsel=None, + repformer_rcut=None, + repformer_rcut_smth=None, + repformer_nsel=None, + **kwargs, + ): + """ + Get the name for the statistic file of the descriptor. + Usually use the combination of descriptor name, rcut, rcut_smth and sel as the statistic file name. + """ + descrpt_type = type_name assert descrpt_type in ["dpa2"] + assert True not in [ + x is None + for x in [ + repinit_rcut, + repinit_rcut_smth, + repinit_nsel, + repformer_rcut, + repformer_rcut_smth, + repformer_nsel, + ] + ] return ( - f'stat_file_dpa2_repinit_rcut{config["repinit_rcut"]:.2f}_smth{config["repinit_rcut_smth"]:.2f}_sel{config["repinit_nsel"]}' - f'_repformer_rcut{config["repformer_rcut"]:.2f}_smth{config["repformer_rcut_smth"]:.2f}_sel{config["repformer_nsel"]}.npz' + f"stat_file_descrpt_dpa2_repinit_rcut{repinit_rcut:.2f}_smth{repinit_rcut_smth:.2f}_sel{repinit_nsel}" + f"_repformer_rcut{repformer_rcut:.2f}_smth{repformer_rcut_smth:.2f}_sel{repformer_nsel}_ntypes{ntypes}.npz" ) @classmethod def get_data_process_key(cls, config): + """ + Get the keys for the data preprocess. + Usually need the information of rcut and sel. + TODO Need to be deprecated when the dataloader has been cleaned up. + """ descrpt_type = config["type"] assert descrpt_type in ["dpa2"] return { @@ -332,6 +376,14 @@ def get_data_process_key(cls, config): "rcut": [config["repinit_rcut"], config["repformer_rcut"]], } + @property + def data_stat_key(self): + """ + Get the keys for the data statistic of the descriptor. + Return a list of statistic names needed, such as "sumr", "suma" or "sumn". + """ + return ["sumr", "suma", "sumn", "sumr2", "suma2"] + def serialize(self) -> dict: """Serialize the obj to dict.""" raise NotImplementedError diff --git a/deepmd/pt/model/descriptor/gaussian_lcc.py b/deepmd/pt/model/descriptor/gaussian_lcc.py index 26ec1175b8..0972b90279 100644 --- a/deepmd/pt/model/descriptor/gaussian_lcc.py +++ b/deepmd/pt/model/descriptor/gaussian_lcc.py @@ -158,7 +158,7 @@ def compute_input_stats(self, merged): """Update mean and stddev for descriptor elements.""" return [], [], [], [], [] - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2, **kwargs): pass def forward( diff --git a/deepmd/pt/model/descriptor/hybrid.py b/deepmd/pt/model/descriptor/hybrid.py index 0698992659..fb7e374ede 100644 --- a/deepmd/pt/model/descriptor/hybrid.py +++ b/deepmd/pt/model/descriptor/hybrid.py @@ -153,23 +153,33 @@ def compute_input_stats(self, merged): } for item in merged ] - ( - sumr_tmp, - suma_tmp, - sumn_tmp, - sumr2_tmp, - suma2_tmp, - ) = descrpt.compute_input_stats(merged_tmp) - sumr.append(sumr_tmp) - suma.append(suma_tmp) - sumn.append(sumn_tmp) - sumr2.append(sumr2_tmp) - suma2.append(suma2_tmp) - return sumr, suma, sumn, sumr2, suma2 + tmp_stat_dict = descrpt.compute_input_stats(merged_tmp) + sumr.append(tmp_stat_dict["sumr"]) + suma.append(tmp_stat_dict["suma"]) + sumn.append(tmp_stat_dict["sumn"]) + sumr2.append(tmp_stat_dict["sumr2"]) + suma2.append(tmp_stat_dict["suma2"]) + return { + "sumr": sumr, + "suma": suma, + "sumn": sumn, + "sumr2": sumr2, + "suma2": suma2, + } - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + def init_desc_stat( + self, sumr=None, suma=None, sumn=None, sumr2=None, suma2=None, **kwargs + ): + assert True not in [x is None for x in [sumr, suma, sumn, sumr2, suma2]] for ii, descrpt in enumerate(self.descriptor_list): - descrpt.init_desc_stat(sumr[ii], suma[ii], sumn[ii], sumr2[ii], suma2[ii]) + stat_dict_ii = { + "sumr": sumr[ii], + "suma": suma[ii], + "sumn": sumn[ii], + "sumr2": sumr2[ii], + "suma2": suma2[ii], + } + descrpt.init_desc_stat(**stat_dict_ii) def forward( self, diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py index 853962de69..0a302b6f92 100644 --- a/deepmd/pt/model/descriptor/repformers.py +++ b/deepmd/pt/model/descriptor/repformers.py @@ -321,9 +321,15 @@ def compute_input_stats(self, merged): sumn = np.sum(sumn, axis=0) sumr2 = np.sum(sumr2, axis=0) suma2 = np.sum(suma2, axis=0) - return sumr, suma, sumn, sumr2, suma2 + return { + "sumr": sumr, + "suma": suma, + "sumn": sumn, + "sumr2": sumr2, + "suma2": suma2, + } - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2, **kwargs): all_davg = [] all_dstd = [] for type_i in range(self.ntypes): diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 23b78dcf34..82e7e5185a 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import logging from typing import ( ClassVar, List, @@ -37,6 +38,8 @@ TypeFilter, ) +log = logging.getLogger(__name__) + @Descriptor.register("se_e2_a") class DescrptSeA(Descriptor): @@ -108,21 +111,44 @@ def compute_input_stats(self, merged): """Update mean and stddev for descriptor elements.""" return self.sea.compute_input_stats(merged) - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + def init_desc_stat( + self, sumr=None, suma=None, sumn=None, sumr2=None, suma2=None, **kwargs + ): + assert True not in [x is None for x in [sumr, suma, sumn, sumr2, suma2]] self.sea.init_desc_stat(sumr, suma, sumn, sumr2, suma2) @classmethod - def get_stat_name(cls, config): - descrpt_type = config["type"] + def get_stat_name( + cls, ntypes, type_name, rcut=None, rcut_smth=None, sel=None, **kwargs + ): + """ + Get the name for the statistic file of the descriptor. + Usually use the combination of descriptor name, rcut, rcut_smth and sel as the statistic file name. + """ + descrpt_type = type_name assert descrpt_type in ["se_e2_a"] - return f'stat_file_sea_rcut{config["rcut"]:.2f}_smth{config["rcut_smth"]:.2f}_sel{config["sel"]}.npz' + assert True not in [x is None for x in [rcut, rcut_smth, sel]] + return f"stat_file_descrpt_sea_rcut{rcut:.2f}_smth{rcut_smth:.2f}_sel{sel}_ntypes{ntypes}.npz" @classmethod def get_data_process_key(cls, config): + """ + Get the keys for the data preprocess. + Usually need the information of rcut and sel. + TODO Need to be deprecated when the dataloader has been cleaned up. + """ descrpt_type = config["type"] assert descrpt_type in ["se_e2_a"] return {"sel": config["sel"], "rcut": config["rcut"]} + @property + def data_stat_key(self): + """ + Get the keys for the data statistic of the descriptor. + Return a list of statistic names needed, such as "sumr", "suma" or "sumn". + """ + return ["sumr", "suma", "sumn", "sumr2", "suma2"] + def forward( self, coord_ext: torch.Tensor, @@ -380,9 +406,15 @@ def compute_input_stats(self, merged): sumn = np.sum(sumn, axis=0) sumr2 = np.sum(sumr2, axis=0) suma2 = np.sum(suma2, axis=0) - return sumr, suma, sumn, sumr2, suma2 + return { + "sumr": sumr, + "suma": suma, + "sumn": sumn, + "sumr2": sumr2, + "suma2": suma2, + } - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2, **kwargs): all_davg = [] all_dstd = [] for type_i in range(self.ntypes): diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index 5d6e16fb96..3469d43e40 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -219,9 +219,15 @@ def compute_input_stats(self, merged): sumn = np.sum(sumn, axis=0) sumr2 = np.sum(sumr2, axis=0) suma2 = np.sum(suma2, axis=0) - return sumr, suma, sumn, sumr2, suma2 + return { + "sumr": sumr, + "suma": suma, + "sumn": sumn, + "sumr2": sumr2, + "suma2": suma2, + } - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): + def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2, **kwargs): all_davg = [] all_dstd = [] for type_i in range(self.ntypes): diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index 6cbab5af4d..1948acd003 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -23,7 +23,7 @@ ) -def get_zbl_model(model_params, sampled=None): +def get_zbl_model(model_params): model_params = copy.deepcopy(model_params) ntypes = len(model_params["type_map"]) # descriptor @@ -41,9 +41,7 @@ def get_zbl_model(model_params, sampled=None): if "ener" in fitting_net["type"]: fitting_net["return_energy"] = True fitting = Fitting(**fitting_net) - dp_model = DPAtomicModel( - descriptor, fitting, type_map=model_params["type_map"], resuming=True - ) + dp_model = DPAtomicModel(descriptor, fitting, type_map=model_params["type_map"]) # pairtab filepath = model_params["use_srtab"] pt_model = PairTabModel( @@ -60,7 +58,7 @@ def get_zbl_model(model_params, sampled=None): ) -def get_model(model_params, sampled=None): +def get_model(model_params): model_params = copy.deepcopy(model_params) ntypes = len(model_params["type_map"]) # descriptor @@ -79,16 +77,7 @@ def get_model(model_params, sampled=None): fitting_net["return_energy"] = True fitting = Fitting(**fitting_net) - return EnergyModel( - descriptor, - fitting, - type_map=model_params["type_map"], - type_embedding=model_params.get("type_embedding", None), - resuming=model_params.get("resuming", False), - stat_file_dir=model_params.get("stat_file_dir", None), - stat_file_path=model_params.get("stat_file_path", None), - sampled=sampled, - ) + return EnergyModel(descriptor, fitting, type_map=model_params["type_map"]) __all__ = [ diff --git a/deepmd/pt/model/model/dp_atomic_model.py b/deepmd/pt/model/model/dp_atomic_model.py index b2ae48628b..273e79b86b 100644 --- a/deepmd/pt/model/model/dp_atomic_model.py +++ b/deepmd/pt/model/model/dp_atomic_model.py @@ -1,10 +1,13 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import copy +import logging +import os import sys from typing import ( Dict, List, Optional, + Union, ) import torch @@ -18,6 +21,9 @@ from deepmd.pt.model.task.ener import ( # noqa # TODO: should import all fittings! InvarFitting, ) +from deepmd.pt.utils.utils import ( + dict_to_device, +) from .base_atomic_model import ( BaseAtomicModel, @@ -26,6 +32,8 @@ BaseModel, ) +log = logging.getLogger(__name__) + class DPAtomicModel(BaseModel, BaseAtomicModel): """Model give atomic prediction of some physical property. @@ -39,31 +47,9 @@ class DPAtomicModel(BaseModel, BaseAtomicModel): type_map Mapping atom type to the name (str) of the type. For example `type_map[1]` gives the name of the type 1. - type_embedding - Type embedding net - resuming - Whether to resume/fine-tune from checkpoint or not. - stat_file_dir - The directory to the state files. - stat_file_path - The path to the state files. - sampled - Sampled frames to compute the statistics. """ - # I am enough with the shit interface! - def __init__( - self, - descriptor, - fitting, - type_map: Optional[List[str]], - type_embedding: Optional[dict] = None, - resuming: bool = False, - stat_file_dir=None, - stat_file_path=None, - sampled=None, - **kwargs, - ): + def __init__(self, descriptor, fitting, type_map: Optional[List[str]]): super().__init__() ntypes = len(type_map) self.type_map = type_map @@ -72,17 +58,6 @@ def __init__( self.rcut = self.descriptor.get_rcut() self.sel = self.descriptor.get_sel() self.fitting_net = fitting - # Statistics - fitting_net = None # TODO: hack!!! not sure if it is correct. - self.compute_or_load_stat( - fitting_net, - ntypes, - resuming=resuming, - type_map=type_map, - stat_file_dir=stat_file_dir, - stat_file_path=stat_file_path, - sampled=sampled, - ) def fitting_output_def(self) -> FittingOutputDef: """Get the output def of the fitting net.""" @@ -122,13 +97,7 @@ def deserialize(cls, data) -> "DPAtomicModel": fitting_obj = getattr(sys.modules[__name__], data["fitting_name"]).deserialize( data["fitting"] ) - # TODO: dirty hack to provide type_map and avoid data stat!!! - obj = cls( - descriptor_obj, - fitting_obj, - type_map=data["type_map"], - resuming=True, - ) + obj = cls(descriptor_obj, fitting_obj, type_map=data["type_map"]) return obj def forward_atomic( @@ -185,3 +154,45 @@ def forward_atomic( aparam=aparam, ) return fit_ret + + def compute_or_load_stat( + self, + type_map: Optional[List[str]] = None, + sampled=None, + stat_file_path_dict: Optional[Dict[str, Union[str, List[str]]]] = None, + ): + """ + Compute or load the statistics parameters of the model, + such as mean and standard deviation of descriptors or the energy bias of the fitting net. + When `sampled` is provided, all the statistics parameters will be calculated (or re-calculated for update), + and saved in the `stat_file_path`(s). + When `sampled` is not provided, it will check the existence of `stat_file_path`(s) + and load the calculated statistics parameters. + + Parameters + ---------- + type_map + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. + sampled + The sampled data frames from different data systems. + stat_file_path_dict + The dictionary of paths to the statistics files. + """ + if sampled is not None: # move data to device + for data_sys in sampled: + dict_to_device(data_sys) + if stat_file_path_dict is not None: + if not isinstance(stat_file_path_dict["descriptor"], list): + stat_file_dir = os.path.dirname(stat_file_path_dict["descriptor"]) + else: + stat_file_dir = os.path.dirname(stat_file_path_dict["descriptor"][0]) + if not os.path.exists(stat_file_dir): + os.mkdir(stat_file_dir) + self.descriptor.compute_or_load_stat( + type_map, sampled, stat_file_path_dict["descriptor"] + ) + if self.fitting_net is not None: + self.fitting_net.compute_or_load_stat( + type_map, sampled, stat_file_path_dict["fitting_net"] + ) diff --git a/deepmd/pt/model/model/model.py b/deepmd/pt/model/model/model.py index 000746a213..51c5fcf123 100644 --- a/deepmd/pt/model/model/model.py +++ b/deepmd/pt/model/model/model.py @@ -1,19 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import logging -import os - -import numpy as np import torch -from deepmd.pt.utils import ( - env, -) -from deepmd.pt.utils.stat import ( - compute_output_stats, -) - -log = logging.getLogger(__name__) - class BaseModel(torch.nn.Module): def __init__(self): @@ -22,127 +9,26 @@ def __init__(self): def compute_or_load_stat( self, - fitting_param, - ntypes, - resuming=False, type_map=None, - stat_file_dir=None, - stat_file_path=None, sampled=None, + stat_file_path=None, ): - if fitting_param is None: - fitting_param = {} - if not resuming: - if sampled is not None: # compute stat - for sys in sampled: - for key in sys: - if isinstance(sys[key], list): - sys[key] = [item.to(env.DEVICE) for item in sys[key]] - else: - if sys[key] is not None: - sys[key] = sys[key].to(env.DEVICE) - sumr, suma, sumn, sumr2, suma2 = self.descriptor.compute_input_stats( - sampled - ) + """ + Compute or load the statistics parameters of the model, + such as mean and standard deviation of descriptors or the energy bias of the fitting net. + When `sampled` is provided, all the statistics parameters will be calculated (or re-calculated for update), + and saved in the `stat_file_path`(s). + When `sampled` is not provided, it will check the existence of `stat_file_path`(s) + and load the calculated statistics parameters. - energy = [item["energy"] for item in sampled] - mixed_type = "real_natoms_vec" in sampled[0] - if mixed_type: - input_natoms = [item["real_natoms_vec"] for item in sampled] - else: - input_natoms = [item["natoms"] for item in sampled] - tmp = compute_output_stats(energy, input_natoms) - fitting_param["bias_atom_e"] = tmp[:, 0] - if stat_file_path is not None: - if not os.path.exists(stat_file_dir): - os.mkdir(stat_file_dir) - if not isinstance(stat_file_path, list): - log.info(f"Saving stat file to {stat_file_path}") - np.savez_compressed( - stat_file_path, - sumr=sumr, - suma=suma, - sumn=sumn, - sumr2=sumr2, - suma2=suma2, - bias_atom_e=fitting_param["bias_atom_e"], - type_map=type_map, - ) - else: - for ii, file_path in enumerate(stat_file_path): - log.info(f"Saving stat file to {file_path}") - np.savez_compressed( - file_path, - sumr=sumr[ii], - suma=suma[ii], - sumn=sumn[ii], - sumr2=sumr2[ii], - suma2=suma2[ii], - bias_atom_e=fitting_param["bias_atom_e"], - type_map=type_map, - ) - else: # load stat - target_type_map = type_map - if not isinstance(stat_file_path, list): - log.info(f"Loading stat file from {stat_file_path}") - stats = np.load(stat_file_path) - stat_type_map = list(stats["type_map"]) - missing_type = [ - i for i in target_type_map if i not in stat_type_map - ] - assert not missing_type, f"These type are not in stat file {stat_file_path}: {missing_type}! Please change the stat file path!" - idx_map = [stat_type_map.index(i) for i in target_type_map] - if stats["sumr"].size: - sumr, suma, sumn, sumr2, suma2 = ( - stats["sumr"][idx_map], - stats["suma"][idx_map], - stats["sumn"][idx_map], - stats["sumr2"][idx_map], - stats["suma2"][idx_map], - ) - else: - sumr, suma, sumn, sumr2, suma2 = [], [], [], [], [] - fitting_param["bias_atom_e"] = stats["bias_atom_e"][idx_map] - else: - sumr, suma, sumn, sumr2, suma2 = [], [], [], [], [] - id_bias_atom_e = None - for ii, file_path in enumerate(stat_file_path): - log.info(f"Loading stat file from {file_path}") - stats = np.load(file_path) - stat_type_map = list(stats["type_map"]) - missing_type = [ - i for i in target_type_map if i not in stat_type_map - ] - assert not missing_type, f"These type are not in stat file {file_path}: {missing_type}! Please change the stat file path!" - idx_map = [stat_type_map.index(i) for i in target_type_map] - if stats["sumr"].size: - sumr_tmp, suma_tmp, sumn_tmp, sumr2_tmp, suma2_tmp = ( - stats["sumr"][idx_map], - stats["suma"][idx_map], - stats["sumn"][idx_map], - stats["sumr2"][idx_map], - stats["suma2"][idx_map], - ) - else: - sumr_tmp, suma_tmp, sumn_tmp, sumr2_tmp, suma2_tmp = ( - [], - [], - [], - [], - [], - ) - sumr.append(sumr_tmp) - suma.append(suma_tmp) - sumn.append(sumn_tmp) - sumr2.append(sumr2_tmp) - suma2.append(suma2_tmp) - fitting_param["bias_atom_e"] = stats["bias_atom_e"][idx_map] - if id_bias_atom_e is None: - id_bias_atom_e = fitting_param["bias_atom_e"] - else: - assert ( - id_bias_atom_e == fitting_param["bias_atom_e"] - ).all(), "bias_atom_e in stat files are not consistent!" - self.descriptor.init_desc_stat(sumr, suma, sumn, sumr2, suma2) - else: # resuming for checkpoint; init model params from scratch - fitting_param["bias_atom_e"] = [0.0] * ntypes + Parameters + ---------- + type_map + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. + sampled + The sampled data frames from different data systems. + stat_file_path + The path to the statistics files. + """ + raise NotImplementedError diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index d73c33545e..c8ade925c0 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -32,6 +32,9 @@ DEFAULT_PRECISION, PRECISION_DICT, ) +from deepmd.pt.utils.stat import ( + compute_output_bias, +) from deepmd.pt.utils.utils import ( to_numpy_array, to_torch_tensor, @@ -173,7 +176,7 @@ def output_def(self) -> FittingOutputDef: def __setitem__(self, key, value): if key in ["bias_atom_e"]: - # correct bias_atom_e shape. user may provide stupid shape + value = value.view([self.ntypes, self.dim_out]) self.bias_atom_e = value elif key in ["fparam_avg"]: self.fparam_avg = value @@ -200,6 +203,33 @@ def __getitem__(self, key): else: raise KeyError(key) + @property + def data_stat_key(self): + """ + Get the keys for the data statistic of the fitting. + Return a list of statistic names needed, such as "bias_atom_e". + """ + return ["bias_atom_e"] + + def compute_output_stats(self, merged): + energy = [item["energy"] for item in merged] + mixed_type = "real_natoms_vec" in merged[0] + if mixed_type: + input_natoms = [item["real_natoms_vec"] for item in merged] + else: + input_natoms = [item["natoms"] for item in merged] + tmp = compute_output_bias(energy, input_natoms) + bias_atom_e = tmp[:, 0] + return {"bias_atom_e": bias_atom_e} + + def init_fitting_stat(self, bias_atom_e=None, **kwargs): + assert True not in [x is None for x in [bias_atom_e]] + self.bias_atom_e.copy_( + torch.tensor(bias_atom_e, device=env.DEVICE).view( + [self.ntypes, self.dim_out] + ) + ) + def serialize(self) -> dict: """Serialize the fitting to dict.""" return { @@ -394,6 +424,16 @@ def __init__( **kwargs, ) + @classmethod + def get_stat_name(cls, ntypes, type_name="ener", **kwargs): + """ + Get the name for the statistic file of the fitting. + Usually use the combination of fitting net name and ntypes as the statistic file name. + """ + fitting_type = type_name + assert fitting_type in ["ener"] + return f"stat_file_fitting_ener_ntypes{ntypes}.npz" + @Fitting.register("direct_force") @Fitting.register("direct_force_ener") @@ -486,6 +526,16 @@ def serialize(self) -> dict: def deserialize(cls) -> "EnergyFittingNetDirect": raise NotImplementedError + @classmethod + def get_stat_name(cls, ntypes, type_name="ener", **kwargs): + """ + Get the name for the statistic file of the fitting. + Usually use the combination of fitting net name and ntypes as the statistic file name. + """ + fitting_type = type_name + assert fitting_type in ["direct_force", "direct_force_ener"] + return f"stat_file_fitting_direct_ntypes{ntypes}.npz" + def forward( self, inputs: torch.Tensor, diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index b03aee7539..360f545975 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -2,6 +2,9 @@ import logging from typing import ( Callable, + List, + Optional, + Union, ) import numpy as np @@ -94,6 +97,107 @@ def share_params(self, base_class, shared_level, resume=False): else: raise NotImplementedError + @classmethod + def get_stat_name(cls, ntypes, type_name="ener", **kwargs): + """ + Get the name for the statistic file of the fitting. + Usually use the combination of fitting net name and ntypes as the statistic file name. + """ + if cls is not Fitting: + raise NotImplementedError("get_stat_name is not implemented!") + fitting_type = type_name + return Fitting.__plugins.plugins[fitting_type].get_stat_name( + ntypes, type_name, **kwargs + ) + + @property + def data_stat_key(self): + """ + Get the keys for the data statistic of the fitting. + Return a list of statistic names needed, such as "bias_atom_e". + """ + raise NotImplementedError("data_stat_key is not implemented!") + + def compute_or_load_stat( + self, + type_map: List[str], + sampled=None, + stat_file_path: Optional[Union[str, List[str]]] = None, + ): + """ + Compute or load the statistics parameters of the fitting net. + Calculate and save the output bias to `stat_file_path` + if `sampled` is not None, otherwise load them from `stat_file_path`. + + Parameters + ---------- + type_map + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. + sampled + The sampled data frames from different data systems. + stat_file_path + The path to the statistics files. + """ + fitting_stat_key = self.data_stat_key + if sampled is not None: + tmp_dict = self.compute_output_stats(sampled) + result_dict = {key: tmp_dict[key] for key in fitting_stat_key} + result_dict["type_map"] = type_map + self.save_stats(result_dict, stat_file_path) + else: # load the statistics results + assert stat_file_path is not None, "No stat file to load!" + result_dict = self.load_stats(type_map, stat_file_path) + self.init_fitting_stat(**result_dict) + + def save_stats(self, result_dict, stat_file_path: str): + """ + Save the statistics results to `stat_file_path`. + + Parameters + ---------- + result_dict + The dictionary of statistics results. + stat_file_path + The path to the statistics file(s). + """ + log.info(f"Saving stat file to {stat_file_path}") + np.savez_compressed(stat_file_path, **result_dict) + + def load_stats(self, type_map, stat_file_path: str): + """ + Load the statistics results to `stat_file_path`. + + Parameters + ---------- + type_map + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. + stat_file_path + The path to the statistics file(s). + + Returns + ------- + result_dict + The dictionary of statistics results. + """ + fitting_stat_key = self.data_stat_key + target_type_map = type_map + log.info(f"Loading stat file from {stat_file_path}") + stats = np.load(stat_file_path) + stat_type_map = list(stats["type_map"]) + missing_type = [i for i in target_type_map if i not in stat_type_map] + assert not missing_type, ( + f"These type are not in stat file {stat_file_path}: {missing_type}! " + f"Please change the stat file path!" + ) + idx_map = [stat_type_map.index(i) for i in target_type_map] + if stats[fitting_stat_key[0]].size: # not empty + result_dict = {key: stats[key][idx_map] for key in fitting_stat_key} + else: + result_dict = {key: [] for key in fitting_stat_key} + return result_dict + def change_energy_bias( self, config, model, old_type_map, new_type_map, bias_shift="delta", ntest=10 ): diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 02367f4aee..b2cac5a5eb 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -67,7 +67,8 @@ def __init__( self, config: Dict[str, Any], training_data, - sampled, + sampled=None, + stat_file_path=None, validation_data=None, init_model=None, restart_model=None, @@ -91,6 +92,8 @@ def __init__( self.model_keys = ( list(model_params["model_dict"]) if self.multi_task else ["Default"] ) + if self.multi_task and sampled is None: + sampled = {key: None for key in self.model_keys} self.rank = dist.get_rank() if dist.is_initialized() else 0 self.world_size = dist.get_world_size() if dist.is_initialized() else 1 self.num_model = len(self.model_keys) @@ -178,8 +181,14 @@ def get_data_loader(_training_data, _validation_data, _training_params): valid_numb_batch, ) - def get_single_model(_model_params, _sampled): - model = get_model(deepcopy(_model_params), _sampled).to(DEVICE) + def get_single_model(_model_params, _sampled, _stat_file_path): + model = get_model(deepcopy(_model_params)).to(DEVICE) + if not model_params.get("resuming", False): + model.compute_or_load_stat( + type_map=_model_params["type_map"], + sampled=_sampled, + stat_file_path_dict=_stat_file_path, + ) return model def get_lr(lr_params): @@ -229,7 +238,7 @@ def get_loss(loss_params, start_lr, _ntypes): self.validation_data, self.valid_numb_batch, ) = get_data_loader(training_data, validation_data, training_params) - self.model = get_single_model(model_params, sampled) + self.model = get_single_model(model_params, sampled, stat_file_path) else: ( self.training_dataloader, @@ -252,7 +261,9 @@ def get_loss(loss_params, start_lr, _ntypes): training_params["data_dict"][model_key], ) self.model[model_key] = get_single_model( - model_params["model_dict"][model_key], sampled[model_key] + model_params["model_dict"][model_key], + sampled[model_key], + stat_file_path[model_key], ) # Learning rate diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index 5fde03c74a..932ba9a409 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging +import os import numpy as np import torch @@ -86,8 +87,8 @@ def make_stat_input(datasets, dataloaders, nbatches): return lst -def compute_output_stats(energy, natoms, rcond=None): - """Update mean and stddev for descriptor elements. +def compute_output_bias(energy, natoms, rcond=None): + """Update output bias for fitting net. Args: - energy: Batched energy with shape [nframes, 1]. @@ -104,3 +105,32 @@ def compute_output_stats(energy, natoms, rcond=None): sys_tynatom = torch.cat(natoms)[:, 2:].cpu() energy_coef, _, _, _ = np.linalg.lstsq(sys_tynatom, sys_ener, rcond) return energy_coef + + +def process_stat_path( + stat_file_dict, stat_file_dir, model_params_dict, descriptor_cls, fitting_cls +): + if stat_file_dict is None: + stat_file_dict = {} + if "descriptor" in model_params_dict: + default_stat_file_name_descrpt = descriptor_cls.get_stat_name( + len(model_params_dict["type_map"]), + model_params_dict["descriptor"]["type"], + **model_params_dict["descriptor"], + ) + stat_file_dict["descriptor"] = default_stat_file_name_descrpt + if "fitting_net" in model_params_dict: + default_stat_file_name_fitting = fitting_cls.get_stat_name( + len(model_params_dict["type_map"]), + model_params_dict["fitting_net"].get("type", "ener"), + **model_params_dict["fitting_net"], + ) + stat_file_dict["fitting_net"] = default_stat_file_name_fitting + stat_file_path = { + key: os.path.join(stat_file_dir, stat_file_dict[key]) for key in stat_file_dict + } + + has_stat_file_path_list = [ + os.path.exists(stat_file_path[key]) for key in stat_file_dict + ] + return stat_file_path, False not in has_stat_file_path_list diff --git a/deepmd/pt/utils/utils.py b/deepmd/pt/utils/utils.py index 2b96925a51..d6621f7b4c 100644 --- a/deepmd/pt/utils/utils.py +++ b/deepmd/pt/utils/utils.py @@ -81,3 +81,12 @@ def to_torch_tensor( if prec is None: raise ValueError(f"unknown precision {xx.dtype}") return torch.tensor(xx, dtype=prec, device=DEVICE) + + +def dict_to_device(sample_dict): + for key in sample_dict: + if isinstance(sample_dict[key], list): + sample_dict[key] = [item.to(DEVICE) for item in sample_dict[key]] + else: + if sample_dict[key] is not None: + sample_dict[key] = sample_dict[key].to(DEVICE) diff --git a/source/tests/pt/model/test_autodiff.py b/source/tests/pt/model/test_autodiff.py index 24dc69458d..e69e894af6 100644 --- a/source/tests/pt/model/test_autodiff.py +++ b/source/tests/pt/model/test_autodiff.py @@ -17,7 +17,6 @@ from .test_permutation import ( eval_model, - make_sample, model_dpa1, model_dpa2, model_se_e2_a, @@ -135,33 +134,29 @@ def ff(bb): class TestEnergyModelSeAForce(unittest.TestCase, ForceTest): def setUp(self): model_params = copy.deepcopy(model_se_e2_a) - sampled = make_sample(model_params) self.type_split = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelSeAVirial(unittest.TestCase, VirialTest): def setUp(self): model_params = copy.deepcopy(model_se_e2_a) - sampled = make_sample(model_params) self.type_split = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelDPA1Force(unittest.TestCase, ForceTest): def setUp(self): model_params = copy.deepcopy(model_dpa1) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelDPA1Virial(unittest.TestCase, VirialTest): def setUp(self): model_params = copy.deepcopy(model_dpa1) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelDPA2Force(unittest.TestCase, ForceTest): @@ -173,10 +168,9 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) model_params = copy.deepcopy(model_dpa2) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelDPAUniVirial(unittest.TestCase, VirialTest): @@ -188,23 +182,20 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) model_params = copy.deepcopy(model_dpa2) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelZBLForce(unittest.TestCase, ForceTest): def setUp(self): model_params = copy.deepcopy(model_zbl) - sampled = make_sample(model_params) self.type_split = False - self.model = get_zbl_model(model_params, sampled).to(env.DEVICE) + self.model = get_zbl_model(model_params).to(env.DEVICE) class TestEnergyModelZBLVirial(unittest.TestCase, VirialTest): def setUp(self): model_params = copy.deepcopy(model_zbl) - sampled = make_sample(model_params) self.type_split = False - self.model = get_zbl_model(model_params, sampled).to(env.DEVICE) + self.model = get_zbl_model(model_params).to(env.DEVICE) diff --git a/source/tests/pt/model/test_dp_atomic_model.py b/source/tests/pt/model/test_dp_atomic_model.py index 2960cb97cc..ef25e574d4 100644 --- a/source/tests/pt/model/test_dp_atomic_model.py +++ b/source/tests/pt/model/test_dp_atomic_model.py @@ -50,8 +50,7 @@ def test_self_consistency(self): distinguish_types=ds.distinguish_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] - # TODO: dirty hack to avoid data stat!!! - md0 = DPAtomicModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + md0 = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) md1 = DPAtomicModel.deserialize(md0.serialize()).to(env.DEVICE) args = [ to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] @@ -107,6 +106,5 @@ def test_jit(self): distinguish_types=ds.distinguish_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] - # TODO: dirty hack to avoid data stat!!! - md0 = DPAtomicModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + md0 = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) torch.jit.script(md0) diff --git a/source/tests/pt/model/test_dp_model.py b/source/tests/pt/model/test_dp_model.py index 51aa5d92f6..6e009d3934 100644 --- a/source/tests/pt/model/test_dp_model.py +++ b/source/tests/pt/model/test_dp_model.py @@ -56,8 +56,7 @@ def test_self_consistency(self): distinguish_types=ds.distinguish_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] - # TODO: dirty hack to avoid data stat!!! - md0 = DPModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + md0 = DPModel(ds, ft, type_map=type_map).to(env.DEVICE) md1 = DPModel.deserialize(md0.serialize()).to(env.DEVICE) args = [to_torch_tensor(ii) for ii in [self.coord, self.atype, self.cell]] ret0 = md0.forward_common(*args) @@ -206,8 +205,7 @@ def test_self_consistency(self): distinguish_types=ds.distinguish_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] - # TODO: dirty hack to avoid data stat!!! - md0 = DPModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + md0 = DPModel(ds, ft, type_map=type_map).to(env.DEVICE) md1 = DPModel.deserialize(md0.serialize()).to(env.DEVICE) args = [ to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] @@ -285,8 +283,7 @@ def test_jit(self): distinguish_types=ds.distinguish_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] - # TODO: dirty hack to avoid data stat!!! - md0 = DPModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + md0 = DPModel(ds, ft, type_map=type_map).to(env.DEVICE) torch.jit.script(md0) @@ -334,8 +331,7 @@ def setUp(self): distinguish_types=ds.distinguish_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] - # TODO: dirty hack to avoid data stat!!! - self.md = DPModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + self.md = DPModel(ds, ft, type_map=type_map).to(env.DEVICE) def test_nlist_eq(self): # n_nnei == nnei @@ -408,8 +404,7 @@ def test_self_consistency(self): distinguish_types=ds.distinguish_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] - # TODO: dirty hack to avoid data stat!!! - md0 = EnergyModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + md0 = EnergyModel(ds, ft, type_map=type_map).to(env.DEVICE) md1 = EnergyModel.deserialize(md0.serialize()).to(env.DEVICE) args = [to_torch_tensor(ii) for ii in [self.coord, self.atype, self.cell]] ret0 = md0.forward(*args) @@ -480,8 +475,7 @@ def test_self_consistency(self): distinguish_types=ds.distinguish_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] - # TODO: dirty hack to avoid data stat!!! - md0 = EnergyModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + md0 = EnergyModel(ds, ft, type_map=type_map).to(env.DEVICE) md1 = EnergyModel.deserialize(md0.serialize()).to(env.DEVICE) args = [ to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] @@ -526,6 +520,5 @@ def test_jit(self): distinguish_types=ds.distinguish_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] - # TODO: dirty hack to avoid data stat!!! - md0 = EnergyModel(ds, ft, type_map=type_map, resuming=True).to(env.DEVICE) + md0 = EnergyModel(ds, ft, type_map=type_map).to(env.DEVICE) torch.jit.script(md0) diff --git a/source/tests/pt/model/test_ener_fitting.py b/source/tests/pt/model/test_ener_fitting.py index cbddf34dd6..42aeeff16a 100644 --- a/source/tests/pt/model/test_ener_fitting.py +++ b/source/tests/pt/model/test_ener_fitting.py @@ -178,4 +178,6 @@ def test_get_set(self): "aparam_inv_std", ]: ifn0[ii] = torch.tensor(foo, dtype=dtype, device=env.DEVICE) - np.testing.assert_allclose(foo, ifn0[ii].detach().cpu().numpy()) + np.testing.assert_allclose( + foo, np.reshape(ifn0[ii].detach().cpu().numpy(), foo.shape) + ) diff --git a/source/tests/pt/model/test_force_grad.py b/source/tests/pt/model/test_force_grad.py index 1ea4321d21..0a4dc32d9f 100644 --- a/source/tests/pt/model/test_force_grad.py +++ b/source/tests/pt/model/test_force_grad.py @@ -19,15 +19,9 @@ from deepmd.pt.utils import ( env, ) -from deepmd.pt.utils.dataloader import ( - DpLoaderSet, -) from deepmd.pt.utils.dataset import ( DeepmdDataSystem, ) -from deepmd.pt.utils.stat import ( - make_stat_input, -) class CheckSymmetry(DeepmdDataSystem): @@ -75,18 +69,7 @@ def setUp(self): self.get_model() def get_model(self): - training_systems = self.config["training"]["training_data"]["systems"] - model_params = self.config["model"] - data_stat_nbatch = model_params.get("data_stat_nbatch", 10) - train_data = DpLoaderSet( - training_systems, - self.config["training"]["training_data"]["batch_size"], - model_params, - ) - sampled = make_stat_input( - train_data.systems, train_data.dataloaders, data_stat_nbatch - ) - self.model = get_model(self.config["model"], sampled).to(env.DEVICE) + self.model = get_model(self.config["model"]).to(env.DEVICE) def get_dataset(self, system_index=0, batch_index=0): systems = self.config["training"]["training_data"]["systems"] diff --git a/source/tests/pt/model/test_linear_atomic_model.py b/source/tests/pt/model/test_linear_atomic_model.py index e9090de86a..e0247f911f 100644 --- a/source/tests/pt/model/test_linear_atomic_model.py +++ b/source/tests/pt/model/test_linear_atomic_model.py @@ -74,9 +74,7 @@ def test_pairwise(self, mock_loadtxt): type_map = ["foo", "bar"] zbl_model = PairTabModel(tab_file=file_path, rcut=0.3, sel=2) - dp_model = DPAtomicModel(ds, ft, type_map=type_map, resuming=True).to( - env.DEVICE - ) + dp_model = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) wgt_model = DPZBLLinearAtomicModel( dp_model, zbl_model, @@ -142,9 +140,7 @@ def setUp(self, mock_loadtxt): distinguish_types=ds.distinguish_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] - dp_model = DPAtomicModel(ds, ft, type_map=type_map, resuming=True).to( - env.DEVICE - ) + dp_model = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) zbl_model = PairTabModel(file_path, self.rcut, sum(self.sel)) self.md0 = DPZBLLinearAtomicModel( dp_model, diff --git a/source/tests/pt/model/test_model.py b/source/tests/pt/model/test_model.py index bb99759d16..522b30b2df 100644 --- a/source/tests/pt/model/test_model.py +++ b/source/tests/pt/model/test_model.py @@ -30,9 +30,6 @@ DEVICE, ) from deepmd.pt.utils.learning_rate import LearningRateExp as MyLRExp -from deepmd.pt.utils.stat import ( - make_stat_input, -) from deepmd.tf.common import ( data_requirement, expand_sys_str, @@ -282,9 +279,6 @@ def test_consistency(self): "type_map": self.type_map, }, ) - sampled = make_stat_input( - my_ds.systems, my_ds.dataloaders, self.data_stat_nbatch - ) my_model = get_model( model_params={ "descriptor": { @@ -299,7 +293,6 @@ def test_consistency(self): "data_stat_nbatch": self.data_stat_nbatch, "type_map": self.type_map, }, - sampled=sampled, ) my_model.to(DEVICE) my_lr = MyLRExp(self.start_lr, self.stop_lr, self.decay_steps, self.stop_steps) diff --git a/source/tests/pt/model/test_permutation.py b/source/tests/pt/model/test_permutation.py index 2301b6ea10..15359f873a 100644 --- a/source/tests/pt/model/test_permutation.py +++ b/source/tests/pt/model/test_permutation.py @@ -1,9 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import copy import unittest -from pathlib import ( - Path, -) import torch @@ -17,12 +14,6 @@ from deepmd.pt.utils import ( env, ) -from deepmd.pt.utils.dataloader import ( - DpLoaderSet, -) -from deepmd.pt.utils.stat import ( - make_stat_input, -) dtype = torch.float64 @@ -205,22 +196,6 @@ } -def make_sample(model_params): - training_systems = [ - str(Path(__file__).parent / "water/data/data_0"), - ] - data_stat_nbatch = model_params.get("data_stat_nbatch", 10) - train_data = DpLoaderSet( - training_systems, - batch_size=4, - model_params=model_params.copy(), - ) - sampled = make_stat_input( - train_data.systems, train_data.dataloaders, data_stat_nbatch - ) - return sampled - - class PermutationTest: def test( self, @@ -262,17 +237,15 @@ def test( class TestEnergyModelSeA(unittest.TestCase, PermutationTest): def setUp(self): model_params = copy.deepcopy(model_se_e2_a) - sampled = make_sample(model_params) self.type_split = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelDPA1(unittest.TestCase, PermutationTest): def setUp(self): model_params = copy.deepcopy(model_dpa1) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelDPA2(unittest.TestCase, PermutationTest): @@ -284,10 +257,9 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) model_params = copy.deepcopy(model_dpa2) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestForceModelDPA2(unittest.TestCase, PermutationTest): @@ -299,21 +271,19 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) model_params = copy.deepcopy(model_dpa2) model_params["fitting_net"]["type"] = "direct_force_ener" self.type_split = True self.test_virial = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) @unittest.skip("hybrid not supported at the moment") class TestEnergyModelHybrid(unittest.TestCase, PermutationTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) @unittest.skip("hybrid not supported at the moment") @@ -321,25 +291,22 @@ class TestForceModelHybrid(unittest.TestCase, PermutationTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) model_params["fitting_net"]["type"] = "direct_force_ener" - sampled = make_sample(model_params) self.type_split = True self.test_virial = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelZBL(unittest.TestCase, PermutationTest): def setUp(self): model_params = copy.deepcopy(model_zbl) - sampled = make_sample(model_params) self.type_split = False - self.model = get_zbl_model(model_params, sampled).to(env.DEVICE) + self.model = get_zbl_model(model_params).to(env.DEVICE) # class TestEnergyFoo(unittest.TestCase): # def test(self): # model_params = model_dpau -# sampled = make_sample(model_params) -# self.model = EnergyModelDPAUni(model_params, sampled).to(env.DEVICE) +# self.model = EnergyModelDPAUni(model_params).to(env.DEVICE) # natoms = 5 # cell = torch.rand([3, 3], dtype=dtype) diff --git a/source/tests/pt/model/test_permutation_denoise.py b/source/tests/pt/model/test_permutation_denoise.py index 6dd61ab7e4..3b6be0c495 100644 --- a/source/tests/pt/model/test_permutation_denoise.py +++ b/source/tests/pt/model/test_permutation_denoise.py @@ -15,7 +15,6 @@ ) from .test_permutation import ( # model_dpau, - make_sample, model_dpa1, model_dpa2, model_hybrid, @@ -70,9 +69,8 @@ def test( class TestDenoiseModelDPA1(unittest.TestCase, PermutationDenoiseTest): def setUp(self): model_params = copy.deepcopy(model_dpa1) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) @unittest.skip("support of the denoise is temporally disabled") @@ -85,19 +83,19 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) model_params = copy.deepcopy(model_dpa2) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model( + model_params, + ).to(env.DEVICE) # @unittest.skip("hybrid not supported at the moment") # class TestDenoiseModelHybrid(unittest.TestCase, TestPermutationDenoise): # def setUp(self): # model_params = copy.deepcopy(model_hybrid_denoise) -# sampled = make_sample(model_params) # self.type_split = True -# self.model = get_model(model_params, sampled).to(env.DEVICE) +# self.model = get_model(model_params).to(env.DEVICE) if __name__ == "__main__": diff --git a/source/tests/pt/model/test_rot.py b/source/tests/pt/model/test_rot.py index 982753e94f..780d193ebd 100644 --- a/source/tests/pt/model/test_rot.py +++ b/source/tests/pt/model/test_rot.py @@ -16,7 +16,6 @@ ) from .test_permutation import ( # model_dpau, - make_sample, model_dpa1, model_dpa2, model_hybrid, @@ -114,17 +113,15 @@ def test( class TestEnergyModelSeA(unittest.TestCase, RotTest): def setUp(self): model_params = copy.deepcopy(model_se_e2_a) - sampled = make_sample(model_params) self.type_split = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelDPA1(unittest.TestCase, RotTest): def setUp(self): model_params = copy.deepcopy(model_dpa1) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelDPA2(unittest.TestCase, RotTest): @@ -136,10 +133,9 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) model_params = copy.deepcopy(model_dpa2) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestForceModelDPA2(unittest.TestCase, RotTest): @@ -151,21 +147,19 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) model_params = copy.deepcopy(model_dpa2) model_params["fitting_net"]["type"] = "direct_force_ener" self.type_split = True self.test_virial = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) @unittest.skip("hybrid not supported at the moment") class TestEnergyModelHybrid(unittest.TestCase, RotTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) @unittest.skip("hybrid not supported at the moment") @@ -173,18 +167,16 @@ class TestForceModelHybrid(unittest.TestCase, RotTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) model_params["fitting_net"]["type"] = "direct_force_ener" - sampled = make_sample(model_params) self.type_split = True self.test_virial = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelZBL(unittest.TestCase, RotTest): def setUp(self): model_params = copy.deepcopy(model_zbl) - sampled = make_sample(model_params) self.type_split = False - self.model = get_zbl_model(model_params, sampled).to(env.DEVICE) + self.model = get_zbl_model(model_params).to(env.DEVICE) if __name__ == "__main__": diff --git a/source/tests/pt/model/test_rot_denoise.py b/source/tests/pt/model/test_rot_denoise.py index 2cbfd8fd38..e4ae02f630 100644 --- a/source/tests/pt/model/test_rot_denoise.py +++ b/source/tests/pt/model/test_rot_denoise.py @@ -15,7 +15,6 @@ ) from .test_permutation_denoise import ( - make_sample, model_dpa1, model_dpa2, ) @@ -101,9 +100,8 @@ def test( class TestDenoiseModelDPA1(unittest.TestCase, RotDenoiseTest): def setUp(self): model_params = copy.deepcopy(model_dpa1) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) @unittest.skip("support of the denoise is temporally disabled") @@ -116,19 +114,17 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) model_params = copy.deepcopy(model_dpa2) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) # @unittest.skip("hybrid not supported at the moment") # class TestEnergyModelHybrid(unittest.TestCase, TestRotDenoise): # def setUp(self): # model_params = copy.deepcopy(model_hybrid_denoise) -# sampled = make_sample(model_params) # self.type_split = True -# self.model = get_model(model_params, sampled).to(env.DEVICE) +# self.model = get_model(model_params).to(env.DEVICE) if __name__ == "__main__": diff --git a/source/tests/pt/model/test_rotation.py b/source/tests/pt/model/test_rotation.py index a62e04eb89..5314959673 100644 --- a/source/tests/pt/model/test_rotation.py +++ b/source/tests/pt/model/test_rotation.py @@ -21,15 +21,9 @@ from deepmd.pt.utils import ( env, ) -from deepmd.pt.utils.dataloader import ( - DpLoaderSet, -) from deepmd.pt.utils.dataset import ( DeepmdDataSystem, ) -from deepmd.pt.utils.stat import ( - make_stat_input, -) class CheckSymmetry(DeepmdDataSystem): @@ -82,18 +76,7 @@ def setUp(self): self.get_model() def get_model(self): - training_systems = self.config["training"]["training_data"]["systems"] - model_params = self.config["model"] - data_stat_nbatch = model_params.get("data_stat_nbatch", 10) - train_data = DpLoaderSet( - training_systems, - self.config["training"]["training_data"]["batch_size"], - model_params, - ) - sampled = make_stat_input( - train_data.systems, train_data.dataloaders, data_stat_nbatch - ) - self.model = get_model(self.config["model"], sampled).to(env.DEVICE) + self.model = get_model(self.config["model"]).to(env.DEVICE) def get_dataset(self, system_index=0, batch_index=0): systems = self.config["training"]["training_data"]["systems"] diff --git a/source/tests/pt/model/test_saveload_dpa1.py b/source/tests/pt/model/test_saveload_dpa1.py index 1b4c41a204..64229b8e9e 100644 --- a/source/tests/pt/model/test_saveload_dpa1.py +++ b/source/tests/pt/model/test_saveload_dpa1.py @@ -109,14 +109,13 @@ def get_model_result(self, read=False, model_file="tmp_model.pt"): def create_wrapper(self, read: bool): model_config = copy.deepcopy(self.config["model"]) - sampled = copy.deepcopy(self.sampled) model_config["resuming"] = read model_config["stat_file_dir"] = "stat_files" model_config["stat_file"] = "stat.npz" model_config["stat_file_path"] = os.path.join( model_config["stat_file_dir"], model_config["stat_file"] ) - model = get_model(model_config, sampled).to(env.DEVICE) + model = get_model(model_config).to(env.DEVICE) return ModelWrapper(model, self.loss) def get_data(self): diff --git a/source/tests/pt/model/test_saveload_se_e2_a.py b/source/tests/pt/model/test_saveload_se_e2_a.py index 7f8364a16f..0632e30b5b 100644 --- a/source/tests/pt/model/test_saveload_se_e2_a.py +++ b/source/tests/pt/model/test_saveload_se_e2_a.py @@ -109,8 +109,7 @@ def get_model_result(self, read=False, model_file="tmp_model.pt"): def create_wrapper(self): model_config = copy.deepcopy(self.config["model"]) - sampled = copy.deepcopy(self.sampled) - model = get_model(model_config, sampled).to(env.DEVICE) + model = get_model(model_config).to(env.DEVICE) return ModelWrapper(model, self.loss) def get_data(self): diff --git a/source/tests/pt/model/test_smooth.py b/source/tests/pt/model/test_smooth.py index f2f45c74aa..fa9042a932 100644 --- a/source/tests/pt/model/test_smooth.py +++ b/source/tests/pt/model/test_smooth.py @@ -16,7 +16,6 @@ ) from .test_permutation import ( # model_dpau, - make_sample, model_dpa1, model_dpa2, model_hybrid, @@ -125,9 +124,8 @@ def compare(ret0, ret1): class TestEnergyModelSeA(unittest.TestCase, SmoothTest): def setUp(self): model_params = copy.deepcopy(model_se_e2_a) - sampled = make_sample(model_params) self.type_split = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) self.epsilon, self.aprec = None, None @@ -135,9 +133,8 @@ def setUp(self): class TestEnergyModelDPA1(unittest.TestCase, SmoothTest): def setUp(self): model_params = copy.deepcopy(model_dpa1) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) # less degree of smoothness, # error can be systematically removed by reducing epsilon self.epsilon = 1e-5 @@ -160,9 +157,8 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) self.epsilon, self.aprec = 1e-5, 1e-4 @@ -177,10 +173,9 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) self.type_split = True self.test_virial = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) self.epsilon, self.aprec = None, None @@ -195,10 +190,9 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) self.type_split = True self.test_virial = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) self.epsilon, self.aprec = None, None @@ -206,26 +200,23 @@ def setUp(self): class TestEnergyModelHybrid(unittest.TestCase, SmoothTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) self.epsilon, self.aprec = None, None class TestEnergyModelZBL(unittest.TestCase, SmoothTest): def setUp(self): model_params = copy.deepcopy(model_zbl) - sampled = make_sample(model_params) self.type_split = False - self.model = get_zbl_model(model_params, sampled).to(env.DEVICE) + self.model = get_zbl_model(model_params).to(env.DEVICE) self.epsilon, self.aprec = None, None # class TestEnergyFoo(unittest.TestCase): # def test(self): # model_params = model_dpau -# sampled = make_sample(model_params) -# self.model = EnergyModelDPAUni(model_params, sampled).to(env.DEVICE) +# self.model = EnergyModelDPAUni(model_params).to(env.DEVICE) # natoms = 5 # cell = torch.rand([3, 3], dtype=dtype) diff --git a/source/tests/pt/model/test_smooth_denoise.py b/source/tests/pt/model/test_smooth_denoise.py index de89f8dccc..777d288f3c 100644 --- a/source/tests/pt/model/test_smooth_denoise.py +++ b/source/tests/pt/model/test_smooth_denoise.py @@ -15,7 +15,6 @@ ) from .test_permutation_denoise import ( - make_sample, model_dpa2, ) @@ -106,12 +105,11 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) model_params = copy.deepcopy(model_dpa2) model_params["descriptor"]["sel"] = 8 model_params["descriptor"]["rcut_smth"] = 3.5 self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) self.epsilon, self.aprec = None, None self.epsilon = 1e-7 self.aprec = 1e-5 @@ -127,11 +125,10 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) model_params = copy.deepcopy(model_dpa2) # model_params["descriptor"]["combine_grrg"] = True self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) self.epsilon, self.aprec = None, None self.epsilon = 1e-7 self.aprec = 1e-5 @@ -141,9 +138,8 @@ def setUp(self): # class TestDenoiseModelHybrid(unittest.TestCase, TestSmoothDenoise): # def setUp(self): # model_params = copy.deepcopy(model_hybrid_denoise) -# sampled = make_sample(model_params) # self.type_split = True -# self.model = get_model(model_params, sampled).to(env.DEVICE) +# self.model = get_model(model_params).to(env.DEVICE) # self.epsilon, self.aprec = None, None # self.epsilon = 1e-7 # self.aprec = 1e-5 diff --git a/source/tests/pt/model/test_trans.py b/source/tests/pt/model/test_trans.py index 967d505c6d..a99d6c893f 100644 --- a/source/tests/pt/model/test_trans.py +++ b/source/tests/pt/model/test_trans.py @@ -16,7 +16,6 @@ ) from .test_permutation import ( # model_dpau, - make_sample, model_dpa1, model_dpa2, model_hybrid, @@ -70,17 +69,15 @@ def test( class TestEnergyModelSeA(unittest.TestCase, TransTest): def setUp(self): model_params = copy.deepcopy(model_se_e2_a) - sampled = make_sample(model_params) self.type_split = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelDPA1(unittest.TestCase, TransTest): def setUp(self): model_params = copy.deepcopy(model_dpa1) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelDPA2(unittest.TestCase, TransTest): @@ -92,10 +89,9 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) model_params = copy.deepcopy(model_dpa2) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestForceModelDPA2(unittest.TestCase, TransTest): @@ -107,21 +103,19 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) model_params = copy.deepcopy(model_dpa2) model_params["fitting_net"]["type"] = "direct_force_ener" self.type_split = True self.test_virial = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) @unittest.skip("hybrid not supported at the moment") class TestEnergyModelHybrid(unittest.TestCase, TransTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) @unittest.skip("hybrid not supported at the moment") @@ -129,18 +123,16 @@ class TestForceModelHybrid(unittest.TestCase, TransTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) model_params["fitting_net"]["type"] = "direct_force_ener" - sampled = make_sample(model_params) self.type_split = True self.test_virial = False - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelZBL(unittest.TestCase, TransTest): def setUp(self): model_params = copy.deepcopy(model_zbl) - sampled = make_sample(model_params) self.type_split = False - self.model = get_zbl_model(model_params, sampled).to(env.DEVICE) + self.model = get_zbl_model(model_params).to(env.DEVICE) if __name__ == "__main__": diff --git a/source/tests/pt/model/test_trans_denoise.py b/source/tests/pt/model/test_trans_denoise.py index 88b926a3ae..9ba93a244a 100644 --- a/source/tests/pt/model/test_trans_denoise.py +++ b/source/tests/pt/model/test_trans_denoise.py @@ -15,7 +15,6 @@ ) from .test_permutation_denoise import ( - make_sample, model_dpa1, model_dpa2, model_hybrid, @@ -60,9 +59,8 @@ def test( class TestDenoiseModelDPA1(unittest.TestCase, TransDenoiseTest): def setUp(self): model_params = copy.deepcopy(model_dpa1) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) @unittest.skip("support of the denoise is temporally disabled") @@ -75,19 +73,17 @@ def setUp(self): model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ "repinit_nsel" ] - sampled = make_sample(model_params_sample) model_params = copy.deepcopy(model_dpa2) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) @unittest.skip("hybrid not supported at the moment") class TestDenoiseModelHybrid(unittest.TestCase, TransDenoiseTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) - sampled = make_sample(model_params) self.type_split = True - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) if __name__ == "__main__": diff --git a/source/tests/pt/model/test_unused_params.py b/source/tests/pt/model/test_unused_params.py index a924979466..f69d8ac835 100644 --- a/source/tests/pt/model/test_unused_params.py +++ b/source/tests/pt/model/test_unused_params.py @@ -15,7 +15,6 @@ ) from .test_permutation import ( - make_sample, model_dpa2, ) @@ -57,8 +56,7 @@ def test_unused(self): self._test_unused(model) def _test_unused(self, model_params): - sampled = make_sample(model_params) - self.model = get_model(model_params, sampled).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) natoms = 5 cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) cell = (cell + cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) diff --git a/source/tests/pt/test_stat.py b/source/tests/pt/test_stat.py index 08fc12ff11..240c354a69 100644 --- a/source/tests/pt/test_stat.py +++ b/source/tests/pt/test_stat.py @@ -19,7 +19,7 @@ DpLoaderSet, ) from deepmd.pt.utils.stat import ( - compute_output_stats, + compute_output_bias, ) from deepmd.pt.utils.stat import make_stat_input as my_make from deepmd.tf.common import ( @@ -124,7 +124,7 @@ def my_merge(energy, natoms): energy, natoms = my_merge(energy, natoms) dp_fn = EnerFitting(self.dp_d, self.n_neuron) dp_fn.compute_output_stats(self.dp_sampled) - bias_atom_e = compute_output_stats(energy, natoms) + bias_atom_e = compute_output_bias(energy, natoms) self.assertTrue(np.allclose(dp_fn.bias_atom_e, bias_atom_e[:, 0])) # temporarily delete this function for performance of seeds in tf and pytorch may be different @@ -172,8 +172,8 @@ def test_descriptor(self): ]: if key in sys.keys(): sys[key] = sys[key].to(env.DEVICE) - sumr, suma, sumn, sumr2, suma2 = my_en.compute_input_stats(sampled) - my_en.init_desc_stat(sumr, suma, sumn, sumr2, suma2) + stat_dict = my_en.compute_input_stats(sampled) + my_en.init_desc_stat(**stat_dict) my_en.mean = my_en.mean my_en.stddev = my_en.stddev self.assertTrue( From dc63793981513f6326f6d2e841792564f8d9e3d3 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 11 Feb 2024 05:04:16 -0500 Subject: [PATCH 069/686] tf: support checkpoint path (instead of directory) in dp freeze (#3254) To have the same behavior between TF and PT. Signed-off-by: Jinzhe Zeng --- deepmd/main.py | 2 +- deepmd/tf/entrypoints/freeze.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/deepmd/main.py b/deepmd/main.py index ff7120c8e7..ab2f0c449e 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -275,7 +275,7 @@ def main_parser() -> argparse.ArgumentParser: "--checkpoint", type=str, default=".", - help="Path to checkpoint. TensorFlow backend: a folder; PyTorch backend: either a folder containing checkpoint, or a pt file", + help="Path to checkpoint, either a folder containing checkpoint or the checkpoint prefix", ) parser_frz.add_argument( "-o", diff --git a/deepmd/tf/entrypoints/freeze.py b/deepmd/tf/entrypoints/freeze.py index 9cb59f4c9d..228f8466cb 100755 --- a/deepmd/tf/entrypoints/freeze.py +++ b/deepmd/tf/entrypoints/freeze.py @@ -12,6 +12,9 @@ from os.path import ( abspath, ) +from pathlib import ( + Path, +) from typing import ( List, Optional, @@ -479,7 +482,7 @@ def freeze( Parameters ---------- checkpoint_folder : str - location of the folder with model + location of either the folder with checkpoint or the checkpoint prefix output : str output file name node_names : Optional[str], optional @@ -492,8 +495,11 @@ def freeze( other arguments """ # We retrieve our checkpoint fullpath - checkpoint = tf.train.get_checkpoint_state(checkpoint_folder) - input_checkpoint = checkpoint.model_checkpoint_path + if Path(checkpoint_folder).is_dir(): + checkpoint = tf.train.get_checkpoint_state(checkpoint_folder) + input_checkpoint = checkpoint.model_checkpoint_path + else: + input_checkpoint = checkpoint_folder # expand the output file to full path output_graph = abspath(output) From c131c8f7d2632f9692226e12769eaa6d4934f336 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 11 Feb 2024 05:06:53 -0500 Subject: [PATCH 070/686] ipi: remove normalize_coord (#3257) (1) Remove `normalize_coord` and add a test to confirm it still works; (2) Remove the include of `SimulationRegion.h` and the direct link to `libdeepmd.so`; (3) Allow using a pre-compiled C library to build `dp_ipi`. --------- Signed-off-by: Jinzhe Zeng --- doc/install/install-from-c-library.md | 5 ++-- source/CMakeLists.txt | 5 +--- source/ipi/CMakeLists.txt | 2 +- source/ipi/driver.cc | 23 --------------- source/ipi/tests/test_driver.py | 40 +++++++++++++++++++++++---- 5 files changed, 40 insertions(+), 35 deletions(-) diff --git a/doc/install/install-from-c-library.md b/doc/install/install-from-c-library.md index 36944f03e6..f1a5496b59 100644 --- a/doc/install/install-from-c-library.md +++ b/doc/install/install-from-c-library.md @@ -14,7 +14,7 @@ tar xzf libdeepmd_c.tar.gz The library is built in Linux (GLIBC 2.17) with CUDA 12.2 (`libdeepmd_c.tar.gz`) or 11.8 (`libdeepmd_c_cu11.tar.gz`). It's noted that this package does not contain CUDA Toolkit and cuDNN, so one needs to download them from the NVIDIA website. -## Use Pre-compiled C Library to build the LAMMPS plugin and GROMACS patch +## Use Pre-compiled C Library to build the LAMMPS plugin, i-PI driver, and GROMACS patch When one [installs DeePMD-kit's C++ interface](./install-from-source.md#install-deepmd-kits-c-interface), one can use the CMake argument `DEEPMD_C_ROOT` to the path `libdeepmd_c`. @@ -27,4 +27,5 @@ make -j8 make install ``` -Then one can follow the manual [Install LAMMPS](./install-lammps.md) and/or [Install GROMACS](./install-gromacs.md). +Then the i-PI driver `dp_ipi` will be built and installed. +One can also follow the manual [Install LAMMPS](./install-lammps.md) and/or [Install GROMACS](./install-gromacs.md). diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index a4971a4d2a..41e596f85e 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -297,10 +297,7 @@ if(BUILD_CPP_IF) endif() if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.8) # add_subdirectory (md/) - if(ENABLE_IPI - OR NOT BUILD_PY_IF - AND NOT DEEPMD_C_ROOT) - # ipi has a dependency on libdeepmd + if(ENABLE_IPI OR NOT BUILD_PY_IF) add_subdirectory(ipi/) endif() if(NOT BUILD_PY_IF) diff --git a/source/ipi/CMakeLists.txt b/source/ipi/CMakeLists.txt index 158f98aea5..5d571a1950 100644 --- a/source/ipi/CMakeLists.txt +++ b/source/ipi/CMakeLists.txt @@ -14,7 +14,7 @@ add_executable(${ipiname} ${DRIVER_SOURCE_FILES}) # link: libdeepmd_cc if(DP_USING_C_API) # SimulationRegion.h - target_link_libraries(${ipiname} PRIVATE ${LIB_DEEPMD_C} ${LIB_DEEPMD}) + target_link_libraries(${ipiname} PRIVATE ${LIB_DEEPMD_C}) target_precompile_headers(${ipiname} PRIVATE [["deepmd.hpp"]]) remove_definitions(-D_GLIBCXX_USE_CXX11_ABI=${OP_CXX_ABI}) else() diff --git a/source/ipi/driver.cc b/source/ipi/driver.cc index 1e3d92eb5e..9a91a27ad3 100644 --- a/source/ipi/driver.cc +++ b/source/ipi/driver.cc @@ -12,7 +12,6 @@ namespace deepmd_compat = deepmd; #include "deepmd.hpp" namespace deepmd_compat = deepmd::hpp; #endif -#include "SimulationRegion.h" #include "XyzFileManager.h" #include "json.hpp" #include "sockets.h" @@ -49,25 +48,6 @@ char *trimwhitespace(char *str) { return str; } -void normalize_coord(std::vector &coord, - const SimulationRegion ®ion) { - int natoms = coord.size() / 3; - - for (int ii = 0; ii < natoms; ++ii) { - double inter[3]; - region.phys2Inter(inter, &coord[3 * ii]); - for (int dd = 0; dd < 3; ++dd) { - inter[dd] -= int(floor(inter[dd])); - if (inter[dd] < 0) { - inter[dd] += 1.; - } else if (inter[dd] >= 1) { - inter[dd] -= 1.; - } - } - region.inter2Phys(&coord[3 * ii], inter); - } -} - int main(int argc, char *argv[]) { if (argc == 1) { std::cerr << "usage " << std::endl; @@ -122,7 +102,6 @@ int main(int argc, char *argv[]) { std::vector dcoord_tmp; std::vector dtype = cvt.get_type(); std::vector dbox(9, 0); - SimulationRegion region; double *msg_buff = NULL; double ener; double virial[9]; @@ -179,7 +158,6 @@ int main(int argc, char *argv[]) { for (int dd = 0; dd < 9; ++dd) { dbox[dd] = cell_h[(dd % 3) * 3 + (dd / 3)] * cvt_len; } - region.reinitBox(&dbox[0]); // get number of atoms readbuffer_(&socket, (char *)(&cbuf), sizeof(int32_t)); @@ -203,7 +181,6 @@ int main(int argc, char *argv[]) { dcoord_tmp[ii] = msg_buff[ii] * cvt_len; } cvt.forward(dcoord, dcoord_tmp, 3); - normalize_coord(dcoord, region); // nnp over writes ener, force and virial nnp_inter.compute(dener, dforce_tmp, dvirial, dcoord, dtype, dbox); diff --git a/source/ipi/tests/test_driver.py b/source/ipi/tests/test_driver.py index 78edeac977..1b2e1dd951 100644 --- a/source/ipi/tests/test_driver.py +++ b/source/ipi/tests/test_driver.py @@ -53,7 +53,7 @@ def write_input(self, atoms, **kwargs): atoms.write(self.xyz_file, format="xyz") -class TestDeepPotALargeBoxNoPBC(unittest.TestCase): +class TestDPIPI(unittest.TestCase): # copy from test_deeppot_a.py @classmethod def setUpClass(cls): @@ -193,8 +193,8 @@ def test_ase_unix(self): cell=self.box.reshape((3, 3)), calculator=calc, ) - ee = water.get_potential_energy() - ff = water.get_forces() + ee = water.get_potential_energy() + ff = water.get_forces() nframes = 1 np.testing.assert_almost_equal( ff.ravel(), self.expected_f.ravel(), default_places @@ -213,8 +213,38 @@ def test_ase_nounix(self): cell=self.box.reshape((3, 3)), calculator=calc, ) - ee = water.get_potential_energy() - ff = water.get_forces() + ee = water.get_potential_energy() + ff = water.get_forces() + nframes = 1 + np.testing.assert_almost_equal( + ff.ravel(), self.expected_f.ravel(), default_places + ) + expected_se = np.sum(self.expected_e.reshape([nframes, -1]), axis=1) + np.testing.assert_almost_equal(ee.ravel(), expected_se.ravel(), default_places) + + def test_normalize_coords(self): + # coordinate nomarlization should happen inside the interface + cell = self.box.reshape((3, 3)) + coord = self.coords.reshape((-1, 3)) + # random unwrap coords + coord[0] += np.array([3, 0, 0]) @ cell + coord[1] += np.array([0, -3, 0]) @ cell + coord[2] += np.array([0, 0, 3]) @ cell + coord[3] += np.array([-3, 0, 0]) @ cell + coord[4] += np.array([0, 3, 0]) @ cell + coord[5] += np.array([0, 0, -3]) @ cell + with SocketIOCalculator( + DPiPICalculator(self.model_file, use_unix=False), + log=sys.stdout, + ) as calc: + water = Atoms( + "OHHOHH", + positions=coord, + cell=cell, + calculator=calc, + ) + ee = water.get_potential_energy() + ff = water.get_forces() nframes = 1 np.testing.assert_almost_equal( ff.ravel(), self.expected_f.ravel(), default_places From beb1b98913ec92fc1141cdd3caa774906a2902b0 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 12 Feb 2024 09:28:16 -0500 Subject: [PATCH 071/686] add `get_type_map` method to model; export model methods (#3247) Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/model/dp_atomic_model.py | 4 ++++ deepmd/dpmodel/model/linear_atomic_model.py | 4 ++++ deepmd/dpmodel/model/make_base_atomic_model.py | 4 ++++ deepmd/dpmodel/model/pairtab_atomic_model.py | 3 +++ deepmd/pt/model/model/dp_atomic_model.py | 6 ++++++ deepmd/pt/model/model/linear_atomic_model.py | 6 ++++++ deepmd/pt/model/model/pairtab_atomic_model.py | 5 +++++ source/tests/pt/model/test_dp_atomic_model.py | 4 +++- source/tests/pt/model/test_dp_model.py | 8 ++++++-- source/tests/pt/model/test_linear_atomic_model.py | 10 ++++++++-- source/tests/pt/model/test_pairtab_atomic_model.py | 6 ++++++ 11 files changed, 55 insertions(+), 5 deletions(-) diff --git a/deepmd/dpmodel/model/dp_atomic_model.py b/deepmd/dpmodel/model/dp_atomic_model.py index 63c44aa1f8..4bb6cb1daf 100644 --- a/deepmd/dpmodel/model/dp_atomic_model.py +++ b/deepmd/dpmodel/model/dp_atomic_model.py @@ -62,6 +62,10 @@ def get_sel(self) -> List[int]: """Get the neighbor selection.""" return self.descriptor.get_sel() + def get_type_map(self) -> Optional[List[str]]: + """Get the type map.""" + return self.type_map + def distinguish_types(self) -> bool: """Returns if model requires a neighbor list that distinguish different atomic types or not. diff --git a/deepmd/dpmodel/model/linear_atomic_model.py b/deepmd/dpmodel/model/linear_atomic_model.py index dc7e9996c8..0da40307a6 100644 --- a/deepmd/dpmodel/model/linear_atomic_model.py +++ b/deepmd/dpmodel/model/linear_atomic_model.py @@ -62,6 +62,10 @@ def get_rcut(self) -> float: """Get the cut-off radius.""" return max(self.get_model_rcuts()) + def get_type_map(self) -> Optional[List[str]]: + """Get the type map.""" + raise NotImplementedError("TODO: get_type_map should be implemented") + def get_model_rcuts(self) -> List[float]: """Get the cut-off radius for each individual models.""" return [model.get_rcut() for model in self.models] diff --git a/deepmd/dpmodel/model/make_base_atomic_model.py b/deepmd/dpmodel/model/make_base_atomic_model.py index 84e685b973..080d9982c9 100644 --- a/deepmd/dpmodel/model/make_base_atomic_model.py +++ b/deepmd/dpmodel/model/make_base_atomic_model.py @@ -44,6 +44,10 @@ def get_rcut(self) -> float: """Get the cut-off radius.""" pass + @abstractmethod + def get_type_map(self) -> Optional[List[str]]: + """Get the type map.""" + @abstractmethod def get_sel(self) -> List[int]: """Returns the number of selected atoms for each type.""" diff --git a/deepmd/dpmodel/model/pairtab_atomic_model.py b/deepmd/dpmodel/model/pairtab_atomic_model.py index d4feb970fb..12073c7f63 100644 --- a/deepmd/dpmodel/model/pairtab_atomic_model.py +++ b/deepmd/dpmodel/model/pairtab_atomic_model.py @@ -82,6 +82,9 @@ def fitting_output_def(self) -> FittingOutputDef: def get_rcut(self) -> float: return self.rcut + def get_type_map(self) -> Optional[List[str]]: + raise NotImplementedError("TODO: get_type_map should be implemented") + def get_sel(self) -> List[int]: return [self.sel] diff --git a/deepmd/pt/model/model/dp_atomic_model.py b/deepmd/pt/model/model/dp_atomic_model.py index 273e79b86b..89b814edaa 100644 --- a/deepmd/pt/model/model/dp_atomic_model.py +++ b/deepmd/pt/model/model/dp_atomic_model.py @@ -67,10 +67,16 @@ def fitting_output_def(self) -> FittingOutputDef: else self.coord_denoise_net.output_def() ) + @torch.jit.export def get_rcut(self) -> float: """Get the cut-off radius.""" return self.rcut + @torch.jit.export + def get_type_map(self) -> List[str]: + """Get the type map.""" + return self.type_map + def get_sel(self) -> List[int]: """Get the neighbor selection.""" return self.sel diff --git a/deepmd/pt/model/model/linear_atomic_model.py b/deepmd/pt/model/model/linear_atomic_model.py index 8b50f5e4f5..0d54e4c091 100644 --- a/deepmd/pt/model/model/linear_atomic_model.py +++ b/deepmd/pt/model/model/linear_atomic_model.py @@ -61,10 +61,16 @@ def distinguish_types(self) -> bool: """If distinguish different types by sorting.""" return False + @torch.jit.export def get_rcut(self) -> float: """Get the cut-off radius.""" return max(self.get_model_rcuts()) + @torch.jit.export + def get_type_map(self) -> List[str]: + """Get the type map.""" + raise NotImplementedError("TODO: implement this method") + def get_model_rcuts(self) -> List[float]: """Get the cut-off radius for each individual models.""" return [model.get_rcut() for model in self.models] diff --git a/deepmd/pt/model/model/pairtab_atomic_model.py b/deepmd/pt/model/model/pairtab_atomic_model.py index 2837aaffe7..0ef1448398 100644 --- a/deepmd/pt/model/model/pairtab_atomic_model.py +++ b/deepmd/pt/model/model/pairtab_atomic_model.py @@ -95,9 +95,14 @@ def fitting_output_def(self) -> FittingOutputDef: ] ) + @torch.jit.export def get_rcut(self) -> float: return self.rcut + @torch.jit.export + def get_type_map(self) -> Optional[List[str]]: + raise NotImplementedError("TODO: implement this method") + def get_sel(self) -> List[int]: return [self.sel] diff --git a/source/tests/pt/model/test_dp_atomic_model.py b/source/tests/pt/model/test_dp_atomic_model.py index ef25e574d4..fb7e684eaa 100644 --- a/source/tests/pt/model/test_dp_atomic_model.py +++ b/source/tests/pt/model/test_dp_atomic_model.py @@ -107,4 +107,6 @@ def test_jit(self): ).to(env.DEVICE) type_map = ["foo", "bar"] md0 = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) - torch.jit.script(md0) + md0 = torch.jit.script(md0) + self.assertEqual(md0.get_rcut(), self.rcut) + self.assertEqual(md0.get_type_map(), type_map) diff --git a/source/tests/pt/model/test_dp_model.py b/source/tests/pt/model/test_dp_model.py index 6e009d3934..f3f899fbe2 100644 --- a/source/tests/pt/model/test_dp_model.py +++ b/source/tests/pt/model/test_dp_model.py @@ -284,7 +284,9 @@ def test_jit(self): ).to(env.DEVICE) type_map = ["foo", "bar"] md0 = DPModel(ds, ft, type_map=type_map).to(env.DEVICE) - torch.jit.script(md0) + md0 = torch.jit.script(md0) + md0.get_rcut() + md0.get_type_map() class TestDPModelFormatNlist(unittest.TestCase): @@ -521,4 +523,6 @@ def test_jit(self): ).to(env.DEVICE) type_map = ["foo", "bar"] md0 = EnergyModel(ds, ft, type_map=type_map).to(env.DEVICE) - torch.jit.script(md0) + md0 = torch.jit.script(md0) + self.assertEqual(md0.get_rcut(), self.rcut) + self.assertEqual(md0.get_type_map(), type_map) diff --git a/source/tests/pt/model/test_linear_atomic_model.py b/source/tests/pt/model/test_linear_atomic_model.py index e0247f911f..14fc6b386a 100644 --- a/source/tests/pt/model/test_linear_atomic_model.py +++ b/source/tests/pt/model/test_linear_atomic_model.py @@ -171,8 +171,14 @@ def test_self_consistency(self): ) def test_jit(self): - torch.jit.script(self.md1) - torch.jit.script(self.md3) + md1 = torch.jit.script(self.md1) + self.assertEqual(md1.get_rcut(), self.rcut) + with self.assertRaises(torch.jit.Error): + self.assertEqual(md1.get_type_map(), ["foo", "bar"]) + md3 = torch.jit.script(self.md3) + self.assertEqual(md3.get_rcut(), self.rcut) + with self.assertRaises(torch.jit.Error): + self.assertEqual(md3.get_type_map(), ["foo", "bar"]) if __name__ == "__main__": diff --git a/source/tests/pt/model/test_pairtab_atomic_model.py b/source/tests/pt/model/test_pairtab_atomic_model.py index 23718c134a..f58ac76211 100644 --- a/source/tests/pt/model/test_pairtab_atomic_model.py +++ b/source/tests/pt/model/test_pairtab_atomic_model.py @@ -82,6 +82,9 @@ def test_with_mask(self): def test_jit(self): model = torch.jit.script(self.model) + self.assertEqual(model.get_rcut(), 0.02) + with self.assertRaises(torch.jit.Error): + self.assertEqual(model.get_type_map(), None) def test_deserialize(self): model1 = PairTabModel.deserialize(self.model.serialize()) @@ -101,6 +104,9 @@ def test_deserialize(self): ) model1 = torch.jit.script(model1) + self.assertEqual(model1.get_rcut(), 0.02) + with self.assertRaises(torch.jit.Error): + self.assertEqual(model1.get_type_map(), None) def test_cross_deserialize(self): model_dict = self.model.serialize() # pytorch model to dict From 1bdc60dafd6e54bdb74915904b4aa7b0ffce7483 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:18:35 +0800 Subject: [PATCH 072/686] [pre-commit.ci] pre-commit autoupdate (#3264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.0 → v0.2.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.2.1) - [github.com/scop/pre-commit-shfmt: v3.7.0-4 → v3.8.0-1](https://github.com/scop/pre-commit-shfmt/compare/v3.7.0-4...v3.8.0-1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 85486e3901..3b16a4cc67 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^source/3rdparty - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.2.0 + rev: v0.2.1 hooks: - id: ruff args: ["--fix"] @@ -64,7 +64,7 @@ repos: - id: csslint # Shell - repo: https://github.com/scop/pre-commit-shfmt - rev: v3.7.0-4 + rev: v3.8.0-1 hooks: - id: shfmt # CMake From 392b9e0c5e44c1f1f93b2db2e63fa36ebcfb7914 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 13 Feb 2024 02:19:46 -0500 Subject: [PATCH 073/686] pt: support loading frozen models in DeepEval (#3253) Signed-off-by: Jinzhe Zeng --- deepmd/main.py | 4 ++-- deepmd/pt/entrypoints/main.py | 10 ++++++--- deepmd/pt/infer/deep_eval.py | 32 ++++++++++++++++----------- source/tests/pt/model/test_deeppot.py | 17 ++++++++++++++ 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/deepmd/main.py b/deepmd/main.py index ab2f0c449e..bede2cf1fe 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -329,7 +329,7 @@ def main_parser() -> argparse.ArgumentParser: "--model", default="frozen_model", type=str, - help="Frozen model file (prefix) to import. TensorFlow backend: suffix is .pb; PyTorch backend: suffix is .pt", + help="Frozen model file (prefix) to import. TensorFlow backend: suffix is .pb; PyTorch backend: suffix is .pth.", ) parser_tst_subgroup = parser_tst.add_mutually_exclusive_group() parser_tst_subgroup.add_argument( @@ -512,7 +512,7 @@ def main_parser() -> argparse.ArgumentParser: default=["graph.000", "graph.001", "graph.002", "graph.003"], nargs="+", type=str, - help="Frozen models file (prefix) to import. TensorFlow backend: suffix is .pb; PyTorch backend: suffix is .pt.", + help="Frozen models file (prefix) to import. TensorFlow backend: suffix is .pb; PyTorch backend: suffix is .pth.", ) parser_model_devi.add_argument( "-s", diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 702fc6f317..bea063a261 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -301,7 +301,11 @@ def main(args: Optional[Union[List[str], argparse.Namespace]] = None): if FLAGS.command == "train": train(FLAGS) elif FLAGS.command == "test": - dict_args["output"] = str(Path(FLAGS.model).with_suffix(".pt")) + dict_args["output"] = ( + str(Path(FLAGS.model).with_suffix(".pth")) + if Path(FLAGS.model).suffix not in (".pt", ".pth") + else FLAGS.model + ) test(**dict_args) elif FLAGS.command == "freeze": if Path(FLAGS.checkpoint_folder).is_dir(): @@ -316,8 +320,8 @@ def main(args: Optional[Union[List[str], argparse.Namespace]] = None): doc_train_input(**dict_args) elif FLAGS.command == "model-devi": dict_args["models"] = [ - str(Path(mm).with_suffix(".pt")) - if Path(mm).suffix not in (".pb", ".pt") + str(Path(mm).with_suffix(".pth")) + if Path(mm).suffix not in (".pb", ".pt", ".pth") else mm for mm in dict_args["models"] ] diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index 894d307d04..9542ff33b1 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -91,19 +91,25 @@ def __init__( ): self.output_def = output_def self.model_path = model_file - state_dict = torch.load(model_file, map_location=env.DEVICE) - if "model" in state_dict: - state_dict = state_dict["model"] - self.input_param = state_dict["_extra_state"]["model_params"] - self.input_param["resuming"] = True - self.multi_task = "model_dict" in self.input_param - assert not self.multi_task, "multitask mode currently not supported!" - self.type_split = self.input_param["descriptor"]["type"] in ["se_e2_a"] - self.type_map = self.input_param["type_map"] - self.dp = ModelWrapper(get_model(self.input_param).to(DEVICE)) - self.dp.load_state_dict(state_dict) - self.rcut = self.dp.model["Default"].descriptor.get_rcut() - self.sec = np.cumsum(self.dp.model["Default"].descriptor.get_sel()) + if str(self.model_path).endswith(".pt"): + state_dict = torch.load(model_file, map_location=env.DEVICE) + if "model" in state_dict: + state_dict = state_dict["model"] + self.input_param = state_dict["_extra_state"]["model_params"] + self.input_param["resuming"] = True + self.multi_task = "model_dict" in self.input_param + assert not self.multi_task, "multitask mode currently not supported!" + model = get_model(self.input_param).to(DEVICE) + model = torch.jit.script(model) + self.dp = ModelWrapper(model) + self.dp.load_state_dict(state_dict) + elif str(self.model_path).endswith(".pth"): + model = torch.jit.load(model_file, map_location=env.DEVICE) + self.dp = ModelWrapper(model) + else: + raise ValueError("Unknown model file format!") + self.rcut = self.dp.model["Default"].get_rcut() + self.type_map = self.dp.model["Default"].get_type_map() if isinstance(auto_batch_size, bool): if auto_batch_size: self.auto_batch_size = AutoBatchSize() diff --git a/source/tests/pt/model/test_deeppot.py b/source/tests/pt/model/test_deeppot.py index d56f08d17c..3ef901d0e3 100644 --- a/source/tests/pt/model/test_deeppot.py +++ b/source/tests/pt/model/test_deeppot.py @@ -1,6 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import json import unittest +from argparse import ( + Namespace, +) from copy import ( deepcopy, ) @@ -12,6 +15,7 @@ from deepmd.infer.deep_pot import DeepPot as DeepPotUni from deepmd.pt.entrypoints.main import ( + freeze, get_trainer, ) from deepmd.pt.infer.deep_eval import ( @@ -95,3 +99,16 @@ def test_uni(self): dp = DeepPotUni("model.pt") self.assertIsInstance(dp, DeepPot) # its methods has been tested in test_dp_test + + +class TestDeepPotFrozen(TestDeepPot): + def setUp(self): + super().setUp() + frozen_model = "frozen_model.pth" + ns = Namespace( + model=self.model, + output=frozen_model, + head=None, + ) + freeze(ns) + self.model = frozen_model From 6018e3c57e70e68d8d7c106b75e55bdca5576f36 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:20:57 +0800 Subject: [PATCH 074/686] fix bug of not passing params in model (#3260) Co-authored-by: Han Wang Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/pt/model/model/ener.py | 21 +++++++++++++++++++-- deepmd/pt/model/model/make_model.py | 8 ++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/deepmd/pt/model/model/ener.py b/deepmd/pt/model/model/ener.py index ea35cf5a82..3c0b66edcd 100644 --- a/deepmd/pt/model/model/ener.py +++ b/deepmd/pt/model/model/ener.py @@ -43,6 +43,9 @@ def forward( coord, atype, box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -63,13 +66,18 @@ def forward_lower( extended_atype, nlist, mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, do_atomic_virial: bool = False, ): model_ret = self.forward_common_lower( extended_coord, extended_atype, nlist, - mapping, + mapping=mapping, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -109,7 +117,12 @@ def forward( do_atomic_virial: bool = False, ) -> Dict[str, torch.Tensor]: model_ret = self.forward_common( - coord, atype, box, do_atomic_virial=do_atomic_virial + coord, + atype, + box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, ) if self.fitting_net is not None: model_predict = {} @@ -135,6 +148,8 @@ def forward_lower( extended_atype, nlist, mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, do_atomic_virial: bool = False, ): model_ret = self.forward_common_lower( @@ -142,6 +157,8 @@ def forward_lower( extended_atype, nlist, mapping, + fparam=fparam, + aparam=aparam, do_atomic_virial=do_atomic_virial, ) if self.fitting_net is not None: diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 8a863b8cdc..9191f8c58f 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -83,6 +83,10 @@ def forward_common( The type of atoms. shape: nf x nloc box The simulation box. shape: nf x 9 + fparam + frame parameter. nf x ndf + aparam + atomic parameter. nf x nloc x nda do_atomic_virial If calculate the atomic virial. @@ -155,6 +159,10 @@ def forward_common_lower( neighbor list. nf x nloc x nsel. mapping mapps the extended indices to local indices. nf x nall. + fparam + frame parameter. nf x ndf + aparam + atomic parameter. nf x nloc x nda do_atomic_virial whether calculate atomic virial. From 398eb7a4e9230ab160bb5bc6fd3ad56ad104cf2c Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 13 Feb 2024 09:37:21 -0500 Subject: [PATCH 075/686] enable docstring code format (#3267) Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/infer/deep_eval.py | 2 +- deepmd/infer/deep_pot.py | 6 +++--- deepmd/infer/model_devi.py | 4 ++-- deepmd/tf/common.py | 12 ++++++------ deepmd/tf/descriptor/descriptor.py | 2 +- deepmd/tf/entrypoints/neighbor_stat.py | 25 ++++++++++++++++++++++++- deepmd/tf/infer/deep_eval.py | 4 ++-- deepmd/tf/utils/parallel_op.py | 2 -- deepmd/utils/plugin.py | 2 +- pyproject.toml | 3 +++ source/install/build_tf.py | 2 +- 11 files changed, 44 insertions(+), 20 deletions(-) diff --git a/deepmd/infer/deep_eval.py b/deepmd/infer/deep_eval.py index b1d17afc8b..3b1eceb16d 100644 --- a/deepmd/infer/deep_eval.py +++ b/deepmd/infer/deep_eval.py @@ -467,7 +467,7 @@ def eval_typeebd(self) -> np.ndarray: Get the output of type embedding network of `graph.pb`: >>> from deepmd.infer import DeepPotential - >>> dp = DeepPotential('graph.pb') + >>> dp = DeepPotential("graph.pb") >>> dp.eval_typeebd() """ return self.deep_eval.eval_typeebd() diff --git a/deepmd/infer/deep_pot.py b/deepmd/infer/deep_pot.py index 463a07115c..e955a3ed65 100644 --- a/deepmd/infer/deep_pot.py +++ b/deepmd/infer/deep_pot.py @@ -43,10 +43,10 @@ class DeepPot(DeepEval): -------- >>> from deepmd.infer import DeepPot >>> import numpy as np - >>> dp = DeepPot('graph.pb') - >>> coord = np.array([[1,0,0], [0,0,1.5], [1,0,3]]).reshape([1, -1]) + >>> dp = DeepPot("graph.pb") + >>> coord = np.array([[1, 0, 0], [0, 0, 1.5], [1, 0, 3]]).reshape([1, -1]) >>> cell = np.diag(10 * np.ones(3)).reshape([1, -1]) - >>> atype = [1,0,1] + >>> atype = [1, 0, 1] >>> e, f, v = dp.eval(coord, cell, atype) where `e`, `f` and `v` are predicted energy, force and virial of the system, respectively. diff --git a/deepmd/infer/model_devi.py b/deepmd/infer/model_devi.py index 1eb639ed68..cb5d79797b 100644 --- a/deepmd/infer/model_devi.py +++ b/deepmd/infer/model_devi.py @@ -296,9 +296,9 @@ def calc_model_devi( >>> from deepmd.tf.infer import calc_model_devi >>> from deepmd.tf.infer import DeepPot as DP >>> import numpy as np - >>> coord = np.array([[1,0,0], [0,0,1.5], [1,0,3]]).reshape([1, -1]) + >>> coord = np.array([[1, 0, 0], [0, 0, 1.5], [1, 0, 3]]).reshape([1, -1]) >>> cell = np.diag(10 * np.ones(3)).reshape([1, -1]) - >>> atype = [1,0,1] + >>> atype = [1, 0, 1] >>> graphs = [DP("graph.000.pb"), DP("graph.001.pb")] >>> model_devi = calc_model_devi(coord, cell, atype, graphs) """ diff --git a/deepmd/tf/common.py b/deepmd/tf/common.py index 9d2f4ee1a7..b1872e72ed 100644 --- a/deepmd/tf/common.py +++ b/deepmd/tf/common.py @@ -243,13 +243,13 @@ def cast_precision(func: Callable) -> Callable: Examples -------- >>> class A: - ... @property - ... def precision(self): - ... return tf.float32 + ... @property + ... def precision(self): + ... return tf.float32 ... - ... @cast_precision - ... def f(x: tf.Tensor, y: tf.Tensor) -> tf.Tensor: - ... return x ** 2 + y + ... @cast_precision + ... def f(x: tf.Tensor, y: tf.Tensor) -> tf.Tensor: + ... return x**2 + y """ @wraps(func) diff --git a/deepmd/tf/descriptor/descriptor.py b/deepmd/tf/descriptor/descriptor.py index 9bdda3ec37..fe49fe11fe 100644 --- a/deepmd/tf/descriptor/descriptor.py +++ b/deepmd/tf/descriptor/descriptor.py @@ -32,7 +32,7 @@ class Descriptor(PluginVariant): Examples -------- - >>> descript = Descriptor(type="se_e2_a", rcut=6., rcut_smth=0.5, sel=[50]) + >>> descript = Descriptor(type="se_e2_a", rcut=6.0, rcut_smth=0.5, sel=[50]) >>> type(descript) diff --git a/deepmd/tf/entrypoints/neighbor_stat.py b/deepmd/tf/entrypoints/neighbor_stat.py index d2e8a01e82..e0999bed4a 100644 --- a/deepmd/tf/entrypoints/neighbor_stat.py +++ b/deepmd/tf/entrypoints/neighbor_stat.py @@ -42,7 +42,30 @@ def neighbor_stat( Examples -------- - >>> neighbor_stat(system='.', rcut=6., type_map=["C", "H", "O", "N", "P", "S", "Mg", "Na", "HW", "OW", "mNa", "mCl", "mC", "mH", "mMg", "mN", "mO", "mP"]) + >>> neighbor_stat( + ... system=".", + ... rcut=6.0, + ... type_map=[ + ... "C", + ... "H", + ... "O", + ... "N", + ... "P", + ... "S", + ... "Mg", + ... "Na", + ... "HW", + ... "OW", + ... "mNa", + ... "mCl", + ... "mC", + ... "mH", + ... "mMg", + ... "mN", + ... "mO", + ... "mP", + ... ], + ... ) min_nbor_dist: 0.6599510670195264 max_nbor_size: [23, 26, 19, 16, 2, 2, 1, 1, 72, 37, 5, 0, 31, 29, 1, 21, 20, 5] """ diff --git a/deepmd/tf/infer/deep_eval.py b/deepmd/tf/infer/deep_eval.py index c87a888c37..27152b9f12 100644 --- a/deepmd/tf/infer/deep_eval.py +++ b/deepmd/tf/infer/deep_eval.py @@ -514,7 +514,7 @@ def eval_typeebd(self) -> np.ndarray: Get the output of type embedding network of `graph.pb`: >>> from deepmd.tf.infer import DeepPotential - >>> dp = DeepPotential('graph.pb') + >>> dp = DeepPotential("graph.pb") >>> dp.eval_typeebd() """ t_typeebd = self._get_tensor("t_typeebd:0") @@ -1429,7 +1429,7 @@ def eval_typeebd(self) -> np.ndarray: Get the output of type embedding network of `graph.pb`: >>> from deepmd.tf.infer import DeepPotential - >>> dp = DeepPotential('graph.pb') + >>> dp = DeepPotential("graph.pb") >>> dp.eval_typeebd() """ t_typeebd = self._get_tensor("t_typeebd:0") diff --git a/deepmd/tf/utils/parallel_op.py b/deepmd/tf/utils/parallel_op.py index b7590ed720..5eeb1fab7f 100644 --- a/deepmd/tf/utils/parallel_op.py +++ b/deepmd/tf/utils/parallel_op.py @@ -35,12 +35,10 @@ class ParallelOp: >>> def builder(): ... x = tf.placeholder(tf.int32, [1]) ... return {"x": x}, (x + 1) - ... >>> p = ParallelOp(builder, nthreads=4) >>> def feed(): ... for ii in range(10): ... yield {"x": [ii]} - ... >>> print(*p.generate(tf.Session(), feed())) [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] """ diff --git a/deepmd/utils/plugin.py b/deepmd/utils/plugin.py index 2a77b744c5..a564ed61af 100644 --- a/deepmd/utils/plugin.py +++ b/deepmd/utils/plugin.py @@ -24,7 +24,7 @@ class Plugin: >>> @plugin.register("xx") def xxx(): pass - >>> print(plugin.plugins['xx']) + >>> print(plugin.plugins["xx"]) """ def __init__(self): diff --git a/pyproject.toml b/pyproject.toml index 1701e10bb8..a71c3d1709 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -223,6 +223,9 @@ ignore = "D413, D416, D203, D107, D213" profile = "black" force_grid_wrap = 1 +[tool.ruff.format] +docstring-code-format = true + [tool.ruff.lint] select = [ "E", # errors diff --git a/source/install/build_tf.py b/source/install/build_tf.py index 3e3700b9ac..d639e2cf51 100755 --- a/source/install/build_tf.py +++ b/source/install/build_tf.py @@ -294,7 +294,7 @@ def set_directory(path: Path): Examples -------- >>> with set_directory("some_path"): - ... do_something() + ... do_something() """ cwd = Path().absolute() path.mkdir(exist_ok=True, parents=True) From e41b091e8c5be18b98c375ee8ddbbe55f5552f63 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Wed, 14 Feb 2024 00:20:48 +0800 Subject: [PATCH 076/686] breaking: pt: remove data preprocess from data stat (#3261) This PR: - Remove data preprocess from data stat. - Cleanup dependency of data preprocess in dataset and dataloader. Note that: - `DeepmdDataSystem` still has dependency for PyTorch, which leaves for @CaRoLZhangxy to clean up. - Denoise part in `DeepmdDataSystem` still needs further clean up, which leaves for @Chengqian-Zhang. --------- Signed-off-by: Duo <50307526+iProzd@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/pt/model/descriptor/dpa1.py | 8 +- deepmd/pt/model/descriptor/dpa2.py | 11 +- deepmd/pt/model/descriptor/hybrid.py | 10 +- deepmd/pt/model/descriptor/repformers.py | 46 +-- deepmd/pt/model/descriptor/se_a.py | 46 ++- deepmd/pt/model/descriptor/se_atten.py | 42 ++- deepmd/pt/model/model/make_model.py | 27 +- deepmd/pt/model/task/ener.py | 2 +- deepmd/pt/utils/dataloader.py | 21 +- deepmd/pt/utils/dataset.py | 290 +----------------- deepmd/pt/utils/nlist.py | 32 ++ deepmd/pt/utils/stat.py | 30 +- source/tests/pt/model/test_descriptor.py | 52 +++- source/tests/pt/model/test_descriptor_dpa1.py | 48 ++- source/tests/pt/model/test_descriptor_dpa2.py | 81 ++--- source/tests/pt/model/test_dp_model.py | 12 +- source/tests/pt/model/test_embedding_net.py | 55 +++- source/tests/pt/model/test_model.py | 4 +- source/tests/pt/test_loss.py | 13 +- source/tests/pt/test_stat.py | 5 +- 20 files changed, 325 insertions(+), 510 deletions(-) diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index 6c1331ec1d..76cff174af 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -1,5 +1,4 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import logging from typing import ( List, Optional, @@ -18,8 +17,6 @@ DescrptBlockSeAtten, ) -log = logging.getLogger(__name__) - @Descriptor.register("dpa1") @Descriptor.register("se_atten") @@ -112,7 +109,7 @@ def distinguish_types(self) -> bool: """Returns if the descriptor requires a neighbor list that distinguish different atomic types or not. """ - return False + return self.se_atten.distinguish_types() @property def dim_out(self): @@ -128,7 +125,7 @@ def compute_input_stats(self, merged): def init_desc_stat( self, sumr=None, suma=None, sumn=None, sumr2=None, suma2=None, **kwargs ): - assert True not in [x is None for x in [sumr, suma, sumn, sumr2, suma2]] + assert all(x is not None for x in [sumr, suma, sumn, sumr2, suma2]) self.se_atten.init_desc_stat(sumr, suma, sumn, sumr2, suma2) @classmethod @@ -141,6 +138,7 @@ def get_stat_name( """ descrpt_type = type_name assert descrpt_type in ["dpa1", "se_atten"] + assert all(x is not None for x in [rcut, rcut_smth, sel]) return f"stat_file_descrpt_dpa1_rcut{rcut:.2f}_smth{rcut_smth:.2f}_sel{sel}_ntypes{ntypes}.npz" @classmethod diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index 05e7cec658..6cefaf6f38 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -1,5 +1,4 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import logging from typing import ( List, Optional, @@ -27,8 +26,6 @@ DescrptBlockSeAtten, ) -log = logging.getLogger(__name__) - @Descriptor.register("dpa2") class DescrptDPA2(Descriptor): @@ -316,7 +313,7 @@ def compute_input_stats(self, merged): def init_desc_stat( self, sumr=None, suma=None, sumn=None, sumr2=None, suma2=None, **kwargs ): - assert True not in [x is None for x in [sumr, suma, sumn, sumr2, suma2]] + assert all(x is not None for x in [sumr, suma, sumn, sumr2, suma2]) for ii, descrpt in enumerate([self.repinit, self.repformers]): stat_dict_ii = { "sumr": sumr[ii], @@ -346,8 +343,8 @@ def get_stat_name( """ descrpt_type = type_name assert descrpt_type in ["dpa2"] - assert True not in [ - x is None + assert all( + x is not None for x in [ repinit_rcut, repinit_rcut_smth, @@ -356,7 +353,7 @@ def get_stat_name( repformer_rcut_smth, repformer_nsel, ] - ] + ) return ( f"stat_file_descrpt_dpa2_repinit_rcut{repinit_rcut:.2f}_smth{repinit_rcut_smth:.2f}_sel{repinit_nsel}" f"_repformer_rcut{repformer_rcut:.2f}_smth{repformer_rcut_smth:.2f}_sel{repformer_nsel}_ntypes{ntypes}.npz" diff --git a/deepmd/pt/model/descriptor/hybrid.py b/deepmd/pt/model/descriptor/hybrid.py index fb7e374ede..c5c08c760d 100644 --- a/deepmd/pt/model/descriptor/hybrid.py +++ b/deepmd/pt/model/descriptor/hybrid.py @@ -103,6 +103,14 @@ def get_dim_in(self) -> int: def get_dim_emb(self): return self.dim_emb + def distinguish_types(self) -> bool: + """Returns if the descriptor requires a neighbor list that distinguish different + atomic types or not. + """ + return any( + descriptor.distinguish_types() for descriptor in self.descriptor_list + ) + @property def dim_out(self): """Returns the output dimension of this descriptor.""" @@ -170,7 +178,7 @@ def compute_input_stats(self, merged): def init_desc_stat( self, sumr=None, suma=None, sumn=None, sumr2=None, suma2=None, **kwargs ): - assert True not in [x is None for x in [sumr, suma, sumn, sumr2, suma2]] + assert all(x is not None for x in [sumr, suma, sumn, sumr2, suma2]) for ii, descrpt in enumerate(self.descriptor_list): stat_dict_ii = { "sumr": sumr[ii], diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py index 0a302b6f92..26467124b8 100644 --- a/deepmd/pt/model/descriptor/repformers.py +++ b/deepmd/pt/model/descriptor/repformers.py @@ -21,7 +21,7 @@ env, ) from deepmd.pt.utils.nlist import ( - build_neighbor_list, + extend_input_and_build_neighbor_list, ) from deepmd.pt.utils.utils import ( get_activation_fn, @@ -178,6 +178,12 @@ def get_dim_emb(self) -> int: """Returns the embedding dimension g2.""" return self.g2_dim + def distinguish_types(self) -> bool: + """Returns if the descriptor requires a neighbor list that distinguish different + atomic types or not. + """ + return False + @property def dim_out(self): """Returns the output dimension of this descriptor.""" @@ -272,28 +278,29 @@ def compute_input_stats(self, merged): suma2 = [] mixed_type = "real_natoms_vec" in merged[0] for system in merged: - index = system["mapping"].unsqueeze(-1).expand(-1, -1, 3) - extended_coord = torch.gather(system["coord"], dim=1, index=index) - extended_coord = extended_coord - system["shift"] - index = system["mapping"] - extended_atype = torch.gather(system["atype"], dim=1, index=index) - nloc = system["atype"].shape[-1] - ####################################################### - # dirty hack here! the interface of dataload should be - # redesigned to support descriptors like dpa2 - ####################################################### - nlist = build_neighbor_list( + coord, atype, box, natoms = ( + system["coord"], + system["atype"], + system["box"], + system["natoms"], + ) + ( extended_coord, extended_atype, - nloc, - self.rcut, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + coord, + atype, + self.get_rcut(), self.get_sel(), - distinguish_types=False, + distinguish_types=self.distinguish_types(), + box=box, ) env_mat, _, _ = prod_env_mat_se_a( extended_coord, nlist, - system["atype"], + atype, self.mean, self.stddev, self.rcut, @@ -301,15 +308,16 @@ def compute_input_stats(self, merged): ) if not mixed_type: sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( - env_mat.detach().cpu().numpy(), ndescrpt, system["natoms"] + env_mat.detach().cpu().numpy(), ndescrpt, natoms ) else: + real_natoms_vec = system["real_natoms_vec"] sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( env_mat.detach().cpu().numpy(), ndescrpt, - system["real_natoms_vec"], + real_natoms_vec, mixed_type=mixed_type, - real_atype=system["atype"].detach().cpu().numpy(), + real_atype=atype.detach().cpu().numpy(), ) sumr.append(sysr) suma.append(sysa) diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 82e7e5185a..700bf6d59b 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -1,5 +1,4 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import logging from typing import ( ClassVar, List, @@ -37,8 +36,9 @@ from deepmd.pt.model.network.network import ( TypeFilter, ) - -log = logging.getLogger(__name__) +from deepmd.pt.utils.nlist import ( + extend_input_and_build_neighbor_list, +) @Descriptor.register("se_e2_a") @@ -100,7 +100,7 @@ def distinguish_types(self): """Returns if the descriptor requires a neighbor list that distinguish different atomic types or not. """ - return True + return self.sea.distinguish_types() @property def dim_out(self): @@ -114,7 +114,7 @@ def compute_input_stats(self, merged): def init_desc_stat( self, sumr=None, suma=None, sumn=None, sumr2=None, suma2=None, **kwargs ): - assert True not in [x is None for x in [sumr, suma, sumn, sumr2, suma2]] + assert all(x is not None for x in [sumr, suma, sumn, sumr2, suma2]) self.sea.init_desc_stat(sumr, suma, sumn, sumr2, suma2) @classmethod @@ -127,7 +127,7 @@ def get_stat_name( """ descrpt_type = type_name assert descrpt_type in ["se_e2_a"] - assert True not in [x is None for x in [rcut, rcut_smth, sel]] + assert all(x is not None for x in [rcut, rcut_smth, sel]) return f"stat_file_descrpt_sea_rcut{rcut:.2f}_smth{rcut_smth:.2f}_sel{sel}_ntypes{ntypes}.npz" @classmethod @@ -347,6 +347,12 @@ def get_dim_in(self) -> int: """Returns the input dimension.""" return self.dim_in + def distinguish_types(self) -> bool: + """Returns if the descriptor requires a neighbor list that distinguish different + atomic types or not. + """ + return True + @property def dim_out(self): """Returns the output dimension of this descriptor.""" @@ -381,20 +387,36 @@ def compute_input_stats(self, merged): sumr2 = [] suma2 = [] for system in merged: - index = system["mapping"].unsqueeze(-1).expand(-1, -1, 3) - extended_coord = torch.gather(system["coord"], dim=1, index=index) - extended_coord = extended_coord - system["shift"] + coord, atype, box, natoms = ( + system["coord"], + system["atype"], + system["box"], + system["natoms"], + ) + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + coord, + atype, + self.get_rcut(), + self.get_sel(), + distinguish_types=self.distinguish_types(), + box=box, + ) env_mat, _, _ = prod_env_mat_se_a( extended_coord, - system["nlist"], - system["atype"], + nlist, + atype, self.mean, self.stddev, self.rcut, self.rcut_smth, ) sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( - env_mat.detach().cpu().numpy(), self.ndescrpt, system["natoms"] + env_mat.detach().cpu().numpy(), self.ndescrpt, natoms ) sumr.append(sysr) suma.append(sysa) diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index 3469d43e40..d4dc0cd054 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -21,6 +21,9 @@ from deepmd.pt.utils import ( env, ) +from deepmd.pt.utils.nlist import ( + extend_input_and_build_neighbor_list, +) @DescriptorBlock.register("se_atten") @@ -161,6 +164,12 @@ def get_dim_emb(self) -> int: """Returns the output dimension of embedding.""" return self.filter_neuron[-1] + def distinguish_types(self) -> bool: + """Returns if the descriptor requires a neighbor list that distinguish different + atomic types or not. + """ + return False + @property def dim_out(self): """Returns the output dimension of this descriptor.""" @@ -185,13 +194,29 @@ def compute_input_stats(self, merged): suma2 = [] mixed_type = "real_natoms_vec" in merged[0] for system in merged: - index = system["mapping"].unsqueeze(-1).expand(-1, -1, 3) - extended_coord = torch.gather(system["coord"], dim=1, index=index) - extended_coord = extended_coord - system["shift"] + coord, atype, box, natoms = ( + system["coord"], + system["atype"], + system["box"], + system["natoms"], + ) + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + coord, + atype, + self.get_rcut(), + self.get_sel(), + distinguish_types=self.distinguish_types(), + box=box, + ) env_mat, _, _ = prod_env_mat_se_a( extended_coord, - system["nlist"], - system["atype"], + nlist, + atype, self.mean, self.stddev, self.rcut, @@ -199,15 +224,16 @@ def compute_input_stats(self, merged): ) if not mixed_type: sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( - env_mat.detach().cpu().numpy(), self.ndescrpt, system["natoms"] + env_mat.detach().cpu().numpy(), self.ndescrpt, natoms ) else: + real_natoms_vec = system["real_natoms_vec"] sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( env_mat.detach().cpu().numpy(), self.ndescrpt, - system["real_natoms_vec"], + real_natoms_vec, mixed_type=mixed_type, - real_atype=system["atype"].detach().cpu().numpy(), + real_atype=atype.detach().cpu().numpy(), ) sumr.append(sysr) suma.append(sysa) diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 9191f8c58f..a68ddd45a5 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -14,13 +14,9 @@ fit_output_to_model_output, ) from deepmd.pt.utils.nlist import ( - build_neighbor_list, - extend_coord_with_ghosts, + extend_input_and_build_neighbor_list, nlist_distinguish_types, ) -from deepmd.pt.utils.region import ( - normalize_coord, -) def make_model(T_AtomicModel): @@ -97,26 +93,19 @@ def forward_common( The keys are defined by the `ModelOutputDef`. """ - nframes, nloc = atype.shape[:2] - if box is not None: - coord_normalized = normalize_coord( - coord.view(nframes, nloc, 3), - box.reshape(nframes, 3, 3), - ) - else: - coord_normalized = coord.clone() - extended_coord, extended_atype, mapping = extend_coord_with_ghosts( - coord_normalized, atype, box, self.get_rcut() - ) - nlist = build_neighbor_list( + ( extended_coord, extended_atype, - nloc, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + coord, + atype, self.get_rcut(), self.get_sel(), distinguish_types=self.distinguish_types(), + box=box, ) - extended_coord = extended_coord.view(nframes, -1, 3) model_predict_lower = self.forward_common_lower( extended_coord, extended_atype, diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index c8ade925c0..1b3e2c3d65 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -223,7 +223,7 @@ def compute_output_stats(self, merged): return {"bias_atom_e": bias_atom_e} def init_fitting_stat(self, bias_atom_e=None, **kwargs): - assert True not in [x is None for x in [bias_atom_e]] + assert all(x is not None for x in [bias_atom_e]) self.bias_atom_e.copy_( torch.tensor(bias_atom_e, device=env.DEVICE).view( [self.ntypes, self.dim_out] diff --git a/deepmd/pt/utils/dataloader.py b/deepmd/pt/utils/dataloader.py index 0ec43f5a75..7f8cf4eb3c 100644 --- a/deepmd/pt/utils/dataloader.py +++ b/deepmd/pt/utils/dataloader.py @@ -267,26 +267,7 @@ def collate_batch(batch): example = batch[0] result = example.copy() for key in example.keys(): - if key == "shift" or key == "mapping": - natoms_extended = max([d[key].shape[0] for d in batch]) - n_frames = len(batch) - list = [] - for x in range(n_frames): - list.append(batch[x][key]) - if key == "shift": - result[key] = torch.zeros( - (n_frames, natoms_extended, 3), - dtype=env.GLOBAL_PT_FLOAT_PRECISION, - ) - else: - result[key] = torch.zeros( - (n_frames, natoms_extended), - dtype=torch.long, - ) - for i in range(len(batch)): - natoms_tmp = list[i].shape[0] - result[key][i, :natoms_tmp] = list[i] - elif "find_" in key: + if "find_" in key: result[key] = batch[0][key] else: if batch[0][key] is None: diff --git a/deepmd/pt/utils/dataset.py b/deepmd/pt/utils/dataset.py index aca4a9ce5b..60055ebda9 100644 --- a/deepmd/pt/utils/dataset.py +++ b/deepmd/pt/utils/dataset.py @@ -9,7 +9,6 @@ import h5py import numpy as np import torch -import torch.distributed as dist from torch.utils.data import ( Dataset, ) @@ -189,85 +188,6 @@ def add( "high_prec": high_prec, } - # deprecated TODO - def get_batch_for_train(self, batch_size: int): - """Get a batch of data with at most `batch_size` frames. The frames are randomly picked from the data system. - - Args: - - batch_size: Frame count. - """ - if not hasattr(self, "_frames"): - self.set_size = 0 - self._set_count = 0 - self._iterator = 0 - if batch_size == "auto": - batch_size = -(-32 // self._natoms) - if self._iterator + batch_size > self.set_size: - set_idx = self._set_count % len(self._dirs) - if self.sets[set_idx] is None: - frames = self._load_set(self._dirs[set_idx]) - frames = self.preprocess(frames) - cnt = 0 - for item in self.sets: - if item is not None: - cnt += 1 - if cnt < env.CACHE_PER_SYS: - self.sets[set_idx] = frames - else: - frames = self.sets[set_idx] - self._frames = frames - self._shuffle_data() - if dist.is_initialized(): - world_size = dist.get_world_size() - rank = dist.get_rank() - ssize = self._frames["coord"].shape[0] - subsize = ssize // world_size - self._iterator = rank * subsize - self.set_size = min((rank + 1) * subsize, ssize) - else: - self.set_size = self._frames["coord"].shape[0] - self._iterator = 0 - self._set_count += 1 - iterator = min(self._iterator + batch_size, self.set_size) - idx = np.arange(self._iterator, iterator) - self._iterator += batch_size - return self._get_subdata(idx) - - # deprecated TODO - def get_batch(self, batch_size: int): - """Get a batch of data with at most `batch_size` frames. The frames are randomly picked from the data system. - Args: - - batch_size: Frame count. - """ - if not hasattr(self, "_frames"): - self.set_size = 0 - self._set_count = 0 - self._iterator = 0 - if batch_size == "auto": - batch_size = -(-32 // self._natoms) - if self._iterator + batch_size > self.set_size: - set_idx = self._set_count % len(self._dirs) - if self.sets[set_idx] is None: - frames = self._load_set(self._dirs[set_idx]) - frames = self.preprocess(frames) - cnt = 0 - for item in self.sets: - if item is not None: - cnt += 1 - if cnt < env.CACHE_PER_SYS: - self.sets[set_idx] = frames - else: - frames = self.sets[set_idx] - self._frames = frames - self._shuffle_data() - self.set_size = self._frames["coord"].shape[0] - self._iterator = 0 - self._set_count += 1 - iterator = min(self._iterator + batch_size, self.set_size) - idx = np.arange(self._iterator, iterator) - self._iterator += batch_size - return self._get_subdata(idx) - def get_ntypes(self): """Number of atom types in the system.""" if self._type_map is not None: @@ -470,63 +390,6 @@ def _load_data( data = np.zeros([nframes, ndof]).astype(env.GLOBAL_NP_FLOAT_PRECISION) return np.float32(0.0), data - # deprecated TODO - def preprocess(self, batch): - n_frames = batch["coord"].shape[0] - for kk in self._data_dict.keys(): - if "find_" in kk: - pass - else: - batch[kk] = torch.tensor(batch[kk], dtype=env.GLOBAL_PT_FLOAT_PRECISION) - if self._data_dict[kk]["atomic"]: - batch[kk] = batch[kk].view( - n_frames, -1, self._data_dict[kk]["ndof"] - ) - - for kk in ["type", "real_natoms_vec"]: - if kk in batch.keys(): - batch[kk] = torch.tensor(batch[kk], dtype=torch.long) - batch["atype"] = batch.pop("type") - - keys = ["nlist", "nlist_loc", "nlist_type", "shift", "mapping"] - coord = batch["coord"] - atype = batch["atype"] - box = batch["box"] - rcut = self.rcut - sec = self.sec - assert batch["atype"].max() < len(self._type_map) - nlist, nlist_loc, nlist_type, shift, mapping = [], [], [], [], [] - - for sid in range(n_frames): - region = Region3D(box[sid]) - nloc = atype[sid].shape[0] - _coord = normalize_coord(coord[sid], region, nloc) - coord[sid] = _coord - a, b, c, d, e = make_env_mat( - _coord, atype[sid], region, rcut, sec, type_split=self.type_split - ) - nlist.append(a) - nlist_loc.append(b) - nlist_type.append(c) - shift.append(d) - mapping.append(e) - nlist = torch.stack(nlist) - nlist_loc = torch.stack(nlist_loc) - nlist_type = torch.stack(nlist_type) - batch["nlist"] = nlist - batch["nlist_loc"] = nlist_loc - batch["nlist_type"] = nlist_type - natoms_extended = max([item.shape[0] for item in shift]) - batch["shift"] = torch.zeros( - (n_frames, natoms_extended, 3), dtype=env.GLOBAL_PT_FLOAT_PRECISION - ) - batch["mapping"] = torch.zeros((n_frames, natoms_extended), dtype=torch.long) - for i in range(len(shift)): - natoms_tmp = shift[i].shape[0] - batch["shift"][i, :natoms_tmp] = shift[i] - batch["mapping"][i, :natoms_tmp] = mapping[i] - return batch - def _shuffle_data(self): nframes = self._frames["coord"].shape[0] idx = np.arange(nframes) @@ -563,46 +426,21 @@ def single_preprocess(self, batch, sid): for kk in ["type", "real_natoms_vec"]: if kk in batch.keys(): batch[kk] = torch.tensor(batch[kk][sid], dtype=torch.long) - clean_coord = batch.pop("coord") - clean_type = batch.pop("type") - nloc = clean_type.shape[0] + batch["atype"] = batch["type"] rcut = self.rcut sec = self.sec - nlist, nlist_loc, nlist_type, shift, mapping = [], [], [], [], [] - if self.pbc: - box = batch["box"] - region = Region3D(box) - else: - box = None + if not self.pbc: batch["box"] = None - region = None if self.noise_settings is None: - batch["atype"] = clean_type - batch["coord"] = clean_coord - coord = clean_coord - atype = batch["atype"] + return batch + else: # TODO need to clean up this method! if self.pbc: - _coord = normalize_coord(coord, region, nloc) - + region = Region3D(batch["box"]) else: - _coord = coord.clone() - batch["coord"] = _coord - nlist, nlist_loc, nlist_type, shift, mapping = make_env_mat( - _coord, - atype, - region, - rcut, - sec, - pbc=self.pbc, - type_split=self.type_split, - ) - batch["nlist"] = nlist - batch["nlist_loc"] = nlist_loc - batch["nlist_type"] = nlist_type - batch["shift"] = shift - batch["mapping"] = mapping - return batch - else: + region = None + clean_coord = batch.pop("coord") + clean_type = batch.pop("type") + nloc = clean_type.shape[0] batch["clean_type"] = clean_type if self.pbc: _clean_coord = normalize_coord(clean_coord, region, nloc) @@ -678,7 +516,7 @@ def single_preprocess(self, batch, sid): else: _coord = noised_coord.clone() try: - nlist, nlist_loc, nlist_type, shift, mapping = make_env_mat( + _ = make_env_mat( _coord, masked_type, region, @@ -694,13 +532,8 @@ def single_preprocess(self, batch, sid): f"Add noise times beyond max tries {self.max_fail_num}!" ) continue - batch["atype"] = masked_type + batch["type"] = masked_type batch["coord"] = noised_coord - batch["nlist"] = nlist - batch["nlist_loc"] = nlist_loc - batch["nlist_type"] = nlist_type - batch["shift"] = shift - batch["mapping"] = mapping return batch def _get_item(self, index): @@ -783,104 +616,3 @@ def __getitem__(self, index): b_data = self._data_system._get_item(index) b_data["natoms"] = torch.tensor(self._natoms_vec) return b_data - - -# deprecated TODO -class DeepmdDataSet(Dataset): - def __init__( - self, - systems: List[str], - batch_size: int, - type_map: List[str], - rcut=None, - sel=None, - weight=None, - type_split=True, - ): - """Construct DeePMD-style dataset containing frames cross different systems. - - Args: - - systems: Paths to systems. - - batch_size: Max frame count in a batch. - - type_map: Atom types. - """ - self._batch_size = batch_size - self._type_map = type_map - if sel is not None: - if isinstance(sel, int): - sel = [sel] - sec = torch.cumsum(torch.tensor(sel), dim=0) - if isinstance(systems, str): - with h5py.File(systems) as file: - systems = [os.path.join(systems, item) for item in file.keys()] - self._data_systems = [ - DeepmdDataSystem( - ii, rcut, sec, type_map=self._type_map, type_split=type_split - ) - for ii in systems - ] - # check mix_type format - error_format_msg = ( - "if one of the system is of mixed_type format, " - "then all of the systems in this dataset should be of mixed_type format!" - ) - self.mixed_type = self._data_systems[0].mixed_type - for sys_item in self._data_systems[1:]: - assert sys_item.mixed_type == self.mixed_type, error_format_msg - - if weight is None: - - def weight(name, sys): - return sys.nframes - - self.probs = [ - weight(item, self._data_systems[i]) for i, item in enumerate(systems) - ] - self.probs = np.array(self.probs, dtype=float) - self.probs /= self.probs.sum() - self._ntypes = max([ii.get_ntypes() for ii in self._data_systems]) - self._natoms_vec = [ - ii.get_natoms_vec(self._ntypes) for ii in self._data_systems - ] - self.cache = [{} for _ in self._data_systems] - - @property - def nsystems(self): - return len(self._data_systems) - - def __len__(self): - return self.nsystems - - def __getitem__(self, index=None): - """Get a batch of frames from the selected system.""" - if index is None: - index = dp_random.choice(np.arange(self.nsystems), p=self.probs) - b_data = self._data_systems[index].get_batch(self._batch_size) - b_data["natoms"] = torch.tensor(self._natoms_vec[index]) - batch_size = b_data["coord"].shape[0] - b_data["natoms"] = b_data["natoms"].unsqueeze(0).expand(batch_size, -1) - return b_data - - # deprecated TODO - def get_training_batch(self, index=None): - """Get a batch of frames from the selected system.""" - if index is None: - index = dp_random.choice(np.arange(self.nsystems), p=self.probs) - b_data = self._data_systems[index].get_batch_for_train(self._batch_size) - b_data["natoms"] = torch.tensor(self._natoms_vec[index]) - batch_size = b_data["coord"].shape[0] - b_data["natoms"] = b_data["natoms"].unsqueeze(0).expand(batch_size, -1) - return b_data - - def get_batch(self, sys_idx=None): - """TF-compatible batch for testing.""" - pt_batch = self[sys_idx] - np_batch = {} - for key in ["coord", "box", "force", "energy", "virial", "atype", "natoms"]: - if key in pt_batch.keys(): - np_batch[key] = pt_batch[key].cpu().numpy() - batch_size = pt_batch["coord"].shape[0] - np_batch["coord"] = np_batch["coord"].reshape(batch_size, -1) - np_batch["natoms"] = np_batch["natoms"][0] - np_batch["force"] = np_batch["force"].reshape(batch_size, -1) - return np_batch, pt_batch diff --git a/deepmd/pt/utils/nlist.py b/deepmd/pt/utils/nlist.py index fdb2627f04..963c9bc9b6 100644 --- a/deepmd/pt/utils/nlist.py +++ b/deepmd/pt/utils/nlist.py @@ -12,10 +12,42 @@ env, ) from deepmd.pt.utils.region import ( + normalize_coord, to_face_distance, ) +def extend_input_and_build_neighbor_list( + coord, + atype, + rcut: float, + sel: List[int], + distinguish_types: bool = False, + box: Optional[torch.Tensor] = None, +): + nframes, nloc = atype.shape[:2] + if box is not None: + coord_normalized = normalize_coord( + coord.view(nframes, nloc, 3), + box.reshape(nframes, 3, 3), + ) + else: + coord_normalized = coord.clone() + extended_coord, extended_atype, mapping = extend_coord_with_ghosts( + coord_normalized, atype, box, rcut + ) + nlist = build_neighbor_list( + extended_coord, + extended_atype, + nloc, + rcut, + sel, + distinguish_types=distinguish_types, + ) + extended_coord = extended_coord.view(nframes, -1, 3) + return extended_coord, extended_atype, mapping, nlist + + def build_neighbor_list( coord1: torch.Tensor, atype: torch.Tensor, diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index 932ba9a409..76b2afe41b 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -5,10 +5,6 @@ import numpy as np import torch -from deepmd.pt.utils import ( - env, -) - log = logging.getLogger(__name__) @@ -31,11 +27,6 @@ def make_stat_input(datasets, dataloaders, nbatches): "atype", "box", "natoms", - "mapping", - "nlist", - "nlist_loc", - "nlist_type", - "shift", ] if datasets[0].mixed_type: keys.append("real_natoms_vec") @@ -53,25 +44,6 @@ def make_stat_input(datasets, dataloaders, nbatches): if dd in keys: sys_stat[dd].append(stat_data[dd]) for key in keys: - if key == "mapping" or key == "shift": - extend = max(d.shape[1] for d in sys_stat[key]) - for jj in range(len(sys_stat[key])): - l = [] - item = sys_stat[key][jj] - for ii in range(item.shape[0]): - l.append(item[ii]) - n_frames = len(item) - if key == "shift": - shape = torch.zeros( - (n_frames, extend, 3), - dtype=env.GLOBAL_PT_FLOAT_PRECISION, - ) - else: - shape = torch.zeros((n_frames, extend), dtype=torch.long) - for i in range(len(item)): - natoms_tmp = l[i].shape[0] - shape[i, :natoms_tmp] = l[i] - sys_stat[key][jj] = shape if not isinstance(sys_stat[key][0], list): if sys_stat[key][0] is None: sys_stat[key] = None @@ -133,4 +105,4 @@ def process_stat_path( has_stat_file_path_list = [ os.path.exists(stat_file_path[key]) for key in stat_file_dict ] - return stat_file_path, False not in has_stat_file_path_list + return stat_file_path, all(has_stat_file_path_list) diff --git a/source/tests/pt/model/test_descriptor.py b/source/tests/pt/model/test_descriptor.py index 2dd996349b..a4493b5b51 100644 --- a/source/tests/pt/model/test_descriptor.py +++ b/source/tests/pt/model/test_descriptor.py @@ -21,13 +21,16 @@ env, ) from deepmd.pt.utils.dataset import ( - DeepmdDataSet, + DeepmdDataSetForLoader, ) from deepmd.pt.utils.env import ( DEVICE, GLOBAL_NP_FLOAT_PRECISION, GLOBAL_PT_FLOAT_PRECISION, ) +from deepmd.pt.utils.nlist import ( + extend_input_and_build_neighbor_list, +) from deepmd.tf.common import ( expand_sys_str, ) @@ -35,6 +38,10 @@ op_module, ) +from .test_embedding_net import ( + get_single_batch, +) + CUR_DIR = os.path.dirname(__file__) @@ -103,10 +110,14 @@ def setUp(self): self.systems = config["training"]["validation_data"]["systems"] if isinstance(self.systems, str): self.systems = expand_sys_str(self.systems) - ds = DeepmdDataSet( - self.systems, self.bsz, model_config["type_map"], self.rcut, self.sel + ds = DeepmdDataSetForLoader( + self.systems[0], + model_config["type_map"], + self.rcut, + self.sel, + type_split=True, ) - self.np_batch, self.pt_batch = ds.get_batch() + self.np_batch, self.pt_batch = get_single_batch(ds) self.sec = np.cumsum(self.sel) self.ntypes = len(self.sel) self.nnei = sum(self.sel) @@ -122,7 +133,7 @@ def test_consistency(self): dtype=GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE, ) - base_d, base_force, nlist = base_se_a( + base_d, base_force, base_nlist = base_se_a( rcut=self.rcut, rcut_smth=self.rcut_smth, sel=self.sel, @@ -132,14 +143,25 @@ def test_consistency(self): ) pt_coord = self.pt_batch["coord"].to(env.DEVICE) + atype = self.pt_batch["atype"].to(env.DEVICE) pt_coord.requires_grad_(True) - index = self.pt_batch["mapping"].unsqueeze(-1).expand(-1, -1, 3).to(env.DEVICE) - extended_coord = torch.gather(pt_coord, dim=1, index=index) - extended_coord = extended_coord - self.pt_batch["shift"].to(env.DEVICE) - my_d, _, _ = prod_env_mat_se_a( - extended_coord.to(DEVICE), - self.pt_batch["nlist"].to(env.DEVICE), + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + pt_coord, self.pt_batch["atype"].to(env.DEVICE), + self.rcut, + self.sel, + distinguish_types=True, + box=self.pt_batch["box"].to(env.DEVICE), + ) + my_d, _, _ = prod_env_mat_se_a( + extended_coord, + nlist, + atype, avg_zero.reshape([-1, self.nnei, 4]).to(DEVICE), std_ones.reshape([-1, self.nnei, 4]).to(DEVICE), self.rcut, @@ -151,16 +173,16 @@ def test_consistency(self): base_force = base_force.reshape(bsz, -1, 3) base_d = base_d.reshape(bsz, -1, self.nnei, 4) my_d = my_d.view(bsz, -1, self.nnei, 4).cpu().detach().numpy() - nlist = nlist.reshape(bsz, -1, self.nnei) + base_nlist = base_nlist.reshape(bsz, -1, self.nnei) - mapping = self.pt_batch["mapping"].cpu() - my_nlist = self.pt_batch["nlist"].view(bsz, -1).cpu() + mapping = mapping.cpu() + my_nlist = nlist.view(bsz, -1).cpu() mask = my_nlist == -1 my_nlist = my_nlist * ~mask my_nlist = torch.gather(mapping, dim=-1, index=my_nlist) my_nlist = my_nlist * ~mask - mask.long() my_nlist = my_nlist.cpu().view(bsz, -1, self.nnei).numpy() - self.assertTrue(np.allclose(nlist, my_nlist)) + self.assertTrue(np.allclose(base_nlist, my_nlist)) self.assertTrue(np.allclose(np.mean(base_d, axis=2), np.mean(my_d, axis=2))) self.assertTrue(np.allclose(np.std(base_d, axis=2), np.std(my_d, axis=2))) # descriptors may be different when there are multiple neighbors in the same distance diff --git a/source/tests/pt/model/test_descriptor_dpa1.py b/source/tests/pt/model/test_descriptor_dpa1.py index 21a43803c9..07d4d34449 100644 --- a/source/tests/pt/model/test_descriptor_dpa1.py +++ b/source/tests/pt/model/test_descriptor_dpa1.py @@ -19,11 +19,7 @@ env, ) from deepmd.pt.utils.nlist import ( - build_neighbor_list, - extend_coord_with_ghosts, -) -from deepmd.pt.utils.region import ( - normalize_coord, + extend_input_and_build_neighbor_list, ) dtype = torch.float64 @@ -245,20 +241,9 @@ def test_descriptor_block(self): **dparams, ).to(env.DEVICE) des.load_state_dict(torch.load(self.file_model_param)) - rcut = dparams["rcut"] - nsel = dparams["sel"] coord = self.coord atype = self.atype box = self.cell - nf, nloc = coord.shape[:2] - coord_normalized = normalize_coord(coord, box.reshape(-1, 3, 3)) - extended_coord, extended_atype, mapping = extend_coord_with_ghosts( - coord_normalized, atype, box, rcut - ) - # single nlist - nlist = build_neighbor_list( - extended_coord, extended_atype, nloc, rcut, nsel, distinguish_types=False - ) # handel type_embedding type_embedding = TypeEmbedNet(ntypes, 8).to(env.DEVICE) type_embedding.load_state_dict(torch.load(self.file_type_embed)) @@ -266,6 +251,19 @@ def test_descriptor_block(self): ## to save model parameters # torch.save(des.state_dict(), 'model_weights.pth') # torch.save(type_embedding.state_dict(), 'model_weights.pth') + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + coord, + atype, + des.get_rcut(), + des.get_sel(), + distinguish_types=des.distinguish_types(), + box=box, + ) descriptor, env_mat, diff, rot_mat, sw = des( nlist, extended_coord, @@ -307,18 +305,18 @@ def test_descriptor(self): coord = self.coord atype = self.atype box = self.cell - nf, nloc = coord.shape[:2] - coord_normalized = normalize_coord(coord, box.reshape(-1, 3, 3)) - extended_coord, extended_atype, mapping = extend_coord_with_ghosts( - coord_normalized, atype, box, des.get_rcut() - ) - nlist = build_neighbor_list( + ( extended_coord, extended_atype, - nloc, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + coord, + atype, des.get_rcut(), - des.get_nsel(), - distinguish_types=False, + des.get_sel(), + distinguish_types=des.distinguish_types(), + box=box, ) descriptor, env_mat, diff, rot_mat, sw = des( extended_coord, diff --git a/source/tests/pt/model/test_descriptor_dpa2.py b/source/tests/pt/model/test_descriptor_dpa2.py index e614e64c2f..6b80eb89a2 100644 --- a/source/tests/pt/model/test_descriptor_dpa2.py +++ b/source/tests/pt/model/test_descriptor_dpa2.py @@ -19,11 +19,9 @@ env, ) from deepmd.pt.utils.nlist import ( - build_neighbor_list, - extend_coord_with_ghosts, -) -from deepmd.pt.utils.region import ( - normalize_coord, + build_multiple_neighbor_list, + extend_input_and_build_neighbor_list, + get_multiple_nlist_key, ) dtype = torch.float64 @@ -114,6 +112,7 @@ def setUp(self): self.file_model_param = Path(CUR_DIR) / "models" / "dpa2.pth" self.file_type_embed = Path(CUR_DIR) / "models" / "dpa2_tebd.pth" + # TODO This test for hybrid descriptor should be removed! def test_descriptor_hyb(self): # torch.manual_seed(0) model_hybrid_dpa2 = self.model_json @@ -129,34 +128,13 @@ def test_descriptor_hyb(self): # type_embd of repformer is removed model_dict.pop("descriptor_list.1.type_embd.embedding.weight") des.load_state_dict(model_dict) - all_rcut = [ii["rcut"] for ii in dlist] - all_nsel = [ii["sel"] for ii in dlist] + all_rcut = sorted([ii["rcut"] for ii in dlist]) + all_nsel = sorted([ii["sel"] for ii in dlist]) rcut_max = max(all_rcut) + sel_max = max(all_nsel) coord = self.coord atype = self.atype box = self.cell - nf, nloc = coord.shape[:2] - coord_normalized = normalize_coord(coord, box.reshape(-1, 3, 3)) - extended_coord, extended_atype, mapping = extend_coord_with_ghosts( - coord_normalized, atype, box, rcut_max - ) - ## single nlist - # nlist = build_neighbor_list( - # extended_coord, extended_atype, nloc, - # rcut_max, nsel, distinguish_types=False) - nlist_list = [] - for rcut, sel in zip(all_rcut, all_nsel): - nlist_list.append( - build_neighbor_list( - extended_coord, - extended_atype, - nloc, - rcut, - sel, - distinguish_types=False, - ) - ) - nlist = torch.cat(nlist_list, -1) # handel type_embedding type_embedding = TypeEmbedNet(ntypes, 8).to(env.DEVICE) type_embedding.load_state_dict(torch.load(self.file_type_embed)) @@ -164,6 +142,31 @@ def test_descriptor_hyb(self): ## to save model parameters # torch.save(des.state_dict(), 'model_weights.pth') # torch.save(type_embedding.state_dict(), 'model_weights.pth') + ( + extended_coord, + extended_atype, + mapping, + nlist_max, + ) = extend_input_and_build_neighbor_list( + coord, + atype, + rcut_max, + sel_max, + distinguish_types=des.distinguish_types(), + box=box, + ) + nlist_dict = build_multiple_neighbor_list( + extended_coord, + nlist_max, + all_rcut, + all_nsel, + ) + nlist_list = [] + for ii in des.descriptor_list: + nlist_list.append( + nlist_dict[get_multiple_nlist_key(ii.get_rcut(), ii.get_nsel())] + ) + nlist = torch.cat(nlist_list, -1) descriptor, env_mat, diff, rot_mat, sw = des( nlist, extended_coord, @@ -202,18 +205,18 @@ def test_descriptor(self): coord = self.coord atype = self.atype box = self.cell - nf, nloc = coord.shape[:2] - coord_normalized = normalize_coord(coord, box.reshape(-1, 3, 3)) - extended_coord, extended_atype, mapping = extend_coord_with_ghosts( - coord_normalized, atype, box, des.repinit.rcut - ) - nlist = build_neighbor_list( + ( extended_coord, extended_atype, - nloc, - des.repinit.rcut, - des.repinit.sel, - distinguish_types=False, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + coord, + atype, + des.get_rcut(), + des.get_sel(), + distinguish_types=des.distinguish_types(), + box=box, ) descriptor, env_mat, diff, rot_mat, sw = des( extended_coord, diff --git a/source/tests/pt/model/test_dp_model.py b/source/tests/pt/model/test_dp_model.py index f3f899fbe2..d970c8a542 100644 --- a/source/tests/pt/model/test_dp_model.py +++ b/source/tests/pt/model/test_dp_model.py @@ -23,6 +23,7 @@ from deepmd.pt.utils.nlist import ( build_neighbor_list, extend_coord_with_ghosts, + extend_input_and_build_neighbor_list, ) from deepmd.pt.utils.utils import ( to_numpy_array, @@ -433,20 +434,13 @@ def test_self_consistency(self): to_numpy_array(ret0["atom_virial"]), to_numpy_array(ret1["atom_virial"]), ) - - coord_ext, atype_ext, mapping = extend_coord_with_ghosts( + coord_ext, atype_ext, mapping, nlist = extend_input_and_build_neighbor_list( to_torch_tensor(self.coord), to_torch_tensor(self.atype), - to_torch_tensor(self.cell), - self.rcut, - ) - nlist = build_neighbor_list( - coord_ext, - atype_ext, - self.nloc, self.rcut, self.sel, distinguish_types=md0.distinguish_types(), + box=to_torch_tensor(self.cell), ) args = [coord_ext, atype_ext, nlist] ret2 = md0.forward_lower(*args, do_atomic_virial=True) diff --git a/source/tests/pt/model/test_embedding_net.py b/source/tests/pt/model/test_embedding_net.py index 407f4949b5..2621b5d135 100644 --- a/source/tests/pt/model/test_embedding_net.py +++ b/source/tests/pt/model/test_embedding_net.py @@ -25,12 +25,15 @@ dp_random, ) from deepmd.pt.utils.dataset import ( - DeepmdDataSet, + DeepmdDataSetForLoader, ) from deepmd.pt.utils.env import ( DEVICE, GLOBAL_NP_FLOAT_PRECISION, ) +from deepmd.pt.utils.nlist import ( + extend_input_and_build_neighbor_list, +) from deepmd.tf.common import ( expand_sys_str, ) @@ -43,6 +46,25 @@ def gen_key(worb, depth, elemid): return (worb, depth, elemid) +def get_single_batch(dataset, index=None): + if index is None: + index = dp_random.choice(np.arange(len(dataset))) + pt_batch = dataset[index] + np_batch = {} + # TODO deprecated + for key in ["mapping", "shift", "nlist"]: + if key in pt_batch.keys(): + pt_batch[key] = pt_batch[key].unsqueeze(0) + for key in ["coord", "box", "force", "energy", "virial", "atype", "natoms"]: + if key in pt_batch.keys(): + pt_batch[key] = pt_batch[key].unsqueeze(0) + np_batch[key] = pt_batch[key].cpu().numpy() + np_batch["coord"] = np_batch["coord"].reshape(1, -1) + np_batch["natoms"] = np_batch["natoms"][0] + np_batch["force"] = np_batch["force"].reshape(1, -1) + return np_batch, pt_batch + + def base_se_a(descriptor, coord, atype, natoms, box): g = tf.Graph() with g.as_default(): @@ -105,12 +127,16 @@ def setUp(self): self.systems = config["training"]["validation_data"]["systems"] if isinstance(self.systems, str): self.systems = expand_sys_str(self.systems) - ds = DeepmdDataSet( - self.systems, self.bsz, model_config["type_map"], self.rcut, self.sel + ds = DeepmdDataSetForLoader( + self.systems[0], + model_config["type_map"], + self.rcut, + self.sel, + type_split=True, ) self.filter_neuron = model_config["descriptor"]["neuron"] self.axis_neuron = model_config["descriptor"]["axis_neuron"] - self.np_batch, self.torch_batch = ds.get_batch() + self.np_batch, self.torch_batch = get_single_batch(ds) def test_consistency(self): dp_d = DescrptSeA_tf( @@ -154,20 +180,23 @@ def test_consistency(self): pt_coord = self.torch_batch["coord"].to(env.DEVICE) pt_coord.requires_grad_(True) - index = ( - self.torch_batch["mapping"].unsqueeze(-1).expand(-1, -1, 3).to(env.DEVICE) - ) - extended_coord = torch.gather(pt_coord, dim=1, index=index) - extended_coord = extended_coord - self.torch_batch["shift"].to(env.DEVICE) - extended_atype = torch.gather( + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + pt_coord, self.torch_batch["atype"].to(env.DEVICE), - dim=1, - index=self.torch_batch["mapping"].to(env.DEVICE), + self.rcut, + self.sel, + distinguish_types=True, + box=self.torch_batch["box"].to(env.DEVICE), ) descriptor_out, _, _, _, _ = descriptor( extended_coord, extended_atype, - self.torch_batch["nlist"].to(env.DEVICE), + nlist, ) my_embedding = descriptor_out.cpu().detach().numpy() fake_energy = torch.sum(descriptor_out) diff --git a/source/tests/pt/model/test_model.py b/source/tests/pt/model/test_model.py index 522b30b2df..efe013a8a1 100644 --- a/source/tests/pt/model/test_model.py +++ b/source/tests/pt/model/test_model.py @@ -331,7 +331,9 @@ def test_consistency(self): # print(dst.mean(), dst.std()) dst.copy_(src) # Start forward computing - batch = my_ds.systems[0]._data_system.preprocess(batch) + batch = my_ds.systems[0]._data_system.single_preprocess(batch, 0) + for key in ["coord", "atype", "box", "energy", "force"]: + batch[key] = batch[key].unsqueeze(0) batch["coord"].requires_grad_(True) batch["natoms"] = torch.tensor( batch["natoms_vec"], device=batch["coord"].device diff --git a/source/tests/pt/test_loss.py b/source/tests/pt/test_loss.py index 14934c7be0..f0c75ef288 100644 --- a/source/tests/pt/test_loss.py +++ b/source/tests/pt/test_loss.py @@ -16,7 +16,7 @@ EnergyStdLoss, ) from deepmd.pt.utils.dataset import ( - DeepmdDataSet, + DeepmdDataSetForLoader, ) from deepmd.tf.common import ( expand_sys_str, @@ -25,6 +25,10 @@ EnerStdLoss, ) +from .model.test_embedding_net import ( + get_single_batch, +) + CUR_DIR = os.path.dirname(__file__) @@ -39,12 +43,13 @@ def get_batch(): rcut = model_config["descriptor"]["rcut"] # self.rcut_smth = model_config['descriptor']['rcut_smth'] sel = model_config["descriptor"]["sel"] - batch_size = config["training"]["training_data"]["batch_size"] systems = config["training"]["validation_data"]["systems"] if isinstance(systems, str): systems = expand_sys_str(systems) - dataset = DeepmdDataSet(systems, batch_size, model_config["type_map"], rcut, sel) - np_batch, pt_batch = dataset.get_batch() + dataset = DeepmdDataSetForLoader( + systems[0], model_config["type_map"], rcut, sel, type_split=True + ) + np_batch, pt_batch = get_single_batch(dataset) return np_batch, pt_batch diff --git a/source/tests/pt/test_stat.py b/source/tests/pt/test_stat.py index 240c354a69..bc95575a5a 100644 --- a/source/tests/pt/test_stat.py +++ b/source/tests/pt/test_stat.py @@ -165,10 +165,7 @@ def test_descriptor(self): "energy", "atype", "natoms", - "extended_coord", - "nlist", - "shift", - "mapping", + "box", ]: if key in sys.keys(): sys[key] = sys[key].to(env.DEVICE) From 1d23383cd17d8dc6ecb99a47a0606d173171f48d Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 14 Feb 2024 02:51:55 -0500 Subject: [PATCH 077/686] add neighbor stat support with NumPy and PyTorch implementation (#3271) I also tested `examples/water`,`examples/nopbc`, and the ANI-1x dataset (compared to the screenshot in #1624) to confirm consistent results. Besides, as the OP supports multiple frames, the PT implementation only takes 9 s on ANI-1x, which is much faster than the TF implementation, which took over 10 min as shown in #1624. ![image](https://github.com/deepmodeling/deepmd-kit/assets/9496702/c3cc1950-33a7-435c-90f4-c18d196b202d) Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/utils/neighbor_stat.py | 154 ++++++++++++++ deepmd/entrypoints/neighbor_stat.py | 101 ++++++++++ deepmd/main.py | 2 +- deepmd/pt/entrypoints/main.py | 5 + deepmd/pt/utils/neighbor_stat.py | 190 ++++++++++++++++++ deepmd/tf/entrypoints/neighbor_stat.py | 87 +------- deepmd/tf/utils/neighbor_stat.py | 64 ++---- deepmd/utils/neighbor_stat.py | 104 ++++++++++ .../common/dpmodel/test_neighbor_stat.py | 61 ++++++ source/tests/pt/test_neighbor_stat.py | 61 ++++++ 10 files changed, 698 insertions(+), 131 deletions(-) create mode 100644 deepmd/dpmodel/utils/neighbor_stat.py create mode 100644 deepmd/entrypoints/neighbor_stat.py create mode 100644 deepmd/pt/utils/neighbor_stat.py create mode 100644 deepmd/utils/neighbor_stat.py create mode 100644 source/tests/common/dpmodel/test_neighbor_stat.py create mode 100644 source/tests/pt/test_neighbor_stat.py diff --git a/deepmd/dpmodel/utils/neighbor_stat.py b/deepmd/dpmodel/utils/neighbor_stat.py new file mode 100644 index 0000000000..a0f3c02131 --- /dev/null +++ b/deepmd/dpmodel/utils/neighbor_stat.py @@ -0,0 +1,154 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Iterator, + Optional, + Tuple, +) + +import numpy as np + +from deepmd.dpmodel.common import ( + NativeOP, +) +from deepmd.dpmodel.utils.nlist import ( + extend_coord_with_ghosts, +) +from deepmd.utils.data_system import ( + DeepmdDataSystem, +) +from deepmd.utils.neighbor_stat import NeighborStat as BaseNeighborStat + + +class NeighborStatOP(NativeOP): + """Class for getting neighbor statics data information. + + Parameters + ---------- + ntypes + The num of atom types + rcut + The cut-off radius + distinguish_types : bool, optional + If False, treat all types as a single type. + """ + + def __init__( + self, + ntypes: int, + rcut: float, + distinguish_types: bool, + ) -> None: + self.rcut = rcut + self.ntypes = ntypes + self.distinguish_types = distinguish_types + + def call( + self, + coord: np.ndarray, + atype: np.ndarray, + cell: Optional[np.ndarray], + ) -> Tuple[float, np.ndarray]: + """Calculate the neareest neighbor distance between atoms, maximum nbor size of + atoms and the output data range of the environment matrix. + + Parameters + ---------- + coord + The coordinates of atoms. + atype + The atom types. + cell + The cell. + + Returns + ------- + float + The minimal squared distance between two atoms + np.ndarray + The maximal number of neighbors + """ + nframes = coord.shape[0] + coord = coord.reshape(nframes, -1, 3) + nloc = coord.shape[1] + coord = coord.reshape(nframes, nloc * 3) + extend_coord, extend_atype, _ = extend_coord_with_ghosts( + coord, atype, cell, self.rcut + ) + + coord1 = extend_coord.reshape(nframes, -1) + nall = coord1.shape[1] // 3 + coord0 = coord1[:, : nloc * 3] + diff = ( + coord1.reshape([nframes, -1, 3])[:, None, :, :] + - coord0.reshape([nframes, -1, 3])[:, :, None, :] + ) + assert list(diff.shape) == [nframes, nloc, nall, 3] + # remove the diagonal elements + mask = np.eye(nloc, nall, dtype=bool) + diff[:, mask] = np.inf + rr2 = np.sum(np.square(diff), axis=-1) + min_rr2 = np.min(rr2, axis=-1) + # count the number of neighbors + if self.distinguish_types: + mask = rr2 < self.rcut**2 + nnei = np.zeros((nframes, nloc, self.ntypes), dtype=int) + for ii in range(self.ntypes): + nnei[:, :, ii] = np.sum( + mask & (extend_atype == ii)[:, None, :], axis=-1 + ) + else: + mask = rr2 < self.rcut**2 + # virtual type (<0) are not counted + nnei = np.sum(mask & (extend_atype >= 0)[:, None, :], axis=-1).reshape( + nframes, nloc, 1 + ) + max_nnei = np.max(nnei, axis=1) + return min_rr2, max_nnei + + +class NeighborStat(BaseNeighborStat): + """Neighbor statistics using pure NumPy. + + Parameters + ---------- + ntypes : int + The num of atom types + rcut : float + The cut-off radius + one_type : bool, optional, default=False + Treat all types as a single type. + """ + + def __init__( + self, + ntypes: int, + rcut: float, + one_type: bool = False, + ) -> None: + super().__init__(ntypes, rcut, one_type) + self.op = NeighborStatOP(ntypes, rcut, not one_type) + + def iterator( + self, data: DeepmdDataSystem + ) -> Iterator[Tuple[np.ndarray, float, str]]: + """Abstract method for producing data. + + Yields + ------ + np.ndarray + The maximal number of neighbors + float + The squared minimal distance between two atoms + str + The directory of the data system + """ + for ii in range(len(data.system_dirs)): + for jj in data.data_systems[ii].dirs: + data_set = data.data_systems[ii] + data_set_data = data_set._load_set(jj) + minrr2, max_nnei = self.op( + data_set_data["coord"], + data_set_data["type"], + data_set_data["box"] if data_set.pbc else None, + ) + yield np.max(max_nnei, axis=0), np.min(minrr2), jj diff --git a/deepmd/entrypoints/neighbor_stat.py b/deepmd/entrypoints/neighbor_stat.py new file mode 100644 index 0000000000..f5ce0f839d --- /dev/null +++ b/deepmd/entrypoints/neighbor_stat.py @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging +from typing import ( + List, +) + +from deepmd.common import ( + expand_sys_str, +) +from deepmd.utils.data_system import ( + DeepmdDataSystem, +) + +log = logging.getLogger(__name__) + + +def neighbor_stat( + *, + system: str, + rcut: float, + type_map: List[str], + one_type: bool = False, + backend: str = "tensorflow", + **kwargs, +): + """Calculate neighbor statistics. + + Parameters + ---------- + system : str + system to stat + rcut : float + cutoff radius + type_map : list[str] + type map + one_type : bool, optional, default=False + treat all types as a single type + backend : str, optional, default="tensorflow" + backend to use + **kwargs + additional arguments + + Examples + -------- + >>> neighbor_stat( + ... system=".", + ... rcut=6.0, + ... type_map=[ + ... "C", + ... "H", + ... "O", + ... "N", + ... "P", + ... "S", + ... "Mg", + ... "Na", + ... "HW", + ... "OW", + ... "mNa", + ... "mCl", + ... "mC", + ... "mH", + ... "mMg", + ... "mN", + ... "mO", + ... "mP", + ... ], + ... ) + min_nbor_dist: 0.6599510670195264 + max_nbor_size: [23, 26, 19, 16, 2, 2, 1, 1, 72, 37, 5, 0, 31, 29, 1, 21, 20, 5] + """ + if backend == "tensorflow": + from deepmd.tf.utils.neighbor_stat import ( + NeighborStat, + ) + elif backend == "pytorch": + from deepmd.pt.utils.neighbor_stat import ( + NeighborStat, + ) + elif backend == "numpy": + from deepmd.dpmodel.utils.neighbor_stat import ( + NeighborStat, + ) + else: + raise ValueError(f"Invalid backend {backend}") + all_sys = expand_sys_str(system) + if not len(all_sys): + raise RuntimeError("Did not find valid system") + data = DeepmdDataSystem( + systems=all_sys, + batch_size=1, + test_size=1, + rcut=rcut, + type_map=type_map, + ) + data.get_batch() + nei = NeighborStat(data.get_ntypes(), rcut, one_type=one_type) + min_nbor_dist, max_nbor_size = nei.get_stat(data) + log.info("min_nbor_dist: %f" % min_nbor_dist) + log.info("max_nbor_size: %s" % str(max_nbor_size)) + return min_nbor_dist, max_nbor_size diff --git a/deepmd/main.py b/deepmd/main.py index bede2cf1fe..d6714e1e26 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -603,7 +603,7 @@ def main_parser() -> argparse.ArgumentParser: parser_neighbor_stat = subparsers.add_parser( "neighbor-stat", parents=[parser_log], - help="(Supported backend: TensorFlow) Calculate neighbor statistics", + help="Calculate neighbor statistics", formatter_class=RawTextArgumentDefaultsHelpFormatter, epilog=textwrap.dedent( """\ diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index bea063a261..29ef8761ff 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -28,6 +28,9 @@ from deepmd.entrypoints.gui import ( start_dpgui, ) +from deepmd.entrypoints.neighbor_stat import ( + neighbor_stat, +) from deepmd.entrypoints.test import ( test, ) @@ -328,6 +331,8 @@ def main(args: Optional[Union[List[str], argparse.Namespace]] = None): make_model_devi(**dict_args) elif FLAGS.command == "gui": start_dpgui(**dict_args) + elif FLAGS.command == "neighbor-stat": + neighbor_stat(**dict_args) else: raise RuntimeError(f"Invalid command {FLAGS.command}!") diff --git a/deepmd/pt/utils/neighbor_stat.py b/deepmd/pt/utils/neighbor_stat.py new file mode 100644 index 0000000000..b85f3ebcd1 --- /dev/null +++ b/deepmd/pt/utils/neighbor_stat.py @@ -0,0 +1,190 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Iterator, + Optional, + Tuple, +) + +import numpy as np +import torch + +from deepmd.pt.utils.auto_batch_size import ( + AutoBatchSize, +) +from deepmd.pt.utils.env import ( + DEVICE, +) +from deepmd.pt.utils.nlist import ( + extend_coord_with_ghosts, +) +from deepmd.utils.data_system import ( + DeepmdDataSystem, +) +from deepmd.utils.neighbor_stat import NeighborStat as BaseNeighborStat + + +class NeighborStatOP(torch.nn.Module): + """Class for getting neighbor statics data information. + + Parameters + ---------- + ntypes + The num of atom types + rcut + The cut-off radius + distinguish_types : bool, optional + If False, treat all types as a single type. + """ + + def __init__( + self, + ntypes: int, + rcut: float, + distinguish_types: bool, + ) -> None: + super().__init__() + self.rcut = rcut + self.ntypes = ntypes + self.distinguish_types = distinguish_types + + def forward( + self, + coord: torch.Tensor, + atype: torch.Tensor, + cell: Optional[torch.Tensor], + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Calculate the neareest neighbor distance between atoms, maximum nbor size of + atoms and the output data range of the environment matrix. + + Parameters + ---------- + coord + The coordinates of atoms. + atype + The atom types. + cell + The cell. + + Returns + ------- + torch.Tensor + The minimal squared distance between two atoms, in the shape of (nframes,) + torch.Tensor + The maximal number of neighbors + """ + nframes = coord.shape[0] + coord = coord.view(nframes, -1, 3) + nloc = coord.shape[1] + coord = coord.view(nframes, nloc * 3) + extend_coord, extend_atype, _ = extend_coord_with_ghosts( + coord, atype, cell, self.rcut + ) + + coord1 = extend_coord.reshape(nframes, -1) + nall = coord1.shape[1] // 3 + coord0 = coord1[:, : nloc * 3] + diff = ( + coord1.reshape([nframes, -1, 3])[:, None, :, :] + - coord0.reshape([nframes, -1, 3])[:, :, None, :] + ) + assert list(diff.shape) == [nframes, nloc, nall, 3] + # remove the diagonal elements + mask = torch.eye(nloc, nall, dtype=torch.bool) + diff[:, mask] = torch.inf + rr2 = torch.sum(torch.square(diff), dim=-1) + min_rr2, _ = torch.min(rr2, dim=-1) + # count the number of neighbors + if self.distinguish_types: + mask = rr2 < self.rcut**2 + nnei = torch.zeros((nframes, nloc, self.ntypes), dtype=torch.int32) + for ii in range(self.ntypes): + nnei[:, :, ii] = torch.sum( + mask & extend_atype.eq(ii)[:, None, :], dim=-1 + ) + else: + mask = rr2 < self.rcut**2 + # virtual types (<0) are not counted + nnei = torch.sum(mask & extend_atype.ge(0)[:, None, :], dim=-1).view( + nframes, nloc, 1 + ) + max_nnei, _ = torch.max(nnei, dim=1) + return min_rr2, max_nnei + + +class NeighborStat(BaseNeighborStat): + """Neighbor statistics using pure NumPy. + + Parameters + ---------- + ntypes : int + The num of atom types + rcut : float + The cut-off radius + one_type : bool, optional, default=False + Treat all types as a single type. + """ + + def __init__( + self, + ntypes: int, + rcut: float, + one_type: bool = False, + ) -> None: + super().__init__(ntypes, rcut, one_type) + op = NeighborStatOP(ntypes, rcut, not one_type) + self.op = torch.jit.script(op) + self.auto_batch_size = AutoBatchSize() + + def iterator( + self, data: DeepmdDataSystem + ) -> Iterator[Tuple[np.ndarray, float, str]]: + """Abstract method for producing data. + + Yields + ------ + np.ndarray + The maximal number of neighbors + float + The squared minimal distance between two atoms + str + The directory of the data system + """ + for ii in range(len(data.system_dirs)): + for jj in data.data_systems[ii].dirs: + data_set = data.data_systems[ii] + data_set_data = data_set._load_set(jj) + minrr2, max_nnei = self.auto_batch_size.execute_all( + self._execute, + data_set_data["coord"].shape[0], + data_set.get_natoms(), + data_set_data["coord"], + data_set_data["type"], + data_set_data["box"] if data_set.pbc else None, + ) + yield np.max(max_nnei, axis=0), np.min(minrr2), jj + + def _execute( + self, + coord: np.ndarray, + atype: np.ndarray, + cell: Optional[np.ndarray], + ): + """Execute the operation. + + Parameters + ---------- + coord + The coordinates of atoms. + atype + The atom types. + cell + The cell. + """ + minrr2, max_nnei = self.op( + torch.from_numpy(coord).to(DEVICE), + torch.from_numpy(atype).to(DEVICE), + torch.from_numpy(cell).to(DEVICE) if cell is not None else None, + ) + minrr2 = minrr2.detach().cpu().numpy() + max_nnei = max_nnei.detach().cpu().numpy() + return minrr2, max_nnei diff --git a/deepmd/tf/entrypoints/neighbor_stat.py b/deepmd/tf/entrypoints/neighbor_stat.py index e0999bed4a..5d31cdd179 100644 --- a/deepmd/tf/entrypoints/neighbor_stat.py +++ b/deepmd/tf/entrypoints/neighbor_stat.py @@ -1,87 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import logging -from typing import ( - List, +from deepmd.entrypoints.neighbor_stat import ( + neighbor_stat, ) -from deepmd.tf.common import ( - expand_sys_str, -) -from deepmd.tf.utils.data_system import ( - DeepmdDataSystem, -) -from deepmd.tf.utils.neighbor_stat import ( - NeighborStat, -) - -log = logging.getLogger(__name__) - - -def neighbor_stat( - *, - system: str, - rcut: float, - type_map: List[str], - one_type: bool = False, - **kwargs, -): - """Calculate neighbor statistics. - - Parameters - ---------- - system : str - system to stat - rcut : float - cutoff radius - type_map : list[str] - type map - one_type : bool, optional, default=False - treat all types as a single type - **kwargs - additional arguments - - Examples - -------- - >>> neighbor_stat( - ... system=".", - ... rcut=6.0, - ... type_map=[ - ... "C", - ... "H", - ... "O", - ... "N", - ... "P", - ... "S", - ... "Mg", - ... "Na", - ... "HW", - ... "OW", - ... "mNa", - ... "mCl", - ... "mC", - ... "mH", - ... "mMg", - ... "mN", - ... "mO", - ... "mP", - ... ], - ... ) - min_nbor_dist: 0.6599510670195264 - max_nbor_size: [23, 26, 19, 16, 2, 2, 1, 1, 72, 37, 5, 0, 31, 29, 1, 21, 20, 5] - """ - all_sys = expand_sys_str(system) - if not len(all_sys): - raise RuntimeError("Did not find valid system") - data = DeepmdDataSystem( - systems=all_sys, - batch_size=1, - test_size=1, - rcut=rcut, - type_map=type_map, - ) - data.get_batch() - nei = NeighborStat(data.get_ntypes(), rcut, one_type=one_type) - min_nbor_dist, max_nbor_size = nei.get_stat(data) - log.info("min_nbor_dist: %f" % min_nbor_dist) - log.info("max_nbor_size: %s" % str(max_nbor_size)) - return min_nbor_dist, max_nbor_size +__all__ = ["neighbor_stat"] diff --git a/deepmd/tf/utils/neighbor_stat.py b/deepmd/tf/utils/neighbor_stat.py index a240b515db..84e71c7a84 100644 --- a/deepmd/tf/utils/neighbor_stat.py +++ b/deepmd/tf/utils/neighbor_stat.py @@ -1,8 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging -import math from typing import ( - List, + Iterator, Tuple, ) @@ -20,11 +19,12 @@ from deepmd.tf.utils.parallel_op import ( ParallelOp, ) +from deepmd.utils.neighbor_stat import NeighborStat as BaseNeighborStat log = logging.getLogger(__name__) -class NeighborStat: +class NeighborStat(BaseNeighborStat): """Class for getting training data information. It loads data from DeepmdData object, and measures the data info, including neareest nbor distance between atoms, max nbor size of atoms and the output data range of the environment matrix. @@ -46,9 +46,7 @@ def __init__( one_type: bool = False, ) -> None: """Constructor.""" - self.rcut = rcut - self.ntypes = ntypes - self.one_type = one_type + super().__init__(ntypes, rcut, one_type) sub_graph = tf.Graph() def builder(): @@ -91,25 +89,20 @@ def builder(): self.sub_sess = tf.Session(graph=sub_graph, config=default_tf_session_config) - def get_stat(self, data: DeepmdDataSystem) -> Tuple[float, List[int]]: - """Get the data statistics of the training data, including nearest nbor distance between atoms, max nbor size of atoms. - - Parameters - ---------- - data - Class for manipulating many data systems. It is implemented with the help of DeepmdData. - - Returns - ------- - min_nbor_dist - The nearest distance between neighbor atoms - max_nbor_size - A list with ntypes integers, denotes the actual achieved max sel + def iterator( + self, data: DeepmdDataSystem + ) -> Iterator[Tuple[np.ndarray, float, str]]: + """Abstract method for producing data. + + Yields + ------ + np.ndarray + The maximal number of neighbors + float + The squared minimal distance between two atoms + str + The directory of the data system """ - self.min_nbor_dist = 100.0 - self.max_nbor_size = [0] - if not self.one_type: - self.max_nbor_size *= self.ntypes def feed(): for ii in range(len(data.system_dirs)): @@ -129,25 +122,4 @@ def feed(): "dir": str(jj), } - for mn, dt, jj in self.p.generate(self.sub_sess, feed()): - if np.isinf(dt): - log.warning( - "Atoms with no neighbors found in %s. Please make sure it's what you expected." - % jj - ) - if dt < self.min_nbor_dist: - if math.isclose(dt, 0.0, rel_tol=1e-6): - # it's unexpected that the distance between two atoms is zero - # zero distance will cause nan (#874) - raise RuntimeError( - "Some atoms are overlapping in %s. Please check your" - " training data to remove duplicated atoms." % jj - ) - self.min_nbor_dist = dt - self.max_nbor_size = np.maximum(mn, self.max_nbor_size) - - # do sqrt in the final - self.min_nbor_dist = math.sqrt(self.min_nbor_dist) - log.info("training data with min nbor dist: " + str(self.min_nbor_dist)) - log.info("training data with max nbor size: " + str(self.max_nbor_size)) - return self.min_nbor_dist, self.max_nbor_size + return self.p.generate(self.sub_sess, feed()) diff --git a/deepmd/utils/neighbor_stat.py b/deepmd/utils/neighbor_stat.py new file mode 100644 index 0000000000..c6327a705e --- /dev/null +++ b/deepmd/utils/neighbor_stat.py @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging +import math +from abc import ( + ABC, + abstractmethod, +) +from typing import ( + Iterator, + Tuple, +) + +import numpy as np + +from deepmd.utils.data_system import ( + DeepmdDataSystem, +) + +log = logging.getLogger(__name__) + + +class NeighborStat(ABC): + """Abstract base class for getting training data information. + + It loads data from DeepmdData object, and measures the data info, including + neareest nbor distance between atoms, max nbor size of atoms and the output + data range of the environment matrix. + + Parameters + ---------- + ntypes : int + The num of atom types + rcut : float + The cut-off radius + one_type : bool, optional, default=False + Treat all types as a single type. + """ + + def __init__( + self, + ntypes: int, + rcut: float, + one_type: bool = False, + ) -> None: + self.rcut = rcut + self.ntypes = ntypes + self.one_type = one_type + + def get_stat(self, data: DeepmdDataSystem) -> Tuple[float, np.ndarray]: + """Get the data statistics of the training data, including nearest nbor distance between atoms, max nbor size of atoms. + + Parameters + ---------- + data + Class for manipulating many data systems. It is implemented with the help of DeepmdData. + + Returns + ------- + min_nbor_dist + The nearest distance between neighbor atoms + max_nbor_size + An array with ntypes integers, denotes the actual achieved max sel + """ + min_nbor_dist = 100.0 + max_nbor_size = np.zeros(1 if self.one_type else self.ntypes, dtype=int) + + for mn, dt, jj in self.iterator(data): + if np.isinf(dt): + log.warning( + "Atoms with no neighbors found in %s. Please make sure it's what you expected." + % jj + ) + if dt < min_nbor_dist: + if math.isclose(dt, 0.0, rel_tol=1e-6): + # it's unexpected that the distance between two atoms is zero + # zero distance will cause nan (#874) + raise RuntimeError( + "Some atoms are overlapping in %s. Please check your" + " training data to remove duplicated atoms." % jj + ) + min_nbor_dist = dt + max_nbor_size = np.maximum(mn, max_nbor_size) + + # do sqrt in the final + min_nbor_dist = math.sqrt(min_nbor_dist) + log.info("training data with min nbor dist: " + str(min_nbor_dist)) + log.info("training data with max nbor size: " + str(max_nbor_size)) + return min_nbor_dist, max_nbor_size + + @abstractmethod + def iterator( + self, data: DeepmdDataSystem + ) -> Iterator[Tuple[np.ndarray, float, str]]: + """Abstract method for producing data. + + Yields + ------ + mn : np.ndarray + The maximal number of neighbors + dt : float + The squared minimal distance between two atoms + jj : str + The directory of the data system + """ diff --git a/source/tests/common/dpmodel/test_neighbor_stat.py b/source/tests/common/dpmodel/test_neighbor_stat.py new file mode 100644 index 0000000000..aa2ed5b3db --- /dev/null +++ b/source/tests/common/dpmodel/test_neighbor_stat.py @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import shutil +import unittest + +import dpdata +import numpy as np + +from deepmd.entrypoints.neighbor_stat import ( + neighbor_stat, +) + + +def gen_sys(nframes): + natoms = 1000 + data = {} + X, Y, Z = np.mgrid[0:2:3j, 0:2:3j, 0:2:3j] + positions = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T # + 0.1 + data["coords"] = np.repeat(positions[np.newaxis, :, :], nframes, axis=0) + data["forces"] = np.random.random([nframes, natoms, 3]) + data["cells"] = np.array([3.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 3.0]).reshape( + 1, 3, 3 + ) + data["energies"] = np.random.random([nframes, 1]) + data["atom_names"] = ["TYPE"] + data["atom_numbs"] = [27] + data["atom_types"] = np.repeat(0, 27) + return data + + +class TestNeighborStat(unittest.TestCase): + def setUp(self): + data0 = gen_sys(1) + sys0 = dpdata.LabeledSystem() + sys0.data = data0 + sys0.to_deepmd_npy("system_0", set_size=1) + + def tearDown(self): + shutil.rmtree("system_0") + + def test_neighbor_stat(self): + for rcut in (0.0, 1.0, 2.0, 4.0): + for one_type in (True, False): + with self.subTest(rcut=rcut, one_type=one_type): + rcut += 1e-3 # prevent numerical errors + min_nbor_dist, max_nbor_size = neighbor_stat( + system="system_0", + rcut=rcut, + type_map=["TYPE"], + one_type=one_type, + backend="numpy", + ) + upper = np.ceil(rcut) + 1 + X, Y, Z = np.mgrid[-upper:upper, -upper:upper, -upper:upper] + positions = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T + # distance to (0,0,0) + distance = np.linalg.norm(positions, axis=1) + expected_neighbors = np.count_nonzero( + np.logical_and(distance > 0, distance <= rcut) + ) + self.assertAlmostEqual(min_nbor_dist, 1.0, 6) + self.assertEqual(max_nbor_size, [expected_neighbors]) diff --git a/source/tests/pt/test_neighbor_stat.py b/source/tests/pt/test_neighbor_stat.py new file mode 100644 index 0000000000..7e4be0909e --- /dev/null +++ b/source/tests/pt/test_neighbor_stat.py @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import shutil +import unittest + +import dpdata +import numpy as np + +from deepmd.entrypoints.neighbor_stat import ( + neighbor_stat, +) + + +def gen_sys(nframes): + natoms = 1000 + data = {} + X, Y, Z = np.mgrid[0:2:3j, 0:2:3j, 0:2:3j] + positions = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T # + 0.1 + data["coords"] = np.repeat(positions[np.newaxis, :, :], nframes, axis=0) + data["forces"] = np.random.random([nframes, natoms, 3]) + data["cells"] = np.array([3.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 3.0]).reshape( + 1, 3, 3 + ) + data["energies"] = np.random.random([nframes, 1]) + data["atom_names"] = ["TYPE"] + data["atom_numbs"] = [27] + data["atom_types"] = np.repeat(0, 27) + return data + + +class TestNeighborStat(unittest.TestCase): + def setUp(self): + data0 = gen_sys(1) + sys0 = dpdata.LabeledSystem() + sys0.data = data0 + sys0.to_deepmd_npy("system_0", set_size=1) + + def tearDown(self): + shutil.rmtree("system_0") + + def test_neighbor_stat(self): + for rcut in (0.0, 1.0, 2.0, 4.0): + for one_type in (True, False): + with self.subTest(rcut=rcut, one_type=one_type): + rcut += 1e-3 # prevent numerical errors + min_nbor_dist, max_nbor_size = neighbor_stat( + system="system_0", + rcut=rcut, + type_map=["TYPE"], + one_type=one_type, + backend="pytorch", + ) + upper = np.ceil(rcut) + 1 + X, Y, Z = np.mgrid[-upper:upper, -upper:upper, -upper:upper] + positions = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T + # distance to (0,0,0) + distance = np.linalg.norm(positions, axis=1) + expected_neighbors = np.count_nonzero( + np.logical_and(distance > 0, distance <= rcut) + ) + self.assertAlmostEqual(min_nbor_dist, 1.0, 6) + self.assertEqual(max_nbor_size, [expected_neighbors]) From 930bc1a1e4b0b328bdab990bcc2c551fdd06e229 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 14 Feb 2024 03:09:05 -0500 Subject: [PATCH 078/686] support TF se_e2_a serialization; add a common test fixture to compare TF, PT, and DP models (#3263) Some codes are split from #2987. --------- Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/dpmodel/common.py | 5 + deepmd/dpmodel/descriptor/se_e2_a.py | 8 +- deepmd/dpmodel/utils/network.py | 6 +- deepmd/pt/model/descriptor/se_a.py | 5 +- deepmd/pt/utils/env.py | 10 + deepmd/tf/descriptor/descriptor.py | 38 ++ deepmd/tf/descriptor/se.py | 171 ++++++++ deepmd/tf/descriptor/se_a.py | 102 +++++ deepmd/tf/env.py | 30 +- deepmd/tf/utils/graph.py | 10 +- deepmd/tf/utils/tabulate.py | 4 + source/tests/consistent/__init__.py | 2 + source/tests/consistent/common.py | 375 ++++++++++++++++++ .../tests/consistent/descriptor/__init__.py | 1 + source/tests/consistent/descriptor/common.py | 95 +++++ .../consistent/descriptor/test_se_e2_a.py | 149 +++++++ 16 files changed, 988 insertions(+), 23 deletions(-) create mode 100644 source/tests/consistent/__init__.py create mode 100644 source/tests/consistent/common.py create mode 100644 source/tests/consistent/descriptor/__init__.py create mode 100644 source/tests/consistent/descriptor/common.py create mode 100644 source/tests/consistent/descriptor/test_se_e2_a.py diff --git a/deepmd/dpmodel/common.py b/deepmd/dpmodel/common.py index 1e35bd4d49..982a4eb834 100644 --- a/deepmd/dpmodel/common.py +++ b/deepmd/dpmodel/common.py @@ -6,6 +6,10 @@ import numpy as np +from deepmd.common import ( + GLOBAL_NP_FLOAT_PRECISION, +) + PRECISION_DICT = { "float16": np.float16, "float32": np.float32, @@ -15,6 +19,7 @@ "double": np.float64, "int32": np.int32, "int64": np.int64, + "default": GLOBAL_NP_FLOAT_PRECISION, } DEFAULT_PRECISION = "float64" diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index 1cbaf69c49..78ff83a056 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -15,6 +15,7 @@ from deepmd.dpmodel import ( DEFAULT_PRECISION, + PRECISION_DICT, NativeOP, ) from deepmd.dpmodel.utils import ( @@ -133,6 +134,8 @@ def __init__( activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, spin: Optional[Any] = None, + # consistent with argcheck, not used though + seed: Optional[int] = None, ) -> None: ## seed, uniform_seed, multi_task, not included. if not type_one_side: @@ -163,6 +166,8 @@ def __init__( ndim=(1 if self.type_one_side else 2), network_type="embedding_network", ) + if not self.type_one_side: + raise NotImplementedError("type_one_side == False not implemented") for ii in range(self.ntypes): self.embeddings[(ii,)] = EmbeddingNet( in_dim, @@ -316,7 +321,8 @@ def serialize(self) -> dict: "exclude_types": self.exclude_types, "set_davg_zero": self.set_davg_zero, "activation_function": self.activation_function, - "precision": self.precision, + # make deterministic + "precision": np.dtype(PRECISION_DICT[self.precision]).name, "spin": self.spin, "env_mat": self.env_mat.serialize(), "embeddings": self.embeddings.serialize(), diff --git a/deepmd/dpmodel/utils/network.py b/deepmd/dpmodel/utils/network.py index 17b3043612..8c826c8771 100644 --- a/deepmd/dpmodel/utils/network.py +++ b/deepmd/dpmodel/utils/network.py @@ -192,7 +192,8 @@ def serialize(self) -> dict: "use_timestep": self.idt is not None, "activation_function": self.activation_function, "resnet": self.resnet, - "precision": self.precision, + # make deterministic + "precision": np.dtype(PRECISION_DICT[self.precision]).name, "@variables": data, } @@ -464,7 +465,8 @@ def serialize(self) -> dict: "neuron": self.neuron.copy(), "activation_function": self.activation_function, "resnet_dt": self.resnet_dt, - "precision": self.precision, + # make deterministic + "precision": np.dtype(PRECISION_DICT[self.precision]).name, "layers": [layer.serialize() for layer in self.layers], } diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 700bf6d59b..c722c2dc02 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -19,6 +19,7 @@ ) from deepmd.pt.utils.env import ( PRECISION_DICT, + RESERVED_PRECISON_DICT, ) try: @@ -207,7 +208,8 @@ def serialize(self) -> dict: "resnet_dt": obj.resnet_dt, "set_davg_zero": obj.set_davg_zero, "activation_function": obj.activation_function, - "precision": obj.precision, + # make deterministic + "precision": RESERVED_PRECISON_DICT[obj.prec], "embeddings": obj.filter_layers.serialize(), "env_mat": DPEnvMat(obj.rcut, obj.rcut_smth).serialize(), "@variables": { @@ -223,6 +225,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "DescrptSeA": + data = data.copy() variables = data.pop("@variables") embeddings = data.pop("embeddings") env_mat = data.pop("env_mat") diff --git a/deepmd/pt/utils/env.py b/deepmd/pt/utils/env.py index 81499b5063..7383cf5c49 100644 --- a/deepmd/pt/utils/env.py +++ b/deepmd/pt/utils/env.py @@ -42,6 +42,15 @@ "int64": torch.int64, } GLOBAL_PT_FLOAT_PRECISION = PRECISION_DICT[np.dtype(GLOBAL_NP_FLOAT_PRECISION).name] +PRECISION_DICT["default"] = GLOBAL_PT_FLOAT_PRECISION +# cannot automatically generated +RESERVED_PRECISON_DICT = { + torch.float16: "float16", + torch.float32: "float32", + torch.float64: "float64", + torch.int32: "int32", + torch.int64: "int64", +} DEFAULT_PRECISION = "float64" # throw warnings if threads not set @@ -58,6 +67,7 @@ "GLOBAL_PT_FLOAT_PRECISION", "DEFAULT_PRECISION", "PRECISION_DICT", + "RESERVED_PRECISON_DICT", "SAMPLER_RECORD", "NUM_WORKERS", "DEVICE", diff --git a/deepmd/tf/descriptor/descriptor.py b/deepmd/tf/descriptor/descriptor.py index fe49fe11fe..1a73d3c273 100644 --- a/deepmd/tf/descriptor/descriptor.py +++ b/deepmd/tf/descriptor/descriptor.py @@ -509,3 +509,41 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): # call subprocess cls = cls.get_class_by_input(local_jdata) return cls.update_sel(global_jdata, local_jdata) + + @classmethod + def deserialize(cls, data: dict, suffix: str = "") -> "Descriptor": + """Deserialize the model. + + There is no suffix in a native DP model, but it is important + for the TF backend. + + Parameters + ---------- + data : dict + The serialized data + suffix : str, optional + Name suffix to identify this descriptor + + Returns + ------- + Descriptor + The deserialized descriptor + """ + if cls is Descriptor: + return Descriptor.get_class_by_input(data).deserialize(data) + raise NotImplementedError("Not implemented in class %s" % cls.__name__) + + def serialize(self, suffix: str = "") -> dict: + """Serialize the model. + + There is no suffix in a native DP model, but it is important + for the TF backend. + + Returns + ------- + dict + The serialized data + suffix : str, optional + Name suffix to identify this descriptor + """ + raise NotImplementedError("Not implemented in class %s" % self.__name__) diff --git a/deepmd/tf/descriptor/se.py b/deepmd/tf/descriptor/se.py index 4f49a8800f..98d98cd467 100644 --- a/deepmd/tf/descriptor/se.py +++ b/deepmd/tf/descriptor/se.py @@ -1,9 +1,17 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import re from typing import ( + List, + Set, Tuple, ) +from deepmd.dpmodel.utils.network import ( + EmbeddingNet, + NetworkCollection, +) from deepmd.tf.env import ( + EMBEDDING_NET_PATTERN, tf, ) from deepmd.tf.utils.graph import ( @@ -160,3 +168,166 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): # default behavior is to update sel which is a list local_jdata_cpy = local_jdata.copy() return update_one_sel(global_jdata, local_jdata_cpy, False) + + def serialize_network( + self, + ntypes: int, + ndim: int, + in_dim: int, + neuron: List[int], + activation_function: str, + resnet_dt: bool, + variables: dict, + excluded_types: Set[Tuple[int, int]] = set(), + suffix: str = "", + ) -> dict: + """Serialize network. + + Parameters + ---------- + ntypes : int + The number of types + ndim : int + The dimension of elements + in_dim : int + The input dimension + neuron : List[int] + The neuron list + activation_function : str + The activation function + resnet_dt : bool + Whether to use resnet + variables : dict + The input variables + excluded_types : Set[Tuple[int, int]], optional + The excluded types + suffix : str, optional + The suffix of the scope + + Returns + ------- + dict + The converted network data + """ + embeddings = NetworkCollection( + ntypes=ntypes, + ndim=ndim, + network_type="embedding_network", + ) + if ndim == 2: + for type_i, type_j in excluded_types: + # initialize an empty network for the excluded types + embeddings[(type_i, type_j)] = EmbeddingNet( + in_dim=in_dim, + neuron=neuron, + activation_function=activation_function, + resnet_dt=resnet_dt, + precision=self.precision.name, + ) + embeddings[(type_j, type_i)] = EmbeddingNet( + in_dim=in_dim, + neuron=neuron, + activation_function=activation_function, + resnet_dt=resnet_dt, + precision=self.precision.name, + ) + for layer in range(len(neuron)): + embeddings[(type_i, type_j)][layer]["w"][:] = 0.0 + embeddings[(type_i, type_j)][layer]["b"][:] = 0.0 + if embeddings[(type_i, type_j)][layer]["idt"] is not None: + embeddings[(type_i, type_j)][layer]["idt"][:] = 0.0 + embeddings[(type_j, type_i)][layer]["w"][:] = 0.0 + embeddings[(type_j, type_i)][layer]["b"][:] = 0.0 + if embeddings[(type_j, type_i)][layer]["idt"] is not None: + embeddings[(type_j, type_i)][layer]["idt"][:] = 0.0 + + if suffix != "": + embedding_net_pattern = ( + EMBEDDING_NET_PATTERN.replace("/(idt)", suffix + "/(idt)") + .replace("/(bias)", suffix + "/(bias)") + .replace("/(matrix)", suffix + "/(matrix)") + ) + else: + embedding_net_pattern = EMBEDDING_NET_PATTERN + for key, value in variables.items(): + m = re.search(embedding_net_pattern, key) + m = [mm for mm in m.groups() if mm is not None] + typei = m[0] + typej = "_".join(m[3:]) if len(m[3:]) else "all" + layer_idx = int(m[2]) - 1 + weight_name = m[1] + if ndim == 0: + network_idx = () + elif ndim == 1: + network_idx = (int(typej),) + elif ndim == 2: + network_idx = (int(typei), int(typej)) + else: + raise ValueError(f"Invalid ndim: {ndim}") + if embeddings[network_idx] is None: + # initialize the network if it is not initialized + embeddings[network_idx] = EmbeddingNet( + in_dim=in_dim, + neuron=neuron, + activation_function=activation_function, + resnet_dt=resnet_dt, + precision=self.precision.name, + ) + assert embeddings[network_idx] is not None + if weight_name == "idt": + value = value.ravel() + embeddings[network_idx][layer_idx][weight_name] = value + return embeddings.serialize() + + @classmethod + def deserialize_network(cls, data: dict, suffix: str = "") -> dict: + """Deserialize network. + + Parameters + ---------- + data : dict + The input network data + suffix : str, optional + The suffix of the scope + + Returns + ------- + variables : dict + The input variables + """ + embedding_net_variables = {} + embeddings = NetworkCollection.deserialize(data) + for ii in range(embeddings.ntypes**embeddings.ndim): + net_idx = [] + rest_ii = ii + for _ in range(embeddings.ndim): + net_idx.append(rest_ii % embeddings.ntypes) + rest_ii //= embeddings.ntypes + net_idx = tuple(net_idx) + if embeddings.ndim in (0, 1): + key0 = "all" + key1 = f"_{ii}" + elif embeddings.ndim == 2: + key0 = f"{net_idx[0]}" + key1 = f"_{net_idx[1]}" + else: + raise ValueError(f"Invalid ndim: {embeddings.ndim}") + network = embeddings[net_idx] + assert network is not None + for layer_idx, layer in enumerate(network.layers): + embedding_net_variables[ + f"filter_type_{key0}{suffix}/matrix_{layer_idx + 1}{key1}" + ] = layer.w + embedding_net_variables[ + f"filter_type_{key0}{suffix}/bias_{layer_idx + 1}{key1}" + ] = layer.b + if layer.idt is not None: + embedding_net_variables[ + f"filter_type_{key0}{suffix}/idt_{layer_idx + 1}{key1}" + ] = layer.idt.reshape(1, -1) + else: + # prevent keyError + embedding_net_variables[ + f"filter_type_{key0}{suffix}/idt_{layer_idx + 1}{key1}" + ] = 0.0 + return embedding_net_variables diff --git a/deepmd/tf/descriptor/se_a.py b/deepmd/tf/descriptor/se_a.py index 01c4ee8844..986328479b 100644 --- a/deepmd/tf/descriptor/se_a.py +++ b/deepmd/tf/descriptor/se_a.py @@ -7,6 +7,9 @@ import numpy as np +from deepmd.dpmodel.utils.env_mat import ( + EnvMat, +) from deepmd.tf.common import ( cast_precision, get_activation_func, @@ -195,6 +198,7 @@ def __init__( self.trainable = trainable self.compress_activation_fn = get_activation_func(activation_function) self.filter_activation_fn = get_activation_func(activation_function) + self.activation_function_name = activation_function self.filter_precision = get_precision(precision) self.filter_np_precision = get_np_precision(precision) self.exclude_types = set() @@ -1345,3 +1349,101 @@ def explicit_ntypes(self) -> bool: if self.stripped_type_embedding: return True return False + + @classmethod + def deserialize(cls, data: dict, suffix: str = ""): + """Deserialize the model. + + Parameters + ---------- + data : dict + The serialized data + + Returns + ------- + Model + The deserialized model + """ + if cls is not DescrptSeA: + raise NotImplementedError("Not implemented in class %s" % cls.__name__) + data = data.copy() + embedding_net_variables = cls.deserialize_network( + data.pop("embeddings"), suffix=suffix + ) + data.pop("env_mat") + variables = data.pop("@variables") + descriptor = cls(**data) + descriptor.embedding_net_variables = embedding_net_variables + descriptor.davg = variables["davg"].reshape( + descriptor.ntypes, descriptor.ndescrpt + ) + descriptor.dstd = variables["dstd"].reshape( + descriptor.ntypes, descriptor.ndescrpt + ) + return descriptor + + def serialize(self, suffix: str = "") -> dict: + """Serialize the model. + + Parameters + ---------- + suffix : str, optional + The suffix of the scope + + Returns + ------- + dict + The serialized data + """ + if type(self) is not DescrptSeA: + raise NotImplementedError( + "Not implemented in class %s" % self.__class__.__name__ + ) + if self.stripped_type_embedding: + raise NotImplementedError( + "stripped_type_embedding is unsupported by the native model" + ) + if (self.original_sel != self.sel_a).any(): + raise NotImplementedError( + "Adjusting sel is unsupported by the native model" + ) + if self.embedding_net_variables is None: + raise RuntimeError("init_variables must be called before serialize") + if self.spin is not None: + raise NotImplementedError("spin is unsupported") + assert self.davg is not None + assert self.dstd is not None + # TODO: not sure how to handle type embedding - type embedding is not a model parameter, + # but instead a part of the input data. Maybe the interface should be refactored... + + return { + "rcut": self.rcut_r, + "rcut_smth": self.rcut_r_smth, + "sel": self.sel_a, + "neuron": self.filter_neuron, + "axis_neuron": self.n_axis_neuron, + "resnet_dt": self.filter_resnet_dt, + "trainable": self.trainable, + "type_one_side": self.type_one_side, + "exclude_types": list(self.exclude_types), + "set_davg_zero": self.set_davg_zero, + "activation_function": self.activation_function_name, + "precision": self.filter_precision.name, + "embeddings": self.serialize_network( + ntypes=self.ntypes, + ndim=(1 if self.type_one_side else 2), + in_dim=1, + neuron=self.filter_neuron, + activation_function=self.activation_function_name, + resnet_dt=self.filter_resnet_dt, + variables=self.embedding_net_variables, + excluded_types=self.exclude_types, + suffix=suffix, + ), + "env_mat": EnvMat(self.rcut_r, self.rcut_r_smth).serialize(), + "@variables": { + "davg": self.davg.reshape(self.ntypes, self.nnei_a, 4), + "dstd": self.dstd.reshape(self.ntypes, self.nnei_a, 4), + }, + "spin": self.spin, + } diff --git a/deepmd/tf/env.py b/deepmd/tf/env.py index e94c052f55..fe5bb81bae 100644 --- a/deepmd/tf/env.py +++ b/deepmd/tf/env.py @@ -120,19 +120,25 @@ def dlopen_library(module: str, filename: str): except AttributeError: tf_py_version = tf.__version__ +# subpatterns: +# \1: type of centeral atom +# \2: weight name +# \3: layer index +# The rest: types of neighbor atoms +# IMPORTANT: the order is critical to match the pattern EMBEDDING_NET_PATTERN = str( - r"filter_type_\d+/matrix_\d+_\d+|" - r"filter_type_\d+/bias_\d+_\d+|" - r"filter_type_\d+/idt_\d+_\d+|" - r"filter_type_all/matrix_\d+|" - r"filter_type_all/matrix_\d+_\d+|" - r"filter_type_all/matrix_\d+_\d+_\d+|" - r"filter_type_all/bias_\d+|" - r"filter_type_all/bias_\d+_\d+|" - r"filter_type_all/bias_\d+_\d+_\d+|" - r"filter_type_all/idt_\d+|" - r"filter_type_all/idt_\d+_\d+|" -) + r"filter_type_(\d+)/(matrix)_(\d+)_(\d+)|" + r"filter_type_(\d+)/(bias)_(\d+)_(\d+)|" + r"filter_type_(\d+)/(idt)_(\d+)_(\d+)|" + r"filter_type_(all)/(matrix)_(\d+)_(\d+)_(\d+)|" + r"filter_type_(all)/(matrix)_(\d+)_(\d+)|" + r"filter_type_(all)/(matrix)_(\d+)|" + r"filter_type_(all)/(bias)_(\d+)_(\d+)_(\d+)|" + r"filter_type_(all)/(bias)_(\d+)_(\d+)|" + r"filter_type_(all)/(bias)_(\d+)|" + r"filter_type_(all)/(idt)_(\d+)_(\d+)|" + r"filter_type_(all)/(idt)_(\d+)|" +)[:-1] FITTING_NET_PATTERN = str( r"layer_\d+/matrix|" diff --git a/deepmd/tf/utils/graph.py b/deepmd/tf/utils/graph.py index 9d2608e34a..7e67cf27a6 100644 --- a/deepmd/tf/utils/graph.py +++ b/deepmd/tf/utils/graph.py @@ -166,9 +166,9 @@ def get_embedding_net_nodes_from_graph_def( # embedding_net_pattern = f"filter_type_\d+{suffix}/matrix_\d+_\d+|filter_type_\d+{suffix}/bias_\d+_\d+|filter_type_\d+{suffix}/idt_\d+_\d+|filter_type_all{suffix}/matrix_\d+_\d+|filter_type_all{suffix}/matrix_\d+_\d+_\d+|filter_type_all{suffix}/bias_\d+_\d+|filter_type_all{suffix}/bias_\d+_\d+_\d+|filter_type_all{suffix}/idt_\d+_\d+" if suffix != "": embedding_net_pattern = ( - EMBEDDING_NET_PATTERN.replace("/idt", suffix + "/idt") - .replace("/bias", suffix + "/bias") - .replace("/matrix", suffix + "/matrix") + EMBEDDING_NET_PATTERN.replace("/(idt)", suffix + "/(idt)") + .replace("/(bias)", suffix + "/(bias)") + .replace("/(matrix)", suffix + "/(matrix)") ) else: embedding_net_pattern = EMBEDDING_NET_PATTERN @@ -176,10 +176,6 @@ def get_embedding_net_nodes_from_graph_def( embedding_net_nodes = get_pattern_nodes_from_graph_def( graph_def, embedding_net_pattern ) - for key in embedding_net_nodes.keys(): - assert ( - key.find("bias") > 0 or key.find("matrix") > 0 - ), "currently, only support weight matrix and bias matrix at the tabulation op!" return embedding_net_nodes diff --git a/deepmd/tf/utils/tabulate.py b/deepmd/tf/utils/tabulate.py index ff5e2b9e09..958e08dd86 100644 --- a/deepmd/tf/utils/tabulate.py +++ b/deepmd/tf/utils/tabulate.py @@ -133,6 +133,10 @@ def __init__( self.embedding_net_nodes = get_embedding_net_nodes_from_graph_def( self.graph_def, suffix=self.suffix ) + for key in self.embedding_net_nodes.keys(): + assert ( + key.find("bias") > 0 or key.find("matrix") > 0 + ), "currently, only support weight matrix and bias matrix at the tabulation op!" # move it to the descriptor class # for tt in self.exclude_types: diff --git a/source/tests/consistent/__init__.py b/source/tests/consistent/__init__.py new file mode 100644 index 0000000000..50b8b8bdc5 --- /dev/null +++ b/source/tests/consistent/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Test whether DP native, TF, and PT models are consistent.""" diff --git a/source/tests/consistent/common.py b/source/tests/consistent/common.py new file mode 100644 index 0000000000..e5633726ef --- /dev/null +++ b/source/tests/consistent/common.py @@ -0,0 +1,375 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import itertools +import os +import sys +from abc import ( + ABC, + abstractmethod, +) +from enum import ( + Enum, +) +from importlib.util import ( + find_spec, +) +from typing import ( + Any, + Callable, + ClassVar, + List, + Optional, + Tuple, +) +from uuid import ( + uuid4, +) + +import numpy as np +from dargs import ( + Argument, +) + +INSTALLED_TF = find_spec("tensorflow") is not None +INSTALLED_PT = find_spec("torch") is not None + +if os.environ.get("CI") and not (INSTALLED_TF and INSTALLED_PT): + raise ImportError("TensorFlow or PyTorch should be tested in the CI") + + +if INSTALLED_TF: + from deepmd.tf.common import ( + clear_session, + ) + from deepmd.tf.env import ( + default_tf_session_config, + tf, + ) + from deepmd.tf.utils.sess import ( + run_sess, + ) + + +__all__ = [ + "CommonTest", + "INSTALLED_TF", + "INSTALLED_PT", +] + + +class CommonTest(ABC): + data: ClassVar[dict] + """Arguments data.""" + tf_class: ClassVar[Optional[type]] + """TensorFlow model class.""" + dp_class: ClassVar[Optional[type]] + """Native DP model class.""" + pt_class: ClassVar[Optional[type]] + """PyTorch model class.""" + args: ClassVar[Optional[List[Argument]]] + """Arguments that maps to the `data`.""" + skip_dp: ClassVar[bool] = False + """Whether to skip the native DP model.""" + skip_tf: ClassVar[bool] = not INSTALLED_TF + """Whether to skip the TensorFlow model.""" + skip_pt: ClassVar[bool] = not INSTALLED_PT + """Whether to skip the PyTorch model.""" + + def setUp(self): + self.unique_id = uuid4().hex + + def reset_unique_id(self): + self.unique_id = uuid4().hex + + def init_backend_cls(self, cls) -> Any: + """Initialize a backend model.""" + assert self.data is not None + if self.args is None: + data = self.data + else: + base = Argument("arg", dict, sub_fields=self.args) + data = base.normalize_value(self.data, trim_pattern="_*") + base.check_value(data, strict=True) + return cls(**data) + + @abstractmethod + def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: + """Build the TF graph. + + Parameters + ---------- + obj : Any + The object of TF + suffix : str + The suffix of the scope + + Returns + ------- + list of tf.Tensor + The list of tensors + dict + The feed_dict + """ + + @abstractmethod + def eval_dp(self, dp_obj: Any) -> Any: + """Evaluate the return value of DP. + + Parameters + ---------- + dp_obj : Any + The object of DP + """ + + @abstractmethod + def eval_pt(self, pt_obj: Any) -> Any: + """Evaluate the return value of PT. + + Parameters + ---------- + pt_obj : Any + The object of PT + """ + + class RefBackend(Enum): + """Reference backend.""" + + TF = 1 + DP = 2 + PT = 3 + + @abstractmethod + def extract_ret(self, ret: Any, backend: RefBackend) -> Tuple[np.ndarray, ...]: + """Extract the return value when comparing with other backends. + + Parameters + ---------- + ret : Any + The return value + backend : RefBackend + The backend + + Returns + ------- + tuple[np.ndarray, ...] + The extracted return value + """ + + def build_eval_tf( + self, sess: "tf.Session", obj: Any, suffix: str + ) -> List[np.ndarray]: + """Build and evaluate the TF graph.""" + t_out, feed_dict = self.build_tf(obj, suffix) + + t_out_indentity = [ + tf.identity(tt, name=f"o_{ii}_{suffix}") for ii, tt in enumerate(t_out) + ] + run_sess(sess, tf.global_variables_initializer()) + return run_sess( + sess, + t_out_indentity, + feed_dict=feed_dict, + ) + + def get_tf_ret_serialization_from_cls(self, obj): + with tf.Session(config=default_tf_session_config) as sess: + graph = tf.get_default_graph() + ret = self.build_eval_tf(sess, obj, suffix=self.unique_id) + output_graph_def = tf.graph_util.convert_variables_to_constants( + sess, + graph.as_graph_def(), + [f"o_{ii}_{self.unique_id}" for ii, _ in enumerate(ret)], + ) + with tf.Graph().as_default() as new_graph: + tf.import_graph_def(output_graph_def, name="") + obj.init_variables(new_graph, output_graph_def, suffix=self.unique_id) + + data = obj.serialize(suffix=self.unique_id) + return ret, data + + def get_pt_ret_serialization_from_cls(self, obj): + ret = self.eval_pt(obj) + data = obj.serialize() + return ret, data + + def get_dp_ret_serialization_from_cls(self, obj): + ret = self.eval_dp(obj) + data = obj.serialize() + return ret, data + + def get_reference_backend(self): + """Get the reference backend. + + Order of checking for ref: DP, TF, PT. + """ + if not self.skip_dp: + return self.RefBackend.DP + if not self.skip_tf: + return self.RefBackend.TF + if not self.skip_pt: + return self.RefBackend.PT + raise ValueError("No available reference") + + def get_reference_ret_serialization(self, ref: RefBackend): + if ref == self.RefBackend.DP: + obj = self.init_backend_cls(self.dp_class) + return self.get_dp_ret_serialization_from_cls(obj) + if ref == self.RefBackend.TF: + obj = self.init_backend_cls(self.tf_class) + self.reset_unique_id() + return self.get_tf_ret_serialization_from_cls(obj) + if ref == self.RefBackend.PT: + obj = self.init_backend_cls(self.pt_class) + return self.get_pt_ret_serialization_from_cls(obj) + raise ValueError("No available reference") + + def test_tf_consistent_with_ref(self): + """Test whether TF and reference are consistent.""" + if self.skip_tf: + self.skipTest("Unsupported backend") + ref_backend = self.get_reference_backend() + if ref_backend == self.RefBackend.TF: + self.skipTest("Reference is self") + ret1, data1 = self.get_reference_ret_serialization(ref_backend) + ret1 = self.extract_ret(ret1, ref_backend) + self.reset_unique_id() + tf_obj = self.tf_class.deserialize(data1, suffix=self.unique_id) + ret2, data2 = self.get_tf_ret_serialization_from_cls(tf_obj) + ret2 = self.extract_ret(ret2, self.RefBackend.TF) + np.testing.assert_equal(data1, data2) + for rr1, rr2 in zip(ret1, ret2): + np.testing.assert_allclose(rr1, rr2) + + def test_tf_self_consistent(self): + """Test whether TF is self consistent.""" + if self.skip_tf: + self.skipTest("Unsupported backend") + obj1 = self.init_backend_cls(self.tf_class) + self.reset_unique_id() + ret1, data1 = self.get_tf_ret_serialization_from_cls(obj1) + self.reset_unique_id() + obj2 = self.tf_class.deserialize(data1, suffix=self.unique_id) + ret2, data2 = self.get_tf_ret_serialization_from_cls(obj2) + np.testing.assert_equal(data1, data2) + for rr1, rr2 in zip(ret1, ret2): + np.testing.assert_allclose(rr1, rr2) + + def test_dp_consistent_with_ref(self): + """Test whether DP and reference are consistent.""" + if self.skip_dp: + self.skipTest("Unsupported backend") + ref_backend = self.get_reference_backend() + if ref_backend == self.RefBackend.DP: + self.skipTest("Reference is self") + ret1, data1 = self.get_reference_ret_serialization(ref_backend) + ret1 = self.extract_ret(ret1, ref_backend) + dp_obj = self.dp_class.deserialize(data1) + ret2 = self.eval_dp(dp_obj) + ret2 = self.extract_ret(ret2, self.RefBackend.DP) + data2 = dp_obj.serialize() + np.testing.assert_equal(data1, data2) + for rr1, rr2 in zip(ret1, ret2): + np.testing.assert_allclose(rr1, rr2) + + def test_dp_self_consistent(self): + """Test whether DP is self consistent.""" + if self.skip_dp: + self.skipTest("Unsupported backend") + obj1 = self.init_backend_cls(self.dp_class) + ret1, data1 = self.get_dp_ret_serialization_from_cls(obj1) + obj1 = self.dp_class.deserialize(data1) + ret2, data2 = self.get_dp_ret_serialization_from_cls(obj1) + np.testing.assert_equal(data1, data2) + for rr1, rr2 in zip(ret1, ret2): + if isinstance(rr1, np.ndarray) and isinstance(rr2, np.ndarray): + np.testing.assert_allclose(rr1, rr2) + else: + self.assertEqual(rr1, rr2) + + def test_pt_consistent_with_ref(self): + """Test whether PT and reference are consistent.""" + if self.skip_pt: + self.skipTest("Unsupported backend") + ref_backend = self.get_reference_backend() + if ref_backend == self.RefBackend.PT: + self.skipTest("Reference is self") + ret1, data1 = self.get_reference_ret_serialization(ref_backend) + ret1 = self.extract_ret(ret1, ref_backend) + obj = self.pt_class.deserialize(data1) + ret2 = self.eval_pt(obj) + ret2 = self.extract_ret(ret2, self.RefBackend.PT) + data2 = obj.serialize() + np.testing.assert_equal(data1, data2) + for rr1, rr2 in zip(ret1, ret2): + np.testing.assert_allclose(rr1, rr2) + + def test_pt_self_consistent(self): + """Test whether PT is self consistent.""" + if self.skip_pt: + self.skipTest("Unsupported backend") + obj1 = self.init_backend_cls(self.pt_class) + ret1, data1 = self.get_pt_ret_serialization_from_cls(obj1) + obj2 = self.pt_class.deserialize(data1) + ret2, data2 = self.get_pt_ret_serialization_from_cls(obj2) + np.testing.assert_equal(data1, data2) + for rr1, rr2 in zip(ret1, ret2): + if isinstance(rr1, np.ndarray) and isinstance(rr2, np.ndarray): + np.testing.assert_allclose(rr1, rr2) + else: + self.assertEqual(rr1, rr2) + + def tearDown(self) -> None: + """Clear the TF session.""" + if not self.skip_tf: + clear_session() + + +def parameterized(*attrs: tuple) -> Callable: + """Parameterized test. + + Orginal class will not be actually generated. Avoid inherbiting from it. + New classes are generated with the name of the original class and the + parameters. + + Parameters + ---------- + *attrs : tuple + The attributes to be parameterized. + + Returns + ------- + object + The decorator. + + Examples + -------- + >>> @parameterized( + ... (True, False), + ... (True, False), + ... ) + ... class TestSeA(CommonTest, unittest.TestCase): + ... @property + ... def data(self) -> dict: + ... ( + ... param1, + ... param2, + ... ) = self.param + ... return { + ... "param1": param1, + ... "param2": param2, + ... } + """ + + def decorator(base_class: type): + class_module = sys.modules[base_class.__module__].__dict__ + for pp in itertools.product(*attrs): + + class TestClass(base_class): + param: ClassVar = pp + + name = f"{base_class.__name__}_{'_'.join(str(x) for x in pp)}" + + class_module[name] = TestClass + # make unittest module happy by ignoring the original one + return object + + return decorator diff --git a/source/tests/consistent/descriptor/__init__.py b/source/tests/consistent/descriptor/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/source/tests/consistent/descriptor/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/source/tests/consistent/descriptor/common.py b/source/tests/consistent/descriptor/common.py new file mode 100644 index 0000000000..ef7b39b52e --- /dev/null +++ b/source/tests/consistent/descriptor/common.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Any, +) + +from deepmd.common import ( + make_default_mesh, +) +from deepmd.dpmodel.utils.nlist import ( + build_neighbor_list, + extend_coord_with_ghosts, +) + +from ..common import ( + INSTALLED_PT, + INSTALLED_TF, +) + +if INSTALLED_PT: + import torch + + from deepmd.pt.utils.env import DEVICE as PT_DEVICE + from deepmd.pt.utils.nlist import build_neighbor_list as build_neighbor_list_pt + from deepmd.pt.utils.nlist import ( + extend_coord_with_ghosts as extend_coord_with_ghosts_pt, + ) +if INSTALLED_TF: + from deepmd.tf.env import ( + GLOBAL_TF_FLOAT_PRECISION, + tf, + ) + + +class DescriptorTest: + """Useful utilities for descriptor tests.""" + + def build_tf_descriptor(self, obj, natoms, coords, atype, box, suffix): + t_coord = tf.placeholder(GLOBAL_TF_FLOAT_PRECISION, [None], name="i_coord") + t_type = tf.placeholder(tf.int32, [None], name="i_type") + t_natoms = tf.placeholder(tf.int32, natoms.shape, name="i_natoms") + t_box = tf.placeholder(GLOBAL_TF_FLOAT_PRECISION, [9], name="i_box") + t_mesh = tf.placeholder(tf.int32, [None], name="i_mesh") + t_des = obj.build( + t_coord, + t_type, + t_natoms, + t_box, + t_mesh, + {}, + suffix=suffix, + ) + return [t_des], { + t_coord: coords, + t_type: atype, + t_natoms: natoms, + t_box: box, + t_mesh: make_default_mesh(True, False), + } + + def eval_dp_descriptor(self, dp_obj: Any, natoms, coords, atype, box) -> Any: + ext_coords, ext_atype, mapping = extend_coord_with_ghosts( + coords.reshape(1, -1, 3), + atype.reshape(1, -1), + box.reshape(1, 3, 3), + dp_obj.get_rcut(), + ) + nlist = build_neighbor_list( + ext_coords, + ext_atype, + natoms[0], + dp_obj.get_rcut(), + dp_obj.get_sel(), + distinguish_types=True, + ) + return dp_obj(ext_coords, ext_atype, nlist=nlist) + + def eval_pt_descriptor(self, pt_obj: Any, natoms, coords, atype, box) -> Any: + ext_coords, ext_atype, mapping = extend_coord_with_ghosts_pt( + torch.from_numpy(coords).to(PT_DEVICE).reshape(1, -1, 3), + torch.from_numpy(atype).to(PT_DEVICE).reshape(1, -1), + torch.from_numpy(box).to(PT_DEVICE).reshape(1, 3, 3), + pt_obj.get_rcut(), + ) + nlist = build_neighbor_list_pt( + ext_coords, + ext_atype, + natoms[0], + pt_obj.get_rcut(), + pt_obj.get_sel(), + distinguish_types=True, + ) + return [ + x.detach().cpu().numpy() if torch.is_tensor(x) else x + for x in pt_obj(ext_coords, ext_atype, nlist=nlist) + ] diff --git a/source/tests/consistent/descriptor/test_se_e2_a.py b/source/tests/consistent/descriptor/test_se_e2_a.py new file mode 100644 index 0000000000..a694a2a20c --- /dev/null +++ b/source/tests/consistent/descriptor/test_se_e2_a.py @@ -0,0 +1,149 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest +from typing import ( + Any, + Tuple, +) + +import numpy as np + +from deepmd.dpmodel.descriptor.se_e2_a import DescrptSeA as DescrptSeADP +from deepmd.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) + +from ..common import ( + INSTALLED_PT, + INSTALLED_TF, + CommonTest, + parameterized, +) +from .common import ( + DescriptorTest, +) + +if INSTALLED_PT: + from deepmd.pt.model.descriptor.se_a import DescrptSeA as DescrptSeAPT +else: + DescrptSeAPT = None +if INSTALLED_TF: + from deepmd.tf.descriptor.se_a import DescrptSeA as DescrptSeATF +else: + DescrptSeATF = None +from deepmd.utils.argcheck import ( + descrpt_se_a_args, +) + + +@parameterized( + (True, False), # resnet_dt + (True, False), # type_one_side + ([], [[0, 1]]), # excluded_types +) +class TestSeA(CommonTest, DescriptorTest, unittest.TestCase): + @property + def data(self) -> dict: + ( + resnet_dt, + type_one_side, + excluded_types, + ) = self.param + return { + "sel": [10, 10], + "rcut_smth": 5.80, + "rcut": 6.00, + "neuron": [6, 12, 24], + "axis_neuron": 3, + "resnet_dt": resnet_dt, + "type_one_side": type_one_side, + "exclude_types": excluded_types, + "seed": 1145141919810, + } + + @property + def skip_pt(self) -> bool: + ( + resnet_dt, + type_one_side, + excluded_types, + ) = self.param + return not type_one_side or excluded_types != [] or CommonTest.skip_pt + + @property + def skip_dp(self) -> bool: + ( + resnet_dt, + type_one_side, + excluded_types, + ) = self.param + return not type_one_side or excluded_types != [] or CommonTest.skip_dp + + tf_class = DescrptSeATF + dp_class = DescrptSeADP + pt_class = DescrptSeAPT + args = descrpt_se_a_args() + + def setUp(self): + CommonTest.setUp(self) + + self.ntypes = 2 + self.coords = np.array( + [ + 12.83, + 2.56, + 2.18, + 12.09, + 2.87, + 2.74, + 00.25, + 3.32, + 1.68, + 3.36, + 3.00, + 1.81, + 3.51, + 2.51, + 2.60, + 4.27, + 3.22, + 1.56, + ], + dtype=GLOBAL_NP_FLOAT_PRECISION, + ) + self.atype = np.array([0, 1, 1, 0, 1, 1], dtype=np.int32) + self.box = np.array( + [13.0, 0.0, 0.0, 0.0, 13.0, 0.0, 0.0, 0.0, 13.0], + dtype=GLOBAL_NP_FLOAT_PRECISION, + ) + self.natoms = np.array([6, 6, 2, 4], dtype=np.int32) + + def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: + return self.build_tf_descriptor( + obj, + self.natoms, + self.coords, + self.atype, + self.box, + suffix, + ) + + def eval_dp(self, dp_obj: Any) -> Any: + return self.eval_dp_descriptor( + dp_obj, + self.natoms, + self.coords, + self.atype, + self.box, + ) + + def eval_pt(self, pt_obj: Any) -> Any: + return self.eval_pt_descriptor( + pt_obj, + self.natoms, + self.coords, + self.atype, + self.box, + ) + + def extract_ret(self, ret: Any, backend) -> Tuple[np.ndarray, ...]: + return (ret[0],) From 977b430c6e3971e01fe523d761fa804be03ad726 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 14 Feb 2024 03:22:30 -0500 Subject: [PATCH 079/686] pt: add exported methods to BaseAtomicModel (#3258) Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/model/dp_atomic_model.py | 24 +++++++++++ deepmd/dpmodel/model/linear_atomic_model.py | 28 +++++++++++++ .../dpmodel/model/make_base_atomic_model.py | 24 +++++++++++ deepmd/dpmodel/model/pairtab_atomic_model.py | 24 +++++++++++ deepmd/pt/infer/deep_eval.py | 6 +-- deepmd/pt/model/model/base_atomic_model.py | 9 ++++- deepmd/pt/model/model/dp_atomic_model.py | 31 ++++++++++++++ deepmd/pt/model/model/linear_atomic_model.py | 40 +++++++++++++++++++ deepmd/pt/model/model/pairtab_atomic_model.py | 28 +++++++++++++ 9 files changed, 210 insertions(+), 4 deletions(-) diff --git a/deepmd/dpmodel/model/dp_atomic_model.py b/deepmd/dpmodel/model/dp_atomic_model.py index 4bb6cb1daf..ee19d15410 100644 --- a/deepmd/dpmodel/model/dp_atomic_model.py +++ b/deepmd/dpmodel/model/dp_atomic_model.py @@ -143,3 +143,27 @@ def deserialize(cls, data) -> "DPAtomicModel": ) obj = cls(descriptor_obj, fitting_obj, type_map=data["type_map"]) return obj + + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this atomic model.""" + return 0 + + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this atomic model.""" + return 0 + + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return [] + + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + + If False, the shape is (nframes, nloc, ndim). + """ + return False diff --git a/deepmd/dpmodel/model/linear_atomic_model.py b/deepmd/dpmodel/model/linear_atomic_model.py index 0da40307a6..8a53cb9229 100644 --- a/deepmd/dpmodel/model/linear_atomic_model.py +++ b/deepmd/dpmodel/model/linear_atomic_model.py @@ -199,6 +199,34 @@ def _compute_weight( """This should be a list of user defined weights that matches the number of models to be combined.""" raise NotImplementedError + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this atomic model.""" + # tricky... + return max([model.get_dim_fparam() for model in self.models]) + + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this atomic model.""" + return max([model.get_dim_aparam() for model in self.models]) + + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + if any(model.get_sel_type() == [] for model in self.models): + return [] + # join all the selected types + return list(set().union(*[model.get_sel_type() for model in self.models])) + + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + + If False, the shape is (nframes, nloc, ndim). + """ + return False + class DPZBLLinearAtomicModel(LinearAtomicModel): """Model linearly combine a list of AtomicModels. diff --git a/deepmd/dpmodel/model/make_base_atomic_model.py b/deepmd/dpmodel/model/make_base_atomic_model.py index 080d9982c9..f1dea4615f 100644 --- a/deepmd/dpmodel/model/make_base_atomic_model.py +++ b/deepmd/dpmodel/model/make_base_atomic_model.py @@ -61,6 +61,30 @@ def get_nnei(self) -> int: """Returns the total number of selected neighboring atoms in the cut-off radius.""" return self.get_nsel() + @abstractmethod + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this atomic model.""" + + @abstractmethod + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this atomic model.""" + + @abstractmethod + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + + @abstractmethod + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + + If False, the shape is (nframes, nloc, ndim). + """ + @abstractmethod def distinguish_types(self) -> bool: """Returns if the model requires a neighbor list that distinguish different diff --git a/deepmd/dpmodel/model/pairtab_atomic_model.py b/deepmd/dpmodel/model/pairtab_atomic_model.py index 12073c7f63..6d2de0e126 100644 --- a/deepmd/dpmodel/model/pairtab_atomic_model.py +++ b/deepmd/dpmodel/model/pairtab_atomic_model.py @@ -312,3 +312,27 @@ def _calculate_ener(coef: np.ndarray, uu: np.ndarray) -> np.ndarray: etmp = (a3 * uu + a2) * uu + a1 # this should be elementwise operations. ener = etmp * uu + a0 # this energy has the extrapolated value when rcut > rmax return ener + + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this atomic model.""" + return 0 + + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this atomic model.""" + return 0 + + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return [] + + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + + If False, the shape is (nframes, nloc, ndim). + """ + return False diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index 9542ff33b1..9679bbb1e6 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -136,11 +136,11 @@ def get_type_map(self) -> List[str]: def get_dim_fparam(self) -> int: """Get the number (dimension) of frame parameters of this DP.""" - return 0 + return self.dp.model["Default"].get_dim_fparam() def get_dim_aparam(self) -> int: """Get the number (dimension) of atomic parameters of this DP.""" - return 0 + return self.dp.model["Default"].get_dim_aparam() @property def model_type(self) -> "DeepEvalWrapper": @@ -169,7 +169,7 @@ def get_sel_type(self) -> List[int]: to the result of the model. If returning an empty list, all atom types are selected. """ - return [] + return self.dp.model["Default"].get_sel_type() def get_numb_dos(self) -> int: """Get the number of DOS.""" diff --git a/deepmd/pt/model/model/base_atomic_model.py b/deepmd/pt/model/model/base_atomic_model.py index 3f3e14257b..3caa9ed5ae 100644 --- a/deepmd/pt/model/model/base_atomic_model.py +++ b/deepmd/pt/model/model/base_atomic_model.py @@ -1,9 +1,16 @@ # SPDX-License-Identifier: LGPL-3.0-or-later + import torch from deepmd.dpmodel.model import ( make_base_atomic_model, ) -BaseAtomicModel = make_base_atomic_model(torch.Tensor) +BaseAtomicModel_ = make_base_atomic_model(torch.Tensor) + + +class BaseAtomicModel(BaseAtomicModel_): + # export public methods that are not abstract + get_nsel = torch.jit.export(BaseAtomicModel_.get_nsel) + get_nnei = torch.jit.export(BaseAtomicModel_.get_nnei) diff --git a/deepmd/pt/model/model/dp_atomic_model.py b/deepmd/pt/model/model/dp_atomic_model.py index 89b814edaa..2d04a2b8cf 100644 --- a/deepmd/pt/model/model/dp_atomic_model.py +++ b/deepmd/pt/model/model/dp_atomic_model.py @@ -202,3 +202,34 @@ def compute_or_load_stat( self.fitting_net.compute_or_load_stat( type_map, sampled, stat_file_path_dict["fitting_net"] ) + + @torch.jit.export + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this atomic model.""" + # TODO: self.fitting_net.get_dim_fparam() + return 0 + + @torch.jit.export + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this atomic model.""" + # TODO: self.fitting_net.get_dim_aparam() + return 0 + + @torch.jit.export + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + # TODO: self.fitting_net.get_sel_type() + return [] + + @torch.jit.export + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + + If False, the shape is (nframes, nloc, ndim). + """ + return False diff --git a/deepmd/pt/model/model/linear_atomic_model.py b/deepmd/pt/model/model/linear_atomic_model.py index 0d54e4c091..7035426402 100644 --- a/deepmd/pt/model/model/linear_atomic_model.py +++ b/deepmd/pt/model/model/linear_atomic_model.py @@ -213,6 +213,46 @@ def _compute_weight( """This should be a list of user defined weights that matches the number of models to be combined.""" raise NotImplementedError + @torch.jit.export + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this atomic model.""" + # tricky... + return max([model.get_dim_fparam() for model in self.models]) + + @torch.jit.export + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this atomic model.""" + return max([model.get_dim_aparam() for model in self.models]) + + @torch.jit.export + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + if any(model.get_sel_type() == [] for model in self.models): + return [] + # join all the selected types + # make torch.jit happy... + return torch.unique( + torch.cat( + [ + torch.as_tensor(model.get_sel_type(), dtype=torch.int32) + for model in self.models + ] + ) + ).tolist() + + @torch.jit.export + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + + If False, the shape is (nframes, nloc, ndim). + """ + return False + class DPZBLLinearAtomicModel(LinearAtomicModel): """Model linearly combine a list of AtomicModels. diff --git a/deepmd/pt/model/model/pairtab_atomic_model.py b/deepmd/pt/model/model/pairtab_atomic_model.py index 0ef1448398..8aa780effb 100644 --- a/deepmd/pt/model/model/pairtab_atomic_model.py +++ b/deepmd/pt/model/model/pairtab_atomic_model.py @@ -337,3 +337,31 @@ def _calculate_ener(coef: torch.Tensor, uu: torch.Tensor) -> torch.Tensor: etmp = (a3 * uu + a2) * uu + a1 # this should be elementwise operations. ener = etmp * uu + a0 # this energy has the extrapolated value when rcut > rmax return ener + + @torch.jit.export + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this atomic model.""" + return 0 + + @torch.jit.export + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this atomic model.""" + return 0 + + @torch.jit.export + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return [] + + @torch.jit.export + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + + If False, the shape is (nframes, nloc, ndim). + """ + return False From 25bf37a9f9a56c9561112fab9224e2e5e6b21024 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:47:34 +0800 Subject: [PATCH 080/686] Implement hessian autodiff calculation (#3262) restrictions: - cannot jit - only the `forward_common` interface has its hessian calculation. not for `forward_common_lower`. - may give nan when nall == nloc. specifically when nloc==1 also fix bug in pt: transform_output. The output shape will be wrong when the dimension of output variable is larger than 1. --------- Co-authored-by: Han Wang Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/dpmodel/__init__.py | 2 + deepmd/dpmodel/output_def.py | 4 + deepmd/dpmodel/utils/env_mat.py | 3 +- deepmd/pt/model/descriptor/env_mat.py | 7 +- deepmd/pt/model/model/__init__.py | 4 + deepmd/pt/model/model/make_hessian_model.py | 216 ++++++++++++++++++ deepmd/pt/model/model/transform_output.py | 7 +- .../tests/pt/model/test_make_hessian_model.py | 171 ++++++++++++++ 8 files changed, 408 insertions(+), 6 deletions(-) create mode 100644 deepmd/pt/model/model/make_hessian_model.py create mode 100644 source/tests/pt/model/test_make_hessian_model.py diff --git a/deepmd/dpmodel/__init__.py b/deepmd/dpmodel/__init__.py index 5a83bb7bd4..906aac662a 100644 --- a/deepmd/dpmodel/__init__.py +++ b/deepmd/dpmodel/__init__.py @@ -14,6 +14,7 @@ OutputVariableDef, fitting_check_output, get_deriv_name, + get_hessian_name, get_reduce_name, model_check_output, ) @@ -31,4 +32,5 @@ "fitting_check_output", "get_reduce_name", "get_deriv_name", + "get_hessian_name", ] diff --git a/deepmd/dpmodel/output_def.py b/deepmd/dpmodel/output_def.py index fac24534eb..d816ed4e84 100644 --- a/deepmd/dpmodel/output_def.py +++ b/deepmd/dpmodel/output_def.py @@ -319,6 +319,10 @@ def get_deriv_name(name: str) -> Tuple[str, str]: return name + "_derv_r", name + "_derv_c" +def get_hessian_name(name: str) -> str: + return name + "_derv_r_derv_r" + + def apply_operation(var_def: OutputVariableDef, op: OutputVariableOperation) -> int: """Apply a operation to the category of a variable definition. diff --git a/deepmd/dpmodel/utils/env_mat.py b/deepmd/dpmodel/utils/env_mat.py index 739b06208c..070b0e1549 100644 --- a/deepmd/dpmodel/utils/env_mat.py +++ b/deepmd/dpmodel/utils/env_mat.py @@ -53,7 +53,8 @@ def _make_env_mat( t0 = 1 / length t1 = diff / length**2 weight = compute_smooth_weight(length, ruct_smth, rcut) - env_mat_se_a = np.concatenate([t0, t1], axis=-1) * weight * np.expand_dims(mask, -1) + weight = weight * np.expand_dims(mask, -1) + env_mat_se_a = np.concatenate([t0, t1], axis=-1) * weight return env_mat_se_a, diff * np.expand_dims(mask, -1), weight diff --git a/deepmd/pt/model/descriptor/env_mat.py b/deepmd/pt/model/descriptor/env_mat.py index 63181388df..b3235de175 100644 --- a/deepmd/pt/model/descriptor/env_mat.py +++ b/deepmd/pt/model/descriptor/env_mat.py @@ -10,8 +10,10 @@ def _make_env_mat_se_a(nlist, coord, rcut: float, ruct_smth: float): """Make smooth environment matrix.""" bsz, natoms, nnei = nlist.shape coord = coord.view(bsz, -1, 3) + nall = coord.shape[1] mask = nlist >= 0 - nlist = nlist * mask + # nlist = nlist * mask ## this impl will contribute nans in Hessian calculation. + nlist = torch.where(mask, nlist, nall - 1) coord_l = coord[:, :natoms].view(bsz, -1, 1, 3) index = nlist.view(bsz, -1).unsqueeze(-1).expand(-1, -1, 3) coord_r = torch.gather(coord, 1, index) @@ -23,7 +25,8 @@ def _make_env_mat_se_a(nlist, coord, rcut: float, ruct_smth: float): t0 = 1 / length t1 = diff / length**2 weight = compute_smooth_weight(length, ruct_smth, rcut) - env_mat_se_a = torch.cat([t0, t1], dim=-1) * weight * mask.unsqueeze(-1) + weight = weight * mask.unsqueeze(-1) + env_mat_se_a = torch.cat([t0, t1], dim=-1) * weight return env_mat_se_a, diff * mask.unsqueeze(-1), weight diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index 1948acd003..25db37a3d7 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -18,6 +18,9 @@ EnergyModel, ZBLModel, ) +from .make_hessian_model import ( + make_hessian_model, +) from .model import ( BaseModel, ) @@ -84,4 +87,5 @@ def get_model(model_params): "BaseModel", "EnergyModel", "get_model", + "make_hessian_model", ] diff --git a/deepmd/pt/model/model/make_hessian_model.py b/deepmd/pt/model/model/make_hessian_model.py new file mode 100644 index 0000000000..0ed14b1931 --- /dev/null +++ b/deepmd/pt/model/model/make_hessian_model.py @@ -0,0 +1,216 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import math +from typing import ( + Dict, + List, + Optional, + Union, +) + +import torch + +from deepmd.dpmodel import ( + get_hessian_name, +) + + +def make_hessian_model(T_Model): + """Make a model that can compute Hessian. + + LIMITATION: this model is not jitable due to the restrictions of torch jit script. + + LIMITATION: only the hessian of `forward_common` is available. + + Parameters + ---------- + T_Model + The model. Should provide the `forward_common` and `fitting_output_def` methods + + Returns + ------- + The model computes hessian. + + """ + + class CM(T_Model): + def __init__( + self, + *args, + **kwargs, + ): + super().__init__( + *args, + **kwargs, + ) + self.hess_fitting_def = copy.deepcopy(super().fitting_output_def()) + + def requires_hessian( + self, + keys: Union[str, List[str]], + ): + """Set which output variable(s) requires hessian.""" + if isinstance(keys, str): + keys = [keys] + for kk in self.hess_fitting_def.keys(): + if kk in keys: + self.hess_fitting_def[kk].r_hessian = True + + def fitting_output_def(self): + """Get the fitting output def.""" + return self.hess_fitting_def + + def forward_common( + self, + coord, + atype, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + """Return model prediction. + + Parameters + ---------- + coord + The coordinates of the atoms. + shape: nf x (nloc x 3) + atype + The type of atoms. shape: nf x nloc + box + The simulation box. shape: nf x 9 + fparam + frame parameter. nf x ndf + aparam + atomic parameter. nf x nloc x nda + do_atomic_virial + If calculate the atomic virial. + + Returns + ------- + ret_dict + The result dict of type Dict[str,torch.Tensor]. + The keys are defined by the `ModelOutputDef`. + + """ + ret = super().forward_common( + coord, + atype, + box=box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + vdef = self.fitting_output_def() + hess_yes = [vdef[kk].r_hessian for kk in vdef.keys()] + if any(hess_yes): + hess = self._cal_hessian_all( + coord, + atype, + box=box, + fparam=fparam, + aparam=aparam, + ) + ret.update(hess) + return ret + + def _cal_hessian_all( + self, + coord: torch.Tensor, + atype: torch.Tensor, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + ) -> Dict[str, torch.Tensor]: + nf, nloc = atype.shape + coord = coord.view([nf, (nloc * 3)]) + box = box.view([nf, 9]) if box is not None else None + fparam = fparam.view([nf, -1]) if fparam is not None else None + aparam = aparam.view([nf, nloc, -1]) if aparam is not None else None + fdef = self.fitting_output_def() + # keys of values that require hessian + hess_keys: List[str] = [] + for kk in fdef.keys(): + if fdef[kk].r_hessian: + hess_keys.append(kk) + # result dict init by empty lists + res = {get_hessian_name(kk): [] for kk in hess_keys} + # loop over variable + for kk in hess_keys: + vdef = fdef[kk] + vshape = vdef.shape + vsize = math.prod(vdef.shape) + # loop over frames + for ii in range(nf): + icoord = coord[ii] + iatype = atype[ii] + ibox = box[ii] if box is not None else None + ifparam = fparam[ii] if fparam is not None else None + iaparam = aparam[ii] if aparam is not None else None + # loop over all components + for idx in range(vsize): + hess = self._cal_hessian_one_component( + idx, icoord, iatype, ibox, ifparam, iaparam + ) + res[get_hessian_name(kk)].append(hess) + res[get_hessian_name(kk)] = torch.stack(res[get_hessian_name(kk)]).view( + (nf, *vshape, nloc * 3, nloc * 3) + ) + return res + + def _cal_hessian_one_component( + self, + ci, + coord, + atype, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + # coord, # (nloc x 3) + # atype, # nloc + # box: Optional[torch.Tensor] = None, # 9 + # fparam: Optional[torch.Tensor] = None, # nfp + # aparam: Optional[torch.Tensor] = None, # (nloc x nap) + wc = wrapper_class_forward_energy(self, ci, atype, box, fparam, aparam) + + hess = torch.autograd.functional.hessian( + wc, + coord, + create_graph=False, + ) + return hess + + class wrapper_class_forward_energy: + def __init__( + self, + obj: CM, + ci: int, + atype: torch.Tensor, + box: Optional[torch.Tensor], + fparam: Optional[torch.Tensor], + aparam: Optional[torch.Tensor], + ): + self.atype, self.box, self.fparam, self.aparam = atype, box, fparam, aparam + self.ci = ci + self.obj = obj + + def __call__( + self, + xx, + ): + ci = self.ci + atype, box, fparam, aparam = self.atype, self.box, self.fparam, self.aparam + res = super(CM, self.obj).forward_common( + xx.unsqueeze(0), + atype.unsqueeze(0), + box.unsqueeze(0) if box is not None else None, + fparam.unsqueeze(0) if fparam is not None else None, + aparam.unsqueeze(0) if aparam is not None else None, + do_atomic_virial=False, + ) + er = res["energy_redu"][0].view([-1])[ci] + return er + + return CM diff --git a/deepmd/pt/model/model/transform_output.py b/deepmd/pt/model/model/transform_output.py index 27e014640d..312bb952b5 100644 --- a/deepmd/pt/model/model/transform_output.py +++ b/deepmd/pt/model/model/transform_output.py @@ -128,10 +128,11 @@ def take_deriv( assert aviri is not None aviri = aviri.unsqueeze(-2) split_avir.append(aviri) - # nf x nloc x v_dim x 3, nf x nloc x v_dim x 9 - ff = torch.concat(split_ff, dim=-2) + # nf x nall x v_dim x 3, nf x nall x v_dim x 9 + out_lead_shape = list(coord_ext.shape[:-1]) + vdef.shape + ff = torch.concat(split_ff, dim=-2).view(out_lead_shape + [3]) # noqa: RUF005 if do_virial: - avir = torch.concat(split_avir, dim=-2) + avir = torch.concat(split_avir, dim=-2).view(out_lead_shape + [9]) # noqa: RUF005 else: avir = None return ff, avir diff --git a/source/tests/pt/model/test_make_hessian_model.py b/source/tests/pt/model/test_make_hessian_model.py new file mode 100644 index 0000000000..650d35e019 --- /dev/null +++ b/source/tests/pt/model/test_make_hessian_model.py @@ -0,0 +1,171 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np +import torch + +from deepmd.dpmodel.output_def import ( + OutputVariableCategory, +) +from deepmd.pt.model.descriptor.se_a import ( + DescrptSeA, +) +from deepmd.pt.model.model import ( + make_hessian_model, +) +from deepmd.pt.model.model.ener import ( + DPModel, +) +from deepmd.pt.model.task.ener import ( + InvarFitting, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, + to_torch_tensor, +) + +dtype = torch.float64 + + +def finite_hessian(f, x, delta=1e-6): + in_shape = x.shape + assert len(in_shape) == 1 + y0 = f(x) + out_shape = y0.shape + res = np.empty(out_shape + in_shape + in_shape) + for iidx in np.ndindex(*in_shape): + for jidx in np.ndindex(*in_shape): + i0 = np.zeros(in_shape) + i1 = np.zeros(in_shape) + i2 = np.zeros(in_shape) + i3 = np.zeros(in_shape) + i0[iidx] += delta + i2[iidx] += delta + i1[iidx] -= delta + i3[iidx] -= delta + i0[jidx] += delta + i1[jidx] += delta + i2[jidx] -= delta + i3[jidx] -= delta + y0 = f(x + i0) + y1 = f(x + i1) + y2 = f(x + i2) + y3 = f(x + i3) + res[(Ellipsis, *iidx, *jidx)] = (y0 + y3 - y1 - y2) / (4 * delta**2.0) + return res + + +class HessianTest: + def test( + self, + ): + # setup test case + places = 6 + delta = 1e-3 + natoms = self.nloc + nf = self.nf + nv = self.nv + cell0 = torch.rand([3, 3], dtype=dtype) + cell0 = 1.0 * (cell0 + cell0.T) + 5.0 * torch.eye(3) + cell1 = torch.rand([3, 3], dtype=dtype) + cell1 = 1.0 * (cell1 + cell1.T) + 5.0 * torch.eye(3) + cell = torch.stack([cell0, cell1]) + coord = torch.rand([nf, natoms, 3], dtype=dtype) + coord = torch.matmul(coord, cell) + cell = cell.view([nf, 9]) + coord = coord.view([nf, natoms * 3]) + atype = torch.stack( + [ + torch.IntTensor([0, 0, 1]), + torch.IntTensor([1, 0, 1]), + ] + ).view([nf, natoms]) + nfp, nap = 2, 3 + fparam = torch.rand([nf, nfp], dtype=dtype) + aparam = torch.rand([nf, natoms * nap], dtype=dtype) + # forward hess and valu models + ret_dict0 = self.model_hess.forward_common( + coord, atype, box=cell, fparam=fparam, aparam=aparam + ) + ret_dict1 = self.model_valu.forward_common( + coord, atype, box=cell, fparam=fparam, aparam=aparam + ) + # compare hess and value models + torch.testing.assert_close(ret_dict0["energy"], ret_dict1["energy"]) + ana_hess = ret_dict0["energy_derv_r_derv_r"] + + # compute finite difference + fnt_hess = [] + for ii in range(nf): + + def np_infer( + xx, + ): + ret = self.model_valu.forward_common( + to_torch_tensor(xx).unsqueeze(0), + atype[ii].unsqueeze(0), + box=cell[ii].unsqueeze(0), + fparam=fparam[ii].unsqueeze(0), + aparam=aparam[ii].unsqueeze(0), + ) + # detach + ret = {kk: to_numpy_array(ret[kk]) for kk in ret} + return ret + + def ff(xx): + return np_infer(xx)["energy_redu"] + + xx = to_numpy_array(coord[ii]) + fnt_hess.append(finite_hessian(ff, xx, delta=delta).squeeze()) + + # compare finite difference with autodiff + fnt_hess = np.stack(fnt_hess).reshape([nf, nv, natoms * 3, natoms * 3]) + np.testing.assert_almost_equal( + fnt_hess, to_numpy_array(ana_hess), decimal=places + ) + + +class TestDPModel(unittest.TestCase, HessianTest): + def setUp(self): + torch.manual_seed(2) + self.nf = 2 + self.nloc = 3 + self.rcut = 4.0 + self.rcut_smth = 3.0 + self.sel = [10, 10] + self.nt = 2 + self.nv = 2 + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + neuron=[2, 4, 8], + axis_neuron=2, + ).to(env.DEVICE) + ft0 = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + self.nv, + distinguish_types=ds.distinguish_types(), + do_hessian=True, + neuron=[4, 4, 4], + ).to(env.DEVICE) + type_map = ["foo", "bar"] + self.model_hess = make_hessian_model(DPModel)(ds, ft0, type_map=type_map).to( + env.DEVICE + ) + self.model_valu = DPModel.deserialize(self.model_hess.serialize()) + self.model_hess.requires_hessian("energy") + + def test_output_def(self): + self.assertTrue(self.model_hess.fitting_output_def()["energy"].r_hessian) + self.assertFalse(self.model_valu.fitting_output_def()["energy"].r_hessian) + self.assertTrue(self.model_hess.model_output_def()["energy"].r_hessian) + self.assertEqual( + self.model_hess.model_output_def()["energy_derv_r_derv_r"].category, + OutputVariableCategory.DERV_R_DERV_R, + ) From 097b3ab2db9a90f1529ebdfc41f71bf44d3470e5 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Wed, 14 Feb 2024 22:45:32 +0800 Subject: [PATCH 081/686] fix bug of output dimension in pt InvarFitting (#3274) Co-authored-by: Han Wang --- deepmd/pt/model/task/ener.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 1b3e2c3d65..81f2cc8cf0 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -119,7 +119,6 @@ def __init__( self.aparam_avg, self.aparam_inv_std = None, None in_dim = self.dim_descrpt + self.numb_fparam + self.numb_aparam - out_dim = 1 self.old_impl = kwargs.get("old_impl", False) if self.old_impl: @@ -144,7 +143,7 @@ def __init__( networks=[ FittingNet( in_dim, - out_dim, + self.dim_out, self.neuron, self.activation_function, self.resnet_dt, @@ -358,7 +357,7 @@ def forward( dim=-1, ) - outs = torch.zeros_like(atype).unsqueeze(-1) # jit assertion + outs = torch.zeros(nf, nloc, self.dim_out) # jit assertion if self.old_impl: outs = torch.zeros_like(atype).unsqueeze(-1) # jit assertion assert self.filter_layers_old is not None From 0b6809783acad6f23af14dd871e3be0f1d06141f Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 15 Feb 2024 02:59:37 -0500 Subject: [PATCH 082/686] pt: fix torchscript converage (#3276) xref: https://github.com/pytorch/pytorch/issues/43146 This plugin marks all code compiled by JIT as coverable. --------- Signed-off-by: Jinzhe Zeng --- .gitattributes | 1 + pyproject.toml | 3 + source/3rdparty/README.md | 7 ++ source/3rdparty/coverage_plugins/__init__.py | 0 .../3rdparty/coverage_plugins/jit_plugin.py | 80 +++++++++++++++++++ 5 files changed, 91 insertions(+) create mode 100644 source/3rdparty/README.md create mode 100644 source/3rdparty/coverage_plugins/__init__.py create mode 100644 source/3rdparty/coverage_plugins/jit_plugin.py diff --git a/.gitattributes b/.gitattributes index e77d446ba6..82d852900b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ # do not show up detailed difference on GitHub source/3rdparty/* linguist-generated=true +source/3rdparty/README.md linguist-generated=false diff --git a/pyproject.toml b/pyproject.toml index a71c3d1709..26dad4fe1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -259,3 +259,6 @@ convention = "numpy" [tool.pytest.ini_options] markers = "run" + +[tool.coverage.run] +plugins = ["source.3rdparty.coverage_plugins.jit_plugin"] diff --git a/source/3rdparty/README.md b/source/3rdparty/README.md new file mode 100644 index 0000000000..ac9cfd4edc --- /dev/null +++ b/source/3rdparty/README.md @@ -0,0 +1,7 @@ +# 3rd-party source codes + +| Name | Repository | Version | License | +| ------------------------- | ---------------------------------- | ------- | ------- | +| json | https://github.com/nlohmann/json | 3.9.1 | MIT | +| Implib.so | https://github.com/yugr/Implib.so | 0ddaa71 | MIT | +| coverage_plugins | https://github.com/pytorch/pytorch | 2.2.0 | BSD-3 | diff --git a/source/3rdparty/coverage_plugins/__init__.py b/source/3rdparty/coverage_plugins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/3rdparty/coverage_plugins/jit_plugin.py b/source/3rdparty/coverage_plugins/jit_plugin.py new file mode 100644 index 0000000000..e6d0786a32 --- /dev/null +++ b/source/3rdparty/coverage_plugins/jit_plugin.py @@ -0,0 +1,80 @@ +""" +This coverage plug-in attempts to cover JIT'd functions and methods that were previously missed in code coverage. Any +function and method that was passed through/decorated with torch.jit.script or torch.jit.script_method should now be +marked covered when coverage is run with this plug-in. + +DISCLAIMER: note that this will mark the entire JIT'd function/method as covered without seeking proof that the +compiled code has been executed. This means that even if the code chunk is merely compiled and not run, it will get +marked as covered. +""" + +from inspect import ( + getsourcefile, + getsourcelines, + isclass, + iscode, + isfunction, + ismethod, + ismodule, +) +from time import time +from typing import Any + +from coverage import CoverageData, CoveragePlugin # type: ignore[import] + +# All coverage stats resulting from this plug-in will be in a separate .coverage file that should be merged later with +# `coverage combine`. The convention seems to be .coverage.dotted.suffix based on the following link: +# https://coverage.readthedocs.io/en/coverage-5.5/cmd.html#combining-data-files-coverage-combine +cov_data = CoverageData(basename=f".coverage.jit.{time()}") + + +def is_not_builtin_class(obj: Any) -> bool: + return isclass(obj) and not type(obj).__module__ == "builtins" + + +class JitPlugin(CoveragePlugin): # type: ignore[misc, no-any-unimported] + """ + dynamic_context is an overridden function that gives us access to every frame run during the coverage process. We + look for when the function being run is `should_drop`, as all functions that get passed into `should_drop` will be + compiled and thus should be marked as covered. + """ + + def dynamic_context(self, frame: Any) -> None: + if frame.f_code.co_name == "should_drop": + obj = frame.f_locals["fn"] + # The many conditions in the if statement below are based on the accepted arguments to getsourcefile. Based + # on its documentation (https://docs.python.org/3/library/inspect.html#inspect.getsourcefile), the argument + # must be a module, class, method, function, traceback, frame, or code object AND it cannot be a built-in + # module, class, or function. + # Currently, we DO NOT include tracebacks or frames as they should not be JIT'd, and we have not checked for + # built-in modules or functions as those do not seem to be JIT'd either. + if ( + is_not_builtin_class(obj) + or ismodule(obj) + or ismethod(obj) + or isfunction(obj) + or iscode(obj) + ): + filename = getsourcefile(obj) + # We don't want to report for filename = None + if filename: + # TODO: Because torch.jit._IgnoreContextManager relies on Python's `exec` method + # which doesn't generate source codelines, getsourcelines(obj) fails. For now, + # we just ignore the exception until we figure out a better way to + # implement torch.jit._IgnoreContextManager. + try: + sourcelines, starting_lineno = getsourcelines(obj) + except OSError: + pass + else: + line_data = { + filename: range( + starting_lineno, starting_lineno + len(sourcelines) + ) + } + cov_data.add_lines(line_data) + super().dynamic_context(frame) + + +def coverage_init(reg: Any, options: Any) -> None: + reg.add_dynamic_context(JitPlugin()) From aae48508e002d194446d71260fdab7622c83b88e Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Thu, 15 Feb 2024 16:16:38 +0800 Subject: [PATCH 083/686] dp: pt: rearrange folder structure of atomic_model and model. (#3268) - create models `dpmodel.atomic_model` and `pt.model.atomic_model`. - place all the atomic model related codes into the new modules. - `PairTabModel` -> `PairTabAtomicModel` - `ZBLModel` -> `DPZBLModel` --------- Co-authored-by: Han Wang --- deepmd/dpmodel/__init__.py | 2 - deepmd/dpmodel/atomic_model/__init__.py | 42 +++++++++ .../base_atomic_model.py | 0 .../dp_atomic_model.py | 0 .../linear_atomic_model.py | 20 ++-- .../make_base_atomic_model.py | 0 .../pairtab_atomic_model.py | 4 +- deepmd/dpmodel/model/__init__.py | 21 +++-- deepmd/dpmodel/model/dp_model.py | 3 +- deepmd/pt/model/atomic_model/__init__.py | 37 ++++++++ .../base_atomic_model.py | 2 +- .../dp_atomic_model.py | 5 +- .../linear_atomic_model.py | 17 ++-- .../pairtab_atomic_model.py | 7 +- deepmd/pt/model/model/__init__.py | 42 ++++++--- deepmd/pt/model/model/dp_model.py | 10 ++ deepmd/pt/model/model/dp_zbl_model.py | 94 +++++++++++++++++++ .../pt/model/model/{ener.py => ener_model.py} | 90 +----------------- .../common/dpmodel/test_dp_atomic_model.py | 6 +- .../dpmodel/test_linear_atomic_model.py | 22 ++--- .../dpmodel/test_pairtab_atomic_model.py | 10 +- source/tests/pt/model/test_dp_atomic_model.py | 8 +- source/tests/pt/model/test_dp_model.py | 2 +- .../pt/model/test_linear_atomic_model.py | 26 +++-- .../tests/pt/model/test_make_hessian_model.py | 2 +- .../pt/model/test_pairtab_atomic_model.py | 14 +-- 26 files changed, 300 insertions(+), 186 deletions(-) create mode 100644 deepmd/dpmodel/atomic_model/__init__.py rename deepmd/dpmodel/{model => atomic_model}/base_atomic_model.py (100%) rename deepmd/dpmodel/{model => atomic_model}/dp_atomic_model.py (100%) rename deepmd/dpmodel/{model => atomic_model}/linear_atomic_model.py (96%) rename deepmd/dpmodel/{model => atomic_model}/make_base_atomic_model.py (100%) rename deepmd/dpmodel/{model => atomic_model}/pairtab_atomic_model.py (99%) create mode 100644 deepmd/pt/model/atomic_model/__init__.py rename deepmd/pt/model/{model => atomic_model}/base_atomic_model.py (89%) rename deepmd/pt/model/{model => atomic_model}/dp_atomic_model.py (98%) rename deepmd/pt/model/{model => atomic_model}/linear_atomic_model.py (96%) rename deepmd/pt/model/{model => atomic_model}/pairtab_atomic_model.py (98%) create mode 100644 deepmd/pt/model/model/dp_model.py create mode 100644 deepmd/pt/model/model/dp_zbl_model.py rename deepmd/pt/model/model/{ener.py => ener_model.py} (51%) diff --git a/deepmd/dpmodel/__init__.py b/deepmd/dpmodel/__init__.py index 906aac662a..6a7bdb3585 100644 --- a/deepmd/dpmodel/__init__.py +++ b/deepmd/dpmodel/__init__.py @@ -5,7 +5,6 @@ NativeOP, ) from .model import ( - DPAtomicModel, DPModel, ) from .output_def import ( @@ -21,7 +20,6 @@ __all__ = [ "DPModel", - "DPAtomicModel", "PRECISION_DICT", "DEFAULT_PRECISION", "NativeOP", diff --git a/deepmd/dpmodel/atomic_model/__init__.py b/deepmd/dpmodel/atomic_model/__init__.py new file mode 100644 index 0000000000..2cd20f54c1 --- /dev/null +++ b/deepmd/dpmodel/atomic_model/__init__.py @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""The atomic model provides the prediction of some property on each +atom. All the atomic models are not supposed to be directly accessed +by users, but it provides a convenient interface for the +implementation of models. + +Taking the energy models for example, the developeres only needs to +implement the atomic energy prediction via an atomic model, and the +model can be automatically made by the `deepmd.dpmodel.make_model` +method. The `DPModel` is made by +``` +DPModel = make_model(DPAtomicModel) +``` + +""" + + +from .base_atomic_model import ( + BaseAtomicModel, +) +from .dp_atomic_model import ( + DPAtomicModel, +) +from .linear_atomic_model import ( + DPZBLLinearAtomicModel, + LinearAtomicModel, +) +from .make_base_atomic_model import ( + make_base_atomic_model, +) +from .pairtab_atomic_model import ( + PairTabAtomicModel, +) + +__all__ = [ + "make_base_atomic_model", + "BaseAtomicModel", + "DPAtomicModel", + "PairTabAtomicModel", + "LinearAtomicModel", + "DPZBLLinearAtomicModel", +] diff --git a/deepmd/dpmodel/model/base_atomic_model.py b/deepmd/dpmodel/atomic_model/base_atomic_model.py similarity index 100% rename from deepmd/dpmodel/model/base_atomic_model.py rename to deepmd/dpmodel/atomic_model/base_atomic_model.py diff --git a/deepmd/dpmodel/model/dp_atomic_model.py b/deepmd/dpmodel/atomic_model/dp_atomic_model.py similarity index 100% rename from deepmd/dpmodel/model/dp_atomic_model.py rename to deepmd/dpmodel/atomic_model/dp_atomic_model.py diff --git a/deepmd/dpmodel/model/linear_atomic_model.py b/deepmd/dpmodel/atomic_model/linear_atomic_model.py similarity index 96% rename from deepmd/dpmodel/model/linear_atomic_model.py rename to deepmd/dpmodel/atomic_model/linear_atomic_model.py index 8a53cb9229..48aacacdee 100644 --- a/deepmd/dpmodel/model/linear_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/linear_atomic_model.py @@ -13,16 +13,16 @@ import numpy as np -from deepmd.dpmodel import ( - FittingOutputDef, - OutputVariableDef, -) from deepmd.dpmodel.utils.nlist import ( build_multiple_neighbor_list, get_multiple_nlist_key, nlist_distinguish_types, ) +from ..output_def import ( + FittingOutputDef, + OutputVariableDef, +) from .base_atomic_model import ( BaseAtomicModel, ) @@ -30,7 +30,7 @@ DPAtomicModel, ) from .pairtab_atomic_model import ( - PairTabModel, + PairTabAtomicModel, ) @@ -39,8 +39,8 @@ class LinearAtomicModel(BaseAtomicModel): Parameters ---------- - models : list[DPAtomicModel or PairTabModel] - A list of models to be combined. PairTabModel must be used together with a DPAtomicModel. + models : list[DPAtomicModel or PairTabAtomicModel] + A list of models to be combined. PairTabAtomicModel must be used together with a DPAtomicModel. """ def __init__( @@ -240,7 +240,7 @@ class DPZBLLinearAtomicModel(LinearAtomicModel): def __init__( self, dp_model: DPAtomicModel, - zbl_model: PairTabModel, + zbl_model: PairTabAtomicModel, sw_rmin: float, sw_rmax: float, smin_alpha: Optional[float] = 0.1, @@ -305,7 +305,9 @@ def _compute_weight( # use the larger rr based on nlist nlist_larger = zbl_nlist if zbl_nnei >= dp_nnei else dp_nlist masked_nlist = np.clip(nlist_larger, 0, None) - pairwise_rr = PairTabModel._get_pairwise_dist(extended_coord, masked_nlist) + pairwise_rr = PairTabAtomicModel._get_pairwise_dist( + extended_coord, masked_nlist + ) numerator = np.sum( pairwise_rr * np.exp(-pairwise_rr / self.smin_alpha), axis=-1 diff --git a/deepmd/dpmodel/model/make_base_atomic_model.py b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py similarity index 100% rename from deepmd/dpmodel/model/make_base_atomic_model.py rename to deepmd/dpmodel/atomic_model/make_base_atomic_model.py diff --git a/deepmd/dpmodel/model/pairtab_atomic_model.py b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py similarity index 99% rename from deepmd/dpmodel/model/pairtab_atomic_model.py rename to deepmd/dpmodel/atomic_model/pairtab_atomic_model.py index 6d2de0e126..d5f2bd2f1a 100644 --- a/deepmd/dpmodel/model/pairtab_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py @@ -21,7 +21,7 @@ ) -class PairTabModel(BaseAtomicModel): +class PairTabAtomicModel(BaseAtomicModel): """Pairwise tabulation energy model. This model can be used to tabulate the pairwise energy between atoms for either @@ -99,7 +99,7 @@ def serialize(self) -> dict: return {"tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel} @classmethod - def deserialize(cls, data) -> "PairTabModel": + def deserialize(cls, data) -> "PairTabAtomicModel": rcut = data["rcut"] sel = data["sel"] tab = PairTab.deserialize(data["tab"]) diff --git a/deepmd/dpmodel/model/__init__.py b/deepmd/dpmodel/model/__init__.py index 5c0a32673d..dda174fa4e 100644 --- a/deepmd/dpmodel/model/__init__.py +++ b/deepmd/dpmodel/model/__init__.py @@ -1,16 +1,23 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from .dp_atomic_model import ( - DPAtomicModel, -) +"""The model that takes the coordinates, cell and atom types as input +and predicts some property. The models are automatically generated from +atomic models by the `deepmd.dpmodel.make_model` method. + +The `make_model` method does the reduction, auto-differentiation +(dummy for dpmodels) and communication of the atomic properties +according to output variable definition +`deepmd.dpmodel.OutputVariableDef`. + +""" + from .dp_model import ( DPModel, ) -from .make_base_atomic_model import ( - make_base_atomic_model, +from .make_model import ( + make_model, ) __all__ = [ "DPModel", - "DPAtomicModel", - "make_base_atomic_model", + "make_model", ] diff --git a/deepmd/dpmodel/model/dp_model.py b/deepmd/dpmodel/model/dp_model.py index 819d46450e..6196bcfe87 100644 --- a/deepmd/dpmodel/model/dp_model.py +++ b/deepmd/dpmodel/model/dp_model.py @@ -1,7 +1,8 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -from .dp_atomic_model import ( +from deepmd.dpmodel.atomic_model import ( DPAtomicModel, ) + from .make_model import ( make_model, ) diff --git a/deepmd/pt/model/atomic_model/__init__.py b/deepmd/pt/model/atomic_model/__init__.py new file mode 100644 index 0000000000..75c1ce3c2e --- /dev/null +++ b/deepmd/pt/model/atomic_model/__init__.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""The atomic model provides the prediction of some property on each +atom. All the atomic models are not supposed to be directly accessed +by users, but it provides a convenient interface for the +implementation of models. + +Taking the energy models for example, the developeres only needs to +implement the atomic energy prediction via an atomic model, and the +model can be automatically made by the `deepmd.dpmodel.make_model` +method. The `DPModel` is made by +``` +DPModel = make_model(DPAtomicModel) +``` + +""" + +from .base_atomic_model import ( + BaseAtomicModel, +) +from .dp_atomic_model import ( + DPAtomicModel, +) +from .linear_atomic_model import ( + DPZBLLinearAtomicModel, + LinearAtomicModel, +) +from .pairtab_atomic_model import ( + PairTabAtomicModel, +) + +__all__ = [ + "BaseAtomicModel", + "DPAtomicModel", + "PairTabAtomicModel", + "LinearAtomicModel", + "DPZBLLinearAtomicModel", +] diff --git a/deepmd/pt/model/model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py similarity index 89% rename from deepmd/pt/model/model/base_atomic_model.py rename to deepmd/pt/model/atomic_model/base_atomic_model.py index 3caa9ed5ae..73b2d76a6d 100644 --- a/deepmd/pt/model/model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -3,7 +3,7 @@ import torch -from deepmd.dpmodel.model import ( +from deepmd.dpmodel.atomic_model import ( make_base_atomic_model, ) diff --git a/deepmd/pt/model/model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py similarity index 98% rename from deepmd/pt/model/model/dp_atomic_model.py rename to deepmd/pt/model/atomic_model/dp_atomic_model.py index 2d04a2b8cf..17b70e4701 100644 --- a/deepmd/pt/model/model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -28,14 +28,11 @@ from .base_atomic_model import ( BaseAtomicModel, ) -from .model import ( - BaseModel, -) log = logging.getLogger(__name__) -class DPAtomicModel(BaseModel, BaseAtomicModel): +class DPAtomicModel(torch.nn.Module, BaseAtomicModel): """Model give atomic prediction of some physical property. Parameters diff --git a/deepmd/pt/model/model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py similarity index 96% rename from deepmd/pt/model/model/linear_atomic_model.py rename to deepmd/pt/model/atomic_model/linear_atomic_model.py index 7035426402..7ccc5e225b 100644 --- a/deepmd/pt/model/model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -28,21 +28,18 @@ from .dp_atomic_model import ( DPAtomicModel, ) -from .model import ( - BaseModel, -) from .pairtab_atomic_model import ( - PairTabModel, + PairTabAtomicModel, ) -class LinearAtomicModel(BaseModel, BaseAtomicModel): +class LinearAtomicModel(torch.nn.Module, BaseAtomicModel): """Linear model make linear combinations of several existing models. Parameters ---------- - models : list[DPAtomicModel or PairTabModel] - A list of models to be combined. PairTabModel must be used together with a DPAtomicModel. + models : list[DPAtomicModel or PairTabAtomicModel] + A list of models to be combined. PairTabAtomicModel must be used together with a DPAtomicModel. """ def __init__( @@ -266,7 +263,7 @@ class DPZBLLinearAtomicModel(LinearAtomicModel): def __init__( self, dp_model: DPAtomicModel, - zbl_model: PairTabModel, + zbl_model: PairTabAtomicModel, sw_rmin: float, sw_rmax: float, smin_alpha: Optional[float] = 0.1, @@ -334,7 +331,9 @@ def _compute_weight( # use the larger rr based on nlist nlist_larger = zbl_nlist if zbl_nnei >= dp_nnei else dp_nlist masked_nlist = torch.clamp(nlist_larger, 0) - pairwise_rr = PairTabModel._get_pairwise_dist(extended_coord, masked_nlist) + pairwise_rr = PairTabAtomicModel._get_pairwise_dist( + extended_coord, masked_nlist + ) numerator = torch.sum( pairwise_rr * torch.exp(-pairwise_rr / self.smin_alpha), dim=-1 ) # masked nnei will be zero, no need to handle diff --git a/deepmd/pt/model/model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py similarity index 98% rename from deepmd/pt/model/model/pairtab_atomic_model.py rename to deepmd/pt/model/atomic_model/pairtab_atomic_model.py index 8aa780effb..cda9071cda 100644 --- a/deepmd/pt/model/model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -19,12 +19,9 @@ from .base_atomic_model import ( BaseAtomicModel, ) -from .model import ( - BaseModel, -) -class PairTabModel(BaseModel, BaseAtomicModel): +class PairTabAtomicModel(torch.nn.Module, BaseAtomicModel): """Pairwise tabulation energy model. This model can be used to tabulate the pairwise energy between atoms for either @@ -117,7 +114,7 @@ def serialize(self) -> dict: return {"tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel} @classmethod - def deserialize(cls, data) -> "PairTabModel": + def deserialize(cls, data) -> "PairTabAtomicModel": rcut = data["rcut"] sel = data["sel"] tab = PairTab.deserialize(data["tab"]) diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index 25db37a3d7..8199a8490b 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -1,26 +1,42 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +"""The model that takes the coordinates, cell and atom types as input +and predicts some property. The models are automatically generated from +atomic models by the `deepmd.dpmodel.make_model` method. + +The `make_model` method does the reduction, auto-differentiation and +communication of the atomic properties according to output variable +definition `deepmd.dpmodel.OutputVariableDef`. + +""" + import copy -from deepmd.pt.model.descriptor.descriptor import ( - Descriptor, -) -from deepmd.pt.model.model.dp_atomic_model import ( +from deepmd.pt.model.atomic_model import ( DPAtomicModel, + PairTabAtomicModel, ) -from deepmd.pt.model.model.pairtab_atomic_model import ( - PairTabModel, +from deepmd.pt.model.descriptor.descriptor import ( + Descriptor, ) from deepmd.pt.model.task import ( Fitting, ) -from .ener import ( +from .dp_model import ( + DPModel, +) +from .dp_zbl_model import ( + DPZBLModel, +) +from .ener_model import ( EnergyModel, - ZBLModel, ) from .make_hessian_model import ( make_hessian_model, ) +from .make_model import ( + make_model, +) from .model import ( BaseModel, ) @@ -47,13 +63,13 @@ def get_zbl_model(model_params): dp_model = DPAtomicModel(descriptor, fitting, type_map=model_params["type_map"]) # pairtab filepath = model_params["use_srtab"] - pt_model = PairTabModel( + pt_model = PairTabAtomicModel( filepath, model_params["descriptor"]["rcut"], model_params["descriptor"]["sel"] ) rmin = model_params["sw_rmin"] rmax = model_params["sw_rmax"] - return ZBLModel( + return DPZBLModel( dp_model, pt_model, rmin, @@ -85,7 +101,11 @@ def get_model(model_params): __all__ = [ "BaseModel", - "EnergyModel", "get_model", + "get_zbl_model", + "DPModel", + "EnergyModel", + "DPZBLModel", + "make_model", "make_hessian_model", ] diff --git a/deepmd/pt/model/model/dp_model.py b/deepmd/pt/model/model/dp_model.py new file mode 100644 index 0000000000..75d3820e45 --- /dev/null +++ b/deepmd/pt/model/model/dp_model.py @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from deepmd.pt.model.atomic_model import ( + DPAtomicModel, +) + +from .make_model import ( + make_model, +) + +DPModel = make_model(DPAtomicModel) diff --git a/deepmd/pt/model/model/dp_zbl_model.py b/deepmd/pt/model/model/dp_zbl_model.py new file mode 100644 index 0000000000..8d71157b60 --- /dev/null +++ b/deepmd/pt/model/model/dp_zbl_model.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + Optional, +) + +import torch + +from deepmd.pt.model.atomic_model import ( + DPZBLLinearAtomicModel, +) + +from .make_model import ( + make_model, +) + +DPZBLModel_ = make_model(DPZBLLinearAtomicModel) + + +class DPZBLModel(DPZBLModel_): + model_type = "ener" + + def __init__( + self, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) + + def forward( + self, + coord, + atype, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + model_ret = self.forward_common( + coord, + atype, + box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + + model_predict = {} + model_predict["atom_energy"] = model_ret["energy"] + model_predict["energy"] = model_ret["energy_redu"] + if self.do_grad("energy"): + model_predict["force"] = model_ret["energy_derv_r"].squeeze(-2) + if do_atomic_virial: + model_predict["atom_virial"] = model_ret["energy_derv_c"].squeeze(-3) + model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) + else: + model_predict["force"] = model_ret["dforce"] + return model_predict + + def forward_lower( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ): + model_ret = self.forward_common_lower( + extended_coord, + extended_atype, + nlist, + mapping=mapping, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + + model_predict = {} + model_predict["atom_energy"] = model_ret["energy"] + model_predict["energy"] = model_ret["energy_redu"] + if self.do_grad("energy"): + model_predict["extended_force"] = model_ret["energy_derv_r"].squeeze(-2) + model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) + if do_atomic_virial: + model_predict["extended_virial"] = model_ret["energy_derv_c"].squeeze( + -2 + ) + else: + assert model_ret["dforce"] is not None + model_predict["dforce"] = model_ret["dforce"] + model_predict = model_ret + return model_predict diff --git a/deepmd/pt/model/model/ener.py b/deepmd/pt/model/model/ener_model.py similarity index 51% rename from deepmd/pt/model/model/ener.py rename to deepmd/pt/model/model/ener_model.py index 3c0b66edcd..2afeb2762b 100644 --- a/deepmd/pt/model/model/ener.py +++ b/deepmd/pt/model/model/ener_model.py @@ -6,95 +6,9 @@ import torch -from .dp_atomic_model import ( - DPAtomicModel, +from .dp_model import ( + DPModel, ) -from .linear_atomic_model import ( - DPZBLLinearAtomicModel, -) -from .make_model import ( - make_model, -) - -DPModel = make_model(DPAtomicModel) -ZBLModel_ = make_model(DPZBLLinearAtomicModel) - - -class ZBLModel(ZBLModel_): - model_type = "ener" - - def __init__( - self, - *args, - **kwargs, - ): - super().__init__(*args, **kwargs) - - def forward( - self, - coord, - atype, - box: Optional[torch.Tensor] = None, - fparam: Optional[torch.Tensor] = None, - aparam: Optional[torch.Tensor] = None, - do_atomic_virial: bool = False, - ) -> Dict[str, torch.Tensor]: - model_ret = self.forward_common( - coord, - atype, - box, - fparam=fparam, - aparam=aparam, - do_atomic_virial=do_atomic_virial, - ) - - model_predict = {} - model_predict["atom_energy"] = model_ret["energy"] - model_predict["energy"] = model_ret["energy_redu"] - if self.do_grad("energy"): - model_predict["force"] = model_ret["energy_derv_r"].squeeze(-2) - if do_atomic_virial: - model_predict["atom_virial"] = model_ret["energy_derv_c"].squeeze(-3) - model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) - else: - model_predict["force"] = model_ret["dforce"] - return model_predict - - def forward_lower( - self, - extended_coord, - extended_atype, - nlist, - mapping: Optional[torch.Tensor] = None, - fparam: Optional[torch.Tensor] = None, - aparam: Optional[torch.Tensor] = None, - do_atomic_virial: bool = False, - ): - model_ret = self.forward_common_lower( - extended_coord, - extended_atype, - nlist, - mapping=mapping, - fparam=fparam, - aparam=aparam, - do_atomic_virial=do_atomic_virial, - ) - - model_predict = {} - model_predict["atom_energy"] = model_ret["energy"] - model_predict["energy"] = model_ret["energy_redu"] - if self.do_grad("energy"): - model_predict["extended_force"] = model_ret["energy_derv_r"].squeeze(-2) - model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) - if do_atomic_virial: - model_predict["extended_virial"] = model_ret["energy_derv_c"].squeeze( - -2 - ) - else: - assert model_ret["dforce"] is not None - model_predict["dforce"] = model_ret["dforce"] - model_predict = model_ret - return model_predict class EnergyModel(DPModel): diff --git a/source/tests/common/dpmodel/test_dp_atomic_model.py b/source/tests/common/dpmodel/test_dp_atomic_model.py index 684bea45c6..ea5d811b77 100644 --- a/source/tests/common/dpmodel/test_dp_atomic_model.py +++ b/source/tests/common/dpmodel/test_dp_atomic_model.py @@ -3,15 +3,15 @@ import numpy as np +from deepmd.dpmodel.atomic_model import ( + DPAtomicModel, +) from deepmd.dpmodel.descriptor import ( DescrptSeA, ) from deepmd.dpmodel.fitting import ( InvarFitting, ) -from deepmd.dpmodel.model import ( - DPAtomicModel, -) from .case_single_frame_with_nlist import ( TestCaseSingleFrameWithNlist, diff --git a/source/tests/common/dpmodel/test_linear_atomic_model.py b/source/tests/common/dpmodel/test_linear_atomic_model.py index 6dcff97d74..74f2daf7be 100644 --- a/source/tests/common/dpmodel/test_linear_atomic_model.py +++ b/source/tests/common/dpmodel/test_linear_atomic_model.py @@ -6,21 +6,21 @@ import numpy as np +from deepmd.dpmodel.atomic_model import ( + DPAtomicModel, +) +from deepmd.dpmodel.atomic_model.linear_atomic_model import ( + DPZBLLinearAtomicModel, +) +from deepmd.dpmodel.atomic_model.pairtab_atomic_model import ( + PairTabAtomicModel, +) from deepmd.dpmodel.descriptor.se_e2_a import ( DescrptSeA, ) from deepmd.dpmodel.fitting.invar_fitting import ( InvarFitting, ) -from deepmd.dpmodel.model.dp_atomic_model import ( - DPAtomicModel, -) -from deepmd.dpmodel.model.linear_atomic_model import ( - DPZBLLinearAtomicModel, -) -from deepmd.dpmodel.model.pairtab_atomic_model import ( - PairTabModel, -) class TestWeightCalculation(unittest.TestCase): @@ -53,7 +53,7 @@ def test_pairwise(self, mock_loadtxt): ) type_map = ["foo", "bar"] - zbl_model = PairTabModel(tab_file=file_path, rcut=0.3, sel=2) + zbl_model = PairTabAtomicModel(tab_file=file_path, rcut=0.3, sel=2) dp_model = DPAtomicModel(ds, ft, type_map=type_map) wgt_model = DPZBLLinearAtomicModel( @@ -145,7 +145,7 @@ def setUp(self, mock_loadtxt): ) type_map = ["foo", "bar"] dp_model = DPAtomicModel(ds, ft, type_map=type_map) - zbl_model = PairTabModel(file_path, self.rcut, sum(self.sel)) + zbl_model = PairTabAtomicModel(file_path, self.rcut, sum(self.sel)) self.md0 = DPZBLLinearAtomicModel( dp_model, zbl_model, diff --git a/source/tests/common/dpmodel/test_pairtab_atomic_model.py b/source/tests/common/dpmodel/test_pairtab_atomic_model.py index f1e7bd257c..d004f9a37a 100644 --- a/source/tests/common/dpmodel/test_pairtab_atomic_model.py +++ b/source/tests/common/dpmodel/test_pairtab_atomic_model.py @@ -6,8 +6,8 @@ import numpy as np -from deepmd.dpmodel.model.pairtab_atomic_model import ( - PairTabModel, +from deepmd.dpmodel.atomic_model.pairtab_atomic_model import ( + PairTabAtomicModel, ) @@ -24,7 +24,7 @@ def setUp(self, mock_loadtxt) -> None: ] ) - self.model = PairTabModel(tab_file=file_path, rcut=0.02, sel=2) + self.model = PairTabAtomicModel(tab_file=file_path, rcut=0.02, sel=2) self.extended_coord = np.array( [ @@ -68,7 +68,7 @@ def test_with_mask(self): np.testing.assert_allclose(result["energy"], expected_result, 0.0001, 0.0001) def test_deserialize(self): - model1 = PairTabModel.deserialize(self.model.serialize()) + model1 = PairTabAtomicModel.deserialize(self.model.serialize()) np.testing.assert_allclose(self.model.tab_data, model1.tab_data) np.testing.assert_allclose(self.model.tab_info, model1.tab_info) @@ -166,7 +166,7 @@ def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: ] ) - model = PairTabModel(tab_file=file_path, rcut=rcut, sel=2) + model = PairTabAtomicModel(tab_file=file_path, rcut=rcut, sel=2) results.append( model.forward_atomic(extended_coord, extended_atype, nlist)["energy"] ) diff --git a/source/tests/pt/model/test_dp_atomic_model.py b/source/tests/pt/model/test_dp_atomic_model.py index fb7e684eaa..d56bec3bb2 100644 --- a/source/tests/pt/model/test_dp_atomic_model.py +++ b/source/tests/pt/model/test_dp_atomic_model.py @@ -4,15 +4,15 @@ import numpy as np import torch -from deepmd.dpmodel import DPAtomicModel as DPDPAtomicModel +from deepmd.dpmodel.atomic_model import DPAtomicModel as DPDPAtomicModel from deepmd.dpmodel.descriptor import DescrptSeA as DPDescrptSeA from deepmd.dpmodel.fitting import InvarFitting as DPInvarFitting +from deepmd.pt.model.atomic_model import ( + DPAtomicModel, +) from deepmd.pt.model.descriptor.se_a import ( DescrptSeA, ) -from deepmd.pt.model.model.dp_atomic_model import ( - DPAtomicModel, -) from deepmd.pt.model.task.ener import ( InvarFitting, ) diff --git a/source/tests/pt/model/test_dp_model.py b/source/tests/pt/model/test_dp_model.py index d970c8a542..fb936e59cb 100644 --- a/source/tests/pt/model/test_dp_model.py +++ b/source/tests/pt/model/test_dp_model.py @@ -10,7 +10,7 @@ from deepmd.pt.model.descriptor.se_a import ( DescrptSeA, ) -from deepmd.pt.model.model.ener import ( +from deepmd.pt.model.model import ( DPModel, EnergyModel, ) diff --git a/source/tests/pt/model/test_linear_atomic_model.py b/source/tests/pt/model/test_linear_atomic_model.py index 14fc6b386a..bb836d4e46 100644 --- a/source/tests/pt/model/test_linear_atomic_model.py +++ b/source/tests/pt/model/test_linear_atomic_model.py @@ -7,23 +7,19 @@ import numpy as np import torch -from deepmd.dpmodel.model.linear_atomic_model import ( +from deepmd.dpmodel.atomic_model import ( DPZBLLinearAtomicModel as DPDPZBLLinearAtomicModel, ) -from deepmd.pt.model.descriptor.se_a import ( - DescrptSeA, -) -from deepmd.pt.model.model.dp_atomic_model import ( +from deepmd.pt.model.atomic_model import ( DPAtomicModel, -) -from deepmd.pt.model.model.ener import ( - ZBLModel, -) -from deepmd.pt.model.model.linear_atomic_model import ( DPZBLLinearAtomicModel, + PairTabAtomicModel, +) +from deepmd.pt.model.descriptor.se_a import ( + DescrptSeA, ) -from deepmd.pt.model.model.pairtab_atomic_model import ( - PairTabModel, +from deepmd.pt.model.model import ( + DPZBLModel, ) from deepmd.pt.model.task.ener import ( InvarFitting, @@ -73,7 +69,7 @@ def test_pairwise(self, mock_loadtxt): ).to(env.DEVICE) type_map = ["foo", "bar"] - zbl_model = PairTabModel(tab_file=file_path, rcut=0.3, sel=2) + zbl_model = PairTabAtomicModel(tab_file=file_path, rcut=0.3, sel=2) dp_model = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) wgt_model = DPZBLLinearAtomicModel( dp_model, @@ -141,7 +137,7 @@ def setUp(self, mock_loadtxt): ).to(env.DEVICE) type_map = ["foo", "bar"] dp_model = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) - zbl_model = PairTabModel(file_path, self.rcut, sum(self.sel)) + zbl_model = PairTabAtomicModel(file_path, self.rcut, sum(self.sel)) self.md0 = DPZBLLinearAtomicModel( dp_model, zbl_model, @@ -152,7 +148,7 @@ def setUp(self, mock_loadtxt): env.DEVICE ) self.md2 = DPDPZBLLinearAtomicModel.deserialize(self.md0.serialize()) - self.md3 = ZBLModel(dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25) + self.md3 = DPZBLModel(dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25) def test_self_consistency(self): args = [ diff --git a/source/tests/pt/model/test_make_hessian_model.py b/source/tests/pt/model/test_make_hessian_model.py index 650d35e019..81aee758bf 100644 --- a/source/tests/pt/model/test_make_hessian_model.py +++ b/source/tests/pt/model/test_make_hessian_model.py @@ -13,7 +13,7 @@ from deepmd.pt.model.model import ( make_hessian_model, ) -from deepmd.pt.model.model.ener import ( +from deepmd.pt.model.model.dp_model import ( DPModel, ) from deepmd.pt.model.task.ener import ( diff --git a/source/tests/pt/model/test_pairtab_atomic_model.py b/source/tests/pt/model/test_pairtab_atomic_model.py index f58ac76211..69e305b40b 100644 --- a/source/tests/pt/model/test_pairtab_atomic_model.py +++ b/source/tests/pt/model/test_pairtab_atomic_model.py @@ -7,9 +7,9 @@ import numpy as np import torch -from deepmd.dpmodel.model.pairtab_atomic_model import PairTabModel as DPPairTabModel -from deepmd.pt.model.model.pairtab_atomic_model import ( - PairTabModel, +from deepmd.dpmodel.atomic_model import PairTabAtomicModel as DPPairTabAtomicModel +from deepmd.pt.model.atomic_model import ( + PairTabAtomicModel, ) from deepmd.pt.utils.utils import ( to_numpy_array, @@ -29,7 +29,7 @@ def setUp(self, mock_loadtxt) -> None: ] ) - self.model = PairTabModel(tab_file=file_path, rcut=0.02, sel=2) + self.model = PairTabAtomicModel(tab_file=file_path, rcut=0.02, sel=2) self.extended_coord = torch.tensor( [ @@ -87,7 +87,7 @@ def test_jit(self): self.assertEqual(model.get_type_map(), None) def test_deserialize(self): - model1 = PairTabModel.deserialize(self.model.serialize()) + model1 = PairTabAtomicModel.deserialize(self.model.serialize()) torch.testing.assert_close(self.model.tab_data, model1.tab_data) torch.testing.assert_close(self.model.tab_info, model1.tab_info) @@ -110,7 +110,7 @@ def test_deserialize(self): def test_cross_deserialize(self): model_dict = self.model.serialize() # pytorch model to dict - model1 = DPPairTabModel.deserialize(model_dict) # dict to numpy model + model1 = DPPairTabAtomicModel.deserialize(model_dict) # dict to numpy model np.testing.assert_allclose(self.model.tab_data, model1.tab_data) np.testing.assert_allclose(self.model.tab_info, model1.tab_info) @@ -209,7 +209,7 @@ def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: ] ) - model = PairTabModel(tab_file=file_path, rcut=rcut, sel=2) + model = PairTabAtomicModel(tab_file=file_path, rcut=rcut, sel=2) results.append( model.forward_atomic(extended_coord, extended_atype, nlist)["energy"] ) From 02080dbfbf3036d1456715895a4713a5b12060fc Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 15 Feb 2024 04:20:21 -0500 Subject: [PATCH 084/686] tf: refactor neighbor stat (#3275) Fix #3272. Apply implementation of #3271 into TF. Confirm consistent results on `examples/water`, `examples/nopbc`, and ANI-1x (#1624). 80x speed up: ![image](https://github.com/deepmodeling/deepmd-kit/assets/9496702/85aa1fed-e3c0-4cb6-9082-db45c9a03f9d) --------- Signed-off-by: Jinzhe Zeng --- deepmd/tf/utils/neighbor_stat.py | 276 ++++++++++++++++++++++++------- deepmd/tf/utils/nlist.py | 103 ++++++++++++ deepmd/tf/utils/region.py | 36 ++++ source/tests/tf/test_nlist.py | 136 +++++++++++++++ 4 files changed, 489 insertions(+), 62 deletions(-) create mode 100644 deepmd/tf/utils/nlist.py create mode 100644 deepmd/tf/utils/region.py create mode 100644 source/tests/tf/test_nlist.py diff --git a/deepmd/tf/utils/neighbor_stat.py b/deepmd/tf/utils/neighbor_stat.py index 84e71c7a84..2c063246e6 100644 --- a/deepmd/tf/utils/neighbor_stat.py +++ b/deepmd/tf/utils/neighbor_stat.py @@ -2,6 +2,7 @@ import logging from typing import ( Iterator, + Optional, Tuple, ) @@ -9,21 +10,153 @@ from deepmd.tf.env import ( GLOBAL_NP_FLOAT_PRECISION, + GLOBAL_TF_FLOAT_PRECISION, default_tf_session_config, - op_module, tf, ) +from deepmd.tf.utils.batch_size import ( + AutoBatchSize, +) from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) -from deepmd.tf.utils.parallel_op import ( - ParallelOp, +from deepmd.tf.utils.nlist import ( + extend_coord_with_ghosts, +) +from deepmd.tf.utils.sess import ( + run_sess, ) from deepmd.utils.neighbor_stat import NeighborStat as BaseNeighborStat log = logging.getLogger(__name__) +class NeighborStatOP: + """Class for getting neighbor statics data information. + + Parameters + ---------- + ntypes + The num of atom types + rcut + The cut-off radius + distinguish_types : bool, optional + If False, treat all types as a single type. + """ + + def __init__( + self, + ntypes: int, + rcut: float, + distinguish_types: bool, + ) -> None: + super().__init__() + self.rcut = rcut + self.ntypes = ntypes + self.distinguish_types = distinguish_types + + def build( + self, + coord: tf.Tensor, + atype: tf.Tensor, + cell: tf.Tensor, + pbc: tf.Tensor, + ) -> Tuple[tf.Tensor, tf.Tensor]: + """Calculate the nearest neighbor distance between atoms, maximum nbor size of + atoms and the output data range of the environment matrix. + + Parameters + ---------- + coord + The coordinates of atoms. + atype + The atom types. + cell + The cell. + + Returns + ------- + tf.Tensor + The minimal squared distance between two atoms, in the shape of (nframes,) + tf.Tensor + The maximal number of neighbors + """ + # generated by GitHub Copilot, converted from PT codes + nframes = tf.shape(coord)[0] + coord = tf.reshape(coord, [nframes, -1, 3]) + nloc = tf.shape(coord)[1] + coord = tf.reshape(coord, [nframes, nloc * 3]) + extend_coord, extend_atype, _ = extend_coord_with_ghosts( + coord, atype, cell, self.rcut, pbc + ) + + coord1 = tf.reshape(extend_coord, [nframes, -1]) + nall = tf.shape(coord1)[1] // 3 + coord0 = coord1[:, : nloc * 3] + diff = ( + tf.reshape(coord1, [nframes, -1, 3])[:, None, :, :] + - tf.reshape(coord0, [nframes, -1, 3])[:, :, None, :] + ) + # shape of diff: nframes, nloc, nall, 3 + # remove the diagonal elements + mask = tf.eye(nloc, nall, dtype=tf.bool) + # expand mask + mask = tf.tile(mask[None, :, :], [nframes, 1, 1]) + # expand inf + inf_mask = tf.constant( + float("inf"), dtype=GLOBAL_TF_FLOAT_PRECISION, shape=[1, 1, 1] + ) + inf_mask = tf.tile(inf_mask, [nframes, nloc, nall]) + # virtual type (<0) are not counted + virtual_type_mask_i = tf.tile(tf.less(atype, 0)[:, :, None], [1, 1, nall]) + virtual_type_mask_j = tf.tile( + tf.less(extend_atype, 0)[:, None, :], [1, nloc, 1] + ) + mask = mask | virtual_type_mask_i | virtual_type_mask_j + rr2 = tf.reduce_sum(tf.square(diff), axis=-1) + rr2 = tf.where(mask, inf_mask, rr2) + min_rr2 = tf.reduce_min(rr2, axis=(1, 2)) + # count the number of neighbors + if self.distinguish_types: + mask = rr2 < self.rcut**2 + nnei = [] + for ii in range(self.ntypes): + nnei.append( + tf.reduce_sum( + tf.cast( + mask & (tf.equal(extend_atype, ii))[:, None, :], tf.int32 + ), + axis=-1, + ) + ) + # shape: nframes, nloc, ntypes + nnei = tf.stack(nnei, axis=-1) + else: + mask = rr2 < self.rcut**2 + # virtual types (<0) are not counted + nnei = tf.reshape( + tf.reduce_sum( + tf.cast( + mask & tf.greater_equal(extend_atype, 0)[:, None, :], tf.int32 + ), + axis=-1, + ), + [nframes, nloc, 1], + ) + # nnei: nframes, nloc, ntypes + # virtual type i (<0) are not counted + nnei = tf.where( + tf.tile( + tf.less(atype, 0)[:, :, None], + [1, 1, self.ntypes if self.distinguish_types else 1], + ), + tf.zeros_like(nnei, dtype=tf.int32), + nnei, + ) + max_nnei = tf.reduce_max(nnei, axis=1) + return min_rr2, max_nnei + + class NeighborStat(BaseNeighborStat): """Class for getting training data information. @@ -47,52 +180,48 @@ def __init__( ) -> None: """Constructor.""" super().__init__(ntypes, rcut, one_type) - sub_graph = tf.Graph() - - def builder(): - place_holders = {} - for ii in ["coord", "box"]: - place_holders[ii] = tf.placeholder( - GLOBAL_NP_FLOAT_PRECISION, [None, None], name="t_" + ii - ) - place_holders["type"] = tf.placeholder( - tf.int32, [None, None], name="t_type" - ) - place_holders["natoms_vec"] = tf.placeholder( - tf.int32, [self.ntypes + 2], name="t_natoms" - ) - place_holders["default_mesh"] = tf.placeholder( - tf.int32, [None], name="t_mesh" - ) - t_type = place_holders["type"] - t_natoms = place_holders["natoms_vec"] - if self.one_type: - # all types = 0, natoms_vec = [natoms, natoms, natoms] - t_type = tf.clip_by_value(t_type, -1, 0) - t_natoms = tf.tile(t_natoms[0:1], [3]) - - _max_nbor_size, _min_nbor_dist = op_module.neighbor_stat( - place_holders["coord"], - t_type, - t_natoms, - place_holders["box"], - place_holders["default_mesh"], - rcut=self.rcut, - ) - place_holders["dir"] = tf.placeholder(tf.string) - _min_nbor_dist = tf.reduce_min(_min_nbor_dist) - _max_nbor_size = tf.reduce_max(_max_nbor_size, axis=0) - return place_holders, (_max_nbor_size, _min_nbor_dist, place_holders["dir"]) + self.auto_batch_size = AutoBatchSize() + self.neighbor_stat = NeighborStatOP(ntypes, rcut, not one_type) + self.place_holders = {} + with tf.Graph().as_default() as sub_graph: + self.op = self.build() + self.sub_sess = tf.Session(graph=sub_graph, config=default_tf_session_config) - with sub_graph.as_default(): - self.p = ParallelOp(builder, config=default_tf_session_config) + def build(self) -> Tuple[tf.Tensor, tf.Tensor]: + """Build the graph. - self.sub_sess = tf.Session(graph=sub_graph, config=default_tf_session_config) + Returns + ------- + tf.Tensor + The minimal squared distance between two atoms, in the shape of (nframes,) + tf.Tensor + The maximal number of neighbors + """ + for ii in ["coord", "box"]: + self.place_holders[ii] = tf.placeholder( + GLOBAL_NP_FLOAT_PRECISION, [None, None], name="t_" + ii + ) + self.place_holders["type"] = tf.placeholder( + tf.int32, [None, None], name="t_type" + ) + self.place_holders["pbc"] = tf.placeholder(tf.bool, [], name="t_pbc") + ret = self.neighbor_stat.build( + self.place_holders["coord"], + self.place_holders["type"], + self.place_holders["box"], + self.place_holders["pbc"], + ) + return ret def iterator( self, data: DeepmdDataSystem ) -> Iterator[Tuple[np.ndarray, float, str]]: - """Abstract method for producing data. + """Produce data. + + Parameters + ---------- + data + The data system Yields ------ @@ -103,23 +232,46 @@ def iterator( str The directory of the data system """ + for ii in range(len(data.system_dirs)): + for jj in data.data_systems[ii].dirs: + data_set = data.data_systems[ii] + data_set_data = data_set._load_set(jj) + minrr2, max_nnei = self.auto_batch_size.execute_all( + self._execute, + data_set_data["coord"].shape[0], + data_set.get_natoms(), + data_set_data["coord"], + data_set_data["type"], + data_set_data["box"], + data_set.pbc, + ) + yield np.max(max_nnei, axis=0), np.min(minrr2), jj - def feed(): - for ii in range(len(data.system_dirs)): - for jj in data.data_systems[ii].dirs: - data_set = data.data_systems[ii]._load_set(jj) - for kk in range(np.array(data_set["type"]).shape[0]): - yield { - "coord": np.array(data_set["coord"])[kk].reshape( - [-1, data.natoms[ii] * 3] - ), - "type": np.array(data_set["type"])[kk].reshape( - [-1, data.natoms[ii]] - ), - "natoms_vec": np.array(data.natoms_vec[ii]), - "box": np.array(data_set["box"])[kk].reshape([-1, 9]), - "default_mesh": np.array(data.default_mesh[ii]), - "dir": str(jj), - } - - return self.p.generate(self.sub_sess, feed()) + def _execute( + self, + coord: np.ndarray, + atype: np.ndarray, + box: Optional[np.ndarray], + pbc: bool, + ): + """Execute the operation. + + Parameters + ---------- + coord + The coordinates of atoms. + atype + The atom types. + box + The box. + pbc + Whether the box is periodic. + """ + feed_dict = { + self.place_holders["coord"]: coord, + self.place_holders["type"]: atype, + self.place_holders["box"]: box, + self.place_holders["pbc"]: pbc, + } + minrr2, max_nnei = run_sess(self.sub_sess, self.op, feed_dict=feed_dict) + return minrr2, max_nnei diff --git a/deepmd/tf/utils/nlist.py b/deepmd/tf/utils/nlist.py new file mode 100644 index 0000000000..87032c3e1d --- /dev/null +++ b/deepmd/tf/utils/nlist.py @@ -0,0 +1,103 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from deepmd.tf.env import ( + GLOBAL_TF_FLOAT_PRECISION, + tf, +) +from deepmd.tf.utils.region import ( + to_face_distance, +) + + +def extend_coord_with_ghosts( + coord: tf.Tensor, + atype: tf.Tensor, + cell: tf.Tensor, + rcut: float, + pbc: tf.Tensor, +): + """Extend the coordinates of the atoms by appending peridoc images. + The number of images is large enough to ensure all the neighbors + within rcut are appended. + + Parameters + ---------- + coord : tf.Tensor + original coordinates of shape [-1, nloc*3]. + atype : tf.Tensor + atom type of shape [-1, nloc]. + cell : tf.Tensor + simulation cell tensor of shape [-1, 9]. + rcut : float + the cutoff radius + pbc : tf.Tensor + whether the simulation cell is periodic or not + + Returns + ------- + extended_coord: tf.Tensor + extended coordinates of shape [-1, nall*3]. + extended_atype: tf.Tensor + extended atom type of shape [-1, nall]. + index_mapping: tf.Tensor + maping extended index to the local index + + """ + # generated by GitHub Copilot, converted from PT codes + nf = tf.shape(atype)[0] + nloc = tf.shape(atype)[1] + aidx = tf.tile(tf.expand_dims(tf.range(nloc), 0), [nf, 1]) + + def extend_coord_with_ghosts_nopbc(coord, atype, cell): + return coord, atype, aidx, nloc + + def extend_coord_with_ghosts_pbc(coord, atype, cell): + coord = tf.reshape(coord, [nf, nloc, 3]) + cell = tf.reshape(cell, [nf, 3, 3]) + # nf x 3 + to_face = to_face_distance(cell) + # nf x 3 + # *2: ghost copies on + and - directions + # +1: central cell + nbuff = tf.cast(tf.math.ceil(rcut / to_face), tf.int32) + # 3 + nbuff = tf.reduce_max(nbuff, axis=0) + xi = tf.range(-nbuff[0], nbuff[0] + 1, 1) + yi = tf.range(-nbuff[1], nbuff[1] + 1, 1) + zi = tf.range(-nbuff[2], nbuff[2] + 1, 1) + xyz = tf.reshape(xi, [-1, 1, 1, 1]) * tf.constant([1, 0, 0], dtype=tf.int32) + xyz = xyz + tf.reshape(yi, [1, -1, 1, 1]) * tf.constant( + [0, 1, 0], dtype=tf.int32 + ) + xyz = xyz + tf.reshape(zi, [1, 1, -1, 1]) * tf.constant( + [0, 0, 1], dtype=tf.int32 + ) + xyz = tf.reshape(xyz, [-1, 3]) + # ns x 3 + shift_idx = tf.gather( + xyz, tf.argsort(tf.norm(tf.cast(xyz, GLOBAL_TF_FLOAT_PRECISION), axis=1)) + ) + ns = tf.shape(shift_idx)[0] + nall = ns * nloc + # nf x ns x 3 + shift_vec = tf.einsum( + "sd,fdk->fsk", tf.cast(shift_idx, GLOBAL_TF_FLOAT_PRECISION), cell + ) + # nf x ns x nloc x 3 + extend_coord = coord[:, None, :, :] + shift_vec[:, :, None, :] + # nf x ns x nloc + extend_atype = tf.tile(tf.expand_dims(atype, -2), [1, ns, 1]) + # nf x ns x nloc + extend_aidx = tf.tile(tf.expand_dims(aidx, -2), [1, ns, 1]) + return extend_coord, extend_atype, extend_aidx, nall + + extend_coord, extend_atype, extend_aidx, nall = tf.cond( + pbc, + lambda: extend_coord_with_ghosts_pbc(coord, atype, cell), + lambda: extend_coord_with_ghosts_nopbc(coord, atype, cell), + ) + + return ( + tf.reshape(extend_coord, [nf, nall * 3]), + tf.reshape(extend_atype, [nf, nall]), + tf.reshape(extend_aidx, [nf, nall]), + ) diff --git a/deepmd/tf/utils/region.py b/deepmd/tf/utils/region.py new file mode 100644 index 0000000000..82183a0413 --- /dev/null +++ b/deepmd/tf/utils/region.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from deepmd.tf.env import ( + tf, +) + + +def to_face_distance(cell): + """Compute the to-face-distance of the simulation cell. + + Parameters + ---------- + cell : tf.Tensor + simulation cell tensor of shape [*, 3, 3]. + + Returns + ------- + dist: tf.Tensor + the to face distances of shape [*, 3] + """ + # generated by GitHub Copilot, converted from PT codes + cshape = tf.shape(cell) + cell_reshaped = tf.reshape(cell, [-1, 3, 3]) + dist = b_to_face_distance(cell_reshaped) + return tf.reshape(dist, tf.concat([cshape[:-2], [3]], 0)) + + +def b_to_face_distance(cell): + # generated by GitHub Copilot, converted from PT codes + volume = tf.linalg.det(cell) + c_yz = tf.linalg.cross(cell[:, 1], cell[:, 2]) + _h2yz = tf.divide(volume, tf.norm(c_yz, axis=-1)) + c_zx = tf.linalg.cross(cell[:, 2], cell[:, 0]) + _h2zx = tf.divide(volume, tf.norm(c_zx, axis=-1)) + c_xy = tf.linalg.cross(cell[:, 0], cell[:, 1]) + _h2xy = tf.divide(volume, tf.norm(c_xy, axis=-1)) + return tf.stack([_h2yz, _h2zx, _h2xy], axis=1) diff --git a/source/tests/tf/test_nlist.py b/source/tests/tf/test_nlist.py new file mode 100644 index 0000000000..9e66b185ee --- /dev/null +++ b/source/tests/tf/test_nlist.py @@ -0,0 +1,136 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np + +from deepmd.dpmodel.utils import ( + inter2phys, +) +from deepmd.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) +from deepmd.tf.env import ( + GLOBAL_TF_FLOAT_PRECISION, + default_tf_session_config, + tf, +) +from deepmd.tf.utils.nlist import ( + extend_coord_with_ghosts, +) + + +class TestNeighList(unittest.TestCase): + def setUp(self): + self.nf = 3 + self.nloc = 2 + self.ns = 5 * 5 * 3 + self.nall = self.ns * self.nloc + self.cell = np.array( + [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=GLOBAL_NP_FLOAT_PRECISION + ) + self.icoord = np.array( + [[0, 0, 0], [0.5, 0.5, 0.1]], dtype=GLOBAL_NP_FLOAT_PRECISION + ) + self.atype = np.array([0, 1], dtype=int) + [self.cell, self.icoord, self.atype] = [ + np.expand_dims(ii, 0) for ii in [self.cell, self.icoord, self.atype] + ] + self.coord = inter2phys(self.icoord, self.cell).reshape([-1, self.nloc * 3]) + self.cell = self.cell.reshape([-1, 9]) + [self.cell, self.coord, self.atype] = [ + np.tile(ii, [self.nf, 1]) for ii in [self.cell, self.coord, self.atype] + ] + self.rcut = 1.01 + self.prec = 1e-10 + self.nsel = [10, 10] + self.ref_nlist = np.array( + [ + [0, 0, 0, 0, 0, 0, -1, -1, -1, -1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1], + [0, 0, 0, 0, -1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1], + ] + ) + + def test_extend_coord(self): + t_coord = tf.placeholder( + GLOBAL_TF_FLOAT_PRECISION, [None, None], name="i_coord" + ) + t_atype = tf.placeholder(tf.int32, [None, None], name="i_atype") + t_cell = tf.placeholder(GLOBAL_TF_FLOAT_PRECISION, [None, None], name="i_cell") + t_pbc = tf.placeholder(tf.bool, [], name="i_pbc") + t_ecoord, t_eatype, t_mapping = extend_coord_with_ghosts( + t_coord, t_atype, t_cell, self.rcut, t_pbc + ) + with tf.Session(config=default_tf_session_config) as sess: + ecoord, eatype, mapping = sess.run( + [t_ecoord, t_eatype, t_mapping], + feed_dict={ + t_coord: self.coord, + t_atype: self.atype, + t_cell: self.cell, + t_pbc: self.cell is not None, + }, + ) + # expected ncopy x nloc + self.assertEqual(list(ecoord.shape), [self.nf, self.nall * 3]) + self.assertEqual(list(eatype.shape), [self.nf, self.nall]) + self.assertEqual(list(mapping.shape), [self.nf, self.nall]) + # check the nloc part is identical with original coord + np.testing.assert_allclose( + ecoord[:, : self.nloc * 3], self.coord, rtol=self.prec, atol=self.prec + ) + # check the shift vectors are aligned with grid + shift_vec = ( + ecoord.reshape([-1, self.ns, self.nloc, 3]) + - self.coord.reshape([-1, self.nloc, 3])[:, None, :, :] + ) + shift_vec = shift_vec.reshape([-1, self.nall, 3]) + # hack!!! assumes identical cell across frames + shift_vec = np.matmul( + shift_vec, np.linalg.inv(self.cell.reshape([self.nf, 3, 3])[0]) + ) + # nf x nall x 3 + shift_vec = np.round(shift_vec) + # check: identical shift vecs + np.testing.assert_allclose( + shift_vec[0], shift_vec[1], rtol=self.prec, atol=self.prec + ) + # check: shift idx aligned with grid + mm, cc = np.unique(shift_vec[0][:, 0], axis=-1, return_counts=True) + np.testing.assert_allclose( + mm, + np.array([-2, -1, 0, 1, 2], dtype=GLOBAL_NP_FLOAT_PRECISION), + rtol=self.prec, + atol=self.prec, + ) + np.testing.assert_allclose( + cc, + np.array([30, 30, 30, 30, 30], dtype=np.int64), + rtol=self.prec, + atol=self.prec, + ) + mm, cc = np.unique(shift_vec[1][:, 1], axis=-1, return_counts=True) + np.testing.assert_allclose( + mm, + np.array([-2, -1, 0, 1, 2], dtype=GLOBAL_NP_FLOAT_PRECISION), + rtol=self.prec, + atol=self.prec, + ) + np.testing.assert_allclose( + cc, + np.array([30, 30, 30, 30, 30], dtype=GLOBAL_NP_FLOAT_PRECISION), + rtol=self.prec, + atol=self.prec, + ) + mm, cc = np.unique(shift_vec[1][:, 2], axis=-1, return_counts=True) + np.testing.assert_allclose( + mm, + np.array([-1, 0, 1], dtype=GLOBAL_NP_FLOAT_PRECISION), + rtol=self.prec, + atol=self.prec, + ) + np.testing.assert_allclose( + cc, + np.array([50, 50, 50], dtype=np.int64), + rtol=self.prec, + atol=self.prec, + ) From 43f17dabf1db5772661857f9347479fe1fc20fd0 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Thu, 15 Feb 2024 21:16:47 +0800 Subject: [PATCH 085/686] feat: pt: support user specified rcond for fitting stat (#3279) the stats are actually not well tested, see https://github.com/deepmodeling/deepmd-kit/issues/3278 Co-authored-by: Han Wang --- deepmd/pt/model/task/ener.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 81f2cc8cf0..1a883a50a2 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -62,6 +62,7 @@ def __init__( activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, distinguish_types: bool = False, + rcond: Optional[float] = None, **kwargs, ): """Construct a fitting net for energy. @@ -87,6 +88,7 @@ def __init__( self.activation_function = activation_function self.precision = precision self.prec = PRECISION_DICT[self.precision] + self.rcond = rcond if bias_atom_e is None: bias_atom_e = np.zeros([self.ntypes, self.dim_out]) bias_atom_e = torch.tensor(bias_atom_e, dtype=self.prec, device=device) @@ -217,8 +219,7 @@ def compute_output_stats(self, merged): input_natoms = [item["real_natoms_vec"] for item in merged] else: input_natoms = [item["natoms"] for item in merged] - tmp = compute_output_bias(energy, input_natoms) - bias_atom_e = tmp[:, 0] + bias_atom_e = compute_output_bias(energy, input_natoms, rcond=self.rcond) return {"bias_atom_e": bias_atom_e} def init_fitting_stat(self, bias_atom_e=None, **kwargs): @@ -244,6 +245,7 @@ def serialize(self) -> dict: "precision": self.precision, "distinguish_types": self.distinguish_types, "nets": self.filter_layers.serialize(), + "rcond": self.rcond, "@variables": { "bias_atom_e": to_numpy_array(self.bias_atom_e), "fparam_avg": to_numpy_array(self.fparam_avg), @@ -259,7 +261,6 @@ def serialize(self) -> dict: # "use_aparam_as_mask": self.use_aparam_as_mask , # "spin": self.spin , ## NOTICE: not supported by far - "rcond": None, "tot_ener_zero": False, "trainable": True, "atom_ener": None, From 15f8d258bcc383a5430e8b0f4c21b8618cfd6c5d Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:41:13 +0800 Subject: [PATCH 086/686] Chore: refactor InvarFitting (#3266) Signed-off-by: Anyang Peng <137014849+anyangml@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../atomic_model/linear_atomic_model.py | 6 +- deepmd/pt/model/task/__init__.py | 4 +- deepmd/pt/model/task/dipole.py | 2 +- deepmd/pt/model/task/ener.py | 317 +++------------- deepmd/pt/model/task/fitting.py | 358 ++++++++++++++++++ .../tests/pt/model/test_make_hessian_model.py | 30 +- 6 files changed, 440 insertions(+), 277 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/linear_atomic_model.py b/deepmd/dpmodel/atomic_model/linear_atomic_model.py index 48aacacdee..a9e57d6270 100644 --- a/deepmd/dpmodel/atomic_model/linear_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/linear_atomic_model.py @@ -320,14 +320,16 @@ def _compute_weight( ), axis=-1, ) # handle masked nnei. - sigma = numerator / denominator + with np.errstate(divide="ignore", invalid="ignore"): + sigma = numerator / denominator u = (sigma - self.sw_rmin) / (self.sw_rmax - self.sw_rmin) coef = np.zeros_like(u) left_mask = sigma < self.sw_rmin mid_mask = (self.sw_rmin <= sigma) & (sigma < self.sw_rmax) right_mask = sigma >= self.sw_rmax coef[left_mask] = 1 - smooth = -6 * u**5 + 15 * u**4 - 10 * u**3 + 1 + with np.errstate(invalid="ignore"): + smooth = -6 * u**5 + 15 * u**4 - 10 * u**3 + 1 coef[mid_mask] = smooth[mid_mask] coef[right_mask] = 0 self.zbl_weight = coef diff --git a/deepmd/pt/model/task/__init__.py b/deepmd/pt/model/task/__init__.py index 0b21033d31..180e5a5211 100644 --- a/deepmd/pt/model/task/__init__.py +++ b/deepmd/pt/model/task/__init__.py @@ -9,7 +9,7 @@ DenoiseNet, ) from .dipole import ( - DipoleFittingNetType, + DipoleFittingNet, ) from .ener import ( EnergyFittingNet, @@ -25,7 +25,7 @@ __all__ = [ "FittingNetAttenLcc", "DenoiseNet", - "DipoleFittingNetType", + "DipoleFittingNet", "EnergyFittingNet", "EnergyFittingNetDirect", "Fitting", diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py index d911613a5b..aa518d2cd3 100644 --- a/deepmd/pt/model/task/dipole.py +++ b/deepmd/pt/model/task/dipole.py @@ -13,7 +13,7 @@ log = logging.getLogger(__name__) -class DipoleFittingNetType(Fitting): +class DipoleFittingNet(Fitting): def __init__( self, ntypes, embedding_width, neuron, out_dim, resnet_dt=True, **kwargs ): diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 1a883a50a2..f1dad4c58d 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -1,5 +1,4 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import copy import logging from typing import ( List, @@ -15,30 +14,22 @@ OutputVariableDef, fitting_check_output, ) -from deepmd.pt.model.network.mlp import ( - FittingNet, - NetworkCollection, -) from deepmd.pt.model.network.network import ( ResidualDeep, ) from deepmd.pt.model.task.fitting import ( Fitting, + GeneralFitting, ) from deepmd.pt.utils import ( env, ) from deepmd.pt.utils.env import ( DEFAULT_PRECISION, - PRECISION_DICT, ) from deepmd.pt.utils.stat import ( compute_output_bias, ) -from deepmd.pt.utils.utils import ( - to_numpy_array, - to_torch_tensor, -) dtype = env.GLOBAL_PT_FLOAT_PRECISION device = env.DEVICE @@ -47,7 +38,41 @@ @fitting_check_output -class InvarFitting(Fitting): +class InvarFitting(GeneralFitting): + """Construct a fitting net for energy. + + Parameters + ---------- + var_name : str + The atomic property to fit, 'energy', 'dipole', and 'polar'. + ntypes : int + Element count. + dim_descrpt : int + Embedding width per atom. + dim_out : int + The output dimension of the fitting net. + neuron : List[int] + Number of neurons in each hidden layers of the fitting net. + bias_atom_e : torch.Tensor, optional + Average enery per atom for each element. + resnet_dt : bool + Using time-step in the ResNet construction. + numb_fparam : int + Number of frame parameters. + numb_aparam : int + Number of atomic parameters. + activation_function : str + Activation function. + precision : str + Numerical precision. + distinguish_types : bool + Neighbor list that distinguish different atomic types or not. + rcond : float, optional + The condition number for the regression of atomic energy. + seed : int, optional + Random seed. + """ + def __init__( self, var_name: str, @@ -63,118 +88,31 @@ def __init__( precision: str = DEFAULT_PRECISION, distinguish_types: bool = False, rcond: Optional[float] = None, + seed: Optional[int] = None, **kwargs, ): - """Construct a fitting net for energy. - - Args: - - ntypes: Element count. - - embedding_width: Embedding width per atom. - - neuron: Number of neurons in each hidden layers of the fitting net. - - bias_atom_e: Average enery per atom for each element. - - resnet_dt: Using time-step in the ResNet construction. - """ - super().__init__() - self.var_name = var_name - self.ntypes = ntypes - self.dim_descrpt = dim_descrpt - self.dim_out = dim_out - self.neuron = neuron - self.distinguish_types = distinguish_types - self.use_tebd = not self.distinguish_types - self.resnet_dt = resnet_dt - self.numb_fparam = numb_fparam - self.numb_aparam = numb_aparam - self.activation_function = activation_function - self.precision = precision - self.prec = PRECISION_DICT[self.precision] - self.rcond = rcond - if bias_atom_e is None: - bias_atom_e = np.zeros([self.ntypes, self.dim_out]) - bias_atom_e = torch.tensor(bias_atom_e, dtype=self.prec, device=device) - bias_atom_e = bias_atom_e.view([self.ntypes, self.dim_out]) - if not self.use_tebd: - assert self.ntypes == bias_atom_e.shape[0], "Element count mismatches!" - self.register_buffer("bias_atom_e", bias_atom_e) - # init constants - if self.numb_fparam > 0: - self.register_buffer( - "fparam_avg", - torch.zeros(self.numb_fparam, dtype=self.prec, device=device), - ) - self.register_buffer( - "fparam_inv_std", - torch.ones(self.numb_fparam, dtype=self.prec, device=device), - ) - else: - self.fparam_avg, self.fparam_inv_std = None, None - if self.numb_aparam > 0: - self.register_buffer( - "aparam_avg", - torch.zeros(self.numb_aparam, dtype=self.prec, device=device), - ) - self.register_buffer( - "aparam_inv_std", - torch.ones(self.numb_aparam, dtype=self.prec, device=device), - ) - else: - self.aparam_avg, self.aparam_inv_std = None, None - - in_dim = self.dim_descrpt + self.numb_fparam + self.numb_aparam - - self.old_impl = kwargs.get("old_impl", False) - if self.old_impl: - filter_layers = [] - for type_i in range(self.ntypes): - bias_type = 0.0 - one = ResidualDeep( - type_i, - self.dim_descrpt, - self.neuron, - bias_type, - resnet_dt=self.resnet_dt, - ) - filter_layers.append(one) - self.filter_layers_old = torch.nn.ModuleList(filter_layers) - self.filter_layers = None - else: - self.filter_layers = NetworkCollection( - 1 if self.distinguish_types else 0, - self.ntypes, - network_type="fitting_network", - networks=[ - FittingNet( - in_dim, - self.dim_out, - self.neuron, - self.activation_function, - self.resnet_dt, - self.precision, - bias_out=True, - ) - for ii in range(self.ntypes if self.distinguish_types else 1) - ], - ) - self.filter_layers_old = None - - # very bad design... - if "seed" in kwargs: - log.info("Set seed to %d in fitting net.", kwargs["seed"]) - torch.manual_seed(kwargs["seed"]) - - def output_def(self) -> FittingOutputDef: - return FittingOutputDef( - [ - OutputVariableDef( - self.var_name, - [self.dim_out], - reduciable=True, - r_differentiable=True, - c_differentiable=True, - ), - ] + super().__init__( + var_name=var_name, + ntypes=ntypes, + dim_descrpt=dim_descrpt, + dim_out=dim_out, + neuron=neuron, + bias_atom_e=bias_atom_e, + resnet_dt=resnet_dt, + numb_fparam=numb_fparam, + numb_aparam=numb_aparam, + activation_function=activation_function, + precision=precision, + distinguish_types=distinguish_types, + rcond=rcond, + seed=seed, + **kwargs, ) + def _net_out_dim(self): + """Set the FittingNet output dim.""" + return self.dim_out + def __setitem__(self, key, value): if key in ["bias_atom_e"]: value = value.view([self.ntypes, self.dim_out]) @@ -230,62 +168,6 @@ def init_fitting_stat(self, bias_atom_e=None, **kwargs): ) ) - def serialize(self) -> dict: - """Serialize the fitting to dict.""" - return { - "var_name": self.var_name, - "ntypes": self.ntypes, - "dim_descrpt": self.dim_descrpt, - "dim_out": self.dim_out, - "neuron": self.neuron, - "resnet_dt": self.resnet_dt, - "numb_fparam": self.numb_fparam, - "numb_aparam": self.numb_aparam, - "activation_function": self.activation_function, - "precision": self.precision, - "distinguish_types": self.distinguish_types, - "nets": self.filter_layers.serialize(), - "rcond": self.rcond, - "@variables": { - "bias_atom_e": to_numpy_array(self.bias_atom_e), - "fparam_avg": to_numpy_array(self.fparam_avg), - "fparam_inv_std": to_numpy_array(self.fparam_inv_std), - "aparam_avg": to_numpy_array(self.aparam_avg), - "aparam_inv_std": to_numpy_array(self.aparam_inv_std), - }, - # "rcond": self.rcond , - # "tot_ener_zero": self.tot_ener_zero , - # "trainable": self.trainable , - # "atom_ener": self.atom_ener , - # "layer_name": self.layer_name , - # "use_aparam_as_mask": self.use_aparam_as_mask , - # "spin": self.spin , - ## NOTICE: not supported by far - "tot_ener_zero": False, - "trainable": True, - "atom_ener": None, - "layer_name": None, - "use_aparam_as_mask": False, - "spin": None, - } - - @classmethod - def deserialize(cls, data: dict) -> "InvarFitting": - data = copy.deepcopy(data) - variables = data.pop("@variables") - nets = data.pop("nets") - obj = cls(**data) - for kk in variables.keys(): - obj[kk] = to_torch_tensor(variables[kk]) - obj.filter_layers = NetworkCollection.deserialize(nets) - return obj - - def _extend_f_avg_std(self, xx: torch.Tensor, nb: int) -> torch.Tensor: - return torch.tile(xx.view([1, self.numb_fparam]), [nb, 1]) - - def _extend_a_avg_std(self, xx: torch.Tensor, nb: int, nloc: int) -> torch.Tensor: - return torch.tile(xx.view([1, 1, self.numb_aparam]), [nb, nloc, 1]) - def forward( self, descriptor: torch.Tensor, @@ -306,90 +188,7 @@ def forward( ------- - `torch.Tensor`: Total energy with shape [nframes, natoms[0]]. """ - xx = descriptor - nf, nloc, nd = xx.shape - # NOTICE in tests/pt/test_model.py - # it happens that the user directly access the data memeber self.bias_atom_e - # and set it to a wrong shape! - self.bias_atom_e = self.bias_atom_e.view([self.ntypes, self.dim_out]) - # check input dim - if nd != self.dim_descrpt: - raise ValueError( - "get an input descriptor of dim {nd}," - "which is not consistent with {self.dim_descrpt}." - ) - # check fparam dim, concate to input descriptor - if self.numb_fparam > 0: - assert fparam is not None, "fparam should not be None" - assert self.fparam_avg is not None - assert self.fparam_inv_std is not None - if fparam.shape[-1] != self.numb_fparam: - raise ValueError( - "get an input fparam of dim {fparam.shape[-1]}, ", - "which is not consistent with {self.numb_fparam}.", - ) - fparam = fparam.view([nf, self.numb_fparam]) - nb, _ = fparam.shape - t_fparam_avg = self._extend_f_avg_std(self.fparam_avg, nb) - t_fparam_inv_std = self._extend_f_avg_std(self.fparam_inv_std, nb) - fparam = (fparam - t_fparam_avg) * t_fparam_inv_std - fparam = torch.tile(fparam.reshape([nf, 1, -1]), [1, nloc, 1]) - xx = torch.cat( - [xx, fparam], - dim=-1, - ) - # check aparam dim, concate to input descriptor - if self.numb_aparam > 0: - assert aparam is not None, "aparam should not be None" - assert self.aparam_avg is not None - assert self.aparam_inv_std is not None - if aparam.shape[-1] != self.numb_aparam: - raise ValueError( - "get an input aparam of dim {aparam.shape[-1]}, ", - "which is not consistent with {self.numb_aparam}.", - ) - aparam = aparam.view([nf, nloc, self.numb_aparam]) - nb, nloc, _ = aparam.shape - t_aparam_avg = self._extend_a_avg_std(self.aparam_avg, nb, nloc) - t_aparam_inv_std = self._extend_a_avg_std(self.aparam_inv_std, nb, nloc) - aparam = (aparam - t_aparam_avg) * t_aparam_inv_std - xx = torch.cat( - [xx, aparam], - dim=-1, - ) - - outs = torch.zeros(nf, nloc, self.dim_out) # jit assertion - if self.old_impl: - outs = torch.zeros_like(atype).unsqueeze(-1) # jit assertion - assert self.filter_layers_old is not None - if self.use_tebd: - atom_energy = self.filter_layers_old[0](xx) + self.bias_atom_e[ - atype - ].unsqueeze(-1) - outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] - else: - for type_i, filter_layer in enumerate(self.filter_layers_old): - mask = atype == type_i - atom_energy = filter_layer(xx) - atom_energy = atom_energy + self.bias_atom_e[type_i] - atom_energy = atom_energy * mask.unsqueeze(-1) - outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] - return {"energy": outs.to(env.GLOBAL_PT_FLOAT_PRECISION)} - else: - if self.use_tebd: - atom_energy = ( - self.filter_layers.networks[0](xx) + self.bias_atom_e[atype] - ) - outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] - else: - for type_i, ll in enumerate(self.filter_layers.networks): - mask = (atype == type_i).unsqueeze(-1) - mask = torch.tile(mask, (1, 1, self.dim_out)) - atom_energy = ll(xx) - atom_energy = atom_energy + self.bias_atom_e[type_i] - atom_energy = atom_energy * mask - outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] - return {self.var_name: outs.to(env.GLOBAL_PT_FLOAT_PRECISION)} + return self._forward_common(descriptor, atype, gr, g2, h2, fparam, aparam) @Fitting.register("ener") diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 360f545975..b2d8c875ce 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -1,5 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy import logging +from abc import ( + abstractmethod, +) from typing import ( Callable, List, @@ -10,14 +14,30 @@ import numpy as np import torch +from deepmd.dpmodel import ( + FittingOutputDef, + OutputVariableDef, +) +from deepmd.pt.model.network.mlp import ( + FittingNet, + NetworkCollection, +) +from deepmd.pt.model.network.network import ( + ResidualDeep, +) from deepmd.pt.model.task.base_fitting import ( BaseFitting, ) +from deepmd.pt.utils import ( + env, +) from deepmd.pt.utils.dataloader import ( DpLoaderSet, ) from deepmd.pt.utils.env import ( + DEFAULT_PRECISION, DEVICE, + PRECISION_DICT, ) from deepmd.pt.utils.plugin import ( Plugin, @@ -25,6 +45,13 @@ from deepmd.pt.utils.stat import ( make_stat_input, ) +from deepmd.pt.utils.utils import ( + to_numpy_array, + to_torch_tensor, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION +device = env.DEVICE log = logging.getLogger(__name__) @@ -316,3 +343,334 @@ def change_energy_bias( ) ) return None + + +class GeneralFitting(Fitting): + """Construct a general fitting net. + + Parameters + ---------- + var_name : str + The atomic property to fit, 'energy', 'dipole', and 'polar'. + ntypes : int + Element count. + dim_descrpt : int + Embedding width per atom. + dim_out : int + The output dimension of the fitting net. + neuron : List[int] + Number of neurons in each hidden layers of the fitting net. + bias_atom_e : torch.Tensor, optional + Average enery per atom for each element. + resnet_dt : bool + Using time-step in the ResNet construction. + numb_fparam : int + Number of frame parameters. + numb_aparam : int + Number of atomic parameters. + activation_function : str + Activation function. + precision : str + Numerical precision. + distinguish_types : bool + Neighbor list that distinguish different atomic types or not. + rcond : float, optional + The condition number for the regression of atomic energy. + seed : int, optional + Random seed. + """ + + def __init__( + self, + var_name: str, + ntypes: int, + dim_descrpt: int, + dim_out: int, + neuron: List[int] = [128, 128, 128], + bias_atom_e: Optional[torch.Tensor] = None, + resnet_dt: bool = True, + numb_fparam: int = 0, + numb_aparam: int = 0, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + distinguish_types: bool = False, + rcond: Optional[float] = None, + seed: Optional[int] = None, + **kwargs, + ): + super().__init__() + self.var_name = var_name + self.ntypes = ntypes + self.dim_descrpt = dim_descrpt + self.dim_out = dim_out + self.neuron = neuron + self.distinguish_types = distinguish_types + self.use_tebd = not self.distinguish_types + self.resnet_dt = resnet_dt + self.numb_fparam = numb_fparam + self.numb_aparam = numb_aparam + self.activation_function = activation_function + self.precision = precision + self.prec = PRECISION_DICT[self.precision] + self.rcond = rcond + + # init constants + if bias_atom_e is None: + bias_atom_e = np.zeros([self.ntypes, self.dim_out]) + bias_atom_e = torch.tensor(bias_atom_e, dtype=self.prec, device=device) + bias_atom_e = bias_atom_e.view([self.ntypes, self.dim_out]) + if not self.use_tebd: + assert self.ntypes == bias_atom_e.shape[0], "Element count mismatches!" + self.register_buffer("bias_atom_e", bias_atom_e) + + if self.numb_fparam > 0: + self.register_buffer( + "fparam_avg", + torch.zeros(self.numb_fparam, dtype=self.prec, device=device), + ) + self.register_buffer( + "fparam_inv_std", + torch.ones(self.numb_fparam, dtype=self.prec, device=device), + ) + else: + self.fparam_avg, self.fparam_inv_std = None, None + if self.numb_aparam > 0: + self.register_buffer( + "aparam_avg", + torch.zeros(self.numb_aparam, dtype=self.prec, device=device), + ) + self.register_buffer( + "aparam_inv_std", + torch.ones(self.numb_aparam, dtype=self.prec, device=device), + ) + else: + self.aparam_avg, self.aparam_inv_std = None, None + + in_dim = self.dim_descrpt + self.numb_fparam + self.numb_aparam + + self.old_impl = kwargs.get("old_impl", False) + net_dim_out = self._net_out_dim() + if self.old_impl: + filter_layers = [] + for type_i in range(self.ntypes): + bias_type = 0.0 + one = ResidualDeep( + type_i, + self.dim_descrpt, + self.neuron, + bias_type, + resnet_dt=self.resnet_dt, + ) + filter_layers.append(one) + self.filter_layers_old = torch.nn.ModuleList(filter_layers) + self.filter_layers = None + else: + self.filter_layers = NetworkCollection( + 1 if self.distinguish_types else 0, + self.ntypes, + network_type="fitting_network", + networks=[ + FittingNet( + in_dim, + net_dim_out, + self.neuron, + self.activation_function, + self.resnet_dt, + self.precision, + bias_out=True, + ) + for ii in range(self.ntypes if self.distinguish_types else 1) + ], + ) + self.filter_layers_old = None + + if seed is not None: + log.info("Set seed to %d in fitting net.", seed) + torch.manual_seed(seed) + + def serialize(self) -> dict: + """Serialize the fitting to dict.""" + return { + "var_name": self.var_name, + "ntypes": self.ntypes, + "dim_descrpt": self.dim_descrpt, + "dim_out": self.dim_out, + "neuron": self.neuron, + "resnet_dt": self.resnet_dt, + "numb_fparam": self.numb_fparam, + "numb_aparam": self.numb_aparam, + "activation_function": self.activation_function, + "precision": self.precision, + "distinguish_types": self.distinguish_types, + "nets": self.filter_layers.serialize(), + "rcond": self.rcond, + "@variables": { + "bias_atom_e": to_numpy_array(self.bias_atom_e), + "fparam_avg": to_numpy_array(self.fparam_avg), + "fparam_inv_std": to_numpy_array(self.fparam_inv_std), + "aparam_avg": to_numpy_array(self.aparam_avg), + "aparam_inv_std": to_numpy_array(self.aparam_inv_std), + }, + # "rcond": self.rcond , + # "tot_ener_zero": self.tot_ener_zero , + # "trainable": self.trainable , + # "atom_ener": self.atom_ener , + # "layer_name": self.layer_name , + # "use_aparam_as_mask": self.use_aparam_as_mask , + # "spin": self.spin , + ## NOTICE: not supported by far + "tot_ener_zero": False, + "trainable": True, + "atom_ener": None, + "layer_name": None, + "use_aparam_as_mask": False, + "spin": None, + } + + @classmethod + def deserialize(cls, data: dict) -> "GeneralFitting": + data = copy.deepcopy(data) + variables = data.pop("@variables") + nets = data.pop("nets") + obj = cls(**data) + for kk in variables.keys(): + obj[kk] = to_torch_tensor(variables[kk]) + obj.filter_layers = NetworkCollection.deserialize(nets) + return obj + + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this atomic model.""" + return self.numb_fparam + + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this atomic model.""" + return self.numb_aparam + + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return [] + + @abstractmethod + def _net_out_dim(self): + """Set the FittingNet output dim.""" + pass + + def output_def(self) -> FittingOutputDef: + return FittingOutputDef( + [ + OutputVariableDef( + self.var_name, + [self.dim_out], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ), + ] + ) + + def _extend_f_avg_std(self, xx: torch.Tensor, nb: int) -> torch.Tensor: + return torch.tile(xx.view([1, self.numb_fparam]), [nb, 1]) + + def _extend_a_avg_std(self, xx: torch.Tensor, nb: int, nloc: int) -> torch.Tensor: + return torch.tile(xx.view([1, 1, self.numb_aparam]), [nb, nloc, 1]) + + def _forward_common( + self, + descriptor: torch.Tensor, + atype: torch.Tensor, + gr: Optional[torch.Tensor] = None, + g2: Optional[torch.Tensor] = None, + h2: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + ): + xx = descriptor + nf, nloc, nd = xx.shape + + if nd != self.dim_descrpt: + raise ValueError( + "get an input descriptor of dim {nd}," + "which is not consistent with {self.dim_descrpt}." + ) + # check fparam dim, concate to input descriptor + if self.numb_fparam > 0: + assert fparam is not None, "fparam should not be None" + assert self.fparam_avg is not None + assert self.fparam_inv_std is not None + if fparam.shape[-1] != self.numb_fparam: + raise ValueError( + "get an input fparam of dim {fparam.shape[-1]}, ", + "which is not consistent with {self.numb_fparam}.", + ) + fparam = fparam.view([nf, self.numb_fparam]) + nb, _ = fparam.shape + t_fparam_avg = self._extend_f_avg_std(self.fparam_avg, nb) + t_fparam_inv_std = self._extend_f_avg_std(self.fparam_inv_std, nb) + fparam = (fparam - t_fparam_avg) * t_fparam_inv_std + fparam = torch.tile(fparam.reshape([nf, 1, -1]), [1, nloc, 1]) + xx = torch.cat( + [xx, fparam], + dim=-1, + ) + # check aparam dim, concate to input descriptor + if self.numb_aparam > 0: + assert aparam is not None, "aparam should not be None" + assert self.aparam_avg is not None + assert self.aparam_inv_std is not None + if aparam.shape[-1] != self.numb_aparam: + raise ValueError( + "get an input aparam of dim {aparam.shape[-1]}, ", + "which is not consistent with {self.numb_aparam}.", + ) + aparam = aparam.view([nf, nloc, self.numb_aparam]) + nb, nloc, _ = aparam.shape + t_aparam_avg = self._extend_a_avg_std(self.aparam_avg, nb, nloc) + t_aparam_inv_std = self._extend_a_avg_std(self.aparam_inv_std, nb, nloc) + aparam = (aparam - t_aparam_avg) * t_aparam_inv_std + xx = torch.cat( + [xx, aparam], + dim=-1, + ) + + outs = torch.zeros( + (nf, nloc, self.dim_out), + dtype=env.GLOBAL_PT_FLOAT_PRECISION, + device=env.DEVICE, + ) # jit assertion + if self.old_impl: + outs = torch.zeros_like(atype).unsqueeze(-1) # jit assertion + assert self.filter_layers_old is not None + if self.use_tebd: + atom_property = self.filter_layers_old[0](xx) + self.bias_atom_e[ + atype + ].unsqueeze(-1) + outs = outs + atom_property # Shape is [nframes, natoms[0], 1] + else: + for type_i, filter_layer in enumerate(self.filter_layers_old): + mask = atype == type_i + atom_property = filter_layer(xx) + atom_property = atom_property + self.bias_atom_e[type_i] + atom_property = atom_property * mask.unsqueeze(-1) + outs = outs + atom_property # Shape is [nframes, natoms[0], 1] + return {self.var_name: outs.to(env.GLOBAL_PT_FLOAT_PRECISION)} + else: + if self.use_tebd: + atom_property = ( + self.filter_layers.networks[0](xx) + self.bias_atom_e[atype] + ) + outs = outs + atom_property # Shape is [nframes, natoms[0], 1] + else: + net_dim_out = self._net_out_dim() + for type_i, ll in enumerate(self.filter_layers.networks): + mask = (atype == type_i).unsqueeze(-1) + mask = torch.tile(mask, (1, 1, net_dim_out)) + atom_property = ll(xx) + atom_property = atom_property + self.bias_atom_e[type_i] + atom_property = atom_property * mask + outs = outs + atom_property # Shape is [nframes, natoms[0], 1] + return {self.var_name: outs.to(env.GLOBAL_PT_FLOAT_PRECISION)} diff --git a/source/tests/pt/model/test_make_hessian_model.py b/source/tests/pt/model/test_make_hessian_model.py index 81aee758bf..6f321b6478 100644 --- a/source/tests/pt/model/test_make_hessian_model.py +++ b/source/tests/pt/model/test_make_hessian_model.py @@ -68,24 +68,28 @@ def test( natoms = self.nloc nf = self.nf nv = self.nv - cell0 = torch.rand([3, 3], dtype=dtype) - cell0 = 1.0 * (cell0 + cell0.T) + 5.0 * torch.eye(3) - cell1 = torch.rand([3, 3], dtype=dtype) - cell1 = 1.0 * (cell1 + cell1.T) + 5.0 * torch.eye(3) + cell0 = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) + cell0 = 1.0 * (cell0 + cell0.T) + 5.0 * torch.eye(3, device=env.DEVICE) + cell1 = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) + cell1 = 1.0 * (cell1 + cell1.T) + 5.0 * torch.eye(3, device=env.DEVICE) cell = torch.stack([cell0, cell1]) - coord = torch.rand([nf, natoms, 3], dtype=dtype) + coord = torch.rand([nf, natoms, 3], dtype=dtype, device=env.DEVICE) coord = torch.matmul(coord, cell) cell = cell.view([nf, 9]) coord = coord.view([nf, natoms * 3]) - atype = torch.stack( - [ - torch.IntTensor([0, 0, 1]), - torch.IntTensor([1, 0, 1]), - ] - ).view([nf, natoms]) + atype = ( + torch.stack( + [ + torch.IntTensor([0, 0, 1]), + torch.IntTensor([1, 0, 1]), + ] + ) + .view([nf, natoms]) + .to(env.DEVICE) + ) nfp, nap = 2, 3 - fparam = torch.rand([nf, nfp], dtype=dtype) - aparam = torch.rand([nf, natoms * nap], dtype=dtype) + fparam = torch.rand([nf, nfp], dtype=dtype, device=env.DEVICE) + aparam = torch.rand([nf, natoms * nap], dtype=dtype, device=env.DEVICE) # forward hess and valu models ret_dict0 = self.model_hess.forward_common( coord, atype, box=cell, fparam=fparam, aparam=aparam From 8f91aea627128baaa0d78bbd7c996a1a84ae02bd Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Fri, 16 Feb 2024 22:20:31 +0800 Subject: [PATCH 087/686] feat: dp and pt: implement exclude types in descriptor se_a (#3280) Co-authored-by: Han Wang --- deepmd/dpmodel/descriptor/exclude_mask.py | 78 ++++++++++++++++++ deepmd/dpmodel/descriptor/se_e2_a.py | 9 ++- deepmd/pt/model/descriptor/descriptor.py | 80 ++++++++++++++++++- deepmd/pt/model/descriptor/hybrid.py | 2 +- deepmd/pt/model/descriptor/repformers.py | 2 +- deepmd/pt/model/descriptor/se_a.py | 32 +++++--- deepmd/pt/model/descriptor/se_atten.py | 2 +- deepmd/tf/descriptor/se_a.py | 3 +- .../common/dpmodel/test_exclusion_mask.py | 34 ++++++++ .../consistent/descriptor/test_se_e2_a.py | 4 +- source/tests/pt/model/test_exclusion_mask.py | 45 +++++++++++ source/tests/pt/model/test_se_e2_a.py | 4 +- 12 files changed, 275 insertions(+), 20 deletions(-) create mode 100644 deepmd/dpmodel/descriptor/exclude_mask.py create mode 100644 source/tests/common/dpmodel/test_exclusion_mask.py create mode 100644 source/tests/pt/model/test_exclusion_mask.py diff --git a/deepmd/dpmodel/descriptor/exclude_mask.py b/deepmd/dpmodel/descriptor/exclude_mask.py new file mode 100644 index 0000000000..ee3edba434 --- /dev/null +++ b/deepmd/dpmodel/descriptor/exclude_mask.py @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + List, + Tuple, +) + +import numpy as np + + +class ExcludeMask: + """Computes the atom type exclusion mask.""" + + def __init__( + self, + ntypes: int, + exclude_types: List[Tuple[int, int]] = [], + ): + super().__init__() + self.ntypes = ntypes + self.exclude_types = set() + for tt in exclude_types: + assert len(tt) == 2 + self.exclude_types.add((tt[0], tt[1])) + self.exclude_types.add((tt[1], tt[0])) + # ntypes + 1 for nlist masks + self.type_mask = np.array( + [ + [ + 1 if (tt_i, tt_j) not in self.exclude_types else 0 + for tt_i in range(ntypes + 1) + ] + for tt_j in range(ntypes + 1) + ], + dtype=np.int32, + ) + # (ntypes+1 x ntypes+1) + self.type_mask = self.type_mask.reshape([-1]) + + def build_type_exclude_mask( + self, + nlist: np.ndarray, + atype_ext: np.ndarray, + ): + """Compute type exclusion mask. + + Parameters + ---------- + nlist + The neighbor list. shape: nf x nloc x nnei + atype_ext + The extended aotm types. shape: nf x nall + + Returns + ------- + mask + The type exclusion mask of shape: nf x nloc x nnei. + Element [ff,ii,jj] being 0 if type(ii), type(nlist[ff,ii,jj]) is excluded, + otherwise being 1. + + """ + if len(self.exclude_types) == 0: + # safely return 1 if nothing is excluded. + return np.ones_like(nlist, dtype=np.int32) + nf, nloc, nnei = nlist.shape + nall = atype_ext.shape[1] + # add virtual atom of type ntypes. nf x nall+1 + ae = np.concatenate( + [atype_ext, self.ntypes * np.ones([nf, 1], dtype=atype_ext.dtype)], axis=-1 + ) + type_i = atype_ext[:, :nloc].reshape(nf, nloc) * (self.ntypes + 1) + # nf x nloc x nnei + index = np.where(nlist == -1, nall, nlist).reshape(nf, nloc * nnei) + type_j = np.take_along_axis(ae, index, axis=1).reshape(nf, nloc, nnei) + type_ij = type_i[:, :, None] + type_j + # nf x (nloc x nnei) + type_ij = type_ij.reshape(nf, nloc * nnei) + mask = self.type_mask[type_ij].reshape(nf, nloc, nnei) + return mask diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index 78ff83a056..26258f4ac7 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -27,6 +27,9 @@ from .base_descriptor import ( BaseDescriptor, ) +from .exclude_mask import ( + ExcludeMask, +) class DescrptSeA(NativeOP, BaseDescriptor): @@ -140,8 +143,6 @@ def __init__( ## seed, uniform_seed, multi_task, not included. if not type_one_side: raise NotImplementedError("type_one_side == False not implemented") - if exclude_types != []: - raise NotImplementedError("exclude_types is not implemented") if spin is not None: raise NotImplementedError("spin is not implemented") @@ -159,6 +160,7 @@ def __init__( self.activation_function = activation_function self.precision = precision self.spin = spin + self.emask = ExcludeMask(self.ntypes, self.exclude_types) in_dim = 1 # not considiering type embedding self.embeddings = NetworkCollection( @@ -292,8 +294,11 @@ def call( ng = self.neuron[-1] gr = np.zeros([nf, nloc, ng, 4]) + exclude_mask = self.emask.build_type_exclude_mask(nlist, atype_ext) for tt in range(self.ntypes): + mm = exclude_mask[:, :, sec[tt] : sec[tt + 1]] tr = rr[:, :, sec[tt] : sec[tt + 1], :] + tr = tr * mm[:, :, :, None] ss = tr[..., 0:1] gg = self.cal_g(ss, tt) # nf x nloc x ng x 4 diff --git a/deepmd/pt/model/descriptor/descriptor.py b/deepmd/pt/model/descriptor/descriptor.py index 177f30d241..63dbe0eb19 100644 --- a/deepmd/pt/model/descriptor/descriptor.py +++ b/deepmd/pt/model/descriptor/descriptor.py @@ -8,6 +8,8 @@ Callable, List, Optional, + Set, + Tuple, Union, ) @@ -20,6 +22,9 @@ from deepmd.pt.utils.plugin import ( Plugin, ) +from deepmd.pt.utils.utils import ( + to_torch_tensor, +) from .base_descriptor import ( BaseDescriptor, @@ -206,6 +211,32 @@ class DescriptorBlock(torch.nn.Module, ABC): __plugins = Plugin() local_cluster = False + def __init__( + self, + ntypes: int, + exclude_types: List[Tuple[int, int]] = [], + ): + super().__init__() + _exclude_types: Set[Tuple[int, int]] = set() + for tt in exclude_types: + assert len(tt) == 2 + _exclude_types.add((tt[0], tt[1])) + _exclude_types.add((tt[1], tt[0])) + # ntypes + 1 for nlist masks + self.type_mask = np.array( + [ + [ + 1 if (tt_i, tt_j) not in _exclude_types else 0 + for tt_i in range(ntypes + 1) + ] + for tt_j in range(ntypes + 1) + ], + dtype=np.int32, + ) + # (ntypes+1 x ntypes+1) + self.type_mask = to_torch_tensor(self.type_mask).view([-1]) + self.no_exclusion = len(_exclude_types) == 0 + @staticmethod def register(key: str) -> Callable: """Register a DescriptorBlock plugin. @@ -332,7 +363,54 @@ def forward( mapping: Optional[torch.Tensor] = None, ): """Calculate DescriptorBlock.""" - raise NotImplementedError + pass + + # may have a better place for this method... + def build_type_exclude_mask( + self, + nlist: torch.Tensor, + atype_ext: torch.Tensor, + ) -> torch.Tensor: + """Compute type exclusion mask. + + Parameters + ---------- + nlist + The neighbor list. shape: nf x nloc x nnei + atype_ext + The extended aotm types. shape: nf x nall + + Returns + ------- + mask + The type exclusion mask of shape: nf x nloc x nnei. + Element [ff,ii,jj] being 0 if type(ii), type(nlist[ff,ii,jj]) is excluded, + otherwise being 1. + + """ + if self.no_exclusion: + # safely return 1 if nothing is excluded. + return torch.ones_like(nlist, dtype=torch.int32, device=nlist.device) + nf, nloc, nnei = nlist.shape + nall = atype_ext.shape[1] + # add virtual atom of type ntypes. nf x nall+1 + ae = torch.cat( + [ + atype_ext, + self.get_ntypes() + * torch.ones([nf, 1], dtype=atype_ext.dtype, device=atype_ext.device), + ], + dim=-1, + ) + type_i = atype_ext[:, :nloc].view(nf, nloc) * (self.get_ntypes() + 1) + # nf x nloc x nnei + index = torch.where(nlist == -1, nall, nlist).view(nf, nloc * nnei) + type_j = torch.gather(ae, 1, index).view(nf, nloc, nnei) + type_ij = type_i[:, :, None] + type_j + # nf x (nloc x nnei) + type_ij = type_ij.view(nf, nloc * nnei) + mask = self.type_mask[type_ij].view(nf, nloc, nnei) + return mask def compute_std(sumv2, sumv, sumn, rcut_r): diff --git a/deepmd/pt/model/descriptor/hybrid.py b/deepmd/pt/model/descriptor/hybrid.py index c5c08c760d..511ac5e79b 100644 --- a/deepmd/pt/model/descriptor/hybrid.py +++ b/deepmd/pt/model/descriptor/hybrid.py @@ -32,7 +32,7 @@ def __init__( - descriptor_list: list of descriptors. - descriptor_param: descriptor configs. """ - super().__init__() + super().__init__(ntypes) supported_descrpt = ["se_atten", "se_uni"] descriptor_list = [] for descriptor_param_item in list: diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py index 26467124b8..de2b5f3565 100644 --- a/deepmd/pt/model/descriptor/repformers.py +++ b/deepmd/pt/model/descriptor/repformers.py @@ -89,7 +89,7 @@ def __init__( whether or not add an type embedding to seq_input. If no seq_input is given, it has no effect. """ - super().__init__() + super().__init__(ntypes) del type self.epsilon = 1e-4 # protection of 1./nnei self.rcut = rcut diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index c722c2dc02..da391d3255 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -3,6 +3,7 @@ ClassVar, List, Optional, + Tuple, ) import numpy as np @@ -55,6 +56,7 @@ def __init__( activation_function: str = "tanh", precision: str = "float64", resnet_dt: bool = False, + exclude_types: List[Tuple[int, int]] = [], old_impl: bool = False, **kwargs, ): @@ -63,13 +65,14 @@ def __init__( rcut, rcut_smth, sel, - neuron, - axis_neuron, - set_davg_zero, - activation_function, - precision, - resnet_dt, - old_impl, + neuron=neuron, + axis_neuron=axis_neuron, + set_davg_zero=set_davg_zero, + activation_function=activation_function, + precision=precision, + resnet_dt=resnet_dt, + exclude_types=exclude_types, + old_impl=old_impl, **kwargs, ) @@ -212,6 +215,7 @@ def serialize(self) -> dict: "precision": RESERVED_PRECISON_DICT[obj.prec], "embeddings": obj.filter_layers.serialize(), "env_mat": DPEnvMat(obj.rcut, obj.rcut_smth).serialize(), + "exclude_types": obj.exclude_types, "@variables": { "davg": obj["davg"].detach().cpu().numpy(), "dstd": obj["dstd"].detach().cpu().numpy(), @@ -219,7 +223,6 @@ def serialize(self) -> dict: ## to be updated when the options are supported. "trainable": True, "type_one_side": True, - "exclude_types": [], "spin": None, } @@ -256,6 +259,7 @@ def __init__( activation_function: str = "tanh", precision: str = "float64", resnet_dt: bool = False, + exclude_types: List[Tuple[int, int]] = [], old_impl: bool = False, **kwargs, ): @@ -268,7 +272,7 @@ def __init__( - filter_neuron: Number of neurons in each hidden layers of the embedding net. - axis_neuron: Number of columns of the sub-matrix of the embedding matrix. """ - super().__init__() + super().__init__(len(sel), exclude_types=exclude_types) self.rcut = rcut self.rcut_smth = rcut_smth self.neuron = neuron @@ -280,8 +284,9 @@ def __init__( self.prec = PRECISION_DICT[self.precision] self.resnet_dt = resnet_dt self.old_impl = old_impl - + self.exclude_types = exclude_types self.ntypes = len(sel) + self.sel = sel self.sec = torch.tensor( np.append([0], np.cumsum(self.sel)), dtype=int, device=env.DEVICE @@ -522,9 +527,16 @@ def forward( xyz_scatter = torch.zeros( [nfnl, 4, self.filter_neuron[-1]], dtype=self.prec, device=env.DEVICE ) + # nfnl x nnei + exclude_mask = self.build_type_exclude_mask(nlist, extended_atype).view( + nfnl, -1 + ) for ii, ll in enumerate(self.filter_layers.networks): + # nfnl x nt + mm = exclude_mask[:, self.sec[ii] : self.sec[ii + 1]] # nfnl x nt x 4 rr = dmatrix[:, self.sec[ii] : self.sec[ii + 1], :] + rr = rr * mm[:, :, None] ss = rr[:, :, :1] # nfnl x nt x ng gg = ll.forward(ss) diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index d4dc0cd054..e1c9942d92 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -64,7 +64,7 @@ def __init__( - filter_neuron: Number of neurons in each hidden layers of the embedding net. - axis_neuron: Number of columns of the sub-matrix of the embedding matrix. """ - super().__init__() + super().__init__(ntypes) del type self.rcut = rcut self.rcut_smth = rcut_smth diff --git a/deepmd/tf/descriptor/se_a.py b/deepmd/tf/descriptor/se_a.py index 986328479b..0e0cb664a4 100644 --- a/deepmd/tf/descriptor/se_a.py +++ b/deepmd/tf/descriptor/se_a.py @@ -201,6 +201,7 @@ def __init__( self.activation_function_name = activation_function self.filter_precision = get_precision(precision) self.filter_np_precision = get_np_precision(precision) + self.orig_exclude_types = exclude_types self.exclude_types = set() for tt in exclude_types: assert len(tt) == 2 @@ -1425,7 +1426,7 @@ def serialize(self, suffix: str = "") -> dict: "resnet_dt": self.filter_resnet_dt, "trainable": self.trainable, "type_one_side": self.type_one_side, - "exclude_types": list(self.exclude_types), + "exclude_types": list(self.orig_exclude_types), "set_davg_zero": self.set_davg_zero, "activation_function": self.activation_function_name, "precision": self.filter_precision.name, diff --git a/source/tests/common/dpmodel/test_exclusion_mask.py b/source/tests/common/dpmodel/test_exclusion_mask.py new file mode 100644 index 0000000000..dc59c57776 --- /dev/null +++ b/source/tests/common/dpmodel/test_exclusion_mask.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np + +from deepmd.dpmodel.descriptor.exclude_mask import ( + ExcludeMask, +) + +from .case_single_frame_with_nlist import ( + TestCaseSingleFrameWithNlist, +) + + +# to be merged with the tf test case +class TestExcludeMask(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_build_type_exclude_mask(self): + exclude_types = [[0, 1]] + expected_mask = np.array( + [ + [1, 1, 1, 1, 1, 0, 1], + [1, 1, 1, 1, 1, 0, 1], + [0, 0, 1, 1, 1, 1, 1], + ] + ).reshape(self.nf, self.nloc, sum(self.sel)) + des = ExcludeMask(self.nt, exclude_types=exclude_types) + mask = des.build_type_exclude_mask( + self.nlist, + self.atype_ext, + ) + np.testing.assert_equal(mask, expected_mask) diff --git a/source/tests/consistent/descriptor/test_se_e2_a.py b/source/tests/consistent/descriptor/test_se_e2_a.py index a694a2a20c..a1f829aeea 100644 --- a/source/tests/consistent/descriptor/test_se_e2_a.py +++ b/source/tests/consistent/descriptor/test_se_e2_a.py @@ -67,7 +67,7 @@ def skip_pt(self) -> bool: type_one_side, excluded_types, ) = self.param - return not type_one_side or excluded_types != [] or CommonTest.skip_pt + return not type_one_side or CommonTest.skip_pt @property def skip_dp(self) -> bool: @@ -76,7 +76,7 @@ def skip_dp(self) -> bool: type_one_side, excluded_types, ) = self.param - return not type_one_side or excluded_types != [] or CommonTest.skip_dp + return not type_one_side or CommonTest.skip_dp tf_class = DescrptSeATF dp_class = DescrptSeADP diff --git a/source/tests/pt/model/test_exclusion_mask.py b/source/tests/pt/model/test_exclusion_mask.py new file mode 100644 index 0000000000..d624a8c178 --- /dev/null +++ b/source/tests/pt/model/test_exclusion_mask.py @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np + +from deepmd.pt.model.descriptor.se_a import ( + DescrptBlockSeA, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, + to_torch_tensor, +) + +from .test_env_mat import ( + TestCaseSingleFrameWithNlist, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION + + +# to be merged with the tf test case +class TestExcludeMask(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_build_type_exclude_mask(self): + exclude_types = [[0, 1]] + expected_mask = np.array( + [ + [1, 1, 1, 1, 1, 0, 1], + [1, 1, 1, 1, 1, 0, 1], + [0, 0, 1, 1, 1, 1, 1], + ] + ).reshape(self.nf, self.nloc, sum(self.sel)) + des = DescrptBlockSeA( + self.rcut, self.rcut_smth, self.sel, exclude_types=exclude_types + ).to(env.DEVICE) + mask = des.build_type_exclude_mask( + to_torch_tensor(self.nlist), + to_torch_tensor(self.atype_ext), + ) + np.testing.assert_equal(to_numpy_array(mask), expected_mask) diff --git a/source/tests/pt/model/test_se_e2_a.py b/source/tests/pt/model/test_se_e2_a.py index 520fdfcdfa..bb15bb423d 100644 --- a/source/tests/pt/model/test_se_e2_a.py +++ b/source/tests/pt/model/test_se_e2_a.py @@ -40,9 +40,10 @@ def test_consistency( dstd = rng.normal(size=(self.nt, nnei, 4)) dstd = 0.1 + np.abs(dstd) - for idt, prec in itertools.product( + for idt, prec, em in itertools.product( [False, True], ["float64", "float32"], + [[], [[0, 1]], [[1, 1]]], ): dtype = PRECISION_DICT[prec] rtol, atol = get_tols(prec) @@ -55,6 +56,7 @@ def test_consistency( precision=prec, resnet_dt=idt, old_impl=False, + exclude_mask=em, ).to(env.DEVICE) dd0.sea.mean = torch.tensor(davg, dtype=dtype, device=env.DEVICE) dd0.sea.dstd = torch.tensor(dstd, dtype=dtype, device=env.DEVICE) From f2b84ff89b98b2fd1f5546d5ba1bf22a7e270713 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Sat, 17 Feb 2024 22:30:47 +0800 Subject: [PATCH 088/686] feat: dp and pt: implement fitting exclude types (#3282) - implement fitting exclude types - pt: refactorize the pair exclusion masks as torch modules. --------- Co-authored-by: Han Wang --- deepmd/dpmodel/descriptor/se_e2_a.py | 6 +- deepmd/dpmodel/fitting/invar_fitting.py | 11 ++ deepmd/dpmodel/utils/__init__.py | 6 + .../{descriptor => utils}/exclude_mask.py | 49 ++++++- deepmd/pt/model/descriptor/descriptor.py | 78 ----------- deepmd/pt/model/descriptor/hybrid.py | 2 +- deepmd/pt/model/descriptor/repformers.py | 2 +- deepmd/pt/model/descriptor/se_a.py | 10 +- deepmd/pt/model/descriptor/se_atten.py | 2 +- deepmd/pt/model/task/ener.py | 2 + deepmd/pt/model/task/fitting.py | 16 ++- deepmd/pt/utils/__init__.py | 10 ++ deepmd/pt/utils/exclude_mask.py | 131 ++++++++++++++++++ .../common/dpmodel/test_exclusion_mask.py | 32 ++++- .../dpmodel/test_fitting_invar_fitting.py | 28 ++++ source/tests/pt/model/test_ener_fitting.py | 8 +- source/tests/pt/model/test_exclusion_mask.py | 38 +++-- 17 files changed, 320 insertions(+), 111 deletions(-) rename deepmd/dpmodel/{descriptor => utils}/exclude_mask.py (64%) create mode 100644 deepmd/pt/utils/exclude_mask.py diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index 26258f4ac7..4e26afa729 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -22,14 +22,12 @@ EmbeddingNet, EnvMat, NetworkCollection, + PairExcludeMask, ) from .base_descriptor import ( BaseDescriptor, ) -from .exclude_mask import ( - ExcludeMask, -) class DescrptSeA(NativeOP, BaseDescriptor): @@ -160,7 +158,7 @@ def __init__( self.activation_function = activation_function self.precision = precision self.spin = spin - self.emask = ExcludeMask(self.ntypes, self.exclude_types) + self.emask = PairExcludeMask(self.ntypes, self.exclude_types) in_dim = 1 # not considiering type embedding self.embeddings = NetworkCollection( diff --git a/deepmd/dpmodel/fitting/invar_fitting.py b/deepmd/dpmodel/fitting/invar_fitting.py index 58607a9f26..0c2a6006cc 100644 --- a/deepmd/dpmodel/fitting/invar_fitting.py +++ b/deepmd/dpmodel/fitting/invar_fitting.py @@ -19,6 +19,7 @@ fitting_check_output, ) from deepmd.dpmodel.utils import ( + AtomExcludeMask, FittingNet, NetworkCollection, ) @@ -126,6 +127,7 @@ def __init__( use_aparam_as_mask: bool = False, spin: Any = None, distinguish_types: bool = False, + exclude_types: List[int] = [], ): # seed, uniform_seed are not included if tot_ener_zero: @@ -159,8 +161,10 @@ def __init__( self.use_aparam_as_mask = use_aparam_as_mask self.spin = spin self.distinguish_types = distinguish_types + self.exclude_types = exclude_types if self.spin is not None: raise NotImplementedError("spin is not supported") + self.emask = AtomExcludeMask(self.ntypes, exclude_types=self.exclude_types) # init constants self.bias_atom_e = np.zeros([self.ntypes, self.dim_out]) @@ -260,6 +264,7 @@ def serialize(self) -> dict: "precision": self.precision, "distinguish_types": self.distinguish_types, "nets": self.nets.serialize(), + "exclude_types": self.exclude_types, "@variables": { "bias_atom_e": self.bias_atom_e, "fparam_avg": self.fparam_avg, @@ -370,4 +375,10 @@ def call( outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] else: outs = self.nets[()](xx) + self.bias_atom_e[atype] + + # nf x nloc + exclude_mask = self.emask.build_type_exclude_mask(atype) + # nf x nloc x nod + outs = outs * exclude_mask[:, :, None] + return {self.var_name: outs} diff --git a/deepmd/dpmodel/utils/__init__.py b/deepmd/dpmodel/utils/__init__.py index d3c31ae246..60a4486d52 100644 --- a/deepmd/dpmodel/utils/__init__.py +++ b/deepmd/dpmodel/utils/__init__.py @@ -2,6 +2,10 @@ from .env_mat import ( EnvMat, ) +from .exclude_mask import ( + AtomExcludeMask, + PairExcludeMask, +) from .network import ( EmbeddingNet, FittingNet, @@ -53,4 +57,6 @@ "inter2phys", "phys2inter", "to_face_distance", + "AtomExcludeMask", + "PairExcludeMask", ] diff --git a/deepmd/dpmodel/descriptor/exclude_mask.py b/deepmd/dpmodel/utils/exclude_mask.py similarity index 64% rename from deepmd/dpmodel/descriptor/exclude_mask.py rename to deepmd/dpmodel/utils/exclude_mask.py index ee3edba434..83e3c7a363 100644 --- a/deepmd/dpmodel/descriptor/exclude_mask.py +++ b/deepmd/dpmodel/utils/exclude_mask.py @@ -7,15 +7,54 @@ import numpy as np -class ExcludeMask: - """Computes the atom type exclusion mask.""" +class AtomExcludeMask: + """Computes the type exclusion mask for atoms.""" + + def __init__( + self, + ntypes: int, + exclude_types: List[int] = [], + ): + self.ntypes = ntypes + self.exclude_types = exclude_types + self.type_mask = np.array( + [1 if tt_i not in self.exclude_types else 0 for tt_i in range(ntypes)], + dtype=np.int32, + ) + # (ntypes) + self.type_mask = self.type_mask.reshape([-1]) + + def build_type_exclude_mask( + self, + atype: np.ndarray, + ): + """Compute type exclusion mask for atoms. + + Parameters + ---------- + atype + The extended aotm types. shape: nf x natom + + Returns + ------- + mask + The type exclusion mask for atoms. shape: nf x natom + Element [ff,ii] being 0 if type(ii) is excluded, + otherwise being 1. + + """ + nf, natom = atype.shape + return self.type_mask[atype].reshape(nf, natom) + + +class PairExcludeMask: + """Computes the type exclusion mask for atom pairs.""" def __init__( self, ntypes: int, exclude_types: List[Tuple[int, int]] = [], ): - super().__init__() self.ntypes = ntypes self.exclude_types = set() for tt in exclude_types: @@ -41,7 +80,7 @@ def build_type_exclude_mask( nlist: np.ndarray, atype_ext: np.ndarray, ): - """Compute type exclusion mask. + """Compute type exclusion mask for atom pairs. Parameters ---------- @@ -53,7 +92,7 @@ def build_type_exclude_mask( Returns ------- mask - The type exclusion mask of shape: nf x nloc x nnei. + The type exclusion mask for pair atoms of shape: nf x nloc x nnei. Element [ff,ii,jj] being 0 if type(ii), type(nlist[ff,ii,jj]) is excluded, otherwise being 1. diff --git a/deepmd/pt/model/descriptor/descriptor.py b/deepmd/pt/model/descriptor/descriptor.py index 63dbe0eb19..bd6839834e 100644 --- a/deepmd/pt/model/descriptor/descriptor.py +++ b/deepmd/pt/model/descriptor/descriptor.py @@ -8,8 +8,6 @@ Callable, List, Optional, - Set, - Tuple, Union, ) @@ -22,9 +20,6 @@ from deepmd.pt.utils.plugin import ( Plugin, ) -from deepmd.pt.utils.utils import ( - to_torch_tensor, -) from .base_descriptor import ( BaseDescriptor, @@ -211,32 +206,6 @@ class DescriptorBlock(torch.nn.Module, ABC): __plugins = Plugin() local_cluster = False - def __init__( - self, - ntypes: int, - exclude_types: List[Tuple[int, int]] = [], - ): - super().__init__() - _exclude_types: Set[Tuple[int, int]] = set() - for tt in exclude_types: - assert len(tt) == 2 - _exclude_types.add((tt[0], tt[1])) - _exclude_types.add((tt[1], tt[0])) - # ntypes + 1 for nlist masks - self.type_mask = np.array( - [ - [ - 1 if (tt_i, tt_j) not in _exclude_types else 0 - for tt_i in range(ntypes + 1) - ] - for tt_j in range(ntypes + 1) - ], - dtype=np.int32, - ) - # (ntypes+1 x ntypes+1) - self.type_mask = to_torch_tensor(self.type_mask).view([-1]) - self.no_exclusion = len(_exclude_types) == 0 - @staticmethod def register(key: str) -> Callable: """Register a DescriptorBlock plugin. @@ -365,53 +334,6 @@ def forward( """Calculate DescriptorBlock.""" pass - # may have a better place for this method... - def build_type_exclude_mask( - self, - nlist: torch.Tensor, - atype_ext: torch.Tensor, - ) -> torch.Tensor: - """Compute type exclusion mask. - - Parameters - ---------- - nlist - The neighbor list. shape: nf x nloc x nnei - atype_ext - The extended aotm types. shape: nf x nall - - Returns - ------- - mask - The type exclusion mask of shape: nf x nloc x nnei. - Element [ff,ii,jj] being 0 if type(ii), type(nlist[ff,ii,jj]) is excluded, - otherwise being 1. - - """ - if self.no_exclusion: - # safely return 1 if nothing is excluded. - return torch.ones_like(nlist, dtype=torch.int32, device=nlist.device) - nf, nloc, nnei = nlist.shape - nall = atype_ext.shape[1] - # add virtual atom of type ntypes. nf x nall+1 - ae = torch.cat( - [ - atype_ext, - self.get_ntypes() - * torch.ones([nf, 1], dtype=atype_ext.dtype, device=atype_ext.device), - ], - dim=-1, - ) - type_i = atype_ext[:, :nloc].view(nf, nloc) * (self.get_ntypes() + 1) - # nf x nloc x nnei - index = torch.where(nlist == -1, nall, nlist).view(nf, nloc * nnei) - type_j = torch.gather(ae, 1, index).view(nf, nloc, nnei) - type_ij = type_i[:, :, None] + type_j - # nf x (nloc x nnei) - type_ij = type_ij.view(nf, nloc * nnei) - mask = self.type_mask[type_ij].view(nf, nloc, nnei) - return mask - def compute_std(sumv2, sumv, sumn, rcut_r): """Compute standard deviation.""" diff --git a/deepmd/pt/model/descriptor/hybrid.py b/deepmd/pt/model/descriptor/hybrid.py index 511ac5e79b..c5c08c760d 100644 --- a/deepmd/pt/model/descriptor/hybrid.py +++ b/deepmd/pt/model/descriptor/hybrid.py @@ -32,7 +32,7 @@ def __init__( - descriptor_list: list of descriptors. - descriptor_param: descriptor configs. """ - super().__init__(ntypes) + super().__init__() supported_descrpt = ["se_atten", "se_uni"] descriptor_list = [] for descriptor_param_item in list: diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py index de2b5f3565..26467124b8 100644 --- a/deepmd/pt/model/descriptor/repformers.py +++ b/deepmd/pt/model/descriptor/repformers.py @@ -89,7 +89,7 @@ def __init__( whether or not add an type embedding to seq_input. If no seq_input is given, it has no effect. """ - super().__init__(ntypes) + super().__init__() del type self.epsilon = 1e-4 # protection of 1./nnei self.rcut = rcut diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index da391d3255..c086fe1cc2 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -38,6 +38,9 @@ from deepmd.pt.model.network.network import ( TypeFilter, ) +from deepmd.pt.utils.exclude_mask import ( + PairExcludeMask, +) from deepmd.pt.utils.nlist import ( extend_input_and_build_neighbor_list, ) @@ -272,7 +275,7 @@ def __init__( - filter_neuron: Number of neurons in each hidden layers of the embedding net. - axis_neuron: Number of columns of the sub-matrix of the embedding matrix. """ - super().__init__(len(sel), exclude_types=exclude_types) + super().__init__() self.rcut = rcut self.rcut_smth = rcut_smth self.neuron = neuron @@ -286,6 +289,7 @@ def __init__( self.old_impl = old_impl self.exclude_types = exclude_types self.ntypes = len(sel) + self.emask = PairExcludeMask(len(sel), exclude_types=exclude_types) self.sel = sel self.sec = torch.tensor( @@ -528,9 +532,7 @@ def forward( [nfnl, 4, self.filter_neuron[-1]], dtype=self.prec, device=env.DEVICE ) # nfnl x nnei - exclude_mask = self.build_type_exclude_mask(nlist, extended_atype).view( - nfnl, -1 - ) + exclude_mask = self.emask(nlist, extended_atype).view(nfnl, -1) for ii, ll in enumerate(self.filter_layers.networks): # nfnl x nt mm = exclude_mask[:, self.sec[ii] : self.sec[ii + 1]] diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index e1c9942d92..d4dc0cd054 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -64,7 +64,7 @@ def __init__( - filter_neuron: Number of neurons in each hidden layers of the embedding net. - axis_neuron: Number of columns of the sub-matrix of the embedding matrix. """ - super().__init__(ntypes) + super().__init__() del type self.rcut = rcut self.rcut_smth = rcut_smth diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index f1dad4c58d..b6ca12b9d8 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -89,6 +89,7 @@ def __init__( distinguish_types: bool = False, rcond: Optional[float] = None, seed: Optional[int] = None, + exclude_types: List[int] = [], **kwargs, ): super().__init__( @@ -106,6 +107,7 @@ def __init__( distinguish_types=distinguish_types, rcond=rcond, seed=seed, + exclude_types=exclude_types, **kwargs, ) diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index b2d8c875ce..db8daff802 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -39,6 +39,9 @@ DEVICE, PRECISION_DICT, ) +from deepmd.pt.utils.exclude_mask import ( + AtomExcludeMask, +) from deepmd.pt.utils.plugin import ( Plugin, ) @@ -396,6 +399,7 @@ def __init__( distinguish_types: bool = False, rcond: Optional[float] = None, seed: Optional[int] = None, + exclude_types: List[int] = [], **kwargs, ): super().__init__() @@ -413,6 +417,9 @@ def __init__( self.precision = precision self.prec = PRECISION_DICT[self.precision] self.rcond = rcond + self.exclude_types = exclude_types + + self.emask = AtomExcludeMask(self.ntypes, self.exclude_types) # init constants if bias_atom_e is None: @@ -504,6 +511,7 @@ def serialize(self) -> dict: "distinguish_types": self.distinguish_types, "nets": self.filter_layers.serialize(), "rcond": self.rcond, + "exclude_types": self.exclude_types, "@variables": { "bias_atom_e": to_numpy_array(self.bias_atom_e), "fparam_avg": to_numpy_array(self.fparam_avg), @@ -511,7 +519,6 @@ def serialize(self) -> dict: "aparam_avg": to_numpy_array(self.aparam_avg), "aparam_inv_std": to_numpy_array(self.aparam_inv_std), }, - # "rcond": self.rcond , # "tot_ener_zero": self.tot_ener_zero , # "trainable": self.trainable , # "atom_ener": self.atom_ener , @@ -657,7 +664,6 @@ def _forward_common( atom_property = atom_property + self.bias_atom_e[type_i] atom_property = atom_property * mask.unsqueeze(-1) outs = outs + atom_property # Shape is [nframes, natoms[0], 1] - return {self.var_name: outs.to(env.GLOBAL_PT_FLOAT_PRECISION)} else: if self.use_tebd: atom_property = ( @@ -673,4 +679,8 @@ def _forward_common( atom_property = atom_property + self.bias_atom_e[type_i] atom_property = atom_property * mask outs = outs + atom_property # Shape is [nframes, natoms[0], 1] - return {self.var_name: outs.to(env.GLOBAL_PT_FLOAT_PRECISION)} + # nf x nloc + mask = self.emask(atype) + # nf x nloc x nod + outs = outs * mask[:, :, None] + return {self.var_name: outs.to(env.GLOBAL_PT_FLOAT_PRECISION)} diff --git a/deepmd/pt/utils/__init__.py b/deepmd/pt/utils/__init__.py index 6ceb116d85..7e1043eda4 100644 --- a/deepmd/pt/utils/__init__.py +++ b/deepmd/pt/utils/__init__.py @@ -1 +1,11 @@ # SPDX-License-Identifier: LGPL-3.0-or-later + +from .exclude_mask import ( + AtomExcludeMask, + PairExcludeMask, +) + +__all__ = [ + "PairExcludeMask", + "AtomExcludeMask", +] diff --git a/deepmd/pt/utils/exclude_mask.py b/deepmd/pt/utils/exclude_mask.py new file mode 100644 index 0000000000..74b1d8dc41 --- /dev/null +++ b/deepmd/pt/utils/exclude_mask.py @@ -0,0 +1,131 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + List, + Set, + Tuple, +) + +import numpy as np +import torch + +from deepmd.pt.utils.utils import ( + to_torch_tensor, +) + + +class AtomExcludeMask(torch.nn.Module): + """Computes the type exclusion mask for atoms.""" + + def __init__( + self, + ntypes: int, + exclude_types: List[int] = [], + ): + super().__init__() + self.ntypes = ntypes + self.exclude_types = exclude_types + self.type_mask = np.array( + [1 if tt_i not in self.exclude_types else 0 for tt_i in range(ntypes)], + dtype=np.int32, + ) + self.type_mask = to_torch_tensor(self.type_mask).view([-1]) + + def forward( + self, + atype: torch.Tensor, + ) -> torch.Tensor: + """Compute type exclusion mask for atoms. + + Parameters + ---------- + atype + The extended aotm types. shape: nf x natom + + Returns + ------- + mask + The type exclusion mask for atoms. shape: nf x natom + Element [ff,ii] being 0 if type(ii) is excluded, + otherwise being 1. + + """ + nf, natom = atype.shape + return self.type_mask[atype].view(nf, natom) + + +class PairExcludeMask(torch.nn.Module): + """Computes the type exclusion mask for atom pairs.""" + + def __init__( + self, + ntypes: int, + exclude_types: List[Tuple[int, int]] = [], + ): + super().__init__() + self.ntypes = ntypes + self._exclude_types: Set[Tuple[int, int]] = set() + for tt in exclude_types: + assert len(tt) == 2 + self._exclude_types.add((tt[0], tt[1])) + self._exclude_types.add((tt[1], tt[0])) + # ntypes + 1 for nlist masks + self.type_mask = np.array( + [ + [ + 1 if (tt_i, tt_j) not in self._exclude_types else 0 + for tt_i in range(ntypes + 1) + ] + for tt_j in range(ntypes + 1) + ], + dtype=np.int32, + ) + # (ntypes+1 x ntypes+1) + self.type_mask = to_torch_tensor(self.type_mask).view([-1]) + self.no_exclusion = len(self._exclude_types) == 0 + + # may have a better place for this method... + def forward( + self, + nlist: torch.Tensor, + atype_ext: torch.Tensor, + ) -> torch.Tensor: + """Compute type exclusion mask. + + Parameters + ---------- + nlist + The neighbor list. shape: nf x nloc x nnei + atype_ext + The extended aotm types. shape: nf x nall + + Returns + ------- + mask + The type exclusion mask of shape: nf x nloc x nnei. + Element [ff,ii,jj] being 0 if type(ii), type(nlist[ff,ii,jj]) is excluded, + otherwise being 1. + + """ + if self.no_exclusion: + # safely return 1 if nothing is excluded. + return torch.ones_like(nlist, dtype=torch.int32, device=nlist.device) + nf, nloc, nnei = nlist.shape + nall = atype_ext.shape[1] + # add virtual atom of type ntypes. nf x nall+1 + ae = torch.cat( + [ + atype_ext, + self.ntypes + * torch.ones([nf, 1], dtype=atype_ext.dtype, device=atype_ext.device), + ], + dim=-1, + ) + type_i = atype_ext[:, :nloc].view(nf, nloc) * (self.ntypes + 1) + # nf x nloc x nnei + index = torch.where(nlist == -1, nall, nlist).view(nf, nloc * nnei) + type_j = torch.gather(ae, 1, index).view(nf, nloc, nnei) + type_ij = type_i[:, :, None] + type_j + # nf x (nloc x nnei) + type_ij = type_ij.view(nf, nloc * nnei) + mask = self.type_mask[type_ij].view(nf, nloc, nnei) + return mask diff --git a/source/tests/common/dpmodel/test_exclusion_mask.py b/source/tests/common/dpmodel/test_exclusion_mask.py index dc59c57776..89727ec6c3 100644 --- a/source/tests/common/dpmodel/test_exclusion_mask.py +++ b/source/tests/common/dpmodel/test_exclusion_mask.py @@ -3,8 +3,9 @@ import numpy as np -from deepmd.dpmodel.descriptor.exclude_mask import ( - ExcludeMask, +from deepmd.dpmodel.utils.exclude_mask import ( + AtomExcludeMask, + PairExcludeMask, ) from .case_single_frame_with_nlist import ( @@ -12,8 +13,31 @@ ) +class TestAtomExcludeMask(unittest.TestCase): + def test_build_type_exclude_mask(self): + nf = 2 + nt = 3 + exclude_types = [0, 2] + atype = np.array( + [ + [0, 2, 1, 2, 0, 1, 0], + [1, 2, 0, 0, 2, 2, 1], + ], + dtype=np.int32, + ).reshape([nf, -1]) + expected_mask = np.array( + [ + [0, 0, 1, 0, 0, 1, 0], + [1, 0, 0, 0, 0, 0, 1], + ] + ).reshape([nf, -1]) + des = AtomExcludeMask(nt, exclude_types=exclude_types) + mask = des.build_type_exclude_mask(atype) + np.testing.assert_equal(mask, expected_mask) + + # to be merged with the tf test case -class TestExcludeMask(unittest.TestCase, TestCaseSingleFrameWithNlist): +class TestPairExcludeMask(unittest.TestCase, TestCaseSingleFrameWithNlist): def setUp(self): TestCaseSingleFrameWithNlist.setUp(self) @@ -26,7 +50,7 @@ def test_build_type_exclude_mask(self): [0, 0, 1, 1, 1, 1, 1], ] ).reshape(self.nf, self.nloc, sum(self.sel)) - des = ExcludeMask(self.nt, exclude_types=exclude_types) + des = PairExcludeMask(self.nt, exclude_types=exclude_types) mask = des.build_type_exclude_mask( self.nlist, self.atype_ext, diff --git a/source/tests/common/dpmodel/test_fitting_invar_fitting.py b/source/tests/common/dpmodel/test_fitting_invar_fitting.py index ea70e98f7c..77d3b429ec 100644 --- a/source/tests/common/dpmodel/test_fitting_invar_fitting.py +++ b/source/tests/common/dpmodel/test_fitting_invar_fitting.py @@ -34,11 +34,13 @@ def test_self_consistency( od, nfp, nap, + et, ) in itertools.product( [True, False], [1, 2], [0, 3], [0, 4], + [[], [0], [1]], ): ifn0 = InvarFitting( "energy", @@ -48,6 +50,7 @@ def test_self_consistency( numb_fparam=nfp, numb_aparam=nap, distinguish_types=distinguish_types, + exclude_types=et, ) ifn1 = InvarFitting.deserialize(ifn0.serialize()) if nfp > 0: @@ -62,6 +65,31 @@ def test_self_consistency( ret1 = ifn1(dd[0], atype, fparam=ifp, aparam=iap) np.testing.assert_allclose(ret0["energy"], ret1["energy"]) + def test_mask(self): + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA(self.rcut, self.rcut_smth, self.sel) + dd = ds.call(self.coord_ext, self.atype_ext, self.nlist) + atype = self.atype_ext[:, :nloc] + od = 2 + distinguish_types = False + # exclude type 1 + et = [1] + ifn0 = InvarFitting( + "energy", + self.nt, + ds.dim_out, + od, + distinguish_types=distinguish_types, + exclude_types=et, + ) + ret0 = ifn0(dd[0], atype) + # atom index 2 is of type 1 that is excluded + zero_idx = 2 + np.testing.assert_allclose( + ret0["energy"][:, zero_idx, :], + np.zeros_like(ret0["energy"][:, zero_idx, :]), + ) + def test_self_exception( self, ): diff --git a/source/tests/pt/model/test_ener_fitting.py b/source/tests/pt/model/test_ener_fitting.py index 42aeeff16a..9e5ec0b903 100644 --- a/source/tests/pt/model/test_ener_fitting.py +++ b/source/tests/pt/model/test_ener_fitting.py @@ -44,11 +44,12 @@ def test_consistency( ) atype = torch.tensor(self.atype_ext[:, :nloc], dtype=int, device=env.DEVICE) - for od, distinguish_types, nfp, nap in itertools.product( + for od, distinguish_types, nfp, nap, et in itertools.product( [1, 3], [True, False], [0, 3], [0, 4], + [[], [0], [1]], ): ft0 = InvarFitting( "foo", @@ -58,6 +59,7 @@ def test_consistency( numb_fparam=nfp, numb_aparam=nap, use_tebd=(not distinguish_types), + exclude_types=et, ).to(env.DEVICE) ft1 = DPInvarFitting.deserialize(ft0.serialize()) ft2 = InvarFitting.deserialize(ft0.serialize()) @@ -144,11 +146,12 @@ def test_new_old( def test_jit( self, ): - for od, distinguish_types, nfp, nap in itertools.product( + for od, distinguish_types, nfp, nap, et in itertools.product( [1, 3], [True, False], [0, 3], [0, 4], + [[], [0]], ): ft0 = InvarFitting( "foo", @@ -158,6 +161,7 @@ def test_jit( numb_fparam=nfp, numb_aparam=nap, use_tebd=(not distinguish_types), + exclude_types=et, ).to(env.DEVICE) torch.jit.script(ft0) diff --git a/source/tests/pt/model/test_exclusion_mask.py b/source/tests/pt/model/test_exclusion_mask.py index d624a8c178..18ab56be49 100644 --- a/source/tests/pt/model/test_exclusion_mask.py +++ b/source/tests/pt/model/test_exclusion_mask.py @@ -3,12 +3,13 @@ import numpy as np -from deepmd.pt.model.descriptor.se_a import ( - DescrptBlockSeA, -) from deepmd.pt.utils import ( env, ) +from deepmd.pt.utils.exclude_mask import ( + AtomExcludeMask, + PairExcludeMask, +) from deepmd.pt.utils.utils import ( to_numpy_array, to_torch_tensor, @@ -21,8 +22,31 @@ dtype = env.GLOBAL_PT_FLOAT_PRECISION +class TestAtomExcludeMask(unittest.TestCase): + def test_build_type_exclude_mask(self): + nf = 2 + nt = 3 + exclude_types = [0, 2] + atype = np.array( + [ + [0, 2, 1, 2, 0, 1, 0], + [1, 2, 0, 0, 2, 2, 1], + ], + dtype=np.int32, + ).reshape([nf, -1]) + expected_mask = np.array( + [ + [0, 0, 1, 0, 0, 1, 0], + [1, 0, 0, 0, 0, 0, 1], + ] + ).reshape([nf, -1]) + des = AtomExcludeMask(nt, exclude_types=exclude_types) + mask = des(to_torch_tensor(atype)) + np.testing.assert_equal(to_numpy_array(mask), expected_mask) + + # to be merged with the tf test case -class TestExcludeMask(unittest.TestCase, TestCaseSingleFrameWithNlist): +class TestPairExcludeMask(unittest.TestCase, TestCaseSingleFrameWithNlist): def setUp(self): TestCaseSingleFrameWithNlist.setUp(self) @@ -35,10 +59,8 @@ def test_build_type_exclude_mask(self): [0, 0, 1, 1, 1, 1, 1], ] ).reshape(self.nf, self.nloc, sum(self.sel)) - des = DescrptBlockSeA( - self.rcut, self.rcut_smth, self.sel, exclude_types=exclude_types - ).to(env.DEVICE) - mask = des.build_type_exclude_mask( + des = PairExcludeMask(self.nt, exclude_types=exclude_types).to(env.DEVICE) + mask = des( to_torch_tensor(self.nlist), to_torch_tensor(self.atype_ext), ) From cd1a9579f5b700b5c103be0db871333a9de1935d Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 17 Feb 2024 09:32:45 -0500 Subject: [PATCH 089/686] improve gh actions (#3283) (1) add merge_group event; (2) cancel the previous run when a new one is triggered; (3) enable test_cuda for merge_group. Signed-off-by: Jinzhe Zeng --- .github/workflows/build_cc.yml | 4 ++++ .github/workflows/build_wheel.yml | 5 +++++ .github/workflows/codeql.yml | 4 +++- .github/workflows/labeler.yml | 2 +- .github/workflows/package_c.yml | 5 ++++- .github/workflows/test_cc.yml | 4 ++++ .github/workflows/test_cuda.yml | 18 +++++++++++++++++- .github/workflows/test_python.yml | 6 ++++-- 8 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_cc.yml b/.github/workflows/build_cc.yml index 991be798aa..3ed939bbc5 100644 --- a/.github/workflows/build_cc.yml +++ b/.github/workflows/build_cc.yml @@ -1,6 +1,10 @@ on: push: pull_request: + merge_group: +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true name: Build C++ jobs: buildcc: diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 3795a5ccce..cd89a8bb39 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -3,6 +3,11 @@ name: Build and upload to PyPI on: push: pull_request: + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true jobs: determine-arm64-runner: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c5460109f4..094ca06df5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -5,7 +5,9 @@ on: pull_request: schedule: - cron: '45 2 * * 2' - +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true jobs: analyze: name: Analyze diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 877c780f1f..be43c5cff2 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -11,4 +11,4 @@ jobs: steps: - uses: actions/labeler@v5 with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/package_c.yml b/.github/workflows/package_c.yml index 5594c79181..f45b5f4aef 100644 --- a/.github/workflows/package_c.yml +++ b/.github/workflows/package_c.yml @@ -3,7 +3,10 @@ name: Build C library on: push: pull_request: - + merge_group: +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true jobs: build_c: name: Build C library diff --git a/.github/workflows/test_cc.yml b/.github/workflows/test_cc.yml index f2af6f45ae..37f5cc2ef6 100644 --- a/.github/workflows/test_cc.yml +++ b/.github/workflows/test_cc.yml @@ -1,6 +1,10 @@ on: push: pull_request: + merge_group: +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true name: Test C++ jobs: testcc: diff --git a/.github/workflows/test_cuda.yml b/.github/workflows/test_cuda.yml index 7c08e50912..06af746ad4 100644 --- a/.github/workflows/test_cuda.yml +++ b/.github/workflows/test_cuda.yml @@ -4,6 +4,12 @@ on: pull_request: types: - "labeled" + # to let the PR pass the test + - "synchronize" + merge_group: +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true name: Test CUDA jobs: test_cuda: @@ -13,7 +19,7 @@ jobs: container: image: nvidia/cuda:12.2.0-devel-ubuntu22.04 options: --gpus all - if: github.repository_owner == 'deepmodeling' && github.event.label.name == 'Test CUDA' || github.event_name == 'workflow_dispatch' + if: github.repository_owner == 'deepmodeling' && (github.event_name == 'pull_request' && github.event.label && github.event.label.name == 'Test CUDA' || github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') steps: - name: Make sudo and git work run: apt-get update && apt-get install -y sudo git @@ -69,3 +75,13 @@ jobs: - uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + pass: + name: Pass testing on CUDA + needs: [test_cuda] + runs-on: ubuntu-latest + if: always() + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index e1ac3a716c..0e51a7e2c3 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -1,6 +1,10 @@ on: push: pull_request: + merge_group: +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true name: Test Python jobs: testpython: @@ -27,8 +31,6 @@ jobs: mpi: openmpi # https://github.com/pypa/pip/issues/11770 - run: python -m pip install -U "pip>=21.3.1,!=23.0.0" - - run: python -m pip install -U "torch==${{ matrix.torch }}" "numpy<1.20" - if: matrix.torch != '' - run: pip install -e .[cpu,test,torch] env: TENSORFLOW_VERSION: ${{ matrix.tf }} From 91e70227d7de74a4862fe51f93c976ff686237fc Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 17 Feb 2024 12:50:26 -0500 Subject: [PATCH 090/686] speed up cuda test (#3284) Signed-off-by: Jinzhe Zeng --- .github/workflows/test_cuda.yml | 23 +++++++++-------------- source/tests/tf/test_tabulate.py | 4 ++-- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test_cuda.yml b/.github/workflows/test_cuda.yml index 06af746ad4..59613cc291 100644 --- a/.github/workflows/test_cuda.yml +++ b/.github/workflows/test_cuda.yml @@ -17,7 +17,7 @@ jobs: runs-on: nvidia # https://github.com/deepmodeling/deepmd-kit/pull/2884#issuecomment-1744216845 container: - image: nvidia/cuda:12.2.0-devel-ubuntu22.04 + image: nvidia/cuda:12.3.1-devel-ubuntu22.04 options: --gpus all if: github.repository_owner == 'deepmodeling' && (github.event_name == 'pull_request' && github.event.label && github.event.label.name == 'Test CUDA' || github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') steps: @@ -33,24 +33,24 @@ jobs: with: mpi: mpich - uses: lukka/get-cmake@latest + with: + useLocalCache: true + useCloudCache: false - run: | wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.0-1_all.deb \ && sudo dpkg -i cuda-keyring_1.0-1_all.deb \ && sudo apt-get update \ - && sudo apt-get -y install cuda-12-2 libcudnn8=8.9.5.*-1+cuda12.2 + && sudo apt-get -y install cuda-12-3 libcudnn8=8.9.5.*-1+cuda12.3 if: false # skip as we use nvidia image - - name: Set PyPI mirror for Aliyun cloud machine - run: python -m pip config --user set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/ - run: python -m pip install -U "pip>=21.3.1,!=23.0.0" - run: python -m pip install "tensorflow>=2.15.0rc0" "torch>=2.2.0" - run: python -m pip install -v -e .[gpu,test,lmp,cu12,torch] "ase @ https://gitlab.com/ase/ase/-/archive/8c5aa5fd6448c5cfb517a014dccf2b214a9dfa8f/ase-8c5aa5fd6448c5cfb517a014dccf2b214a9dfa8f.tar.gz" env: - DP_BUILD_TESTING: 1 DP_VARIANT: cuda - CUDA_PATH: /usr/local/cuda-12.2 NUM_WORKERS: 0 + DP_ENABLE_NATIVE_OPTIMIZATION: 1 - run: dp --version - - run: python -m pytest --cov=deepmd source/tests --durations=0 + - run: python -m pytest source/tests --durations=0 - run: source/install/test_cc_local.sh env: OMP_NUM_THREADS: 1 @@ -60,21 +60,16 @@ jobs: CMAKE_GENERATOR: Ninja DP_VARIANT: cuda DP_USE_MPICH2: 1 - CUDA_PATH: /usr/local/cuda-12.2 - run: | export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/dp_test/lib:$CUDA_PATH/lib64:$LD_LIBRARY_PATH export PATH=$GITHUB_WORKSPACE/dp_test/bin:$PATH - python -m pytest --cov=deepmd source/lmp/tests - python -m pytest --cov=deepmd source/ipi/tests + python -m pytest source/lmp/tests + python -m pytest source/ipi/tests env: OMP_NUM_THREADS: 1 TF_INTRA_OP_PARALLELISM_THREADS: 1 TF_INTER_OP_PARALLELISM_THREADS: 1 LAMMPS_PLUGIN_PATH: ${{ github.workspace }}/dp_test/lib/deepmd_lmp - CUDA_PATH: /usr/local/cuda-12.2 - - uses: codecov/codecov-action@v4 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} pass: name: Pass testing on CUDA needs: [test_cuda] diff --git a/source/tests/tf/test_tabulate.py b/source/tests/tf/test_tabulate.py index 2ffb5e19c6..0d46293b62 100644 --- a/source/tests/tf/test_tabulate.py +++ b/source/tests/tf/test_tabulate.py @@ -58,7 +58,7 @@ def test_op_tanh(self): ] ) - places = 18 + places = 15 np.testing.assert_almost_equal(dy_array, answer, places) def test_op_gelu(self): @@ -104,7 +104,7 @@ def test_op_gelu(self): ] ) - places = 18 + places = 15 np.testing.assert_almost_equal(dy_array, answer, places) From 9053caf76c0ee73afe4ad97ef3a28ecdeb529930 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 17 Feb 2024 13:17:29 -0500 Subject: [PATCH 091/686] fix gh actions issues (#3288) Signed-off-by: Jinzhe Zeng --- .github/workflows/build_cc.yml | 2 ++ .github/workflows/build_wheel.yml | 2 ++ .github/workflows/codeql.yml | 2 ++ .github/workflows/package_c.yml | 2 ++ .github/workflows/test_cc.yml | 2 ++ .github/workflows/test_cuda.yml | 2 ++ .github/workflows/test_python.yml | 2 ++ 7 files changed, 14 insertions(+) diff --git a/.github/workflows/build_cc.yml b/.github/workflows/build_cc.yml index 3ed939bbc5..e85742cd7e 100644 --- a/.github/workflows/build_cc.yml +++ b/.github/workflows/build_cc.yml @@ -1,5 +1,7 @@ on: push: + branches-ignore: + - "gh-readonly-queue/*" pull_request: merge_group: concurrency: diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index cd89a8bb39..b8e8256b3c 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -2,6 +2,8 @@ name: Build and upload to PyPI on: push: + branches-ignore: + - "gh-readonly-queue/*" pull_request: merge_group: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 094ca06df5..babed2b937 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,6 +2,8 @@ name: "CodeQL" on: push: + branches-ignore: + - "gh-readonly-queue/*" pull_request: schedule: - cron: '45 2 * * 2' diff --git a/.github/workflows/package_c.yml b/.github/workflows/package_c.yml index f45b5f4aef..a9d4606a22 100644 --- a/.github/workflows/package_c.yml +++ b/.github/workflows/package_c.yml @@ -2,6 +2,8 @@ name: Build C library on: push: + branches-ignore: + - "gh-readonly-queue/*" pull_request: merge_group: concurrency: diff --git a/.github/workflows/test_cc.yml b/.github/workflows/test_cc.yml index 37f5cc2ef6..6a2865712f 100644 --- a/.github/workflows/test_cc.yml +++ b/.github/workflows/test_cc.yml @@ -1,5 +1,7 @@ on: push: + branches-ignore: + - "gh-readonly-queue/*" pull_request: merge_group: concurrency: diff --git a/.github/workflows/test_cuda.yml b/.github/workflows/test_cuda.yml index 59613cc291..26e6fabcfe 100644 --- a/.github/workflows/test_cuda.yml +++ b/.github/workflows/test_cuda.yml @@ -5,6 +5,7 @@ on: types: - "labeled" # to let the PR pass the test + - "created" - "synchronize" merge_group: concurrency: @@ -80,3 +81,4 @@ jobs: uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} + allowed-skips: test_cuda diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index 0e51a7e2c3..e6081305e4 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -1,5 +1,7 @@ on: push: + branches-ignore: + - "gh-readonly-queue/*" pull_request: merge_group: concurrency: From c55ceacf89c811f14ba6f3f164fd929cfa86425a Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 18 Feb 2024 07:46:02 -0500 Subject: [PATCH 092/686] pt: refactor data stat (#3285) Signed-off-by: Jinzhe Zeng --- deepmd/common.py | 14 ++ .../descriptor/make_base_descriptor.py | 12 +- deepmd/dpmodel/descriptor/se_e2_a.py | 10 +- deepmd/pt/entrypoints/main.py | 36 ++- .../pt/model/atomic_model/dp_atomic_model.py | 41 ++-- deepmd/pt/model/descriptor/__init__.py | 2 - deepmd/pt/model/descriptor/descriptor.py | 166 ++------------ deepmd/pt/model/descriptor/dpa1.py | 26 +-- deepmd/pt/model/descriptor/dpa2.py | 69 +----- deepmd/pt/model/descriptor/gaussian_lcc.py | 13 +- deepmd/pt/model/descriptor/hybrid.py | 34 +-- deepmd/pt/model/descriptor/repformers.py | 124 ++-------- deepmd/pt/model/descriptor/se_a.py | 136 +++-------- deepmd/pt/model/descriptor/se_atten.py | 119 ++-------- deepmd/pt/model/model/model.py | 16 +- deepmd/pt/model/task/ener.py | 37 +-- deepmd/pt/model/task/fitting.py | 94 -------- deepmd/pt/train/training.py | 3 +- deepmd/pt/utils/env_mat_stat.py | 206 +++++++++++++++++ deepmd/pt/utils/stat.py | 30 --- deepmd/utils/env_mat_stat.py | 213 ++++++++++++++++++ deepmd/utils/path.py | 150 ++++++++++-- source/tests/pt/test_stat.py | 153 ++++++++++--- 23 files changed, 879 insertions(+), 825 deletions(-) create mode 100644 deepmd/pt/utils/env_mat_stat.py create mode 100644 deepmd/utils/env_mat_stat.py diff --git a/deepmd/common.py b/deepmd/common.py index 05d02234b4..691cc262df 100644 --- a/deepmd/common.py +++ b/deepmd/common.py @@ -5,6 +5,9 @@ import platform import shutil import warnings +from hashlib import ( + sha1, +) from pathlib import ( Path, ) @@ -299,3 +302,14 @@ def symlink_prefix_files(old_prefix: str, new_prefix: str): os.symlink(os.path.relpath(ori_ff, os.path.dirname(new_ff)), new_ff) else: shutil.copyfile(ori_ff, new_ff) + + +def get_hash(obj) -> str: + """Get hash of object. + + Parameters + ---------- + obj + object to hash + """ + return sha1(json.dumps(obj).encode("utf-8")).hexdigest() diff --git a/deepmd/dpmodel/descriptor/make_base_descriptor.py b/deepmd/dpmodel/descriptor/make_base_descriptor.py index 29d3ad6d92..b7a8bfebcf 100644 --- a/deepmd/dpmodel/descriptor/make_base_descriptor.py +++ b/deepmd/dpmodel/descriptor/make_base_descriptor.py @@ -9,6 +9,10 @@ Optional, ) +from deepmd.utils.path import ( + DPPath, +) + def make_base_descriptor( t_tensor, @@ -69,14 +73,12 @@ def distinguish_types(self) -> bool: """ pass - def compute_input_stats(self, merged): + def compute_input_stats( + self, merged: List[dict], path: Optional[DPPath] = None + ): """Update mean and stddev for descriptor elements.""" raise NotImplementedError - def init_desc_stat(self, **kwargs): - """Initialize the model bias by the statistics.""" - raise NotImplementedError - @abstractmethod def fwd( self, diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index 4e26afa729..3b98f9dc67 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -1,6 +1,10 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np +from deepmd.utils.path import ( + DPPath, +) + try: from deepmd._version import version as __version__ except ImportError: @@ -229,14 +233,10 @@ def get_ntypes(self) -> int: """Returns the number of element types.""" return self.ntypes - def compute_input_stats(self, merged): + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): """Update mean and stddev for descriptor elements.""" raise NotImplementedError - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2): - """Initialize the model bias by the statistics.""" - raise NotImplementedError - def cal_g( self, ss, diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 29ef8761ff..b260000d87 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -12,6 +12,7 @@ Union, ) +import h5py import torch import torch.distributed as dist import torch.version @@ -46,12 +47,6 @@ from deepmd.pt.infer import ( inference, ) -from deepmd.pt.model.descriptor import ( - Descriptor, -) -from deepmd.pt.model.task import ( - Fitting, -) from deepmd.pt.train import ( training, ) @@ -69,7 +64,9 @@ ) from deepmd.pt.utils.stat import ( make_stat_input, - process_stat_path, +) +from deepmd.utils.path import ( + DPPath, ) from deepmd.utils.summary import SummaryPrinter as BaseSummaryPrinter @@ -134,19 +131,16 @@ def prepare_trainer_input_single( # noise_settings = None # stat files - hybrid_descrpt = model_params_single["descriptor"]["type"] == "hybrid" - if not hybrid_descrpt: - stat_file_path_single, has_stat_file_path = process_stat_path( - data_dict_single.get("stat_file", None), - data_dict_single.get("stat_file_dir", f"stat_files{suffix}"), - model_params_single, - Descriptor, - Fitting, - ) - else: ### TODO hybrid descriptor not implemented - raise NotImplementedError( - "data stat for hybrid descriptor is not implemented!" - ) + stat_file_path_single = data_dict_single.get("stat_file", None) + if stat_file_path_single is not None: + if Path(stat_file_path_single).is_dir(): + raise ValueError( + f"stat_file should be a file, not a directory: {stat_file_path_single}" + ) + if not Path(stat_file_path_single).is_file(): + with h5py.File(stat_file_path_single, "w") as f: + pass + stat_file_path_single = DPPath(stat_file_path_single, "a") # validation and training data validation_data_single = DpLoaderSet( @@ -156,7 +150,7 @@ def prepare_trainer_input_single( type_split=type_split, noise_settings=noise_settings, ) - if ckpt or finetune_model or has_stat_file_path: + if ckpt or finetune_model: train_data_single = DpLoaderSet( training_systems, training_dataset_params["batch_size"], diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index 17b70e4701..aafd2831b3 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -1,13 +1,11 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import copy import logging -import os import sys from typing import ( Dict, List, Optional, - Union, ) import torch @@ -24,6 +22,9 @@ from deepmd.pt.utils.utils import ( dict_to_device, ) +from deepmd.utils.path import ( + DPPath, +) from .base_atomic_model import ( BaseAtomicModel, @@ -160,9 +161,8 @@ def forward_atomic( def compute_or_load_stat( self, - type_map: Optional[List[str]] = None, - sampled=None, - stat_file_path_dict: Optional[Dict[str, Union[str, List[str]]]] = None, + sampled, + stat_file_path: Optional[DPPath] = None, ): """ Compute or load the statistics parameters of the model, @@ -174,31 +174,22 @@ def compute_or_load_stat( Parameters ---------- - type_map - Mapping atom type to the name (str) of the type. - For example `type_map[1]` gives the name of the type 1. sampled The sampled data frames from different data systems. - stat_file_path_dict + stat_file_path The dictionary of paths to the statistics files. """ - if sampled is not None: # move data to device - for data_sys in sampled: - dict_to_device(data_sys) - if stat_file_path_dict is not None: - if not isinstance(stat_file_path_dict["descriptor"], list): - stat_file_dir = os.path.dirname(stat_file_path_dict["descriptor"]) - else: - stat_file_dir = os.path.dirname(stat_file_path_dict["descriptor"][0]) - if not os.path.exists(stat_file_dir): - os.mkdir(stat_file_dir) - self.descriptor.compute_or_load_stat( - type_map, sampled, stat_file_path_dict["descriptor"] - ) + if stat_file_path is not None and self.type_map is not None: + # descriptors and fitting net with different type_map + # should not share the same parameters + stat_file_path /= " ".join(self.type_map) + for data_sys in sampled: + dict_to_device(data_sys) + if sampled is None: + sampled = [] + self.descriptor.compute_input_stats(sampled, stat_file_path) if self.fitting_net is not None: - self.fitting_net.compute_or_load_stat( - type_map, sampled, stat_file_path_dict["fitting_net"] - ) + self.fitting_net.compute_output_stats(sampled, stat_file_path) @torch.jit.export def get_dim_fparam(self) -> int: diff --git a/deepmd/pt/model/descriptor/__init__.py b/deepmd/pt/model/descriptor/__init__.py index 4252e34905..1c2e943369 100644 --- a/deepmd/pt/model/descriptor/__init__.py +++ b/deepmd/pt/model/descriptor/__init__.py @@ -2,7 +2,6 @@ from .descriptor import ( Descriptor, DescriptorBlock, - compute_std, make_default_type_embedding, ) from .dpa1 import ( @@ -32,7 +31,6 @@ __all__ = [ "Descriptor", "DescriptorBlock", - "compute_std", "make_default_type_embedding", "DescrptBlockSeA", "DescrptBlockSeAtten", diff --git a/deepmd/pt/model/descriptor/descriptor.py b/deepmd/pt/model/descriptor/descriptor.py index bd6839834e..16659e444d 100644 --- a/deepmd/pt/model/descriptor/descriptor.py +++ b/deepmd/pt/model/descriptor/descriptor.py @@ -6,20 +6,31 @@ ) from typing import ( Callable, + Dict, List, Optional, - Union, ) -import numpy as np import torch from deepmd.pt.model.network.network import ( TypeEmbedNet, ) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.env_mat_stat import ( + EnvMatStatSeA, +) from deepmd.pt.utils.plugin import ( Plugin, ) +from deepmd.utils.env_mat_stat import ( + StatItem, +) +from deepmd.utils.path import ( + DPPath, +) from .base_descriptor import ( BaseDescriptor, @@ -59,19 +70,6 @@ class SomeDescript(Descriptor): """ return Descriptor.__plugins.register(key) - @classmethod - def get_stat_name(cls, ntypes, type_name, **kwargs): - """ - Get the name for the statistic file of the descriptor. - Usually use the combination of descriptor name, rcut, rcut_smth and sel as the statistic file name. - """ - if cls is not Descriptor: - raise NotImplementedError("get_stat_name is not implemented!") - descrpt_type = type_name - return Descriptor.__plugins.plugins[descrpt_type].get_stat_name( - ntypes, type_name, **kwargs - ) - @classmethod def get_data_process_key(cls, config): """ @@ -92,98 +90,6 @@ def data_stat_key(self): """ raise NotImplementedError("data_stat_key is not implemented!") - def compute_or_load_stat( - self, - type_map: List[str], - sampled=None, - stat_file_path: Optional[Union[str, List[str]]] = None, - ): - """ - Compute or load the statistics parameters of the descriptor. - Calculate and save the mean and standard deviation of the descriptor to `stat_file_path` - if `sampled` is not None, otherwise load them from `stat_file_path`. - - Parameters - ---------- - type_map - Mapping atom type to the name (str) of the type. - For example `type_map[1]` gives the name of the type 1. - sampled - The sampled data frames from different data systems. - stat_file_path - The path to the statistics files. - """ - # TODO support hybrid descriptor - descrpt_stat_key = self.data_stat_key - if sampled is not None: # compute the statistics results - tmp_dict = self.compute_input_stats(sampled) - result_dict = {key: tmp_dict[key] for key in descrpt_stat_key} - result_dict["type_map"] = type_map - if stat_file_path is not None: - self.save_stats(result_dict, stat_file_path) - else: # load the statistics results - assert stat_file_path is not None, "No stat file to load!" - result_dict = self.load_stats(type_map, stat_file_path) - self.init_desc_stat(**result_dict) - - def save_stats(self, result_dict, stat_file_path: Union[str, List[str]]): - """ - Save the statistics results to `stat_file_path`. - - Parameters - ---------- - result_dict - The dictionary of statistics results. - stat_file_path - The path to the statistics file(s). - """ - if not isinstance(stat_file_path, list): - log.info(f"Saving stat file to {stat_file_path}") - np.savez_compressed(stat_file_path, **result_dict) - else: # TODO hybrid descriptor not implemented - raise NotImplementedError( - "save_stats for hybrid descriptor is not implemented!" - ) - - def load_stats(self, type_map, stat_file_path: Union[str, List[str]]): - """ - Load the statistics results to `stat_file_path`. - - Parameters - ---------- - type_map - Mapping atom type to the name (str) of the type. - For example `type_map[1]` gives the name of the type 1. - stat_file_path - The path to the statistics file(s). - - Returns - ------- - result_dict - The dictionary of statistics results. - """ - descrpt_stat_key = self.data_stat_key - target_type_map = type_map - if not isinstance(stat_file_path, list): - log.info(f"Loading stat file from {stat_file_path}") - stats = np.load(stat_file_path) - stat_type_map = list(stats["type_map"]) - missing_type = [i for i in target_type_map if i not in stat_type_map] - assert not missing_type, ( - f"These type are not in stat file {stat_file_path}: {missing_type}! " - f"Please change the stat file path!" - ) - idx_map = [stat_type_map.index(i) for i in target_type_map] - if stats[descrpt_stat_key[0]].size: # not empty - result_dict = {key: stats[key][idx_map] for key in descrpt_stat_key} - else: - result_dict = {key: [] for key in descrpt_stat_key} - else: # TODO hybrid descriptor not implemented - raise NotImplementedError( - "load_stats for hybrid descriptor is not implemented!" - ) - return result_dict - def __new__(cls, *args, **kwargs): if cls is Descriptor: try: @@ -275,12 +181,12 @@ def get_dim_emb(self) -> int: """Returns the embedding dimension.""" pass - def compute_input_stats(self, merged): + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): """Update mean and stddev for DescriptorBlock elements.""" raise NotImplementedError - def init_desc_stat(self, **kwargs): - """Initialize mean and stddev by the statistics.""" + def get_stats(self) -> Dict[str, StatItem]: + """Get the statistics of the descriptor.""" raise NotImplementedError def share_params(self, base_class, shared_level, resume=False): @@ -291,28 +197,14 @@ def share_params(self, base_class, shared_level, resume=False): # link buffers if hasattr(self, "mean") and not resume: # in case of change params during resume - sumr_base, suma_base, sumn_base, sumr2_base, suma2_base = ( - base_class.sumr, - base_class.suma, - base_class.sumn, - base_class.sumr2, - base_class.suma2, - ) - sumr, suma, sumn, sumr2, suma2 = ( - self.sumr, - self.suma, - self.sumn, - self.sumr2, - self.suma2, - ) - stat_dict = { - "sumr": sumr_base + sumr, - "suma": suma_base + suma, - "sumn": sumn_base + sumn, - "sumr2": sumr2_base + sumr2, - "suma2": suma2_base + suma2, - } - base_class.init_desc_stat(**stat_dict) + base_env = EnvMatStatSeA(base_class) + base_env.stats = base_class.stats + for kk in base_class.get_stats(): + base_env.stats[kk] += self.get_stats()[kk] + mean, stddev = base_env() + if not base_class.set_davg_zero: + base_class.mean.copy_(torch.tensor(mean, device=env.DEVICE)) + base_class.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) self.mean = base_class.mean self.stddev = base_class.stddev # self.load_state_dict(base_class.state_dict()) # this does not work, because it only inits the model @@ -335,16 +227,6 @@ def forward( pass -def compute_std(sumv2, sumv, sumn, rcut_r): - """Compute standard deviation.""" - if sumn == 0: - return 1.0 / rcut_r - val = np.sqrt(sumv2 / sumn - np.multiply(sumv / sumn, sumv / sumn)) - if np.abs(val) < 1e-2: - val = 1e-2 - return val - - def make_default_type_embedding( ntypes, ): diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index 76cff174af..6bdb5c2cb3 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -12,6 +12,9 @@ from deepmd.pt.model.network.network import ( TypeEmbedNet, ) +from deepmd.utils.path import ( + DPPath, +) from .se_atten import ( DescrptBlockSeAtten, @@ -119,27 +122,8 @@ def dim_out(self): def dim_emb(self): return self.get_dim_emb() - def compute_input_stats(self, merged): - return self.se_atten.compute_input_stats(merged) - - def init_desc_stat( - self, sumr=None, suma=None, sumn=None, sumr2=None, suma2=None, **kwargs - ): - assert all(x is not None for x in [sumr, suma, sumn, sumr2, suma2]) - self.se_atten.init_desc_stat(sumr, suma, sumn, sumr2, suma2) - - @classmethod - def get_stat_name( - cls, ntypes, type_name, rcut=None, rcut_smth=None, sel=None, **kwargs - ): - """ - Get the name for the statistic file of the descriptor. - Usually use the combination of descriptor name, rcut, rcut_smth and sel as the statistic file name. - """ - descrpt_type = type_name - assert descrpt_type in ["dpa1", "se_atten"] - assert all(x is not None for x in [rcut, rcut_smth, sel]) - return f"stat_file_descrpt_dpa1_rcut{rcut:.2f}_smth{rcut_smth:.2f}_sel{sel}_ntypes{ntypes}.npz" + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): + return self.se_atten.compute_input_stats(merged, path) @classmethod def get_data_process_key(cls, config): diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index 6cefaf6f38..0122dcacb8 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -18,6 +18,9 @@ build_multiple_neighbor_list, get_multiple_nlist_key, ) +from deepmd.utils.path import ( + DPPath, +) from .repformers import ( DescrptBlockRepformers, @@ -286,8 +289,7 @@ def dim_emb(self): """Returns the embedding dimension g2.""" return self.get_dim_emb() - def compute_input_stats(self, merged): - sumr, suma, sumn, sumr2, suma2 = [], [], [], [], [] + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): for ii, descrpt in enumerate([self.repinit, self.repformers]): merged_tmp = [ { @@ -296,68 +298,7 @@ def compute_input_stats(self, merged): } for item in merged ] - tmp_stat_dict = descrpt.compute_input_stats(merged_tmp) - sumr.append(tmp_stat_dict["sumr"]) - suma.append(tmp_stat_dict["suma"]) - sumn.append(tmp_stat_dict["sumn"]) - sumr2.append(tmp_stat_dict["sumr2"]) - suma2.append(tmp_stat_dict["suma2"]) - return { - "sumr": sumr, - "suma": suma, - "sumn": sumn, - "sumr2": sumr2, - "suma2": suma2, - } - - def init_desc_stat( - self, sumr=None, suma=None, sumn=None, sumr2=None, suma2=None, **kwargs - ): - assert all(x is not None for x in [sumr, suma, sumn, sumr2, suma2]) - for ii, descrpt in enumerate([self.repinit, self.repformers]): - stat_dict_ii = { - "sumr": sumr[ii], - "suma": suma[ii], - "sumn": sumn[ii], - "sumr2": sumr2[ii], - "suma2": suma2[ii], - } - descrpt.init_desc_stat(**stat_dict_ii) - - @classmethod - def get_stat_name( - cls, - ntypes, - type_name, - repinit_rcut=None, - repinit_rcut_smth=None, - repinit_nsel=None, - repformer_rcut=None, - repformer_rcut_smth=None, - repformer_nsel=None, - **kwargs, - ): - """ - Get the name for the statistic file of the descriptor. - Usually use the combination of descriptor name, rcut, rcut_smth and sel as the statistic file name. - """ - descrpt_type = type_name - assert descrpt_type in ["dpa2"] - assert all( - x is not None - for x in [ - repinit_rcut, - repinit_rcut_smth, - repinit_nsel, - repformer_rcut, - repformer_rcut_smth, - repformer_nsel, - ] - ) - return ( - f"stat_file_descrpt_dpa2_repinit_rcut{repinit_rcut:.2f}_smth{repinit_rcut_smth:.2f}_sel{repinit_nsel}" - f"_repformer_rcut{repformer_rcut:.2f}_smth{repformer_rcut_smth:.2f}_sel{repformer_nsel}_ntypes{ntypes}.npz" - ) + descrpt.compute_input_stats(merged_tmp) @classmethod def get_data_process_key(cls, config): diff --git a/deepmd/pt/model/descriptor/gaussian_lcc.py b/deepmd/pt/model/descriptor/gaussian_lcc.py index 0972b90279..72c9f27b2a 100644 --- a/deepmd/pt/model/descriptor/gaussian_lcc.py +++ b/deepmd/pt/model/descriptor/gaussian_lcc.py @@ -1,4 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + List, + Optional, +) + import torch import torch.nn as nn @@ -13,6 +18,9 @@ from deepmd.pt.utils import ( env, ) +from deepmd.utils.path import ( + DPPath, +) class DescrptGaussianLcc(Descriptor): @@ -154,11 +162,8 @@ def dim_emb(self): """Returns the output dimension of pair representation.""" return self.pair_embed_dim - def compute_input_stats(self, merged): + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): """Update mean and stddev for descriptor elements.""" - return [], [], [], [], [] - - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2, **kwargs): pass def forward( diff --git a/deepmd/pt/model/descriptor/hybrid.py b/deepmd/pt/model/descriptor/hybrid.py index c5c08c760d..d6678f2a4b 100644 --- a/deepmd/pt/model/descriptor/hybrid.py +++ b/deepmd/pt/model/descriptor/hybrid.py @@ -13,6 +13,9 @@ Identity, Linear, ) +from deepmd.utils.path import ( + DPPath, +) @DescriptorBlock.register("hybrid") @@ -150,9 +153,8 @@ def share_params(self, base_class, shared_level, resume=False): else: raise NotImplementedError - def compute_input_stats(self, merged): + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): """Update mean and stddev for descriptor elements.""" - sumr, suma, sumn, sumr2, suma2 = [], [], [], [], [] for ii, descrpt in enumerate(self.descriptor_list): merged_tmp = [ { @@ -161,33 +163,7 @@ def compute_input_stats(self, merged): } for item in merged ] - tmp_stat_dict = descrpt.compute_input_stats(merged_tmp) - sumr.append(tmp_stat_dict["sumr"]) - suma.append(tmp_stat_dict["suma"]) - sumn.append(tmp_stat_dict["sumn"]) - sumr2.append(tmp_stat_dict["sumr2"]) - suma2.append(tmp_stat_dict["suma2"]) - return { - "sumr": sumr, - "suma": suma, - "sumn": sumn, - "sumr2": sumr2, - "suma2": suma2, - } - - def init_desc_stat( - self, sumr=None, suma=None, sumn=None, sumr2=None, suma2=None, **kwargs - ): - assert all(x is not None for x in [sumr, suma, sumn, sumr2, suma2]) - for ii, descrpt in enumerate(self.descriptor_list): - stat_dict_ii = { - "sumr": sumr[ii], - "suma": suma[ii], - "sumn": sumn[ii], - "sumr2": sumr2[ii], - "suma2": suma2[ii], - } - descrpt.init_desc_stat(**stat_dict_ii) + descrpt.compute_input_stats(merged_tmp, path) def forward( self, diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py index 26467124b8..76051a52ed 100644 --- a/deepmd/pt/model/descriptor/repformers.py +++ b/deepmd/pt/model/descriptor/repformers.py @@ -1,15 +1,14 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( + Dict, List, Optional, ) -import numpy as np import torch from deepmd.pt.model.descriptor.descriptor import ( DescriptorBlock, - compute_std, ) from deepmd.pt.model.descriptor.env_mat import ( prod_env_mat_se_a, @@ -20,19 +19,22 @@ from deepmd.pt.utils import ( env, ) -from deepmd.pt.utils.nlist import ( - extend_input_and_build_neighbor_list, +from deepmd.pt.utils.env_mat_stat import ( + EnvMatStatSeA, ) from deepmd.pt.utils.utils import ( get_activation_fn, ) +from deepmd.utils.env_mat_stat import ( + StatItem, +) +from deepmd.utils.path import ( + DPPath, +) from .repformer_layer import ( RepformerLayer, ) -from .se_atten import ( - analyze_descrpt, -) mydtype = env.GLOBAL_PT_FLOAT_PRECISION mydev = env.DEVICE @@ -149,6 +151,7 @@ def __init__( stddev = torch.ones(sshape, dtype=mydtype, device=mydev) self.register_buffer("mean", mean) self.register_buffer("stddev", stddev) + self.stats = None def get_rcut(self) -> float: """Returns the cut-off radius.""" @@ -268,99 +271,22 @@ def forward( return g1, g2, h2, rot_mat.view(-1, nloc, self.dim_emb, 3), sw - def compute_input_stats(self, merged): + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): """Update mean and stddev for descriptor elements.""" - ndescrpt = self.nnei * 4 - sumr = [] - suma = [] - sumn = [] - sumr2 = [] - suma2 = [] - mixed_type = "real_natoms_vec" in merged[0] - for system in merged: - coord, atype, box, natoms = ( - system["coord"], - system["atype"], - system["box"], - system["natoms"], - ) - ( - extended_coord, - extended_atype, - mapping, - nlist, - ) = extend_input_and_build_neighbor_list( - coord, - atype, - self.get_rcut(), - self.get_sel(), - distinguish_types=self.distinguish_types(), - box=box, - ) - env_mat, _, _ = prod_env_mat_se_a( - extended_coord, - nlist, - atype, - self.mean, - self.stddev, - self.rcut, - self.rcut_smth, - ) - if not mixed_type: - sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( - env_mat.detach().cpu().numpy(), ndescrpt, natoms - ) - else: - real_natoms_vec = system["real_natoms_vec"] - sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( - env_mat.detach().cpu().numpy(), - ndescrpt, - real_natoms_vec, - mixed_type=mixed_type, - real_atype=atype.detach().cpu().numpy(), - ) - sumr.append(sysr) - suma.append(sysa) - sumn.append(sysn) - sumr2.append(sysr2) - suma2.append(sysa2) - sumr = np.sum(sumr, axis=0) - suma = np.sum(suma, axis=0) - sumn = np.sum(sumn, axis=0) - sumr2 = np.sum(sumr2, axis=0) - suma2 = np.sum(suma2, axis=0) - return { - "sumr": sumr, - "suma": suma, - "sumn": sumn, - "sumr2": sumr2, - "suma2": suma2, - } - - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2, **kwargs): - all_davg = [] - all_dstd = [] - for type_i in range(self.ntypes): - davgunit = [[sumr[type_i] / (sumn[type_i] + 1e-15), 0, 0, 0]] - dstdunit = [ - [ - compute_std(sumr2[type_i], sumr[type_i], sumn[type_i], self.rcut), - compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), - compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), - compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), - ] - ] - davg = np.tile(davgunit, [self.nnei, 1]) - dstd = np.tile(dstdunit, [self.nnei, 1]) - all_davg.append(davg) - all_dstd.append(dstd) - self.sumr = sumr - self.suma = suma - self.sumn = sumn - self.sumr2 = sumr2 - self.suma2 = suma2 + env_mat_stat = EnvMatStatSeA(self) + if path is not None: + path = path / env_mat_stat.get_hash() + env_mat_stat.load_or_compute_stats(merged, path) + self.stats = env_mat_stat.stats + mean, stddev = env_mat_stat() if not self.set_davg_zero: - mean = np.stack(all_davg) self.mean.copy_(torch.tensor(mean, device=env.DEVICE)) - stddev = np.stack(all_dstd) self.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) + + def get_stats(self) -> Dict[str, StatItem]: + """Get the statistics of the descriptor.""" + if self.stats is None: + raise RuntimeError( + "The statistics of the descriptor has not been computed." + ) + return self.stats diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index c086fe1cc2..33cc3ee9e2 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( ClassVar, + Dict, List, Optional, Tuple, @@ -12,7 +13,6 @@ from deepmd.pt.model.descriptor import ( Descriptor, DescriptorBlock, - compute_std, prod_env_mat_se_a, ) from deepmd.pt.utils import ( @@ -22,6 +22,15 @@ PRECISION_DICT, RESERVED_PRECISON_DICT, ) +from deepmd.pt.utils.env_mat_stat import ( + EnvMatStatSeA, +) +from deepmd.utils.env_mat_stat import ( + StatItem, +) +from deepmd.utils.path import ( + DPPath, +) try: from typing import ( @@ -41,9 +50,6 @@ from deepmd.pt.utils.exclude_mask import ( PairExcludeMask, ) -from deepmd.pt.utils.nlist import ( - extend_input_and_build_neighbor_list, -) @Descriptor.register("se_e2_a") @@ -114,28 +120,9 @@ def dim_out(self): """Returns the output dimension of this descriptor.""" return self.sea.dim_out - def compute_input_stats(self, merged): + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): """Update mean and stddev for descriptor elements.""" - return self.sea.compute_input_stats(merged) - - def init_desc_stat( - self, sumr=None, suma=None, sumn=None, sumr2=None, suma2=None, **kwargs - ): - assert all(x is not None for x in [sumr, suma, sumn, sumr2, suma2]) - self.sea.init_desc_stat(sumr, suma, sumn, sumr2, suma2) - - @classmethod - def get_stat_name( - cls, ntypes, type_name, rcut=None, rcut_smth=None, sel=None, **kwargs - ): - """ - Get the name for the statistic file of the descriptor. - Usually use the combination of descriptor name, rcut, rcut_smth and sel as the statistic file name. - """ - descrpt_type = type_name - assert descrpt_type in ["se_e2_a"] - assert all(x is not None for x in [rcut, rcut_smth, sel]) - return f"stat_file_descrpt_sea_rcut{rcut:.2f}_smth{rcut_smth:.2f}_sel{sel}_ntypes{ntypes}.npz" + return self.sea.compute_input_stats(merged, path) @classmethod def get_data_process_key(cls, config): @@ -330,6 +317,7 @@ def __init__( resnet_dt=self.resnet_dt, ) self.filter_layers = filter_layers + self.stats = None def get_rcut(self) -> float: """Returns the cut-off radius.""" @@ -391,91 +379,29 @@ def __getitem__(self, key): else: raise KeyError(key) - def compute_input_stats(self, merged): + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): """Update mean and stddev for descriptor elements.""" - sumr = [] - suma = [] - sumn = [] - sumr2 = [] - suma2 = [] - for system in merged: - coord, atype, box, natoms = ( - system["coord"], - system["atype"], - system["box"], - system["natoms"], - ) - ( - extended_coord, - extended_atype, - mapping, - nlist, - ) = extend_input_and_build_neighbor_list( - coord, - atype, - self.get_rcut(), - self.get_sel(), - distinguish_types=self.distinguish_types(), - box=box, - ) - env_mat, _, _ = prod_env_mat_se_a( - extended_coord, - nlist, - atype, - self.mean, - self.stddev, - self.rcut, - self.rcut_smth, - ) - sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( - env_mat.detach().cpu().numpy(), self.ndescrpt, natoms - ) - sumr.append(sysr) - suma.append(sysa) - sumn.append(sysn) - sumr2.append(sysr2) - suma2.append(sysa2) - sumr = np.sum(sumr, axis=0) - suma = np.sum(suma, axis=0) - sumn = np.sum(sumn, axis=0) - sumr2 = np.sum(sumr2, axis=0) - suma2 = np.sum(suma2, axis=0) - return { - "sumr": sumr, - "suma": suma, - "sumn": sumn, - "sumr2": sumr2, - "suma2": suma2, - } - - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2, **kwargs): - all_davg = [] - all_dstd = [] - for type_i in range(self.ntypes): - davgunit = [[sumr[type_i] / (sumn[type_i] + 1e-15), 0, 0, 0]] - dstdunit = [ - [ - compute_std(sumr2[type_i], sumr[type_i], sumn[type_i], self.rcut), - compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), - compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), - compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), - ] - ] - davg = np.tile(davgunit, [self.nnei, 1]) - dstd = np.tile(dstdunit, [self.nnei, 1]) - all_davg.append(davg) - all_dstd.append(dstd) - self.sumr = sumr - self.suma = suma - self.sumn = sumn - self.sumr2 = sumr2 - self.suma2 = suma2 + env_mat_stat = EnvMatStatSeA(self) + if path is not None: + path = path / env_mat_stat.get_hash() + env_mat_stat.load_or_compute_stats(merged, path) + self.stats = env_mat_stat.stats + mean, stddev = env_mat_stat() + if not self.set_davg_zero: + self.mean.copy_(torch.tensor(mean, device=env.DEVICE)) + self.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) if not self.set_davg_zero: - mean = np.stack(all_davg) self.mean.copy_(torch.tensor(mean, device=env.DEVICE)) - stddev = np.stack(all_dstd) self.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) + def get_stats(self) -> Dict[str, StatItem]: + """Get the statistics of the descriptor.""" + if self.stats is None: + raise RuntimeError( + "The statistics of the descriptor has not been computed." + ) + return self.stats + def forward( self, nlist: torch.Tensor, diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index d4dc0cd054..410a2039aa 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( + Dict, List, Optional, ) @@ -9,7 +10,6 @@ from deepmd.pt.model.descriptor.descriptor import ( DescriptorBlock, - compute_std, ) from deepmd.pt.model.descriptor.env_mat import ( prod_env_mat_se_a, @@ -21,8 +21,14 @@ from deepmd.pt.utils import ( env, ) -from deepmd.pt.utils.nlist import ( - extend_input_and_build_neighbor_list, +from deepmd.pt.utils.env_mat_stat import ( + EnvMatStatSeA, +) +from deepmd.utils.env_mat_stat import ( + StatItem, +) +from deepmd.utils.path import ( + DPPath, ) @@ -135,6 +141,7 @@ def __init__( ) filter_layers.append(one) self.filter_layers = torch.nn.ModuleList(filter_layers) + self.stats = None def get_rcut(self) -> float: """Returns the cut-off radius.""" @@ -185,102 +192,26 @@ def dim_emb(self): """Returns the output dimension of embedding.""" return self.get_dim_emb() - def compute_input_stats(self, merged): + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): """Update mean and stddev for descriptor elements.""" - sumr = [] - suma = [] - sumn = [] - sumr2 = [] - suma2 = [] - mixed_type = "real_natoms_vec" in merged[0] - for system in merged: - coord, atype, box, natoms = ( - system["coord"], - system["atype"], - system["box"], - system["natoms"], - ) - ( - extended_coord, - extended_atype, - mapping, - nlist, - ) = extend_input_and_build_neighbor_list( - coord, - atype, - self.get_rcut(), - self.get_sel(), - distinguish_types=self.distinguish_types(), - box=box, - ) - env_mat, _, _ = prod_env_mat_se_a( - extended_coord, - nlist, - atype, - self.mean, - self.stddev, - self.rcut, - self.rcut_smth, - ) - if not mixed_type: - sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( - env_mat.detach().cpu().numpy(), self.ndescrpt, natoms - ) - else: - real_natoms_vec = system["real_natoms_vec"] - sysr, sysr2, sysa, sysa2, sysn = analyze_descrpt( - env_mat.detach().cpu().numpy(), - self.ndescrpt, - real_natoms_vec, - mixed_type=mixed_type, - real_atype=atype.detach().cpu().numpy(), - ) - sumr.append(sysr) - suma.append(sysa) - sumn.append(sysn) - sumr2.append(sysr2) - suma2.append(sysa2) - sumr = np.sum(sumr, axis=0) - suma = np.sum(suma, axis=0) - sumn = np.sum(sumn, axis=0) - sumr2 = np.sum(sumr2, axis=0) - suma2 = np.sum(suma2, axis=0) - return { - "sumr": sumr, - "suma": suma, - "sumn": sumn, - "sumr2": sumr2, - "suma2": suma2, - } - - def init_desc_stat(self, sumr, suma, sumn, sumr2, suma2, **kwargs): - all_davg = [] - all_dstd = [] - for type_i in range(self.ntypes): - davgunit = [[sumr[type_i] / (sumn[type_i] + 1e-15), 0, 0, 0]] - dstdunit = [ - [ - compute_std(sumr2[type_i], sumr[type_i], sumn[type_i], self.rcut), - compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), - compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), - compute_std(suma2[type_i], suma[type_i], sumn[type_i], self.rcut), - ] - ] - davg = np.tile(davgunit, [self.nnei, 1]) - dstd = np.tile(dstdunit, [self.nnei, 1]) - all_davg.append(davg) - all_dstd.append(dstd) - self.sumr = sumr - self.suma = suma - self.sumn = sumn - self.sumr2 = sumr2 - self.suma2 = suma2 + env_mat_stat = EnvMatStatSeA(self) + if path is not None: + path = path / env_mat_stat.get_hash() + env_mat_stat.load_or_compute_stats(merged, path) + self.stats = env_mat_stat.stats + mean, stddev = env_mat_stat() if not self.set_davg_zero: - mean = np.stack(all_davg) self.mean.copy_(torch.tensor(mean, device=env.DEVICE)) - stddev = np.stack(all_dstd) self.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) + def get_stats(self) -> Dict[str, StatItem]: + """Get the statistics of the descriptor.""" + if self.stats is None: + raise RuntimeError( + "The statistics of the descriptor has not been computed." + ) + return self.stats + def forward( self, nlist: torch.Tensor, diff --git a/deepmd/pt/model/model/model.py b/deepmd/pt/model/model/model.py index 51c5fcf123..d98d25d539 100644 --- a/deepmd/pt/model/model/model.py +++ b/deepmd/pt/model/model/model.py @@ -1,6 +1,14 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Optional, +) + import torch +from deepmd.utils.path import ( + DPPath, +) + class BaseModel(torch.nn.Module): def __init__(self): @@ -9,9 +17,8 @@ def __init__(self): def compute_or_load_stat( self, - type_map=None, - sampled=None, - stat_file_path=None, + sampled, + stat_file_path: Optional[DPPath] = None, ): """ Compute or load the statistics parameters of the model, @@ -23,9 +30,6 @@ def compute_or_load_stat( Parameters ---------- - type_map - Mapping atom type to the name (str) of the type. - For example `type_map[1]` gives the name of the type 1. sampled The sampled data frames from different data systems. stat_file_path diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index b6ca12b9d8..2f5afaf26e 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -30,6 +30,9 @@ from deepmd.pt.utils.stat import ( compute_output_bias, ) +from deepmd.utils.path import ( + DPPath, +) dtype = env.GLOBAL_PT_FLOAT_PRECISION device = env.DEVICE @@ -152,17 +155,21 @@ def data_stat_key(self): """ return ["bias_atom_e"] - def compute_output_stats(self, merged): + def compute_output_stats(self, merged, stat_file_path: Optional[DPPath] = None): energy = [item["energy"] for item in merged] mixed_type = "real_natoms_vec" in merged[0] if mixed_type: input_natoms = [item["real_natoms_vec"] for item in merged] else: input_natoms = [item["natoms"] for item in merged] - bias_atom_e = compute_output_bias(energy, input_natoms, rcond=self.rcond) - return {"bias_atom_e": bias_atom_e} - - def init_fitting_stat(self, bias_atom_e=None, **kwargs): + if stat_file_path is not None: + stat_file_path = stat_file_path / "bias_atom_e" + if stat_file_path is not None and stat_file_path.is_file(): + bias_atom_e = stat_file_path.load_numpy() + else: + bias_atom_e = compute_output_bias(energy, input_natoms, rcond=self.rcond) + if stat_file_path is not None: + stat_file_path.save_numpy(bias_atom_e) assert all(x is not None for x in [bias_atom_e]) self.bias_atom_e.copy_( torch.tensor(bias_atom_e, device=env.DEVICE).view( @@ -225,16 +232,6 @@ def __init__( **kwargs, ) - @classmethod - def get_stat_name(cls, ntypes, type_name="ener", **kwargs): - """ - Get the name for the statistic file of the fitting. - Usually use the combination of fitting net name and ntypes as the statistic file name. - """ - fitting_type = type_name - assert fitting_type in ["ener"] - return f"stat_file_fitting_ener_ntypes{ntypes}.npz" - @Fitting.register("direct_force") @Fitting.register("direct_force_ener") @@ -327,16 +324,6 @@ def serialize(self) -> dict: def deserialize(cls) -> "EnergyFittingNetDirect": raise NotImplementedError - @classmethod - def get_stat_name(cls, ntypes, type_name="ener", **kwargs): - """ - Get the name for the statistic file of the fitting. - Usually use the combination of fitting net name and ntypes as the statistic file name. - """ - fitting_type = type_name - assert fitting_type in ["direct_force", "direct_force_ener"] - return f"stat_file_fitting_direct_ntypes{ntypes}.npz" - def forward( self, inputs: torch.Tensor, diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index db8daff802..f8f6e3f5dc 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -8,7 +8,6 @@ Callable, List, Optional, - Union, ) import numpy as np @@ -127,19 +126,6 @@ def share_params(self, base_class, shared_level, resume=False): else: raise NotImplementedError - @classmethod - def get_stat_name(cls, ntypes, type_name="ener", **kwargs): - """ - Get the name for the statistic file of the fitting. - Usually use the combination of fitting net name and ntypes as the statistic file name. - """ - if cls is not Fitting: - raise NotImplementedError("get_stat_name is not implemented!") - fitting_type = type_name - return Fitting.__plugins.plugins[fitting_type].get_stat_name( - ntypes, type_name, **kwargs - ) - @property def data_stat_key(self): """ @@ -148,86 +134,6 @@ def data_stat_key(self): """ raise NotImplementedError("data_stat_key is not implemented!") - def compute_or_load_stat( - self, - type_map: List[str], - sampled=None, - stat_file_path: Optional[Union[str, List[str]]] = None, - ): - """ - Compute or load the statistics parameters of the fitting net. - Calculate and save the output bias to `stat_file_path` - if `sampled` is not None, otherwise load them from `stat_file_path`. - - Parameters - ---------- - type_map - Mapping atom type to the name (str) of the type. - For example `type_map[1]` gives the name of the type 1. - sampled - The sampled data frames from different data systems. - stat_file_path - The path to the statistics files. - """ - fitting_stat_key = self.data_stat_key - if sampled is not None: - tmp_dict = self.compute_output_stats(sampled) - result_dict = {key: tmp_dict[key] for key in fitting_stat_key} - result_dict["type_map"] = type_map - self.save_stats(result_dict, stat_file_path) - else: # load the statistics results - assert stat_file_path is not None, "No stat file to load!" - result_dict = self.load_stats(type_map, stat_file_path) - self.init_fitting_stat(**result_dict) - - def save_stats(self, result_dict, stat_file_path: str): - """ - Save the statistics results to `stat_file_path`. - - Parameters - ---------- - result_dict - The dictionary of statistics results. - stat_file_path - The path to the statistics file(s). - """ - log.info(f"Saving stat file to {stat_file_path}") - np.savez_compressed(stat_file_path, **result_dict) - - def load_stats(self, type_map, stat_file_path: str): - """ - Load the statistics results to `stat_file_path`. - - Parameters - ---------- - type_map - Mapping atom type to the name (str) of the type. - For example `type_map[1]` gives the name of the type 1. - stat_file_path - The path to the statistics file(s). - - Returns - ------- - result_dict - The dictionary of statistics results. - """ - fitting_stat_key = self.data_stat_key - target_type_map = type_map - log.info(f"Loading stat file from {stat_file_path}") - stats = np.load(stat_file_path) - stat_type_map = list(stats["type_map"]) - missing_type = [i for i in target_type_map if i not in stat_type_map] - assert not missing_type, ( - f"These type are not in stat file {stat_file_path}: {missing_type}! " - f"Please change the stat file path!" - ) - idx_map = [stat_type_map.index(i) for i in target_type_map] - if stats[fitting_stat_key[0]].size: # not empty - result_dict = {key: stats[key][idx_map] for key in fitting_stat_key} - else: - result_dict = {key: [] for key in fitting_stat_key} - return result_dict - def change_energy_bias( self, config, model, old_type_map, new_type_map, bias_shift="delta", ntest=10 ): diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index b2cac5a5eb..8537be6e12 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -185,9 +185,8 @@ def get_single_model(_model_params, _sampled, _stat_file_path): model = get_model(deepcopy(_model_params)).to(DEVICE) if not model_params.get("resuming", False): model.compute_or_load_stat( - type_map=_model_params["type_map"], sampled=_sampled, - stat_file_path_dict=_stat_file_path, + stat_file_path=_stat_file_path, ) return model diff --git a/deepmd/pt/utils/env_mat_stat.py b/deepmd/pt/utils/env_mat_stat.py new file mode 100644 index 0000000000..5247ce08ba --- /dev/null +++ b/deepmd/pt/utils/env_mat_stat.py @@ -0,0 +1,206 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + TYPE_CHECKING, + Dict, + Iterator, + List, +) + +import numpy as np +import torch + +from deepmd.common import ( + get_hash, +) +from deepmd.pt.model.descriptor.env_mat import ( + prod_env_mat_se_a, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.nlist import ( + extend_input_and_build_neighbor_list, +) +from deepmd.utils.env_mat_stat import EnvMatStat as BaseEnvMatStat +from deepmd.utils.env_mat_stat import ( + StatItem, +) + +if TYPE_CHECKING: + from deepmd.pt.model.descriptor import ( + DescriptorBlock, + ) + + +class EnvMatStat(BaseEnvMatStat): + def compute_stat(self, env_mat: Dict[str, torch.Tensor]) -> Dict[str, StatItem]: + """Compute the statistics of the environment matrix for a single system. + + Parameters + ---------- + env_mat : torch.Tensor + The environment matrix. + + Returns + ------- + Dict[str, StatItem] + The statistics of the environment matrix. + """ + stats = {} + for kk, vv in env_mat.items(): + stats[kk] = StatItem( + number=vv.numel(), + sum=vv.sum().item(), + squared_sum=torch.square(vv).sum().item(), + ) + return stats + + +class EnvMatStatSeA(EnvMatStat): + """Environmental matrix statistics for the se_a environemntal matrix. + + Parameters + ---------- + descriptor : DescriptorBlock + The descriptor of the model. + """ + + def __init__(self, descriptor: "DescriptorBlock"): + super().__init__() + self.descriptor = descriptor + + def iter( + self, data: List[Dict[str, torch.Tensor]] + ) -> Iterator[Dict[str, StatItem]]: + """Get the iterator of the environment matrix. + + Parameters + ---------- + data : List[Dict[str, torch.Tensor]] + The environment matrix. + + Yields + ------ + Dict[str, StatItem] + The statistics of the environment matrix. + """ + zero_mean = torch.zeros( + self.descriptor.get_ntypes(), + self.descriptor.get_nsel(), + 4, + dtype=env.GLOBAL_PT_FLOAT_PRECISION, + device=env.DEVICE, + ) + one_stddev = torch.ones( + self.descriptor.get_ntypes(), + self.descriptor.get_nsel(), + 4, + dtype=env.GLOBAL_PT_FLOAT_PRECISION, + device=env.DEVICE, + ) + for system in data: + coord, atype, box, natoms = ( + system["coord"], + system["atype"], + system["box"], + system["natoms"], + ) + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + coord, + atype, + self.descriptor.get_rcut(), + self.descriptor.get_sel(), + distinguish_types=self.descriptor.distinguish_types(), + box=box, + ) + env_mat, _, _ = prod_env_mat_se_a( + extended_coord, + nlist, + atype, + zero_mean, + one_stddev, + self.descriptor.get_rcut(), + # TODO: export rcut_smth from DescriptorBlock + self.descriptor.rcut_smth, + ) + env_mat = env_mat.view( + coord.shape[0], coord.shape[1], self.descriptor.get_nsel(), 4 + ) + + if "real_natoms_vec" not in system: + end_indexes = torch.cumsum(natoms[0, 2:], 0) + start_indexes = torch.cat( + [ + torch.zeros(1, dtype=torch.int32, device=env.DEVICE), + end_indexes[:-1], + ] + ) + for type_i in range(self.descriptor.get_ntypes()): + dd = env_mat[ + :, start_indexes[type_i] : end_indexes[type_i], :, : + ] # all descriptors for this element + env_mats = {} + env_mats[f"r_{type_i}"] = dd[:, :, :, :1] + env_mats[f"a_{type_i}"] = dd[:, :, :, 1:] + yield self.compute_stat(env_mats) + else: + for frame_item in range(env_mat.shape[0]): + dd_ff = env_mat[frame_item] + atype_frame = atype[frame_item] + for type_i in range(self.descriptor.get_ntypes()): + type_idx = atype_frame == type_i + dd = dd_ff[type_idx] + dd = dd.reshape([-1, 4]) # typen_atoms * nnei, 4 + env_mats = {} + env_mats[f"r_{type_i}"] = dd[:, :1] + env_mats[f"a_{type_i}"] = dd[:, 1:] + yield self.compute_stat(env_mats) + + def get_hash(self) -> str: + """Get the hash of the environment matrix. + + Returns + ------- + str + The hash of the environment matrix. + """ + return get_hash( + { + "type": "se_a", + "ntypes": self.descriptor.get_ntypes(), + "rcut": round(self.descriptor.get_rcut(), 2), + "rcut_smth": round(self.descriptor.rcut_smth, 2), + "nsel": self.descriptor.get_nsel(), + "sel": self.descriptor.get_sel(), + "distinguish_types": self.descriptor.distinguish_types(), + } + ) + + def __call__(self): + avgs = self.get_avg() + stds = self.get_std() + + all_davg = [] + all_dstd = [] + for type_i in range(self.descriptor.get_ntypes()): + davgunit = [[avgs[f"r_{type_i}"], 0, 0, 0]] + dstdunit = [ + [ + stds[f"r_{type_i}"], + stds[f"a_{type_i}"], + stds[f"a_{type_i}"], + stds[f"a_{type_i}"], + ] + ] + davg = np.tile(davgunit, [self.descriptor.get_nsel(), 1]) + dstd = np.tile(dstdunit, [self.descriptor.get_nsel(), 1]) + all_davg.append(davg) + all_dstd.append(dstd) + mean = np.stack(all_davg) + stddev = np.stack(all_dstd) + return mean, stddev diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index 76b2afe41b..051fddd14b 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging -import os import numpy as np import torch @@ -77,32 +76,3 @@ def compute_output_bias(energy, natoms, rcond=None): sys_tynatom = torch.cat(natoms)[:, 2:].cpu() energy_coef, _, _, _ = np.linalg.lstsq(sys_tynatom, sys_ener, rcond) return energy_coef - - -def process_stat_path( - stat_file_dict, stat_file_dir, model_params_dict, descriptor_cls, fitting_cls -): - if stat_file_dict is None: - stat_file_dict = {} - if "descriptor" in model_params_dict: - default_stat_file_name_descrpt = descriptor_cls.get_stat_name( - len(model_params_dict["type_map"]), - model_params_dict["descriptor"]["type"], - **model_params_dict["descriptor"], - ) - stat_file_dict["descriptor"] = default_stat_file_name_descrpt - if "fitting_net" in model_params_dict: - default_stat_file_name_fitting = fitting_cls.get_stat_name( - len(model_params_dict["type_map"]), - model_params_dict["fitting_net"].get("type", "ener"), - **model_params_dict["fitting_net"], - ) - stat_file_dict["fitting_net"] = default_stat_file_name_fitting - stat_file_path = { - key: os.path.join(stat_file_dir, stat_file_dict[key]) for key in stat_file_dict - } - - has_stat_file_path_list = [ - os.path.exists(stat_file_path[key]) for key in stat_file_dict - ] - return stat_file_path, all(has_stat_file_path_list) diff --git a/deepmd/utils/env_mat_stat.py b/deepmd/utils/env_mat_stat.py new file mode 100644 index 0000000000..2fa497b9b6 --- /dev/null +++ b/deepmd/utils/env_mat_stat.py @@ -0,0 +1,213 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from abc import ( + ABC, + abstractmethod, +) +from collections import ( + defaultdict, +) +from typing import ( + Dict, + Iterator, + List, + Optional, +) + +import numpy as np + +from deepmd.utils.path import ( + DPPath, +) + + +class StatItem: + """A class to store the statistics of the environment matrix. + + Parameters + ---------- + number : int + The total size of given array. + sum : float + The sum value of the matrix. + squared_sum : float + The sum squared value of the matrix. + """ + + def __init__(self, number: int = 0, sum: float = 0, squared_sum: float = 0) -> None: + self.number = number + self.sum = sum + self.squared_sum = squared_sum + + def __add__(self, other: "StatItem") -> "StatItem": + return StatItem( + number=self.number + other.number, + sum=self.sum + other.sum, + squared_sum=self.squared_sum + other.squared_sum, + ) + + def compute_avg(self, default: float = 0) -> float: + """Compute the average of the environment matrix. + + Parameters + ---------- + default : float, optional + The default value of the average, by default 0. + + Returns + ------- + float + The average of the environment matrix. + """ + if self.number == 0: + return default + return self.sum / self.number + + def compute_std(self, default: float = 1e-1, protection: float = 1e-2) -> float: + """Compute the standard deviation of the environment matrix. + + Parameters + ---------- + default : float, optional + The default value of the standard deviation, by default 1e-1. + protection : float, optional + The protection value for the standard deviation, by default 1e-2. + + Returns + ------- + float + The standard deviation of the environment matrix. + """ + if self.number == 0: + return default + val = np.sqrt( + self.squared_sum / self.number + - np.multiply(self.sum / self.number, self.sum / self.number) + ) + if np.abs(val) < protection: + val = protection + return val + + +class EnvMatStat(ABC): + """A base class to store and calculate the statistics of the environment matrix.""" + + def __init__(self) -> None: + super().__init__() + self.stats = defaultdict(StatItem) + + def compute_stats(self, data: List[Dict[str, np.ndarray]]) -> None: + """Compute the statistics of the environment matrix. + + Parameters + ---------- + data : List[Dict[str, np.ndarray]] + The environment matrix. + """ + if len(self.stats) > 0: + raise ValueError("The statistics has already been computed.") + for iter_stats in self.iter(data): + for kk in iter_stats: + self.stats[kk] += iter_stats[kk] + + @abstractmethod + def iter(self, data: List[Dict[str, np.ndarray]]) -> Iterator[Dict[str, StatItem]]: + """Get the iterator of the environment matrix. + + Parameters + ---------- + data : List[Dict[str, np.ndarray]] + The environment matrix. + + Yields + ------ + Dict[str, StatItem] + The statistics of the environment matrix. + """ + + def save_stats(self, path: DPPath) -> None: + """Save the statistics of the environment matrix. + + Parameters + ---------- + path : DPH5Path + The path to save the statistics of the environment matrix. + """ + if len(self.stats) == 0: + raise ValueError("The statistics hasn't been computed.") + for kk, vv in self.stats.items(): + path.mkdir(parents=True, exist_ok=True) + (path / kk).save_numpy(np.array([vv.number, vv.sum, vv.squared_sum])) + + def load_stats(self, path: DPPath) -> None: + """Load the statistics of the environment matrix. + + Parameters + ---------- + path : DPH5Path + The path to load the statistics of the environment matrix. + """ + if len(self.stats) > 0: + raise ValueError("The statistics has already been computed.") + for kk in path.glob("*"): + arr = kk.load_numpy() + self.stats[kk.name] = StatItem( + number=arr[0], + sum=arr[1], + squared_sum=arr[2], + ) + + def load_or_compute_stats( + self, data: List[Dict[str, np.ndarray]], path: Optional[DPPath] = None + ) -> None: + """Load the statistics of the environment matrix if it exists, otherwise compute and save it. + + Parameters + ---------- + path : DPH5Path + The path to load the statistics of the environment matrix. + data : List[Dict[str, np.ndarray]] + The environment matrix. + """ + if path is not None and path.is_dir(): + self.load_stats(path) + else: + self.compute_stats(data) + if path is not None: + self.save_stats(path) + + def get_avg(self, default: float = 0) -> Dict[str, float]: + """Get the average of the environment matrix. + + Parameters + ---------- + default : float, optional + The default value of the average, by default 0. + + Returns + ------- + Dict[str, float] + The average of the environment matrix. + """ + return {kk: vv.compute_avg(default=default) for kk, vv in self.stats.items()} + + def get_std( + self, default: float = 1e-1, protection: float = 1e-2 + ) -> Dict[str, float]: + """Get the standard deviation of the environment matrix. + + Parameters + ---------- + default : float, optional + The default value of the standard deviation, by default 1e-1. + protection : float, optional + The protection value for the standard deviation, by default 1e-2. + + Returns + ------- + Dict[str, float] + The standard deviation of the environment matrix. + """ + return { + kk: vv.compute_std(default=default, protection=protection) + for kk, vv in self.stats.items() + } diff --git a/deepmd/utils/path.py b/deepmd/utils/path.py index a8e4bc329f..c9a7cd8554 100644 --- a/deepmd/utils/path.py +++ b/deepmd/utils/path.py @@ -29,9 +29,11 @@ class DPPath(ABC): ---------- path : str path + mode : str, optional + mode, by default "r" """ - def __new__(cls, path: str): + def __new__(cls, path: str, mode: str = "r"): if cls is DPPath: if os.path.isdir(path): return super().__new__(DPOSPath) @@ -62,6 +64,16 @@ def load_txt(self, **kwargs) -> np.ndarray: loaded NumPy array """ + @abstractmethod + def save_numpy(self, arr: np.ndarray) -> None: + """Save NumPy array. + + Parameters + ---------- + arr : np.ndarray + NumPy array + """ + @abstractmethod def glob(self, pattern: str) -> List["DPPath"]: """Search path using the glob pattern. @@ -122,6 +134,23 @@ def __eq__(self, other) -> bool: def __hash__(self): return hash(str(self)) + @property + @abstractmethod + def name(self) -> str: + """Name of the path.""" + + @abstractmethod + def mkdir(self, parents: bool = False, exist_ok: bool = False) -> None: + """Make directory. + + Parameters + ---------- + parents : bool, optional + If true, any missing parents of this directory are created as well. + exist_ok : bool, optional + If true, no error will be raised if the target directory already exists. + """ + class DPOSPath(DPPath): """The OS path class to data system (DeepmdData) for real directories. @@ -130,10 +159,13 @@ class DPOSPath(DPPath): ---------- path : str path + mode : str, optional + mode, by default "r" """ - def __init__(self, path: str) -> None: + def __init__(self, path: str, mode: str = "r") -> None: super().__init__() + self.mode = mode if isinstance(path, Path): self.path = path else: @@ -159,6 +191,18 @@ def load_txt(self, **kwargs) -> np.ndarray: """ return np.loadtxt(str(self.path), **kwargs) + def save_numpy(self, arr: np.ndarray) -> None: + """Save NumPy array. + + Parameters + ---------- + arr : np.ndarray + NumPy array + """ + if self.mode == "r": + raise ValueError("Cannot save to read-only path") + np.save(str(self.path), arr) + def glob(self, pattern: str) -> List["DPPath"]: """Search path using the glob pattern. @@ -174,7 +218,7 @@ def glob(self, pattern: str) -> List["DPPath"]: """ # currently DPOSPath will only derivative DPOSPath # TODO: discuss if we want to mix DPOSPath and DPH5Path? - return [type(self)(p) for p in self.path.glob(pattern)] + return [type(self)(p, mode=self.mode) for p in self.path.glob(pattern)] def rglob(self, pattern: str) -> List["DPPath"]: """This is like calling :meth:`DPPath.glob()` with `**/` added in front @@ -190,7 +234,7 @@ def rglob(self, pattern: str) -> List["DPPath"]: List[DPPath] list of paths """ - return [type(self)(p) for p in self.path.rglob(pattern)] + return [type(self)(p, mode=self.mode) for p in self.path.rglob(pattern)] def is_file(self) -> bool: """Check if self is file.""" @@ -202,7 +246,7 @@ def is_dir(self) -> bool: def __truediv__(self, key: str) -> "DPPath": """Used for / operator.""" - return type(self)(self.path / key) + return type(self)(self.path / key, mode=self.mode) def __lt__(self, other: "DPOSPath") -> bool: """Whether this DPPath is less than other for sorting.""" @@ -212,6 +256,25 @@ def __str__(self) -> str: """Represent string.""" return str(self.path) + @property + def name(self) -> str: + """Name of the path.""" + return self.path.name + + def mkdir(self, parents: bool = False, exist_ok: bool = False) -> None: + """Make directory. + + Parameters + ---------- + parents : bool, optional + If true, any missing parents of this directory are created as well. + exist_ok : bool, optional + If true, no error will be raised if the target directory already exists. + """ + if self.mode == "r": + raise ValueError("Cannot mkdir to read-only path") + self.path.mkdir(parents=parents, exist_ok=exist_ok) + class DPH5Path(DPPath): """The path class to data system (DeepmdData) for HDF5 files. @@ -226,32 +289,37 @@ class DPH5Path(DPPath): ---------- path : str path + mode : str, optional + mode, by default "r" """ - def __init__(self, path: str) -> None: + def __init__(self, path: str, mode: str = "r") -> None: super().__init__() + self.mode = mode # we use "#" to split path # so we do not support file names containing #... s = path.split("#") self.root_path = s[0] - self.root = self._load_h5py(s[0]) + self.root = self._load_h5py(s[0], mode) # h5 path: default is the root path - self.name = s[1] if len(s) > 1 else "/" + self._name = s[1] if len(s) > 1 else "/" @classmethod @lru_cache(None) - def _load_h5py(cls, path: str) -> h5py.File: + def _load_h5py(cls, path: str, mode: str = "r") -> h5py.File: """Load hdf5 file. Parameters ---------- path : str path to hdf5 file + mode : str, optional + mode, by default 'r' """ # this method has cache to avoid duplicated # loading from different DPH5Path # However the file will be never closed? - return h5py.File(path, "r") + return h5py.File(path, mode) def load_numpy(self) -> np.ndarray: """Load NumPy array. @@ -261,7 +329,7 @@ def load_numpy(self) -> np.ndarray: np.ndarray loaded NumPy array """ - return self.root[self.name][:] + return self.root[self._name][:] def load_txt(self, dtype: Optional[np.dtype] = None, **kwargs) -> np.ndarray: """Load NumPy array from text. @@ -276,6 +344,18 @@ def load_txt(self, dtype: Optional[np.dtype] = None, **kwargs) -> np.ndarray: arr = arr.astype(dtype) return arr + def save_numpy(self, arr: np.ndarray) -> None: + """Save NumPy array. + + Parameters + ---------- + arr : np.ndarray + NumPy array + """ + if self._name in self._keys: + del self.root[self._name] + self.root.create_dataset(self._name, data=arr) + def glob(self, pattern: str) -> List["DPPath"]: """Search path using the glob pattern. @@ -290,9 +370,9 @@ def glob(self, pattern: str) -> List["DPPath"]: list of paths """ # got paths starts with current path first, which is faster - subpaths = [ii for ii in self._keys if ii.startswith(self.name)] + subpaths = [ii for ii in self._keys if ii.startswith(self._name)] return [ - type(self)(f"{self.root_path}#{pp}") + type(self)(f"{self.root_path}#{pp}", mode=self.mode) for pp in globfilter(subpaths, self._connect_path(pattern)) ] @@ -327,32 +407,56 @@ def _file_keys(cls, file: h5py.File) -> List[str]: def is_file(self) -> bool: """Check if self is file.""" - if self.name not in self._keys: + if self._name not in self._keys: return False - return isinstance(self.root[self.name], h5py.Dataset) + return isinstance(self.root[self._name], h5py.Dataset) def is_dir(self) -> bool: """Check if self is directory.""" - if self.name not in self._keys: + if self._name not in self._keys: return False - return isinstance(self.root[self.name], h5py.Group) + return isinstance(self.root[self._name], h5py.Group) def __truediv__(self, key: str) -> "DPPath": """Used for / operator.""" - return type(self)(f"{self.root_path}#{self._connect_path(key)}") + return type(self)(f"{self.root_path}#{self._connect_path(key)}", mode=self.mode) def _connect_path(self, path: str) -> str: """Connect self with path.""" - if self.name.endswith("/"): - return f"{self.name}{path}" - return f"{self.name}/{path}" + if self._name.endswith("/"): + return f"{self._name}{path}" + return f"{self._name}/{path}" def __lt__(self, other: "DPH5Path") -> bool: """Whether this DPPath is less than other for sorting.""" if self.root_path == other.root_path: - return self.name < other.name + return self._name < other._name return self.root_path < other.root_path def __str__(self) -> str: """Returns path of self.""" - return f"{self.root_path}#{self.name}" + return f"{self.root_path}#{self._name}" + + @property + def name(self) -> str: + """Name of the path.""" + return self._name.split("/")[-1] + + def mkdir(self, parents: bool = False, exist_ok: bool = False) -> None: + """Make directory. + + Parameters + ---------- + parents : bool, optional + If true, any missing parents of this directory are created as well. + exist_ok : bool, optional + If true, no error will be raised if the target directory already exists. + """ + if self._name in self._keys: + if not exist_ok: + raise FileExistsError(f"{self} already exists") + return + if parents: + self.root.require_group(self._name) + else: + self.root.create_group(self._name) diff --git a/source/tests/pt/test_stat.py b/source/tests/pt/test_stat.py index bc95575a5a..12af6ba866 100644 --- a/source/tests/pt/test_stat.py +++ b/source/tests/pt/test_stat.py @@ -2,16 +2,24 @@ import json import os import unittest +from abc import ( + ABC, + abstractmethod, +) from pathlib import ( Path, ) +import dpdata import numpy as np import torch from deepmd.pt.model.descriptor import ( DescrptSeA, ) +from deepmd.pt.model.descriptor.dpa1 import ( + DescrptDPA1, +) from deepmd.pt.utils import ( env, ) @@ -26,6 +34,7 @@ expand_sys_str, ) from deepmd.tf.descriptor.se_a import DescrptSeA as DescrptSeA_tf +from deepmd.tf.descriptor.se_atten import DescrptSeAtten as DescrptSeAtten_tf from deepmd.tf.fit.ener import ( EnerFitting, ) @@ -50,12 +59,29 @@ def compare(ut, base, given): ut.assertEqual(base, given) -class TestDataset(unittest.TestCase): +class DatasetTest(ABC): + @abstractmethod + def setup_data(self): + pass + + @abstractmethod + def setup_tf(self): + pass + + @abstractmethod + def setup_pt(self): + pass + + @abstractmethod + def tf_compute_input_stats(self): + pass + def setUp(self): with open(str(Path(__file__).parent / "water/se_e2_a.json")) as fin: content = fin.read() config = json.loads(content) - data_file = [str(Path(__file__).parent / "water/data/data_0")] + data_file = [self.setup_data()] + config["training"]["training_data"]["systems"] = data_file config["training"]["validation_data"]["systems"] = data_file model_config = config["model"] @@ -97,13 +123,7 @@ def setUp(self): self.dp_sampled = dp_make(dp_dataset, self.data_stat_nbatch, False) self.dp_merged = dp_merge(self.dp_sampled) self.dp_mesh = self.dp_merged.pop("default_mesh") - self.dp_d = DescrptSeA_tf( - rcut=self.rcut, - rcut_smth=self.rcut_smth, - sel=self.sel, - neuron=self.filter_neuron, - axis_neuron=self.axis_neuron, - ) + self.dp_d = self.setup_tf() def test_stat_output(self): def my_merge(energy, natoms): @@ -147,16 +167,9 @@ def test_stat_input(self): """ def test_descriptor(self): - coord = self.dp_merged["coord"] - atype = self.dp_merged["type"] - natoms = self.dp_merged["natoms_vec"] - box = self.dp_merged["box"] - self.dp_d.compute_input_stats(coord, box, atype, natoms, self.dp_mesh, {}) + self.tf_compute_input_stats() - my_en = DescrptSeA( - self.rcut, self.rcut_smth, self.sel, self.filter_neuron, self.axis_neuron - ) - my_en = my_en.sea # get the block who has stat as private vars + my_en = self.setup_pt() sampled = self.my_sampled for sys in sampled: for key in [ @@ -170,20 +183,102 @@ def test_descriptor(self): if key in sys.keys(): sys[key] = sys[key].to(env.DEVICE) stat_dict = my_en.compute_input_stats(sampled) - my_en.init_desc_stat(**stat_dict) my_en.mean = my_en.mean my_en.stddev = my_en.stddev - self.assertTrue( - np.allclose( - self.dp_d.davg.reshape([-1]), my_en.mean.cpu().reshape([-1]), rtol=0.01 - ) + np.testing.assert_allclose( + self.dp_d.davg.reshape([-1]), + my_en.mean.cpu().reshape([-1]), + rtol=1e-14, + atol=1e-14, + ) + np.testing.assert_allclose( + self.dp_d.dstd.reshape([-1]), + my_en.stddev.cpu().reshape([-1]), + rtol=1e-14, + atol=1e-14, ) - self.assertTrue( - np.allclose( - self.dp_d.dstd.reshape([-1]), - my_en.stddev.cpu().reshape([-1]), - rtol=0.01, - ) + + +class TestDatasetNoMixed(DatasetTest, unittest.TestCase): + def setup_data(self): + original_data = str(Path(__file__).parent / "water/data/data_0") + picked_data = str(Path(__file__).parent / "picked_data_for_test_stat") + dpdata.LabeledSystem(original_data, fmt="deepmd/npy")[:2].to_deepmd_npy( + picked_data + ) + self.mixed_type = False + return picked_data + + def setup_tf(self): + return DescrptSeA_tf( + rcut=self.rcut, + rcut_smth=self.rcut_smth, + sel=self.sel, + neuron=self.filter_neuron, + axis_neuron=self.axis_neuron, + ) + + def setup_pt(self): + return DescrptSeA( + self.rcut, self.rcut_smth, self.sel, self.filter_neuron, self.axis_neuron + ).sea # get the block who has stat as private vars + + def tf_compute_input_stats(self): + coord = self.dp_merged["coord"] + atype = self.dp_merged["type"] + natoms = self.dp_merged["natoms_vec"] + box = self.dp_merged["box"] + self.dp_d.compute_input_stats(coord, box, atype, natoms, self.dp_mesh, {}) + + +class TestDatasetMixed(DatasetTest, unittest.TestCase): + def setup_data(self): + original_data = str(Path(__file__).parent / "water/data/data_0") + picked_data = str(Path(__file__).parent / "picked_data_for_test_stat") + dpdata.LabeledSystem(original_data, fmt="deepmd/npy")[:2].to_deepmd_npy_mixed( + picked_data + ) + self.mixed_type = True + return picked_data + + def setup_tf(self): + return DescrptSeAtten_tf( + ntypes=2, + rcut=self.rcut, + rcut_smth=self.rcut_smth, + sel=sum(self.sel), + neuron=self.filter_neuron, + axis_neuron=self.axis_neuron, + set_davg_zero=False, + ) + + def setup_pt(self): + return DescrptDPA1( + self.rcut, + self.rcut_smth, + sum(self.sel), + 2, + self.filter_neuron, + self.axis_neuron, + set_davg_zero=False, + ).se_atten + + def tf_compute_input_stats(self): + coord = self.dp_merged["coord"] + atype = self.dp_merged["type"] + natoms = self.dp_merged["natoms_vec"] + box = self.dp_merged["box"] + real_natoms_vec = self.dp_merged["real_natoms_vec"] + + self.dp_d.compute_input_stats( + coord, + box, + atype, + natoms, + self.dp_mesh, + {}, + mixed_type=True, + real_natoms_vec=real_natoms_vec, ) From db6c666eb5d7ca9461a83799570511990ade9d70 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 18 Feb 2024 15:04:18 -0500 Subject: [PATCH 093/686] gh actions: fix branches ignore pattern & fix activity types (#3290) Signed-off-by: Jinzhe Zeng --- .github/workflows/build_cc.yml | 2 +- .github/workflows/build_wheel.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/package_c.yml | 2 +- .github/workflows/test_cc.yml | 2 +- .github/workflows/test_cuda.yml | 3 ++- .github/workflows/test_python.yml | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build_cc.yml b/.github/workflows/build_cc.yml index e85742cd7e..adcb615a0a 100644 --- a/.github/workflows/build_cc.yml +++ b/.github/workflows/build_cc.yml @@ -1,7 +1,7 @@ on: push: branches-ignore: - - "gh-readonly-queue/*" + - "gh-readonly-queue/**" pull_request: merge_group: concurrency: diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index b8e8256b3c..467969f7a2 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -3,7 +3,7 @@ name: Build and upload to PyPI on: push: branches-ignore: - - "gh-readonly-queue/*" + - "gh-readonly-queue/**" pull_request: merge_group: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index babed2b937..c912ece8d5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -3,7 +3,7 @@ name: "CodeQL" on: push: branches-ignore: - - "gh-readonly-queue/*" + - "gh-readonly-queue/**" pull_request: schedule: - cron: '45 2 * * 2' diff --git a/.github/workflows/package_c.yml b/.github/workflows/package_c.yml index a9d4606a22..ac93bb79a2 100644 --- a/.github/workflows/package_c.yml +++ b/.github/workflows/package_c.yml @@ -3,7 +3,7 @@ name: Build C library on: push: branches-ignore: - - "gh-readonly-queue/*" + - "gh-readonly-queue/**" pull_request: merge_group: concurrency: diff --git a/.github/workflows/test_cc.yml b/.github/workflows/test_cc.yml index 6a2865712f..2082e7e4cc 100644 --- a/.github/workflows/test_cc.yml +++ b/.github/workflows/test_cc.yml @@ -1,7 +1,7 @@ on: push: branches-ignore: - - "gh-readonly-queue/*" + - "gh-readonly-queue/**" pull_request: merge_group: concurrency: diff --git a/.github/workflows/test_cuda.yml b/.github/workflows/test_cuda.yml index 26e6fabcfe..0d934e6d77 100644 --- a/.github/workflows/test_cuda.yml +++ b/.github/workflows/test_cuda.yml @@ -5,7 +5,8 @@ on: types: - "labeled" # to let the PR pass the test - - "created" + - "opened" + - "reopened" - "synchronize" merge_group: concurrency: diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index e6081305e4..60b5ecf0e0 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -1,7 +1,7 @@ on: push: branches-ignore: - - "gh-readonly-queue/*" + - "gh-readonly-queue/**" pull_request: merge_group: concurrency: From 6451cdba0a9129a7496eadf35028404e056aae0d Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Mon, 19 Feb 2024 04:11:49 +0800 Subject: [PATCH 094/686] Feat: Refactor dipole fitting pytorch (#3281) This PR is to provide implementation of equivariant diplole fitting in pytorch and backend-independent numpy. --------- Signed-off-by: Anyang Peng <137014849+anyangml@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/dpmodel/fitting/__init__.py | 4 + deepmd/dpmodel/fitting/dipole_fitting.py | 199 +++++++++++ deepmd/dpmodel/fitting/general_fitting.py | 333 +++++++++++++++++++ deepmd/dpmodel/fitting/invar_fitting.py | 256 +++----------- deepmd/pt/model/task/dipole.py | 179 +++++++--- deepmd/pt/model/task/ener.py | 47 +-- deepmd/pt/model/task/fitting.py | 59 ++-- source/tests/pt/model/test_dipole_fitting.py | 273 +++++++++++++++ 8 files changed, 1041 insertions(+), 309 deletions(-) create mode 100644 deepmd/dpmodel/fitting/dipole_fitting.py create mode 100644 deepmd/dpmodel/fitting/general_fitting.py create mode 100644 source/tests/pt/model/test_dipole_fitting.py diff --git a/deepmd/dpmodel/fitting/__init__.py b/deepmd/dpmodel/fitting/__init__.py index 2bd5e23f5b..2da752eaa7 100644 --- a/deepmd/dpmodel/fitting/__init__.py +++ b/deepmd/dpmodel/fitting/__init__.py @@ -1,4 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from .dipole_fitting import ( + DipoleFitting, +) from .invar_fitting import ( InvarFitting, ) @@ -9,4 +12,5 @@ __all__ = [ "InvarFitting", "make_base_fitting", + "DipoleFitting", ] diff --git a/deepmd/dpmodel/fitting/dipole_fitting.py b/deepmd/dpmodel/fitting/dipole_fitting.py new file mode 100644 index 0000000000..64cad75b62 --- /dev/null +++ b/deepmd/dpmodel/fitting/dipole_fitting.py @@ -0,0 +1,199 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Any, + Dict, + List, + Optional, +) + +import numpy as np + +from deepmd.dpmodel import ( + DEFAULT_PRECISION, +) +from deepmd.dpmodel.output_def import ( + FittingOutputDef, + OutputVariableDef, + fitting_check_output, +) + +from .general_fitting import ( + GeneralFitting, +) + + +@fitting_check_output +class DipoleFitting(GeneralFitting): + r"""Fitting rotationally invariant diploe of the system. + + Parameters + ---------- + var_name + The name of the output variable. + ntypes + The number of atom types. + dim_descrpt + The dimension of the input descriptor. + dim_rot_mat : int + The dimension of rotation matrix, m1. + neuron + Number of neurons :math:`N` in each hidden layer of the fitting net + resnet_dt + Time-step `dt` in the resnet construction: + :math:`y = x + dt * \phi (Wx + b)` + numb_fparam + Number of frame parameter + numb_aparam + Number of atomic parameter + rcond + The condition number for the regression of atomic energy. + tot_ener_zero + Force the total energy to zero. Useful for the charge fitting. + trainable + If the weights of fitting net are trainable. + Suppose that we have :math:`N_l` hidden layers in the fitting net, + this list is of length :math:`N_l + 1`, specifying if the hidden layers and the output layer are trainable. + atom_ener + Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. + activation_function + The activation function :math:`\boldsymbol{\phi}` in the embedding net. Supported options are |ACTIVATION_FN| + precision + The precision of the embedding net parameters. Supported options are |PRECISION| + layer_name : list[Optional[str]], optional + The name of the each layer. If two layers, either in the same fitting or different fittings, + have the same name, they will share the same neural network parameters. + use_aparam_as_mask: bool, optional + If True, the atomic parameters will be used as a mask that determines the atom is real/virtual. + And the aparam will not be used as the atomic parameters for embedding. + distinguish_types + Different atomic types uses different fitting net. + + """ + + def __init__( + self, + var_name: str, + ntypes: int, + dim_descrpt: int, + dim_rot_mat: int, + neuron: List[int] = [120, 120, 120], + resnet_dt: bool = True, + numb_fparam: int = 0, + numb_aparam: int = 0, + rcond: Optional[float] = None, + tot_ener_zero: bool = False, + trainable: Optional[List[bool]] = None, + atom_ener: Optional[List[Optional[float]]] = None, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + layer_name: Optional[List[Optional[str]]] = None, + use_aparam_as_mask: bool = False, + spin: Any = None, + distinguish_types: bool = False, + exclude_types: List[int] = [], + old_impl=False, + ): + # seed, uniform_seed are not included + if tot_ener_zero: + raise NotImplementedError("tot_ener_zero is not implemented") + if spin is not None: + raise NotImplementedError("spin is not implemented") + if use_aparam_as_mask: + raise NotImplementedError("use_aparam_as_mask is not implemented") + if layer_name is not None: + raise NotImplementedError("layer_name is not implemented") + if atom_ener is not None: + raise NotImplementedError("atom_ener is not implemented") + + self.dim_rot_mat = dim_rot_mat + super().__init__( + var_name=var_name, + ntypes=ntypes, + dim_descrpt=dim_descrpt, + neuron=neuron, + resnet_dt=resnet_dt, + numb_fparam=numb_fparam, + numb_aparam=numb_aparam, + rcond=rcond, + tot_ener_zero=tot_ener_zero, + trainable=trainable, + atom_ener=atom_ener, + activation_function=activation_function, + precision=precision, + layer_name=layer_name, + use_aparam_as_mask=use_aparam_as_mask, + spin=spin, + distinguish_types=distinguish_types, + exclude_types=exclude_types, + ) + self.old_impl = False + + def _net_out_dim(self): + """Set the FittingNet output dim.""" + return self.dim_rot_mat + + def serialize(self) -> dict: + data = super().serialize() + data["dim_rot_mat"] = self.dim_rot_mat + data["old_impl"] = self.old_impl + return data + + def output_def(self): + return FittingOutputDef( + [ + OutputVariableDef( + self.var_name, + [3], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ), + ] + ) + + def call( + self, + descriptor: np.ndarray, + atype: np.ndarray, + gr: Optional[np.ndarray] = None, + g2: Optional[np.ndarray] = None, + h2: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + ) -> Dict[str, np.ndarray]: + """Calculate the fitting. + + Parameters + ---------- + descriptor + input descriptor. shape: nf x nloc x nd + atype + the atom type. shape: nf x nloc + gr + The rotationally equivariant and permutationally invariant single particle + representation. shape: nf x nloc x ng x 3 + g2 + The rotationally invariant pair-partical representation. + shape: nf x nloc x nnei x ng + h2 + The rotationally equivariant pair-partical representation. + shape: nf x nloc x nnei x 3 + fparam + The frame parameter. shape: nf x nfp. nfp being `numb_fparam` + aparam + The atomic parameter. shape: nf x nloc x nap. nap being `numb_aparam` + + """ + nframes, nloc, _ = descriptor.shape + assert gr is not None, "Must provide the rotation matrix for dipole fitting." + # (nframes, nloc, m1) + out = self._call_common(descriptor, atype, gr, g2, h2, fparam, aparam)[ + self.var_name + ] + # (nframes * nloc, 1, m1) + out = out.reshape(-1, 1, self.dim_rot_mat) + # (nframes * nloc, m1, 3) + gr = gr.reshape(nframes * nloc, -1, 3) + # (nframes, nloc, 3) + out = np.einsum("bim,bmj->bij", out, gr).squeeze(-2).reshape(nframes, nloc, 3) + return {self.var_name: out} diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py new file mode 100644 index 0000000000..d585ed1c97 --- /dev/null +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -0,0 +1,333 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +from abc import ( + abstractmethod, +) +from typing import ( + Any, + Dict, + List, + Optional, +) + +import numpy as np + +from deepmd.dpmodel import ( + DEFAULT_PRECISION, + NativeOP, +) +from deepmd.dpmodel.utils import ( + AtomExcludeMask, + FittingNet, + NetworkCollection, +) + +from .base_fitting import ( + BaseFitting, +) + + +class GeneralFitting(NativeOP, BaseFitting): + r"""General fitting class. + + Parameters + ---------- + var_name + The name of the output variable. + ntypes + The number of atom types. + dim_descrpt + The dimension of the input descriptor. + neuron + Number of neurons :math:`N` in each hidden layer of the fitting net + resnet_dt + Time-step `dt` in the resnet construction: + :math:`y = x + dt * \phi (Wx + b)` + numb_fparam + Number of frame parameter + numb_aparam + Number of atomic parameter + rcond + The condition number for the regression of atomic energy. + tot_ener_zero + Force the total energy to zero. Useful for the charge fitting. + trainable + If the weights of fitting net are trainable. + Suppose that we have :math:`N_l` hidden layers in the fitting net, + this list is of length :math:`N_l + 1`, specifying if the hidden layers and the output layer are trainable. + atom_ener + Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. + activation_function + The activation function :math:`\boldsymbol{\phi}` in the embedding net. Supported options are |ACTIVATION_FN| + precision + The precision of the embedding net parameters. Supported options are |PRECISION| + layer_name : list[Optional[str]], optional + The name of the each layer. If two layers, either in the same fitting or different fittings, + have the same name, they will share the same neural network parameters. + use_aparam_as_mask: bool, optional + If True, the atomic parameters will be used as a mask that determines the atom is real/virtual. + And the aparam will not be used as the atomic parameters for embedding. + distinguish_types + Different atomic types uses different fitting net. + + """ + + def __init__( + self, + var_name: str, + ntypes: int, + dim_descrpt: int, + neuron: List[int] = [120, 120, 120], + resnet_dt: bool = True, + numb_fparam: int = 0, + numb_aparam: int = 0, + rcond: Optional[float] = None, + tot_ener_zero: bool = False, + trainable: Optional[List[bool]] = None, + atom_ener: Optional[List[float]] = None, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + layer_name: Optional[List[Optional[str]]] = None, + use_aparam_as_mask: bool = False, + spin: Any = None, + distinguish_types: bool = False, + exclude_types: List[int] = [], + ): + self.var_name = var_name + self.ntypes = ntypes + self.dim_descrpt = dim_descrpt + self.neuron = neuron + self.resnet_dt = resnet_dt + self.numb_fparam = numb_fparam + self.numb_aparam = numb_aparam + self.rcond = rcond + self.tot_ener_zero = tot_ener_zero + self.trainable = trainable + self.atom_ener = atom_ener + self.activation_function = activation_function + self.precision = precision + self.layer_name = layer_name + self.use_aparam_as_mask = use_aparam_as_mask + self.spin = spin + self.distinguish_types = distinguish_types + self.exclude_types = exclude_types + if self.spin is not None: + raise NotImplementedError("spin is not supported") + + self.emask = AtomExcludeMask(self.ntypes, self.exclude_types) + + net_dim_out = self._net_out_dim() + # init constants + self.bias_atom_e = np.zeros([self.ntypes, net_dim_out]) + if self.numb_fparam > 0: + self.fparam_avg = np.zeros(self.numb_fparam) + self.fparam_inv_std = np.ones(self.numb_fparam) + else: + self.fparam_avg, self.fparam_inv_std = None, None + if self.numb_aparam > 0: + self.aparam_avg = np.zeros(self.numb_aparam) + self.aparam_inv_std = np.ones(self.numb_aparam) + else: + self.aparam_avg, self.aparam_inv_std = None, None + # init networks + in_dim = self.dim_descrpt + self.numb_fparam + self.numb_aparam + self.nets = NetworkCollection( + 1 if self.distinguish_types else 0, + self.ntypes, + network_type="fitting_network", + networks=[ + FittingNet( + in_dim, + net_dim_out, + self.neuron, + self.activation_function, + self.resnet_dt, + self.precision, + bias_out=True, + ) + for ii in range(self.ntypes if self.distinguish_types else 1) + ], + ) + + @abstractmethod + def _net_out_dim(self): + """Set the FittingNet output dim.""" + pass + + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this atomic model.""" + return self.numb_fparam + + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this atomic model.""" + return self.numb_aparam + + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return [] + + def __setitem__(self, key, value): + if key in ["bias_atom_e"]: + self.bias_atom_e = value + elif key in ["fparam_avg"]: + self.fparam_avg = value + elif key in ["fparam_inv_std"]: + self.fparam_inv_std = value + elif key in ["aparam_avg"]: + self.aparam_avg = value + elif key in ["aparam_inv_std"]: + self.aparam_inv_std = value + else: + raise KeyError(key) + + def __getitem__(self, key): + if key in ["bias_atom_e"]: + return self.bias_atom_e + elif key in ["fparam_avg"]: + return self.fparam_avg + elif key in ["fparam_inv_std"]: + return self.fparam_inv_std + elif key in ["aparam_avg"]: + return self.aparam_avg + elif key in ["aparam_inv_std"]: + return self.aparam_inv_std + else: + raise KeyError(key) + + def serialize(self) -> dict: + """Serialize the fitting to dict.""" + return { + "var_name": self.var_name, + "ntypes": self.ntypes, + "dim_descrpt": self.dim_descrpt, + "neuron": self.neuron, + "resnet_dt": self.resnet_dt, + "numb_fparam": self.numb_fparam, + "numb_aparam": self.numb_aparam, + "rcond": self.rcond, + "activation_function": self.activation_function, + "precision": self.precision, + "distinguish_types": self.distinguish_types, + "exclude_types": self.exclude_types, + "nets": self.nets.serialize(), + "@variables": { + "bias_atom_e": self.bias_atom_e, + "fparam_avg": self.fparam_avg, + "fparam_inv_std": self.fparam_inv_std, + "aparam_avg": self.aparam_avg, + "aparam_inv_std": self.aparam_inv_std, + }, + # not supported + "tot_ener_zero": self.tot_ener_zero, + "trainable": self.trainable, + "atom_ener": self.atom_ener, + "layer_name": self.layer_name, + "use_aparam_as_mask": self.use_aparam_as_mask, + "spin": self.spin, + } + + @classmethod + def deserialize(cls, data: dict) -> "GeneralFitting": + data = copy.deepcopy(data) + variables = data.pop("@variables") + nets = data.pop("nets") + obj = cls(**data) + for kk in variables.keys(): + obj[kk] = variables[kk] + obj.nets = NetworkCollection.deserialize(nets) + return obj + + def _call_common( + self, + descriptor: np.ndarray, + atype: np.ndarray, + gr: Optional[np.ndarray] = None, + g2: Optional[np.ndarray] = None, + h2: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + ) -> Dict[str, np.ndarray]: + """Calculate the fitting. + + Parameters + ---------- + descriptor + input descriptor. shape: nf x nloc x nd + atype + the atom type. shape: nf x nloc + gr + The rotationally equivariant and permutationally invariant single particle + representation. shape: nf x nloc x ng x 3 + g2 + The rotationally invariant pair-partical representation. + shape: nf x nloc x nnei x ng + h2 + The rotationally equivariant pair-partical representation. + shape: nf x nloc x nnei x 3 + fparam + The frame parameter. shape: nf x nfp. nfp being `numb_fparam` + aparam + The atomic parameter. shape: nf x nloc x nap. nap being `numb_aparam` + + """ + nf, nloc, nd = descriptor.shape + net_dim_out = self._net_out_dim() + # check input dim + if nd != self.dim_descrpt: + raise ValueError( + "get an input descriptor of dim {nd}," + "which is not consistent with {self.dim_descrpt}." + ) + xx = descriptor + # check fparam dim, concate to input descriptor + if self.numb_fparam > 0: + assert fparam is not None, "fparam should not be None" + if fparam.shape[-1] != self.numb_fparam: + raise ValueError( + "get an input fparam of dim {fparam.shape[-1]}, ", + "which is not consistent with {self.numb_fparam}.", + ) + fparam = (fparam - self.fparam_avg) * self.fparam_inv_std + fparam = np.tile(fparam.reshape([nf, 1, self.numb_fparam]), [1, nloc, 1]) + xx = np.concatenate( + [xx, fparam], + axis=-1, + ) + # check aparam dim, concate to input descriptor + if self.numb_aparam > 0: + assert aparam is not None, "aparam should not be None" + if aparam.shape[-1] != self.numb_aparam: + raise ValueError( + "get an input aparam of dim {aparam.shape[-1]}, ", + "which is not consistent with {self.numb_aparam}.", + ) + aparam = aparam.reshape([nf, nloc, self.numb_aparam]) + aparam = (aparam - self.aparam_avg) * self.aparam_inv_std + xx = np.concatenate( + [xx, aparam], + axis=-1, + ) + + # calcualte the prediction + if self.distinguish_types: + outs = np.zeros([nf, nloc, net_dim_out]) + for type_i in range(self.ntypes): + mask = np.tile( + (atype == type_i).reshape([nf, nloc, 1]), [1, 1, net_dim_out] + ) + atom_energy = self.nets[(type_i,)](xx) + atom_energy = atom_energy + self.bias_atom_e[type_i] + atom_energy = atom_energy * mask + outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] + else: + outs = self.nets[()](xx) + self.bias_atom_e[atype] + # nf x nloc + exclude_mask = self.emask.build_type_exclude_mask(atype) + # nf x nloc x nod + outs = outs * exclude_mask[:, :, None] + return {self.var_name: outs} diff --git a/deepmd/dpmodel/fitting/invar_fitting.py b/deepmd/dpmodel/fitting/invar_fitting.py index 0c2a6006cc..80c51154d6 100644 --- a/deepmd/dpmodel/fitting/invar_fitting.py +++ b/deepmd/dpmodel/fitting/invar_fitting.py @@ -1,5 +1,4 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import copy from typing import ( Any, Dict, @@ -11,26 +10,20 @@ from deepmd.dpmodel import ( DEFAULT_PRECISION, - NativeOP, ) from deepmd.dpmodel.output_def import ( FittingOutputDef, OutputVariableDef, fitting_check_output, ) -from deepmd.dpmodel.utils import ( - AtomExcludeMask, - FittingNet, - NetworkCollection, -) -from .base_fitting import ( - BaseFitting, +from .general_fitting import ( + GeneralFitting, ) @fitting_check_output -class InvarFitting(NativeOP, BaseFitting): +class InvarFitting(GeneralFitting): r"""Fitting the energy (or a rotationally invariant porperty of `dim_out`) of the system. The force and the virial can also be trained. Lets take the energy fitting task as an example. @@ -143,62 +136,45 @@ def __init__( if atom_ener is not None: raise NotImplementedError("atom_ener is not implemented") - self.var_name = var_name - self.ntypes = ntypes - self.dim_descrpt = dim_descrpt self.dim_out = dim_out - self.neuron = neuron - self.resnet_dt = resnet_dt - self.numb_fparam = numb_fparam - self.numb_aparam = numb_aparam - self.rcond = rcond - self.tot_ener_zero = tot_ener_zero - self.trainable = trainable - self.atom_ener = atom_ener - self.activation_function = activation_function - self.precision = precision - self.layer_name = layer_name - self.use_aparam_as_mask = use_aparam_as_mask - self.spin = spin - self.distinguish_types = distinguish_types - self.exclude_types = exclude_types - if self.spin is not None: - raise NotImplementedError("spin is not supported") - self.emask = AtomExcludeMask(self.ntypes, exclude_types=self.exclude_types) - - # init constants - self.bias_atom_e = np.zeros([self.ntypes, self.dim_out]) - if self.numb_fparam > 0: - self.fparam_avg = np.zeros(self.numb_fparam) - self.fparam_inv_std = np.ones(self.numb_fparam) - else: - self.fparam_avg, self.fparam_inv_std = None, None - if self.numb_aparam > 0: - self.aparam_avg = np.zeros(self.numb_aparam) - self.aparam_inv_std = np.ones(self.numb_aparam) - else: - self.aparam_avg, self.aparam_inv_std = None, None - # init networks - in_dim = self.dim_descrpt + self.numb_fparam + self.numb_aparam - out_dim = self.dim_out - self.nets = NetworkCollection( - 1 if self.distinguish_types else 0, - self.ntypes, - network_type="fitting_network", - networks=[ - FittingNet( - in_dim, - out_dim, - self.neuron, - self.activation_function, - self.resnet_dt, - self.precision, - bias_out=True, - ) - for ii in range(self.ntypes if self.distinguish_types else 1) - ], + super().__init__( + var_name=var_name, + ntypes=ntypes, + dim_descrpt=dim_descrpt, + neuron=neuron, + resnet_dt=resnet_dt, + numb_fparam=numb_fparam, + numb_aparam=numb_aparam, + rcond=rcond, + tot_ener_zero=tot_ener_zero, + trainable=trainable, + atom_ener=atom_ener, + activation_function=activation_function, + precision=precision, + layer_name=layer_name, + use_aparam_as_mask=use_aparam_as_mask, + spin=spin, + distinguish_types=distinguish_types, + exclude_types=exclude_types, ) + def serialize(self) -> dict: + data = super().serialize() + data["dim_out"] = self.dim_out + return data + + def _net_out_dim(self): + """Set the FittingNet output dim.""" + return self.dim_out + + def compute_output_stats(self, merged): + """Update the output bias for fitting net.""" + raise NotImplementedError + + def init_fitting_stat(self, result_dict): + """Initialize the model bias by the statistics.""" + raise NotImplementedError + def output_def(self): return FittingOutputDef( [ @@ -212,96 +188,16 @@ def output_def(self): ] ) - def __setitem__(self, key, value): - if key in ["bias_atom_e"]: - self.bias_atom_e = value - elif key in ["fparam_avg"]: - self.fparam_avg = value - elif key in ["fparam_inv_std"]: - self.fparam_inv_std = value - elif key in ["aparam_avg"]: - self.aparam_avg = value - elif key in ["aparam_inv_std"]: - self.aparam_inv_std = value - else: - raise KeyError(key) - - def __getitem__(self, key): - if key in ["bias_atom_e"]: - return self.bias_atom_e - elif key in ["fparam_avg"]: - return self.fparam_avg - elif key in ["fparam_inv_std"]: - return self.fparam_inv_std - elif key in ["aparam_avg"]: - return self.aparam_avg - elif key in ["aparam_inv_std"]: - return self.aparam_inv_std - else: - raise KeyError(key) - - def compute_output_stats(self, merged): - """Update the output bias for fitting net.""" - raise NotImplementedError - - def init_fitting_stat(self, result_dict): - """Initialize the model bias by the statistics.""" - raise NotImplementedError - - def serialize(self) -> dict: - """Serialize the fitting to dict.""" - return { - "var_name": self.var_name, - "ntypes": self.ntypes, - "dim_descrpt": self.dim_descrpt, - "dim_out": self.dim_out, - "neuron": self.neuron, - "resnet_dt": self.resnet_dt, - "numb_fparam": self.numb_fparam, - "numb_aparam": self.numb_aparam, - "rcond": self.rcond, - "activation_function": self.activation_function, - "precision": self.precision, - "distinguish_types": self.distinguish_types, - "nets": self.nets.serialize(), - "exclude_types": self.exclude_types, - "@variables": { - "bias_atom_e": self.bias_atom_e, - "fparam_avg": self.fparam_avg, - "fparam_inv_std": self.fparam_inv_std, - "aparam_avg": self.aparam_avg, - "aparam_inv_std": self.aparam_inv_std, - }, - # not supported - "tot_ener_zero": self.tot_ener_zero, - "trainable": self.trainable, - "atom_ener": self.atom_ener, - "layer_name": self.layer_name, - "use_aparam_as_mask": self.use_aparam_as_mask, - "spin": self.spin, - } - - @classmethod - def deserialize(cls, data: dict) -> "InvarFitting": - data = copy.deepcopy(data) - variables = data.pop("@variables") - nets = data.pop("nets") - obj = cls(**data) - for kk in variables.keys(): - obj[kk] = variables[kk] - obj.nets = NetworkCollection.deserialize(nets) - return obj - def call( self, - descriptor: np.array, - atype: np.array, - gr: Optional[np.array] = None, - g2: Optional[np.array] = None, - h2: Optional[np.array] = None, - fparam: Optional[np.array] = None, - aparam: Optional[np.array] = None, - ) -> Dict[str, np.array]: + descriptor: np.ndarray, + atype: np.ndarray, + gr: Optional[np.ndarray] = None, + g2: Optional[np.ndarray] = None, + h2: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + ) -> Dict[str, np.ndarray]: """Calculate the fitting. Parameters @@ -325,60 +221,4 @@ def call( The atomic parameter. shape: nf x nloc x nap. nap being `numb_aparam` """ - nf, nloc, nd = descriptor.shape - # check input dim - if nd != self.dim_descrpt: - raise ValueError( - "get an input descriptor of dim {nd}," - "which is not consistent with {self.dim_descrpt}." - ) - xx = descriptor - # check fparam dim, concate to input descriptor - if self.numb_fparam > 0: - assert fparam is not None, "fparam should not be None" - if fparam.shape[-1] != self.numb_fparam: - raise ValueError( - "get an input fparam of dim {fparam.shape[-1]}, ", - "which is not consistent with {self.numb_fparam}.", - ) - fparam = (fparam - self.fparam_avg) * self.fparam_inv_std - fparam = np.tile(fparam.reshape([nf, 1, self.numb_fparam]), [1, nloc, 1]) - xx = np.concatenate( - [xx, fparam], - axis=-1, - ) - # check aparam dim, concate to input descriptor - if self.numb_aparam > 0: - assert aparam is not None, "aparam should not be None" - if aparam.shape[-1] != self.numb_aparam: - raise ValueError( - "get an input aparam of dim {aparam.shape[-1]}, ", - "which is not consistent with {self.numb_aparam}.", - ) - aparam = aparam.reshape([nf, nloc, self.numb_aparam]) - aparam = (aparam - self.aparam_avg) * self.aparam_inv_std - xx = np.concatenate( - [xx, aparam], - axis=-1, - ) - - # calcualte the prediction - if self.distinguish_types: - outs = np.zeros([nf, nloc, self.dim_out]) - for type_i in range(self.ntypes): - mask = np.tile( - (atype == type_i).reshape([nf, nloc, 1]), [1, 1, self.dim_out] - ) - atom_energy = self.nets[(type_i,)](xx) - atom_energy = atom_energy + self.bias_atom_e[type_i] - atom_energy = atom_energy * mask - outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] - else: - outs = self.nets[()](xx) + self.bias_atom_e[atype] - - # nf x nloc - exclude_mask = self.emask.build_type_exclude_mask(atype) - # nf x nloc x nod - outs = outs * exclude_mask[:, :, None] - - return {self.var_name: outs} + return self._call_common(descriptor, atype, gr, g2, h2, fparam, aparam) diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py index aa518d2cd3..fedf4386c0 100644 --- a/deepmd/pt/model/task/dipole.py +++ b/deepmd/pt/model/task/dipole.py @@ -1,67 +1,152 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging +from typing import ( + List, + Optional, +) import torch -from deepmd.pt.model.network.network import ( - ResidualDeep, +from deepmd.dpmodel import ( + FittingOutputDef, + OutputVariableDef, ) from deepmd.pt.model.task.fitting import ( - Fitting, + GeneralFitting, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.env import ( + DEFAULT_PRECISION, ) log = logging.getLogger(__name__) -class DipoleFittingNet(Fitting): - def __init__( - self, ntypes, embedding_width, neuron, out_dim, resnet_dt=True, **kwargs - ): - """Construct a fitting net for dipole. +class DipoleFittingNet(GeneralFitting): + """Construct a general fitting net. - Args: - - ntypes: Element count. - - embedding_width: Embedding width per atom. - - neuron: Number of neurons in each hidden layers of the fitting net. - - bias_atom_e: Average enery per atom for each element. - - resnet_dt: Using time-step in the ResNet construction. - """ - super().__init__() - self.ntypes = ntypes - self.embedding_width = embedding_width - self.out_dim = out_dim + Parameters + ---------- + var_name : str + The atomic property to fit, 'dipole'. + ntypes : int + Element count. + dim_descrpt : int + Embedding width per atom. + dim_out : int + The output dimension of the fitting net. + dim_rot_mat : int + The dimension of rotation matrix, m1. + neuron : List[int] + Number of neurons in each hidden layers of the fitting net. + resnet_dt : bool + Using time-step in the ResNet construction. + numb_fparam : int + Number of frame parameters. + numb_aparam : int + Number of atomic parameters. + activation_function : str + Activation function. + precision : str + Numerical precision. + distinguish_types : bool + Neighbor list that distinguish different atomic types or not. + rcond : float, optional + The condition number for the regression of atomic energy. + seed : int, optional + Random seed. + """ - filter_layers = [] - one = ResidualDeep( - 0, embedding_width, neuron, 0.0, out_dim=self.out_dim, resnet_dt=resnet_dt + def __init__( + self, + var_name: str, + ntypes: int, + dim_descrpt: int, + dim_rot_mat: int, + neuron: List[int] = [128, 128, 128], + resnet_dt: bool = True, + numb_fparam: int = 0, + numb_aparam: int = 0, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + distinguish_types: bool = False, + rcond: Optional[float] = None, + seed: Optional[int] = None, + exclude_types: List[int] = [], + **kwargs, + ): + self.dim_rot_mat = dim_rot_mat + super().__init__( + var_name=var_name, + ntypes=ntypes, + dim_descrpt=dim_descrpt, + neuron=neuron, + resnet_dt=resnet_dt, + numb_fparam=numb_fparam, + numb_aparam=numb_aparam, + activation_function=activation_function, + precision=precision, + distinguish_types=distinguish_types, + rcond=rcond, + seed=seed, + exclude_types=exclude_types, + **kwargs, ) - filter_layers.append(one) - self.filter_layers = torch.nn.ModuleList(filter_layers) + self.old_impl = False # this only supports the new implementation. - if "seed" in kwargs: - log.info("Set seed to %d in fitting net.", kwargs["seed"]) - torch.manual_seed(kwargs["seed"]) + def _net_out_dim(self): + """Set the FittingNet output dim.""" + return self.dim_rot_mat - def forward(self, inputs, atype, atype_tebd, rot_mat): - """Based on embedding net output, alculate total energy. + def serialize(self) -> dict: + data = super().serialize() + data["dim_rot_mat"] = self.dim_rot_mat + data["old_impl"] = self.old_impl + return data - Args: - - inputs: Descriptor. Its shape is [nframes, nloc, self.embedding_width]. - - atype: Atom type. Its shape is [nframes, nloc]. - - atype_tebd: Atom type embedding. Its shape is [nframes, nloc, tebd_dim] - - rot_mat: GR during descriptor calculation. Its shape is [nframes * nloc, m1, 3]. + def output_def(self) -> FittingOutputDef: + return FittingOutputDef( + [ + OutputVariableDef( + self.var_name, + [3], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ), + ] + ) - Returns - ------- - - vec_out: output vector. Its shape is [nframes, nloc, 3]. + @property + def data_stat_key(self): """ - nframes, nloc, _ = inputs.size() - if atype_tebd is not None: - inputs = torch.concat([inputs, atype_tebd], dim=-1) - vec_out = self.filter_layers[0](inputs) # Shape is [nframes, nloc, m1] - assert list(vec_out.size()) == [nframes, nloc, self.out_dim] - vec_out = vec_out.view(-1, 1, self.out_dim) - vec_out = ( - torch.bmm(vec_out, rot_mat).squeeze(-2).view(nframes, nloc, 3) - ) # Shape is [nframes, nloc, 3] - return vec_out + Get the keys for the data statistic of the fitting. + Return a list of statistic names needed, such as "bias_atom_e". + """ + return [] + + def forward( + self, + descriptor: torch.Tensor, + atype: torch.Tensor, + gr: Optional[torch.Tensor] = None, + g2: Optional[torch.Tensor] = None, + h2: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + ): + nframes, nloc, _ = descriptor.shape + assert gr is not None, "Must provide the rotation matrix for dipole fitting." + # (nframes, nloc, m1) + out = self._forward_common(descriptor, atype, gr, g2, h2, fparam, aparam)[ + self.var_name + ] + # (nframes * nloc, 1, m1) + out = out.view(-1, 1, self.dim_rot_mat) + # (nframes * nloc, m1, 3) + gr = gr.view(nframes * nloc, -1, 3) + # (nframes, nloc, 3) + out = torch.bmm(out, gr).squeeze(-2).view(nframes, nloc, 3) + return {self.var_name: out.to(env.GLOBAL_PT_FLOAT_PRECISION)} diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 2f5afaf26e..343e5b7db5 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -95,11 +95,11 @@ def __init__( exclude_types: List[int] = [], **kwargs, ): + self.dim_out = dim_out super().__init__( var_name=var_name, ntypes=ntypes, dim_descrpt=dim_descrpt, - dim_out=dim_out, neuron=neuron, bias_atom_e=bias_atom_e, resnet_dt=resnet_dt, @@ -118,34 +118,10 @@ def _net_out_dim(self): """Set the FittingNet output dim.""" return self.dim_out - def __setitem__(self, key, value): - if key in ["bias_atom_e"]: - value = value.view([self.ntypes, self.dim_out]) - self.bias_atom_e = value - elif key in ["fparam_avg"]: - self.fparam_avg = value - elif key in ["fparam_inv_std"]: - self.fparam_inv_std = value - elif key in ["aparam_avg"]: - self.aparam_avg = value - elif key in ["aparam_inv_std"]: - self.aparam_inv_std = value - else: - raise KeyError(key) - - def __getitem__(self, key): - if key in ["bias_atom_e"]: - return self.bias_atom_e - elif key in ["fparam_avg"]: - return self.fparam_avg - elif key in ["fparam_inv_std"]: - return self.fparam_inv_std - elif key in ["aparam_avg"]: - return self.aparam_avg - elif key in ["aparam_inv_std"]: - return self.aparam_inv_std - else: - raise KeyError(key) + def serialize(self) -> dict: + data = super().serialize() + data["dim_out"] = self.dim_out + return data @property def data_stat_key(self): @@ -177,6 +153,19 @@ def compute_output_stats(self, merged, stat_file_path: Optional[DPPath] = None): ) ) + def output_def(self) -> FittingOutputDef: + return FittingOutputDef( + [ + OutputVariableDef( + self.var_name, + [self.dim_out], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ), + ] + ) + def forward( self, descriptor: torch.Tensor, diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index f8f6e3f5dc..5ae177941c 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -13,10 +13,6 @@ import numpy as np import torch -from deepmd.dpmodel import ( - FittingOutputDef, - OutputVariableDef, -) from deepmd.pt.model.network.mlp import ( FittingNet, NetworkCollection, @@ -294,7 +290,6 @@ def __init__( var_name: str, ntypes: int, dim_descrpt: int, - dim_out: int, neuron: List[int] = [128, 128, 128], bias_atom_e: Optional[torch.Tensor] = None, resnet_dt: bool = True, @@ -312,7 +307,6 @@ def __init__( self.var_name = var_name self.ntypes = ntypes self.dim_descrpt = dim_descrpt - self.dim_out = dim_out self.neuron = neuron self.distinguish_types = distinguish_types self.use_tebd = not self.distinguish_types @@ -327,11 +321,12 @@ def __init__( self.emask = AtomExcludeMask(self.ntypes, self.exclude_types) + net_dim_out = self._net_out_dim() # init constants if bias_atom_e is None: - bias_atom_e = np.zeros([self.ntypes, self.dim_out]) + bias_atom_e = np.zeros([self.ntypes, net_dim_out], dtype=np.float64) bias_atom_e = torch.tensor(bias_atom_e, dtype=self.prec, device=device) - bias_atom_e = bias_atom_e.view([self.ntypes, self.dim_out]) + bias_atom_e = bias_atom_e.view([self.ntypes, net_dim_out]) if not self.use_tebd: assert self.ntypes == bias_atom_e.shape[0], "Element count mismatches!" self.register_buffer("bias_atom_e", bias_atom_e) @@ -362,7 +357,6 @@ def __init__( in_dim = self.dim_descrpt + self.numb_fparam + self.numb_aparam self.old_impl = kwargs.get("old_impl", False) - net_dim_out = self._net_out_dim() if self.old_impl: filter_layers = [] for type_i in range(self.ntypes): @@ -407,7 +401,6 @@ def serialize(self) -> dict: "var_name": self.var_name, "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, - "dim_out": self.dim_out, "neuron": self.neuron, "resnet_dt": self.resnet_dt, "numb_fparam": self.numb_fparam, @@ -468,24 +461,40 @@ def get_sel_type(self) -> List[int]: """ return [] + def __setitem__(self, key, value): + if key in ["bias_atom_e"]: + value = value.view([self.ntypes, self._net_out_dim()]) + self.bias_atom_e = value + elif key in ["fparam_avg"]: + self.fparam_avg = value + elif key in ["fparam_inv_std"]: + self.fparam_inv_std = value + elif key in ["aparam_avg"]: + self.aparam_avg = value + elif key in ["aparam_inv_std"]: + self.aparam_inv_std = value + else: + raise KeyError(key) + + def __getitem__(self, key): + if key in ["bias_atom_e"]: + return self.bias_atom_e + elif key in ["fparam_avg"]: + return self.fparam_avg + elif key in ["fparam_inv_std"]: + return self.fparam_inv_std + elif key in ["aparam_avg"]: + return self.aparam_avg + elif key in ["aparam_inv_std"]: + return self.aparam_inv_std + else: + raise KeyError(key) + @abstractmethod def _net_out_dim(self): """Set the FittingNet output dim.""" pass - def output_def(self) -> FittingOutputDef: - return FittingOutputDef( - [ - OutputVariableDef( - self.var_name, - [self.dim_out], - reduciable=True, - r_differentiable=True, - c_differentiable=True, - ), - ] - ) - def _extend_f_avg_std(self, xx: torch.Tensor, nb: int) -> torch.Tensor: return torch.tile(xx.view([1, self.numb_fparam]), [nb, 1]) @@ -504,6 +513,7 @@ def _forward_common( ): xx = descriptor nf, nloc, nd = xx.shape + net_dim_out = self._net_out_dim() if nd != self.dim_descrpt: raise ValueError( @@ -551,7 +561,7 @@ def _forward_common( ) outs = torch.zeros( - (nf, nloc, self.dim_out), + (nf, nloc, net_dim_out), dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE, ) # jit assertion @@ -577,7 +587,6 @@ def _forward_common( ) outs = outs + atom_property # Shape is [nframes, natoms[0], 1] else: - net_dim_out = self._net_out_dim() for type_i, ll in enumerate(self.filter_layers.networks): mask = (atype == type_i).unsqueeze(-1) mask = torch.tile(mask, (1, 1, net_dim_out)) diff --git a/source/tests/pt/model/test_dipole_fitting.py b/source/tests/pt/model/test_dipole_fitting.py new file mode 100644 index 0000000000..fffed123e0 --- /dev/null +++ b/source/tests/pt/model/test_dipole_fitting.py @@ -0,0 +1,273 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import itertools +import unittest + +import numpy as np +import torch +from scipy.stats import ( + special_ortho_group, +) + +from deepmd.dpmodel.fitting import DipoleFitting as DPDipoleFitting +from deepmd.pt.model.descriptor.se_a import ( + DescrptSeA, +) +from deepmd.pt.model.task.dipole import ( + DipoleFittingNet, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.nlist import ( + extend_input_and_build_neighbor_list, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, +) + +from .test_env_mat import ( + TestCaseSingleFrameWithNlist, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION + + +class TestDipoleFitting(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + self.rng = np.random.default_rng() + self.nf, self.nloc, nnei = self.nlist.shape + self.dd0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) + + def test_consistency( + self, + ): + rd0, gr, _, _, _ = self.dd0( + torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), + torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), + torch.tensor(self.nlist, dtype=int, device=env.DEVICE), + ) + atype = torch.tensor( + self.atype_ext[:, : self.nloc], dtype=int, device=env.DEVICE + ) + + for distinguish_types, nfp, nap in itertools.product( + [True, False], + [0, 3], + [0, 4], + ): + ft0 = DipoleFittingNet( + "foo", + self.nt, + self.dd0.dim_out, + dim_rot_mat=self.dd0.get_dim_emb(), + numb_fparam=nfp, + numb_aparam=nap, + use_tebd=(not distinguish_types), + ).to(env.DEVICE) + ft1 = DPDipoleFitting.deserialize(ft0.serialize()) + ft2 = DipoleFittingNet.deserialize(ft1.serialize()) + + if nfp > 0: + ifp = torch.tensor( + self.rng.normal(size=(self.nf, nfp)), dtype=dtype, device=env.DEVICE + ) + else: + ifp = None + if nap > 0: + iap = torch.tensor( + self.rng.normal(size=(self.nf, self.nloc, nap)), + dtype=dtype, + device=env.DEVICE, + ) + else: + iap = None + + ret0 = ft0(rd0, atype, gr, fparam=ifp, aparam=iap) + ret1 = ft1( + rd0.detach().cpu().numpy(), + atype.detach().cpu().numpy(), + gr.detach().cpu().numpy(), + fparam=to_numpy_array(ifp), + aparam=to_numpy_array(iap), + ) + ret2 = ft2(rd0, atype, gr, fparam=ifp, aparam=iap) + np.testing.assert_allclose( + to_numpy_array(ret0["foo"]), + ret1["foo"], + ) + np.testing.assert_allclose( + to_numpy_array(ret0["foo"]), + to_numpy_array(ret2["foo"]), + ) + + def test_jit( + self, + ): + for distinguish_types, nfp, nap in itertools.product( + [True, False], + [0, 3], + [0, 4], + ): + ft0 = DipoleFittingNet( + "foo", + self.nt, + self.dd0.dim_out, + dim_rot_mat=self.dd0.get_dim_emb(), + numb_fparam=nfp, + numb_aparam=nap, + use_tebd=(not distinguish_types), + ).to(env.DEVICE) + torch.jit.script(ft0) + + +class TestEquivalence(unittest.TestCase): + def setUp(self) -> None: + self.natoms = 5 + self.rcut = 4 + self.rcut_smth = 0.5 + self.sel = [46, 92, 4] + self.nf = 1 + self.coord = 2 * torch.rand([self.natoms, 3], dtype=dtype).to(env.DEVICE) + self.shift = torch.tensor([4, 4, 4], dtype=dtype).to(env.DEVICE) + self.atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + self.dd0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) + self.cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) + self.cell = (self.cell + self.cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) + + def test_rot(self): + atype = self.atype.reshape(1, 5) + rmat = torch.tensor(special_ortho_group.rvs(3), dtype=dtype).to(env.DEVICE) + coord_rot = torch.matmul(self.coord, rmat) + rng = np.random.default_rng() + for distinguish_types, nfp, nap in itertools.product( + [True, False], + [0, 3], + [0, 4], + ): + ft0 = DipoleFittingNet( + "foo", + 3, # ntype + self.dd0.dim_out, # dim_descrpt + dim_rot_mat=self.dd0.get_dim_emb(), + numb_fparam=nfp, + numb_aparam=nap, + use_tebd=False, + ).to(env.DEVICE) + if nfp > 0: + ifp = torch.tensor( + rng.normal(size=(self.nf, nfp)), dtype=dtype, device=env.DEVICE + ) + else: + ifp = None + if nap > 0: + iap = torch.tensor( + rng.normal(size=(self.nf, self.natoms, nap)), + dtype=dtype, + device=env.DEVICE, + ) + else: + iap = None + + res = [] + for xyz in [self.coord, coord_rot]: + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + xyz + self.shift, atype, self.rcut, self.sel, distinguish_types + ) + + rd0, gr0, _, _, _ = self.dd0( + extended_coord, + extended_atype, + nlist, + ) + + ret0 = ft0(rd0, extended_atype, gr0, fparam=ifp, aparam=iap) + res.append(ret0["foo"]) + + np.testing.assert_allclose( + to_numpy_array(res[1]), to_numpy_array(torch.matmul(res[0], rmat)) + ) + + def test_permu(self): + coord = torch.matmul(self.coord, self.cell) + ft0 = DipoleFittingNet( + "foo", + 3, # ntype + self.dd0.dim_out, + dim_rot_mat=self.dd0.get_dim_emb(), + numb_fparam=0, + numb_aparam=0, + use_tebd=False, + ).to(env.DEVICE) + res = [] + for idx_perm in [[0, 1, 2, 3, 4], [1, 0, 4, 3, 2]]: + atype = self.atype[idx_perm].reshape(1, 5) + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + coord[idx_perm], atype, self.rcut, self.sel, False + ) + + rd0, gr0, _, _, _ = self.dd0( + extended_coord, + extended_atype, + nlist, + ) + + ret0 = ft0(rd0, extended_atype, gr0, fparam=0, aparam=0) + res.append(ret0["foo"]) + + np.testing.assert_allclose( + to_numpy_array(res[0][:, idx_perm]), to_numpy_array(res[1]) + ) + + def test_trans(self): + atype = self.atype.reshape(1, 5) + coord_s = torch.matmul( + torch.remainder( + torch.matmul(self.coord + self.shift, torch.linalg.inv(self.cell)), 1.0 + ), + self.cell, + ) + ft0 = DipoleFittingNet( + "foo", + 3, # ntype + self.dd0.dim_out, + dim_rot_mat=self.dd0.get_dim_emb(), + numb_fparam=0, + numb_aparam=0, + use_tebd=False, + ).to(env.DEVICE) + res = [] + for xyz in [self.coord, coord_s]: + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + xyz, atype, self.rcut, self.sel, False + ) + + rd0, gr0, _, _, _ = self.dd0( + extended_coord, + extended_atype, + nlist, + ) + + ret0 = ft0(rd0, extended_atype, gr0, fparam=0, aparam=0) + res.append(ret0["foo"]) + + np.testing.assert_allclose(to_numpy_array(res[0]), to_numpy_array(res[1])) + + +if __name__ == "__main__": + unittest.main() From 5600e454b8c5f9114e1cbd5b808f1c848c19bb01 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 18 Feb 2024 18:52:23 -0500 Subject: [PATCH 095/686] consistent energy fitting (#3286) Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/dpmodel/fitting/dipole_fitting.py | 2 +- deepmd/dpmodel/fitting/ener_fitting.py | 72 ++++++++ deepmd/dpmodel/fitting/general_fitting.py | 4 + deepmd/dpmodel/fitting/invar_fitting.py | 2 +- deepmd/pt/model/model/__init__.py | 2 + deepmd/pt/model/task/ener.py | 12 +- deepmd/pt/model/task/fitting.py | 4 +- deepmd/tf/env.py | 27 +-- deepmd/tf/fit/ener.py | 103 ++++++++++- deepmd/tf/fit/fitting.py | 134 ++++++++++++++ deepmd/tf/model/model.py | 8 +- deepmd/tf/model/multi.py | 6 +- deepmd/tf/utils/graph.py | 6 +- source/tests/consistent/common.py | 18 +- source/tests/consistent/fitting/__init__.py | 1 + source/tests/consistent/fitting/common.py | 44 +++++ source/tests/consistent/fitting/test_ener.py | 185 +++++++++++++++++++ source/tests/pt/model/test_fitting_net.py | 4 +- source/tests/pt/model/test_model.py | 4 +- source/tests/pt/test_stat.py | 4 +- source/tests/tf/test_data_large_batch.py | 9 +- source/tests/tf/test_fitting_ener_type.py | 3 +- source/tests/tf/test_fitting_stat.py | 3 +- source/tests/tf/test_gen_stat_data.py | 13 +- source/tests/tf/test_layer_name.py | 3 +- source/tests/tf/test_model_loc_frame.py | 6 +- source/tests/tf/test_model_multi.py | 2 + source/tests/tf/test_model_se_a.py | 9 +- source/tests/tf/test_model_se_a_aparam.py | 3 +- source/tests/tf/test_model_se_a_ebd.py | 3 +- source/tests/tf/test_model_se_a_ebd_v2.py | 3 +- source/tests/tf/test_model_se_a_fparam.py | 3 +- source/tests/tf/test_model_se_a_srtab.py | 3 +- source/tests/tf/test_model_se_a_type.py | 3 +- source/tests/tf/test_model_se_atten.py | 12 +- source/tests/tf/test_model_se_r.py | 3 +- source/tests/tf/test_model_se_t.py | 3 +- source/tests/tf/test_model_spin.py | 3 +- 38 files changed, 663 insertions(+), 66 deletions(-) create mode 100644 deepmd/dpmodel/fitting/ener_fitting.py create mode 100644 source/tests/consistent/fitting/__init__.py create mode 100644 source/tests/consistent/fitting/common.py create mode 100644 source/tests/consistent/fitting/test_ener.py diff --git a/deepmd/dpmodel/fitting/dipole_fitting.py b/deepmd/dpmodel/fitting/dipole_fitting.py index 64cad75b62..d40639b1cd 100644 --- a/deepmd/dpmodel/fitting/dipole_fitting.py +++ b/deepmd/dpmodel/fitting/dipole_fitting.py @@ -102,7 +102,7 @@ def __init__( raise NotImplementedError("use_aparam_as_mask is not implemented") if layer_name is not None: raise NotImplementedError("layer_name is not implemented") - if atom_ener is not None: + if atom_ener is not None and atom_ener != []: raise NotImplementedError("atom_ener is not implemented") self.dim_rot_mat = dim_rot_mat diff --git a/deepmd/dpmodel/fitting/ener_fitting.py b/deepmd/dpmodel/fitting/ener_fitting.py new file mode 100644 index 0000000000..11444f0942 --- /dev/null +++ b/deepmd/dpmodel/fitting/ener_fitting.py @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +from typing import ( + TYPE_CHECKING, + Any, + List, + Optional, +) + +from deepmd.dpmodel.common import ( + DEFAULT_PRECISION, +) +from deepmd.dpmodel.fitting.invar_fitting import ( + InvarFitting, +) + +if TYPE_CHECKING: + from deepmd.dpmodel.fitting.general_fitting import ( + GeneralFitting, + ) + + +class EnergyFittingNet(InvarFitting): + def __init__( + self, + ntypes: int, + dim_descrpt: int, + neuron: List[int] = [120, 120, 120], + resnet_dt: bool = True, + numb_fparam: int = 0, + numb_aparam: int = 0, + rcond: Optional[float] = None, + tot_ener_zero: bool = False, + trainable: Optional[List[bool]] = None, + atom_ener: Optional[List[float]] = None, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + layer_name: Optional[List[Optional[str]]] = None, + use_aparam_as_mask: bool = False, + spin: Any = None, + distinguish_types: bool = False, + exclude_types: List[int] = [], + # not used + seed: Optional[int] = None, + ): + super().__init__( + var_name="energy", + ntypes=ntypes, + dim_descrpt=dim_descrpt, + dim_out=1, + neuron=neuron, + resnet_dt=resnet_dt, + numb_fparam=numb_fparam, + numb_aparam=numb_aparam, + rcond=rcond, + tot_ener_zero=tot_ener_zero, + trainable=trainable, + atom_ener=atom_ener, + activation_function=activation_function, + precision=precision, + layer_name=layer_name, + use_aparam_as_mask=use_aparam_as_mask, + spin=spin, + distinguish_types=distinguish_types, + ) + + @classmethod + def deserialize(cls, data: dict) -> "GeneralFitting": + data = copy.deepcopy(data) + data.pop("var_name") + data.pop("dim_out") + return super().deserialize(data) diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index d585ed1c97..3fdb124869 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -103,6 +103,10 @@ def __init__( self.rcond = rcond self.tot_ener_zero = tot_ener_zero self.trainable = trainable + if self.trainable is None: + self.trainable = [True for ii in range(len(self.neuron) + 1)] + if isinstance(self.trainable, bool): + self.trainable = [self.trainable] * (len(self.neuron) + 1) self.atom_ener = atom_ener self.activation_function = activation_function self.precision = precision diff --git a/deepmd/dpmodel/fitting/invar_fitting.py b/deepmd/dpmodel/fitting/invar_fitting.py index 80c51154d6..c065a615e6 100644 --- a/deepmd/dpmodel/fitting/invar_fitting.py +++ b/deepmd/dpmodel/fitting/invar_fitting.py @@ -133,7 +133,7 @@ def __init__( raise NotImplementedError("use_aparam_as_mask is not implemented") if layer_name is not None: raise NotImplementedError("layer_name is not implemented") - if atom_ener is not None: + if atom_ener is not None and atom_ener != []: raise NotImplementedError("atom_ener is not implemented") self.dim_out = dim_out diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index 8199a8490b..81a3d9ffa0 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -54,6 +54,7 @@ def get_zbl_model(model_params): fitting_net["ntypes"] = descriptor.get_ntypes() fitting_net["distinguish_types"] = descriptor.distinguish_types() fitting_net["embedding_width"] = descriptor.get_dim_out() + fitting_net["dim_descrpt"] = descriptor.get_dim_out() grad_force = "direct" not in fitting_net["type"] if not grad_force: fitting_net["out_dim"] = descriptor.get_dim_emb() @@ -89,6 +90,7 @@ def get_model(model_params): fitting_net["ntypes"] = descriptor.get_ntypes() fitting_net["distinguish_types"] = descriptor.distinguish_types() fitting_net["embedding_width"] = descriptor.get_dim_out() + fitting_net["dim_descrpt"] = descriptor.get_dim_out() grad_force = "direct" not in fitting_net["type"] if not grad_force: fitting_net["out_dim"] = descriptor.get_dim_emb() diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 343e5b7db5..c67294c3cd 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy import logging from typing import ( List, @@ -194,7 +195,7 @@ class EnergyFittingNet(InvarFitting): def __init__( self, ntypes: int, - embedding_width: int, + dim_descrpt: int, neuron: List[int] = [128, 128, 128], bias_atom_e: Optional[torch.Tensor] = None, resnet_dt: bool = True, @@ -208,7 +209,7 @@ def __init__( super().__init__( "energy", ntypes, - embedding_width, + dim_descrpt, 1, neuron=neuron, bias_atom_e=bias_atom_e, @@ -221,6 +222,13 @@ def __init__( **kwargs, ) + @classmethod + def deserialize(cls, data: dict) -> "GeneralFitting": + data = copy.deepcopy(data) + data.pop("var_name") + data.pop("dim_out") + return super().deserialize(data) + @Fitting.register("direct_force") @Fitting.register("direct_force_ener") diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 5ae177941c..c6b6959896 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -426,8 +426,8 @@ def serialize(self) -> dict: # "spin": self.spin , ## NOTICE: not supported by far "tot_ener_zero": False, - "trainable": True, - "atom_ener": None, + "trainable": [True] * (len(self.neuron) + 1), + "atom_ener": [], "layer_name": None, "use_aparam_as_mask": False, "spin": None, diff --git a/deepmd/tf/env.py b/deepmd/tf/env.py index fe5bb81bae..2afe5cc862 100644 --- a/deepmd/tf/env.py +++ b/deepmd/tf/env.py @@ -140,17 +140,22 @@ def dlopen_library(module: str, filename: str): r"filter_type_(all)/(idt)_(\d+)|" )[:-1] +# subpatterns: +# \1: layer index or "final" +# \2: type of centeral atom, optional +# the last: weight name FITTING_NET_PATTERN = str( - r"layer_\d+/matrix|" - r"layer_\d+_type_\d+/matrix|" - r"layer_\d+/bias|" - r"layer_\d+_type_\d+/bias|" - r"layer_\d+/idt|" - r"layer_\d+_type_\d+/idt|" - r"final_layer/matrix|" - r"final_layer_type_\d+/matrix|" - r"final_layer/bias|" - r"final_layer_type_\d+/bias|" + r"layer_(\d+)/(matrix)|" + r"layer_(\d+)_type_(\d+)/(matrix)|" + r"layer_(\d+)/(bias)|" + r"layer_(\d+)_type_(\d+)/(bias)|" + r"layer_(\d+)/(idt)|" + r"layer_(\d+)_type_(\d+)/(idt)|" + r"(final)_layer/(matrix)|" + r"(final)_layer_type_(\d+)/(matrix)|" + r"(final)_layer/(bias)|" + r"(final)_layer_type_(\d+)/(bias)|" + # TODO: not sure how to parse for shared layers... # layer_name r"share_.+_type_\d/matrix|" r"share_.+_type_\d/bias|" @@ -158,7 +163,7 @@ def dlopen_library(module: str, filename: str): r"share_.+/matrix|" r"share_.+/bias|" r"share_.+/idt|" -) +)[:-1] TYPE_EMBEDDING_PATTERN = str( r"type_embed_net+/matrix_\d+|" diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index 751e5091bd..8b4a573a58 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging from typing import ( + TYPE_CHECKING, List, Optional, ) @@ -53,6 +54,9 @@ Spin, ) +if TYPE_CHECKING: + pass + log = logging.getLogger(__name__) @@ -130,7 +134,8 @@ class EnerFitting(Fitting): def __init__( self, - descrpt: tf.Tensor, + ntypes: int, + dim_descrpt: int, neuron: List[int] = [120, 120, 120], resnet_dt: bool = True, numb_fparam: int = 0, @@ -150,8 +155,8 @@ def __init__( ) -> None: """Constructor.""" # model param - self.ntypes = descrpt.get_ntypes() - self.dim_descrpt = descrpt.get_dim_out() + self.ntypes = ntypes + self.dim_descrpt = dim_descrpt self.use_aparam_as_mask = use_aparam_as_mask # args = ()\ # .add('numb_fparam', int, default = 0)\ @@ -176,6 +181,7 @@ def __init__( self.ntypes_spin = self.spin.get_ntypes_spin() if self.spin is not None else 0 self.seed_shift = one_layer_rand_seed_shift() self.tot_ener_zero = tot_ener_zero + self.activation_function_name = activation_function self.fitting_activation_fn = get_activation_func(activation_function) self.fitting_precision = get_precision(precision) self.trainable = trainable @@ -202,16 +208,16 @@ def __init__( add_data_requirement( "fparam", self.numb_fparam, atomic=False, must=True, high_prec=False ) - self.fparam_avg = None - self.fparam_std = None - self.fparam_inv_std = None + self.fparam_avg = None + self.fparam_std = None + self.fparam_inv_std = None if self.numb_aparam > 0: add_data_requirement( "aparam", self.numb_aparam, atomic=True, must=True, high_prec=False ) - self.aparam_avg = None - self.aparam_std = None - self.aparam_inv_std = None + self.aparam_avg = None + self.aparam_std = None + self.aparam_inv_std = None self.fitting_net_variables = None self.mixed_prec = None @@ -921,3 +927,82 @@ def get_loss(self, loss: dict, lr) -> Loss: return EnerSpinLoss(**loss, use_spin=self.spin.use_spin) else: raise RuntimeError("unknown loss type") + + @classmethod + def deserialize(cls, data: dict, suffix: str): + """Deserialize the model. + + Parameters + ---------- + data : dict + The serialized data + + Returns + ------- + Model + The deserialized model + """ + fitting = cls(**data) + fitting.fitting_net_variables = cls.deserialize_network( + data["nets"], + suffix=suffix, + ) + fitting.bias_atom_e = data["@variables"]["bias_atom_e"] + if fitting.numb_fparam > 0: + fitting.fparam_avg = data["@variables"]["fparam_avg"] + fitting.fparam_inv_std = data["@variables"]["fparam_inv_std"] + if fitting.numb_aparam > 0: + fitting.aparam_avg = data["@variables"]["aparam_avg"] + fitting.aparam_inv_std = data["@variables"]["aparam_inv_std"] + return fitting + + def serialize(self, suffix: str) -> dict: + """Serialize the model. + + Returns + ------- + dict + The serialized data + """ + data = { + "var_name": "energy", + "ntypes": self.ntypes, + "dim_descrpt": self.dim_descrpt, + # very bad design: type embedding is not passed to the class + # TODO: refactor the class + "distinguish_types": True, + "dim_out": 1, + "neuron": self.n_neuron, + "resnet_dt": self.resnet_dt, + "numb_fparam": self.numb_fparam, + "numb_aparam": self.numb_aparam, + "rcond": self.rcond, + "tot_ener_zero": self.tot_ener_zero, + "trainable": self.trainable, + "atom_ener": self.atom_ener, + "activation_function": self.activation_function_name, + "precision": self.fitting_precision.name, + "layer_name": self.layer_name, + "use_aparam_as_mask": self.use_aparam_as_mask, + "spin": self.spin, + "exclude_types": [], + "nets": self.serialize_network( + ntypes=self.ntypes, + # TODO: consider type embeddings + ndim=1, + in_dim=self.dim_descrpt + self.numb_fparam + self.numb_aparam, + neuron=self.n_neuron, + activation_function=self.activation_function_name, + resnet_dt=self.resnet_dt, + variables=self.fitting_net_variables, + suffix=suffix, + ), + "@variables": { + "bias_atom_e": self.bias_atom_e, + "fparam_avg": self.fparam_avg, + "fparam_inv_std": self.fparam_inv_std, + "aparam_avg": self.aparam_avg, + "aparam_inv_std": self.aparam_inv_std, + }, + } + return data diff --git a/deepmd/tf/fit/fitting.py b/deepmd/tf/fit/fitting.py index 5d666a19f7..2307fb957d 100644 --- a/deepmd/tf/fit/fitting.py +++ b/deepmd/tf/fit/fitting.py @@ -1,12 +1,19 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import re from abc import ( abstractmethod, ) from typing import ( Callable, + List, ) +from deepmd.dpmodel.utils.network import ( + FittingNet, + NetworkCollection, +) from deepmd.tf.env import ( + FITTING_NET_PATTERN, tf, ) from deepmd.tf.loss.loss import ( @@ -102,3 +109,130 @@ def get_loss(self, loss: dict, lr) -> Loss: Loss the loss function """ + + def serialize_network( + self, + ntypes: int, + ndim: int, + in_dim: int, + neuron: List[int], + activation_function: str, + resnet_dt: bool, + variables: dict, + suffix: str = "", + ) -> dict: + """Serialize network. + + Parameters + ---------- + ntypes : int + The number of types + ndim : int + The dimension of elements + in_dim : int + The input dimension + neuron : List[int] + The neuron list + activation_function : str + The activation function + resnet_dt : bool + Whether to use resnet + variables : dict + The input variables + suffix : str, optional + The suffix of the scope + + Returns + ------- + dict + The converted network data + """ + fittings = NetworkCollection( + ntypes=ntypes, + ndim=ndim, + network_type="fitting_network", + ) + if suffix != "": + fitting_net_pattern = ( + FITTING_NET_PATTERN.replace("/(idt)", suffix + "/(idt)") + .replace("/(bias)", suffix + "/(bias)") + .replace("/(matrix)", suffix + "/(matrix)") + ) + else: + fitting_net_pattern = FITTING_NET_PATTERN + for key, value in variables.items(): + m = re.search(fitting_net_pattern, key) + m = [mm for mm in m.groups() if mm is not None] + layer_idx = int(m[0]) if m[0] != "final" else len(neuron) + weight_name = m[-1] + if ndim == 0: + network_idx = () + elif ndim == 1: + network_idx = (int(m[1]),) + else: + raise ValueError(f"Invalid ndim: {ndim}") + if fittings[network_idx] is None: + # initialize the network if it is not initialized + fittings[network_idx] = FittingNet( + in_dim=in_dim, + out_dim=1, + neuron=neuron, + activation_function=activation_function, + resnet_dt=resnet_dt, + precision=self.precision.name, + bias_out=True, + ) + assert fittings[network_idx] is not None + if weight_name == "idt": + value = value.ravel() + fittings[network_idx][layer_idx][weight_name] = value + return fittings.serialize() + + @classmethod + def deserialize_network(cls, data: dict, suffix: str = "") -> dict: + """Deserialize network. + + Parameters + ---------- + data : dict + The input network data + suffix : str, optional + The suffix of the scope + + Returns + ------- + variables : dict + The input variables + """ + fitting_net_variables = {} + fittings = NetworkCollection.deserialize(data) + for ii in range(fittings.ntypes**fittings.ndim): + net_idx = [] + rest_ii = ii + for _ in range(fittings.ndim): + net_idx.append(rest_ii % fittings.ntypes) + rest_ii //= fittings.ntypes + net_idx = tuple(net_idx) + if fittings.ndim == 0: + key = "" + elif fittings.ndim == 1: + key = "_type_" + str(net_idx[0]) + else: + raise ValueError(f"Invalid ndim: {fittings.ndim}") + network = fittings[net_idx] + assert network is not None + for layer_idx, layer in enumerate(network.layers): + if layer_idx == len(network.layers) - 1: + layer_name = "final_layer" + else: + layer_name = f"layer_{layer_idx}" + fitting_net_variables[f"{layer_name}{key}{suffix}/matrix"] = layer.w + fitting_net_variables[f"{layer_name}{key}{suffix}/bias"] = layer.b + if layer.idt is not None: + fitting_net_variables[ + f"{layer_name}{key}{suffix}/idt" + ] = layer.idt.reshape(1, -1) + else: + # prevent keyError + fitting_net_variables[f"{layer_name}{key}{suffix}/idt"] = 0.0 + return fitting_net_variables diff --git a/deepmd/tf/model/model.py b/deepmd/tf/model/model.py index eee138907f..ac970e0b53 100644 --- a/deepmd/tf/model/model.py +++ b/deepmd/tf/model/model.py @@ -631,7 +631,13 @@ def __init__( if isinstance(fitting_net, Fitting): self.fitting = fitting_net else: - self.fitting = Fitting(**fitting_net, descrpt=self.descrpt, spin=self.spin) + self.fitting = Fitting( + **fitting_net, + descrpt=self.descrpt, + spin=self.spin, + ntypes=self.descrpt.get_ntypes(), + dim_descrpt=self.descrpt.get_dim_out(), + ) self.rcut = self.descrpt.get_rcut() self.ntypes = self.descrpt.get_ntypes() diff --git a/deepmd/tf/model/multi.py b/deepmd/tf/model/multi.py index 52bbcebf4d..833a700ebc 100644 --- a/deepmd/tf/model/multi.py +++ b/deepmd/tf/model/multi.py @@ -134,7 +134,11 @@ def __init__( fitting_dict[item] = item_fitting_param else: fitting_dict[item] = Fitting( - **item_fitting_param, descrpt=self.descrpt, spin=self.spin + **item_fitting_param, + descrpt=self.descrpt, + spin=self.spin, + ntypes=self.descrpt.get_ntypes(), + dim_descrpt=self.descrpt.get_dim_out(), ) # type embedding diff --git a/deepmd/tf/utils/graph.py b/deepmd/tf/utils/graph.py index 7e67cf27a6..8c4b0fcc84 100644 --- a/deepmd/tf/utils/graph.py +++ b/deepmd/tf/utils/graph.py @@ -356,9 +356,9 @@ def get_fitting_net_nodes_from_graph_def( """ if suffix != "": fitting_net_pattern = ( - FITTING_NET_PATTERN.replace("/idt", suffix + "/idt") - .replace("/bias", suffix + "/bias") - .replace("/matrix", suffix + "/matrix") + FITTING_NET_PATTERN.replace("/(idt)", suffix + "/(idt)") + .replace("/(bias)", suffix + "/(bias)") + .replace("/(matrix)", suffix + "/(matrix)") ) else: fitting_net_pattern = FITTING_NET_PATTERN diff --git a/source/tests/consistent/common.py b/source/tests/consistent/common.py index e5633726ef..abf2507ef5 100644 --- a/source/tests/consistent/common.py +++ b/source/tests/consistent/common.py @@ -59,6 +59,8 @@ class CommonTest(ABC): data: ClassVar[dict] """Arguments data.""" + addtional_data: ClassVar[dict] = {} + """Additional data that will not be checked.""" tf_class: ClassVar[Optional[type]] """TensorFlow model class.""" dp_class: ClassVar[Optional[type]] @@ -73,6 +75,8 @@ class CommonTest(ABC): """Whether to skip the TensorFlow model.""" skip_pt: ClassVar[bool] = not INSTALLED_PT """Whether to skip the PyTorch model.""" + rtol = 1e-10 + """Relative tolerance for comparing the return value. Override for float32.""" def setUp(self): self.unique_id = uuid4().hex @@ -89,7 +93,7 @@ def init_backend_cls(self, cls) -> Any: base = Argument("arg", dict, sub_fields=self.args) data = base.normalize_value(self.data, trim_pattern="_*") base.check_value(data, strict=True) - return cls(**data) + return cls(**data, **self.addtional_data) @abstractmethod def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: @@ -237,7 +241,7 @@ def test_tf_consistent_with_ref(self): ret2 = self.extract_ret(ret2, self.RefBackend.TF) np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): - np.testing.assert_allclose(rr1, rr2) + np.testing.assert_allclose(rr1, rr2, rtol=self.rtol) def test_tf_self_consistent(self): """Test whether TF is self consistent.""" @@ -251,7 +255,7 @@ def test_tf_self_consistent(self): ret2, data2 = self.get_tf_ret_serialization_from_cls(obj2) np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): - np.testing.assert_allclose(rr1, rr2) + np.testing.assert_allclose(rr1, rr2, rtol=self.rtol) def test_dp_consistent_with_ref(self): """Test whether DP and reference are consistent.""" @@ -268,7 +272,7 @@ def test_dp_consistent_with_ref(self): data2 = dp_obj.serialize() np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): - np.testing.assert_allclose(rr1, rr2) + np.testing.assert_allclose(rr1, rr2, rtol=self.rtol) def test_dp_self_consistent(self): """Test whether DP is self consistent.""" @@ -281,7 +285,7 @@ def test_dp_self_consistent(self): np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): if isinstance(rr1, np.ndarray) and isinstance(rr2, np.ndarray): - np.testing.assert_allclose(rr1, rr2) + np.testing.assert_allclose(rr1, rr2, rtol=self.rtol) else: self.assertEqual(rr1, rr2) @@ -300,7 +304,7 @@ def test_pt_consistent_with_ref(self): data2 = obj.serialize() np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): - np.testing.assert_allclose(rr1, rr2) + np.testing.assert_allclose(rr1, rr2, rtol=self.rtol) def test_pt_self_consistent(self): """Test whether PT is self consistent.""" @@ -313,7 +317,7 @@ def test_pt_self_consistent(self): np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): if isinstance(rr1, np.ndarray) and isinstance(rr2, np.ndarray): - np.testing.assert_allclose(rr1, rr2) + np.testing.assert_allclose(rr1, rr2, rtol=self.rtol) else: self.assertEqual(rr1, rr2) diff --git a/source/tests/consistent/fitting/__init__.py b/source/tests/consistent/fitting/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/source/tests/consistent/fitting/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/source/tests/consistent/fitting/common.py b/source/tests/consistent/fitting/common.py new file mode 100644 index 0000000000..276e81dbc6 --- /dev/null +++ b/source/tests/consistent/fitting/common.py @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from ..common import ( + INSTALLED_PT, + INSTALLED_TF, +) + +if INSTALLED_PT: + pass +if INSTALLED_TF: + from deepmd.tf.env import ( + GLOBAL_TF_FLOAT_PRECISION, + tf, + ) + + +class FittingTest: + """Useful utilities for descriptor tests.""" + + def build_tf_fitting(self, obj, inputs, natoms, atype, fparam, suffix): + t_inputs = tf.placeholder(GLOBAL_TF_FLOAT_PRECISION, [None], name="i_inputs") + t_natoms = tf.placeholder(tf.int32, natoms.shape, name="i_natoms") + t_atype = tf.placeholder(tf.int32, [None], name="i_atype") + extras = {} + feed_dict = {} + if fparam is not None: + t_fparam = tf.placeholder( + GLOBAL_TF_FLOAT_PRECISION, [None], name="i_fparam" + ) + extras["fparam"] = t_fparam + feed_dict[t_fparam] = fparam + t_out = obj.build( + t_inputs, + t_natoms, + {"atype": t_atype, **extras}, + suffix=suffix, + ) + return [t_out], { + t_inputs: inputs, + t_natoms: natoms, + t_atype: atype, + **feed_dict, + } diff --git a/source/tests/consistent/fitting/test_ener.py b/source/tests/consistent/fitting/test_ener.py new file mode 100644 index 0000000000..222c4d84a5 --- /dev/null +++ b/source/tests/consistent/fitting/test_ener.py @@ -0,0 +1,185 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest +from typing import ( + Any, + Tuple, +) + +import numpy as np + +from deepmd.dpmodel.fitting.ener_fitting import EnergyFittingNet as EnerFittingDP +from deepmd.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) + +from ..common import ( + INSTALLED_PT, + INSTALLED_TF, + CommonTest, + parameterized, +) +from .common import ( + FittingTest, +) + +if INSTALLED_PT: + import torch + + from deepmd.pt.model.task.ener import EnergyFittingNet as EnerFittingPT + from deepmd.pt.utils.env import DEVICE as PT_DEVICE +else: + EnerFittingPT = object +if INSTALLED_TF: + from deepmd.tf.fit.ener import EnerFitting as EnerFittingTF +else: + EnerFittingTF = object +from deepmd.utils.argcheck import ( + fitting_ener, +) + + +@parameterized( + (True, False), # resnet_dt + ("float64", "float32"), # precision + (True, False), # distinguish_types + (0, 1), # numb_fparam +) +class TestEner(CommonTest, FittingTest, unittest.TestCase): + @property + def data(self) -> dict: + ( + resnet_dt, + precision, + distinguish_types, + numb_fparam, + ) = self.param + return { + "neuron": [5, 5, 5], + "resnet_dt": resnet_dt, + "precision": precision, + "numb_fparam": numb_fparam, + "seed": 20240217, + } + + @property + def skip_tf(self) -> bool: + ( + resnet_dt, + precision, + distinguish_types, + numb_fparam, + ) = self.param + # TODO: distinguish_types + return not distinguish_types or CommonTest.skip_pt + + @property + def skip_pt(self) -> bool: + ( + resnet_dt, + precision, + distinguish_types, + numb_fparam, + ) = self.param + # TODO: float32 has bug + return precision == "float32" or CommonTest.skip_pt + + tf_class = EnerFittingTF + dp_class = EnerFittingDP + pt_class = EnerFittingPT + args = fitting_ener() + + def setUp(self): + CommonTest.setUp(self) + + self.ntypes = 2 + self.natoms = np.array([6, 6, 2, 4], dtype=np.int32) + self.inputs = np.ones((1, 6, 20), dtype=GLOBAL_NP_FLOAT_PRECISION) + self.atype = np.array([0, 1, 1, 0, 1, 1], dtype=np.int32) + # inconsistent if not sorted + self.atype.sort() + self.fparam = -np.ones((1,), dtype=GLOBAL_NP_FLOAT_PRECISION) + + @property + def addtional_data(self) -> dict: + ( + resnet_dt, + precision, + distinguish_types, + numb_fparam, + ) = self.param + return { + "ntypes": self.ntypes, + "dim_descrpt": self.inputs.shape[-1], + "distinguish_types": distinguish_types, + } + + def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: + ( + resnet_dt, + precision, + distinguish_types, + numb_fparam, + ) = self.param + return self.build_tf_fitting( + obj, + self.inputs.ravel(), + self.natoms, + self.atype, + self.fparam if numb_fparam else None, + suffix, + ) + + def eval_pt(self, pt_obj: Any) -> Any: + ( + resnet_dt, + precision, + distinguish_types, + numb_fparam, + ) = self.param + return ( + pt_obj( + torch.from_numpy(self.inputs).to(device=PT_DEVICE), + torch.from_numpy(self.atype.reshape(1, -1)).to(device=PT_DEVICE), + fparam=torch.from_numpy(self.fparam).to(device=PT_DEVICE) + if numb_fparam + else None, + )["energy"] + .detach() + .cpu() + .numpy() + ) + + def eval_dp(self, dp_obj: Any) -> Any: + ( + resnet_dt, + precision, + distinguish_types, + numb_fparam, + ) = self.param + return dp_obj( + self.inputs, + self.atype.reshape(1, -1), + fparam=self.fparam if numb_fparam else None, + )["energy"] + + def extract_ret(self, ret: Any, backend) -> Tuple[np.ndarray, ...]: + if backend == self.RefBackend.TF: + # shape is not same + ret = ret[0].reshape(-1, self.natoms[0], 1) + return (ret,) + + @property + def rtol(self) -> float: + """Relative tolerance for comparing the return value.""" + ( + resnet_dt, + precision, + distinguish_types, + numb_fparam, + ) = self.param + if precision == "float64": + return 1e-10 + elif precision == "float32": + return 1e-4 + else: + raise ValueError(f"Unknown precision: {precision}") diff --git a/source/tests/pt/model/test_fitting_net.py b/source/tests/pt/model/test_fitting_net.py index e12a397347..2bcfb7b64c 100644 --- a/source/tests/pt/model/test_fitting_net.py +++ b/source/tests/pt/model/test_fitting_net.py @@ -95,7 +95,9 @@ def setUp(self): cnt += self.natoms[i + 2] fake_d = FakeDescriptor(2, 30) - self.dp_fn = EnerFitting(fake_d, self.n_neuron) + self.dp_fn = EnerFitting( + fake_d.get_ntypes(), fake_d.get_dim_out(), self.n_neuron + ) self.dp_fn.bias_atom_e = rng.uniform(size=[self.ntypes]) def test_consistency(self): diff --git a/source/tests/pt/model/test_model.py b/source/tests/pt/model/test_model.py index efe013a8a1..856b48064b 100644 --- a/source/tests/pt/model/test_model.py +++ b/source/tests/pt/model/test_model.py @@ -195,7 +195,9 @@ def _get_dp_model(self): neuron=self.filter_neuron, axis_neuron=self.axis_neuron, ) - dp_fitting = EnerFitting(descrpt=dp_descrpt, neuron=self.n_neuron) + dp_fitting = EnerFitting( + dp_descrpt.get_ntypes(), dp_descrpt.get_dim_out(), neuron=self.n_neuron + ) return EnerModel( dp_descrpt, dp_fitting, diff --git a/source/tests/pt/test_stat.py b/source/tests/pt/test_stat.py index 12af6ba866..5cf6a953cc 100644 --- a/source/tests/pt/test_stat.py +++ b/source/tests/pt/test_stat.py @@ -142,7 +142,9 @@ def my_merge(energy, natoms): energy = self.dp_sampled["energy"] natoms = self.dp_sampled["natoms_vec"] energy, natoms = my_merge(energy, natoms) - dp_fn = EnerFitting(self.dp_d, self.n_neuron) + dp_fn = EnerFitting( + self.dp_d.get_ntypes(), self.dp_d.get_dim_out(), self.n_neuron + ) dp_fn.compute_output_stats(self.dp_sampled) bias_atom_e = compute_output_bias(energy, natoms) self.assertTrue(np.allclose(dp_fn.bias_atom_e, bias_atom_e[:, 0])) diff --git a/source/tests/tf/test_data_large_batch.py b/source/tests/tf/test_data_large_batch.py index c31a4c4a9a..53991fa7f2 100644 --- a/source/tests/tf/test_data_large_batch.py +++ b/source/tests/tf/test_data_large_batch.py @@ -112,7 +112,8 @@ def test_data_mixed_type(self): jdata["model"]["descriptor"].pop("type", None) jdata["model"]["descriptor"]["ntypes"] = 2 descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( @@ -308,7 +309,8 @@ def test_stripped_data_mixed_type(self): jdata["model"]["descriptor"]["ntypes"] = 2 jdata["model"]["descriptor"]["stripped_type_embedding"] = True descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( @@ -504,7 +506,8 @@ def test_compressible_data_mixed_type(self): jdata["model"]["descriptor"]["stripped_type_embedding"] = True jdata["model"]["descriptor"]["attn_layer"] = 0 descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( diff --git a/source/tests/tf/test_fitting_ener_type.py b/source/tests/tf/test_fitting_ener_type.py index 4dd6fb80a1..f88692be74 100644 --- a/source/tests/tf/test_fitting_ener_type.py +++ b/source/tests/tf/test_fitting_ener_type.py @@ -54,7 +54,8 @@ def test_fitting(self): jdata["model"]["descriptor"].pop("type", None) descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) # model._compute_dstats([test_data['coord']], [test_data['box']], [test_data['type']], [test_data['natoms_vec']], [test_data['default_mesh']]) diff --git a/source/tests/tf/test_fitting_stat.py b/source/tests/tf/test_fitting_stat.py index 9e2408c57b..100868fd18 100644 --- a/source/tests/tf/test_fitting_stat.py +++ b/source/tests/tf/test_fitting_stat.py @@ -81,7 +81,8 @@ def test(self): # fitting = EnerFitting(jdata['fitting_net'], descrpt) descrpt = DescrptSeA(6.0, 5.8, [46, 92], neuron=[25, 50, 100], axis_neuron=16) fitting = EnerFitting( - descrpt, + descrpt.get_ntypes(), + descrpt.get_dim_out(), neuron=[240, 240, 240], resnet_dt=True, numb_fparam=2, diff --git a/source/tests/tf/test_gen_stat_data.py b/source/tests/tf/test_gen_stat_data.py index 18191eb21d..fe4ec36b24 100644 --- a/source/tests/tf/test_gen_stat_data.py +++ b/source/tests/tf/test_gen_stat_data.py @@ -119,7 +119,12 @@ def test_ener_shift(self): ener_shift0 = data.compute_energy_shift(rcond=1) all_stat = make_stat_input(data, 4, merge_sys=False) descrpt = DescrptSeA(6.0, 5.8, [46, 92], neuron=[25, 50, 100], axis_neuron=16) - fitting = EnerFitting(descrpt, neuron=[240, 240, 240], resnet_dt=True) + fitting = EnerFitting( + descrpt.get_ntypes(), + descrpt.get_dim_out(), + neuron=[240, 240, 240], + resnet_dt=True, + ) ener_shift1 = fitting._compute_output_stats(all_stat, rcond=1) np.testing.assert_almost_equal(ener_shift0, ener_shift1) @@ -131,7 +136,11 @@ def test_ener_shift_assigned(self): all_stat = make_stat_input(data, 4, merge_sys=False) descrpt = DescrptSeA(6.0, 5.8, [46, 92], neuron=[25, 50, 100], axis_neuron=16) fitting = EnerFitting( - descrpt, neuron=[240, 240, 240], resnet_dt=True, atom_ener=[ae0, None, None] + descrpt.get_ntypes(), + descrpt.get_dim_out(), + neuron=[240, 240, 240], + resnet_dt=True, + atom_ener=[ae0, None, None], ) ener_shift1 = fitting._compute_output_stats(all_stat, rcond=1) # check assigned energy diff --git a/source/tests/tf/test_layer_name.py b/source/tests/tf/test_layer_name.py index 8c2264315f..089bd19dd1 100644 --- a/source/tests/tf/test_layer_name.py +++ b/source/tests/tf/test_layer_name.py @@ -66,7 +66,8 @@ def test_model(self): fitting_type_dict[fitting_key] = item_fitting_type item_fitting_param.pop("type", None) item_fitting_param.pop("fit_diag", None) - item_fitting_param["descrpt"] = descrpt + item_fitting_param["ntypes"] = descrpt.get_ntypes() + item_fitting_param["dim_descrpt"] = descrpt.get_dim_out() if item_fitting_type == "ener": fitting_dict[fitting_key] = EnerFitting( **item_fitting_param, uniform_seed=True diff --git a/source/tests/tf/test_model_loc_frame.py b/source/tests/tf/test_model_loc_frame.py index f97e349145..84467b436a 100644 --- a/source/tests/tf/test_model_loc_frame.py +++ b/source/tests/tf/test_model_loc_frame.py @@ -53,7 +53,11 @@ def test_model(self): jdata["model"]["descriptor"].pop("_comment", None) descrpt = DescrptLocFrame(**jdata["model"]["descriptor"]) fitting = EnerFitting( - descrpt, neuron=[240, 120, 60, 30, 10], seed=1, uniform_seed=True + descrpt.get_ntypes(), + descrpt.get_dim_out(), + neuron=[240, 120, 60, 30, 10], + seed=1, + uniform_seed=True, ) model = EnerModel( descrpt, diff --git a/source/tests/tf/test_model_multi.py b/source/tests/tf/test_model_multi.py index c526b479a6..b978dff1ab 100644 --- a/source/tests/tf/test_model_multi.py +++ b/source/tests/tf/test_model_multi.py @@ -69,6 +69,8 @@ def test_model(self): item_fitting_param.pop("type", None) item_fitting_param.pop("fit_diag", None) item_fitting_param["descrpt"] = descrpt + item_fitting_param["ntypes"] = descrpt.get_ntypes() + item_fitting_param["dim_descrpt"] = descrpt.get_dim_out() if item_fitting_type == "ener": fitting_dict[fitting_key] = EnerFitting( **item_fitting_param, uniform_seed=True diff --git a/source/tests/tf/test_model_se_a.py b/source/tests/tf/test_model_se_a.py index 57a8f4af52..e60cb2307f 100644 --- a/source/tests/tf/test_model_se_a.py +++ b/source/tests/tf/test_model_se_a.py @@ -74,7 +74,8 @@ def test_model_atom_ener(self): jdata["model"]["descriptor"].pop("type", None) descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) model = EnerModel(descrpt, fitting) @@ -154,7 +155,8 @@ def test_model(self): jdata["model"]["descriptor"].pop("type", None) descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) model = EnerModel(descrpt, fitting) @@ -298,7 +300,8 @@ def test_model_atom_ener_type_embedding(self): typeebd = TypeEmbedNet(**jdata["model"]["type_embeding"]) jdata["model"]["descriptor"].pop("type", None) descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) model = EnerModel(descrpt, fitting, typeebd=typeebd) diff --git a/source/tests/tf/test_model_se_a_aparam.py b/source/tests/tf/test_model_se_a_aparam.py index 6b37dfd459..6bf059f8fa 100644 --- a/source/tests/tf/test_model_se_a_aparam.py +++ b/source/tests/tf/test_model_se_a_aparam.py @@ -53,7 +53,8 @@ def test_model(self): jdata["model"]["descriptor"].pop("type", None) descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) model = EnerModel(descrpt, fitting) diff --git a/source/tests/tf/test_model_se_a_ebd.py b/source/tests/tf/test_model_se_a_ebd.py index b819c2ddc9..599cce6386 100644 --- a/source/tests/tf/test_model_se_a_ebd.py +++ b/source/tests/tf/test_model_se_a_ebd.py @@ -54,7 +54,8 @@ def test_model(self): descrpt = DescrptSeAEbd( **jdata["model"]["descriptor"], ) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting( **jdata["model"]["fitting_net"], ) diff --git a/source/tests/tf/test_model_se_a_ebd_v2.py b/source/tests/tf/test_model_se_a_ebd_v2.py index 0cc89f5151..22b3c3389d 100644 --- a/source/tests/tf/test_model_se_a_ebd_v2.py +++ b/source/tests/tf/test_model_se_a_ebd_v2.py @@ -70,7 +70,8 @@ def test_model(self): descrpt = DescrptSeAEbdV2( **jdata["model"]["descriptor"], ) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting( **jdata["model"]["fitting_net"], ) diff --git a/source/tests/tf/test_model_se_a_fparam.py b/source/tests/tf/test_model_se_a_fparam.py index 806ae13582..4f94fc1655 100644 --- a/source/tests/tf/test_model_se_a_fparam.py +++ b/source/tests/tf/test_model_se_a_fparam.py @@ -52,7 +52,8 @@ def test_model(self): jdata["model"]["descriptor"].pop("type", None) descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) # descrpt = DescrptSeA(jdata['model']['descriptor']) # fitting = EnerFitting(jdata['model']['fitting_net'], descrpt) diff --git a/source/tests/tf/test_model_se_a_srtab.py b/source/tests/tf/test_model_se_a_srtab.py index 3a93349741..00f59668a0 100644 --- a/source/tests/tf/test_model_se_a_srtab.py +++ b/source/tests/tf/test_model_se_a_srtab.py @@ -69,7 +69,8 @@ def test_model(self): jdata["model"]["descriptor"].pop("type", None) descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) # descrpt = DescrptSeA(jdata['model']['descriptor']) # fitting = EnerFitting(jdata['model']['fitting_net'], descrpt) diff --git a/source/tests/tf/test_model_se_a_type.py b/source/tests/tf/test_model_se_a_type.py index 4b19378cf6..9c0a07cc98 100644 --- a/source/tests/tf/test_model_se_a_type.py +++ b/source/tests/tf/test_model_se_a_type.py @@ -55,7 +55,8 @@ def test_model(self): jdata["model"]["descriptor"].pop("type", None) descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( diff --git a/source/tests/tf/test_model_se_atten.py b/source/tests/tf/test_model_se_atten.py index ad6926e0da..13e4c554ca 100644 --- a/source/tests/tf/test_model_se_atten.py +++ b/source/tests/tf/test_model_se_atten.py @@ -67,7 +67,8 @@ def test_model(self): jdata["model"]["descriptor"].pop("type", None) jdata["model"]["descriptor"]["ntypes"] = 2 descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( @@ -292,7 +293,8 @@ def test_compressible_model(self): jdata["model"]["descriptor"]["stripped_type_embedding"] = True jdata["model"]["descriptor"]["attn_layer"] = 0 descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( @@ -519,7 +521,8 @@ def test_stripped_type_embedding_model(self): jdata["model"]["descriptor"]["stripped_type_embedding"] = True jdata["model"]["descriptor"]["attn_layer"] = 2 descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( @@ -757,7 +760,8 @@ def test_smoothness_of_stripped_type_embedding_smooth_model(self): jdata["model"]["descriptor"]["rcut"] = 6.0 jdata["model"]["descriptor"]["rcut_smth"] = 4.0 descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( diff --git a/source/tests/tf/test_model_se_r.py b/source/tests/tf/test_model_se_r.py index a635e6c3c4..1e63922e19 100644 --- a/source/tests/tf/test_model_se_r.py +++ b/source/tests/tf/test_model_se_r.py @@ -52,7 +52,8 @@ def test_model(self): jdata["model"]["descriptor"].pop("type", None) descrpt = DescrptSeR(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) # fitting = EnerFitting(jdata['model']['fitting_net'], descrpt) model = EnerModel(descrpt, fitting) diff --git a/source/tests/tf/test_model_se_t.py b/source/tests/tf/test_model_se_t.py index 881a0e06c4..d75fac2f07 100644 --- a/source/tests/tf/test_model_se_t.py +++ b/source/tests/tf/test_model_se_t.py @@ -52,7 +52,8 @@ def test_model(self): jdata["model"]["descriptor"].pop("type", None) descrpt = DescrptSeT(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) model = EnerModel(descrpt, fitting) diff --git a/source/tests/tf/test_model_spin.py b/source/tests/tf/test_model_spin.py index 26100c19d0..5d20c76c35 100644 --- a/source/tests/tf/test_model_spin.py +++ b/source/tests/tf/test_model_spin.py @@ -71,7 +71,8 @@ def test_model_spin(self): descrpt_param["spin"] = spin descrpt = DescrptSeA(**descrpt_param, uniform_seed=True) fitting_param.pop("type", None) - fitting_param["descrpt"] = descrpt + fitting_param["ntypes"] = descrpt.get_ntypes() + fitting_param["dim_descrpt"] = descrpt.get_dim_out() fitting_param["spin"] = spin fitting = EnerFitting(**fitting_param, uniform_seed=True) model = EnerModel(descrpt, fitting, spin=spin) From 027d902b7414caf984bc81216f70b46877834a4a Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 18 Feb 2024 20:39:53 -0500 Subject: [PATCH 096/686] dp&pt: let DPAtomicModel fetch attributes from Fitting (#3292) Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/atomic_model/dp_atomic_model.py | 6 +++--- deepmd/pt/model/atomic_model/dp_atomic_model.py | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/dp_atomic_model.py b/deepmd/dpmodel/atomic_model/dp_atomic_model.py index ee19d15410..8c63ee40fc 100644 --- a/deepmd/dpmodel/atomic_model/dp_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/dp_atomic_model.py @@ -146,11 +146,11 @@ def deserialize(cls, data) -> "DPAtomicModel": def get_dim_fparam(self) -> int: """Get the number (dimension) of frame parameters of this atomic model.""" - return 0 + return self.fitting.get_dim_fparam() def get_dim_aparam(self) -> int: """Get the number (dimension) of atomic parameters of this atomic model.""" - return 0 + return self.fitting.get_dim_aparam() def get_sel_type(self) -> List[int]: """Get the selected atom types of this model. @@ -159,7 +159,7 @@ def get_sel_type(self) -> List[int]: to the result of the model. If returning an empty list, all atom types are selected. """ - return [] + return self.fitting.get_sel_type() def is_aparam_nall(self) -> bool: """Check whether the shape of atomic parameters is (nframes, nall, ndim). diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index aafd2831b3..5888b84b98 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -194,14 +194,12 @@ def compute_or_load_stat( @torch.jit.export def get_dim_fparam(self) -> int: """Get the number (dimension) of frame parameters of this atomic model.""" - # TODO: self.fitting_net.get_dim_fparam() - return 0 + return self.fitting_net.get_dim_fparam() @torch.jit.export def get_dim_aparam(self) -> int: """Get the number (dimension) of atomic parameters of this atomic model.""" - # TODO: self.fitting_net.get_dim_aparam() - return 0 + return self.fitting_net.get_dim_aparam() @torch.jit.export def get_sel_type(self) -> List[int]: @@ -211,8 +209,7 @@ def get_sel_type(self) -> List[int]: to the result of the model. If returning an empty list, all atom types are selected. """ - # TODO: self.fitting_net.get_sel_type() - return [] + return self.fitting_net.get_sel_type() @torch.jit.export def is_aparam_nall(self) -> bool: From 63bec2262ad132b66269bc951508a327e73946bc Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 18 Feb 2024 21:43:39 -0500 Subject: [PATCH 097/686] pluggable backend (#3294) Signed-off-by: Jinzhe Zeng --- deepmd/backend/__init__.py | 29 ++++ deepmd/backend/backend.py | 201 ++++++++++++++++++++++++++++ deepmd/backend/dpmodel.py | 86 ++++++++++++ deepmd/backend/pytorch.py | 95 +++++++++++++ deepmd/backend/tensorflow.py | 104 ++++++++++++++ deepmd/entrypoints/neighbor_stat.py | 21 ++- deepmd/infer/backend.py | 34 ----- deepmd/infer/deep_eval.py | 21 +-- deepmd/main.py | 52 +++---- source/tests/consistent/common.py | 11 +- 10 files changed, 561 insertions(+), 93 deletions(-) create mode 100644 deepmd/backend/__init__.py create mode 100644 deepmd/backend/backend.py create mode 100644 deepmd/backend/dpmodel.py create mode 100644 deepmd/backend/pytorch.py create mode 100644 deepmd/backend/tensorflow.py delete mode 100644 deepmd/infer/backend.py diff --git a/deepmd/backend/__init__.py b/deepmd/backend/__init__.py new file mode 100644 index 0000000000..8969edd480 --- /dev/null +++ b/deepmd/backend/__init__.py @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Backends. + +Avoid directly importing third-party libraries in this module for performance. +""" +# copy from dpdata +from importlib import ( + import_module, + metadata, +) +from pathlib import ( + Path, +) + +PACKAGE_BASE = "deepmd.backend" +NOT_LOADABLE = ("__init__.py",) + +for module_file in Path(__file__).parent.glob("*.py"): + if module_file.name not in NOT_LOADABLE: + module_name = f".{module_file.stem}" + import_module(module_name, PACKAGE_BASE) + +# https://setuptools.readthedocs.io/en/latest/userguide/entry_point.html +try: + eps = metadata.entry_points(group="deepmd.backend") +except TypeError: + eps = metadata.entry_points().get("deepmd.backend", []) +for ep in eps: + plugin = ep.load() diff --git a/deepmd/backend/backend.py b/deepmd/backend/backend.py new file mode 100644 index 0000000000..179b2e556a --- /dev/null +++ b/deepmd/backend/backend.py @@ -0,0 +1,201 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from abc import ( + abstractmethod, +) +from enum import ( + Flag, + auto, +) +from typing import ( + TYPE_CHECKING, + Callable, + ClassVar, + Dict, + List, + Type, +) + +from deepmd.utils.plugin import ( + Plugin, + PluginVariant, +) + +if TYPE_CHECKING: + from argparse import ( + Namespace, + ) + + from deepmd.infer.deep_eval import ( + DeepEvalBackend, + ) + from deepmd.utils.neighbor_stat import ( + NeighborStat, + ) + + +class Backend(PluginVariant): + r"""General backend class. + + Examples + -------- + >>> @Backend.register("tf") + >>> @Backend.register("tensorflow") + >>> class TensorFlowBackend(Backend): + ... pass + """ + + __plugins = Plugin() + + @staticmethod + def register(key: str) -> Callable[[object], object]: + """Register a backend plugin. + + Parameters + ---------- + key : str + the key of a backend + + Returns + ------- + Callable[[object], object] + the decorator to register backend + """ + return Backend.__plugins.register(key.lower()) + + @staticmethod + def get_backend(key: str) -> Type["Backend"]: + """Get the backend by key. + + Parameters + ---------- + key : str + the key of a backend + + Returns + ------- + Backend + the backend + """ + try: + backend = Backend.__plugins.get_plugin(key.lower()) + except KeyError: + raise KeyError(f"Backend {key} is not registered.") + assert isinstance(backend, type) + return backend + + @staticmethod + def get_backends() -> Dict[str, Type["Backend"]]: + """Get all the registered backend names. + + Returns + ------- + list + all the registered backends + """ + return Backend.__plugins.plugins + + @staticmethod + def get_backends_by_feature( + feature: "Backend.Feature", + ) -> Dict[str, Type["Backend"]]: + """Get all the registered backend names with a specific feature. + + Parameters + ---------- + feature : Backend.Feature + the feature flag + + Returns + ------- + list + all the registered backends with the feature + """ + return { + key: backend + for key, backend in Backend.__plugins.plugins.items() + if backend.features & feature + } + + @staticmethod + def detect_backend_by_model(filename: str) -> Type["Backend"]: + """Detect the backend of the given model file. + + Parameters + ---------- + filename : str + The model file name + """ + filename = str(filename).lower() + for backend in Backend.get_backends().values(): + for suffix in backend.suffixes: + if filename.endswith(suffix): + return backend + raise ValueError(f"Cannot detect the backend of the model file {filename}.") + + class Feature(Flag): + """Feature flag to indicate whether the backend supports certain features.""" + + ENTRY_POINT = auto() + """Support entry point hook.""" + DEEP_EVAL = auto() + """Support Deep Eval backend.""" + NEIGHBOR_STAT = auto() + """Support neighbor statistics.""" + + name: ClassVar[str] = "Unknown" + """The formal name of the backend. + + To be consistent, this name should be also registered in the plugin system.""" + + features: ClassVar[Feature] = Feature(0) + """The features of the backend.""" + suffixes: ClassVar[List[str]] = [] + """The supported suffixes of the saved model. + + The first element is considered as the default suffix.""" + + @abstractmethod + def is_available(self) -> bool: + """Check if the backend is available. + + Returns + ------- + bool + Whether the backend is available. + """ + + @property + @abstractmethod + def entry_point_hook(self) -> Callable[["Namespace"], None]: + """The entry point hook of the backend. + + Returns + ------- + Callable[[Namespace], None] + The entry point hook of the backend. + """ + pass + + @property + @abstractmethod + def deep_eval(self) -> Type["DeepEvalBackend"]: + """The Deep Eval backend of the backend. + + Returns + ------- + type[DeepEvalBackend] + The Deep Eval backend of the backend. + """ + pass + + @property + @abstractmethod + def neighbor_stat(self) -> Type["NeighborStat"]: + """The neighbor statistics of the backend. + + Returns + ------- + type[NeighborStat] + The neighbor statistics of the backend. + """ + pass diff --git a/deepmd/backend/dpmodel.py b/deepmd/backend/dpmodel.py new file mode 100644 index 0000000000..8745ca6d5a --- /dev/null +++ b/deepmd/backend/dpmodel.py @@ -0,0 +1,86 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + TYPE_CHECKING, + Callable, + ClassVar, + List, + Type, +) + +from deepmd.backend.backend import ( + Backend, +) + +if TYPE_CHECKING: + from argparse import ( + Namespace, + ) + + from deepmd.infer.deep_eval import ( + DeepEvalBackend, + ) + from deepmd.utils.neighbor_stat import ( + NeighborStat, + ) + + +@Backend.register("dp") +@Backend.register("dpmodel") +@Backend.register("np") +@Backend.register("numpy") +class DPModelBackend(Backend): + """DPModel backend that uses NumPy as the reference implementation.""" + + name = "DPModel" + """The formal name of the backend.""" + features: ClassVar[Backend.Feature] = Backend.Feature.NEIGHBOR_STAT + """The features of the backend.""" + suffixes: ClassVar[List[str]] = [".dp"] + """The suffixes of the backend.""" + + def is_available(self) -> bool: + """Check if the backend is available. + + Returns + ------- + bool + Whether the backend is available. + """ + return True + + @property + def entry_point_hook(self) -> Callable[["Namespace"], None]: + """The entry point hook of the backend. + + Returns + ------- + Callable[[Namespace], None] + The entry point hook of the backend. + """ + raise NotImplementedError(f"Unsupported backend: {self.name}") + + @property + def deep_eval(self) -> Type["DeepEvalBackend"]: + """The Deep Eval backend of the backend. + + Returns + ------- + type[DeepEvalBackend] + The Deep Eval backend of the backend. + """ + raise NotImplementedError(f"Unsupported backend: {self.name}") + + @property + def neighbor_stat(self) -> Type["NeighborStat"]: + """The neighbor statistics of the backend. + + Returns + ------- + type[NeighborStat] + The neighbor statistics of the backend. + """ + from deepmd.dpmodel.utils.neighbor_stat import ( + NeighborStat, + ) + + return NeighborStat diff --git a/deepmd/backend/pytorch.py b/deepmd/backend/pytorch.py new file mode 100644 index 0000000000..4c0b0699f9 --- /dev/null +++ b/deepmd/backend/pytorch.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from importlib.util import ( + find_spec, +) +from typing import ( + TYPE_CHECKING, + Callable, + ClassVar, + List, + Type, +) + +from deepmd.backend.backend import ( + Backend, +) + +if TYPE_CHECKING: + from argparse import ( + Namespace, + ) + + from deepmd.infer.deep_eval import ( + DeepEvalBackend, + ) + from deepmd.utils.neighbor_stat import ( + NeighborStat, + ) + + +@Backend.register("pt") +@Backend.register("pytorch") +class TensorFlowBackend(Backend): + """TensorFlow backend.""" + + name = "PyTorch" + """The formal name of the backend.""" + features: ClassVar[Backend.Feature] = ( + Backend.Feature.ENTRY_POINT + | Backend.Feature.DEEP_EVAL + | Backend.Feature.NEIGHBOR_STAT + ) + """The features of the backend.""" + suffixes: ClassVar[List[str]] = [".pth", ".pt"] + """The suffixes of the backend.""" + + def is_available(self) -> bool: + """Check if the backend is available. + + Returns + ------- + bool + Whether the backend is available. + """ + return find_spec("torch") is not None + + @property + def entry_point_hook(self) -> Callable[["Namespace"], None]: + """The entry point hook of the backend. + + Returns + ------- + Callable[[Namespace], None] + The entry point hook of the backend. + """ + from deepmd.pt.entrypoints.main import main as deepmd_main + + return deepmd_main + + @property + def deep_eval(self) -> Type["DeepEvalBackend"]: + """The Deep Eval backend of the backend. + + Returns + ------- + type[DeepEvalBackend] + The Deep Eval backend of the backend. + """ + from deepmd.pt.infer.deep_eval import DeepEval as DeepEvalPT + + return DeepEvalPT + + @property + def neighbor_stat(self) -> Type["NeighborStat"]: + """The neighbor statistics of the backend. + + Returns + ------- + type[NeighborStat] + The neighbor statistics of the backend. + """ + from deepmd.pt.utils.neighbor_stat import ( + NeighborStat, + ) + + return NeighborStat diff --git a/deepmd/backend/tensorflow.py b/deepmd/backend/tensorflow.py new file mode 100644 index 0000000000..80569afa97 --- /dev/null +++ b/deepmd/backend/tensorflow.py @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from importlib.util import ( + find_spec, +) +from typing import ( + TYPE_CHECKING, + Callable, + ClassVar, + List, + Type, +) + +from deepmd.backend.backend import ( + Backend, +) + +if TYPE_CHECKING: + from argparse import ( + Namespace, + ) + + from deepmd.infer.deep_eval import ( + DeepEvalBackend, + ) + from deepmd.utils.neighbor_stat import ( + NeighborStat, + ) + + +@Backend.register("tf") +@Backend.register("tensorflow") +class TensorFlowBackend(Backend): + """TensorFlow backend.""" + + name = "TensorFlow" + """The formal name of the backend.""" + features: ClassVar[Backend.Feature] = ( + Backend.Feature.ENTRY_POINT + | Backend.Feature.DEEP_EVAL + | Backend.Feature.NEIGHBOR_STAT + ) + """The features of the backend.""" + suffixes: ClassVar[List[str]] = [".pb"] + """The suffixes of the backend.""" + + def is_available(self) -> bool: + """Check if the backend is available. + + Returns + ------- + bool + Whether the backend is available. + """ + # deepmd.env imports expensive numpy + # avoid import outside the method + from deepmd.env import ( + GLOBAL_CONFIG, + ) + + return ( + find_spec("tensorflow") is not None + and GLOBAL_CONFIG["enable_tensorflow"] != "0" + ) + + @property + def entry_point_hook(self) -> Callable[["Namespace"], None]: + """The entry point hook of the backend. + + Returns + ------- + Callable[[Namespace], None] + The entry point hook of the backend. + """ + from deepmd.tf.entrypoints.main import main as deepmd_main + + return deepmd_main + + @property + def deep_eval(self) -> Type["DeepEvalBackend"]: + """The Deep Eval backend of the backend. + + Returns + ------- + type[DeepEvalBackend] + The Deep Eval backend of the backend. + """ + from deepmd.tf.infer.deep_eval import DeepEval as DeepEvalTF + + return DeepEvalTF + + @property + def neighbor_stat(self) -> Type["NeighborStat"]: + """The neighbor statistics of the backend. + + Returns + ------- + type[NeighborStat] + The neighbor statistics of the backend. + """ + from deepmd.tf.utils.neighbor_stat import ( + NeighborStat, + ) + + return NeighborStat diff --git a/deepmd/entrypoints/neighbor_stat.py b/deepmd/entrypoints/neighbor_stat.py index f5ce0f839d..8a496fb6f0 100644 --- a/deepmd/entrypoints/neighbor_stat.py +++ b/deepmd/entrypoints/neighbor_stat.py @@ -4,6 +4,9 @@ List, ) +from deepmd.backend.backend import ( + Backend, +) from deepmd.common import ( expand_sys_str, ) @@ -69,20 +72,12 @@ def neighbor_stat( min_nbor_dist: 0.6599510670195264 max_nbor_size: [23, 26, 19, 16, 2, 2, 1, 1, 72, 37, 5, 0, 31, 29, 1, 21, 20, 5] """ - if backend == "tensorflow": - from deepmd.tf.utils.neighbor_stat import ( - NeighborStat, - ) - elif backend == "pytorch": - from deepmd.pt.utils.neighbor_stat import ( - NeighborStat, - ) - elif backend == "numpy": - from deepmd.dpmodel.utils.neighbor_stat import ( - NeighborStat, - ) - else: + backends = Backend.get_backends_by_feature(Backend.Feature.NEIGHBOR_STAT) + try: + backend_obj = backends[backend]() + except KeyError: raise ValueError(f"Invalid backend {backend}") + NeighborStat = backend_obj.neighbor_stat all_sys = expand_sys_str(system) if not len(all_sys): raise RuntimeError("Did not find valid system") diff --git a/deepmd/infer/backend.py b/deepmd/infer/backend.py deleted file mode 100644 index 26eef22eb4..0000000000 --- a/deepmd/infer/backend.py +++ /dev/null @@ -1,34 +0,0 @@ -# SPDX-License-Identifier: LGPL-3.0-or-later -from enum import ( - Enum, -) - - -class DPBackend(Enum): - """DeePMD-kit backend.""" - - TensorFlow = 1 - PyTorch = 2 - Paddle = 3 - Unknown = 4 - - -def detect_backend(filename: str) -> DPBackend: - """Detect the backend of the given model file. - - Parameters - ---------- - filename : str - The model file name - """ - filename = str(filename).lower() - if filename.endswith(".pb"): - return DPBackend.TensorFlow - elif filename.endswith(".pth") or filename.endswith(".pt"): - return DPBackend.PyTorch - elif filename.endswith(".pdmodel"): - return DPBackend.Paddle - return DPBackend.Unknown - - -__all__ = ["DPBackend", "detect_backend"] diff --git a/deepmd/infer/deep_eval.py b/deepmd/infer/deep_eval.py index 3b1eceb16d..35d170cdab 100644 --- a/deepmd/infer/deep_eval.py +++ b/deepmd/infer/deep_eval.py @@ -16,6 +16,9 @@ import numpy as np +from deepmd.backend.backend import ( + Backend, +) from deepmd.dpmodel.output_def import ( FittingOutputDef, ModelOutputDef, @@ -24,11 +27,6 @@ AutoBatchSize, ) -from .backend import ( - DPBackend, - detect_backend, -) - if TYPE_CHECKING: import ase.neighborlist @@ -89,17 +87,8 @@ def __init__( def __new__(cls, model_file: str, *args, **kwargs): if cls is DeepEvalBackend: - backend = detect_backend(model_file) - if backend == DPBackend.TensorFlow: - from deepmd.tf.infer.deep_eval import DeepEval as DeepEvalTF - - return super().__new__(DeepEvalTF) - elif backend == DPBackend.PyTorch: - from deepmd.pt.infer.deep_eval import DeepEval as DeepEvalPT - - return super().__new__(DeepEvalPT) - else: - raise NotImplementedError("Unsupported backend: " + str(backend)) + backend = Backend.detect_backend_by_model(model_file) + return super().__new__(backend().deep_eval) return super().__new__(cls) @abstractmethod diff --git a/deepmd/main.py b/deepmd/main.py index d6714e1e26..d31cab30c2 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -8,9 +8,18 @@ import logging import os import textwrap +from collections import ( + defaultdict, +) from typing import ( + Dict, List, Optional, + Type, +) + +from deepmd.backend.backend import ( + Backend, ) try: @@ -46,12 +55,10 @@ class RawTextArgumentDefaultsHelpFormatter( """This formatter is used to print multile-line help message with default value.""" -BACKEND_TABLE = { - "tensorflow": "tensorflow", - "tf": "tensorflow", - "pytorch": "pytorch", - "pt": "pytorch", -} +BACKENDS: Dict[str, Type[Backend]] = Backend.get_backends_by_feature( + Backend.Feature.ENTRY_POINT +) +BACKEND_TABLE: Dict[str, str] = {kk: vv.name.lower() for kk, vv in BACKENDS.items()} class BackendOption(argparse.Action): @@ -102,20 +109,18 @@ def main_parser() -> argparse.ArgumentParser: "DP_BACKEND." ), ) - parser_backend.add_argument( - "--tf", - action="store_const", - dest="backend", - const="tensorflow", - help="Alias for --backend tensorflow", - ) - parser_backend.add_argument( - "--pt", - action="store_const", - dest="backend", - const="pytorch", - help="Alias for --backend pytorch", - ) + + BACKEND_ALIAS: Dict[str, List[str]] = defaultdict(list) + for alias, backend in BACKEND_TABLE.items(): + BACKEND_ALIAS[backend].append(alias) + for backend, alias in BACKEND_ALIAS.items(): + parser_backend.add_argument( + *[f"--{aa}" for aa in alias], + action="store_const", + dest="backend", + const=backend, + help=f"Alias for --backend {backend}", + ) subparsers = parser.add_subparsers(title="Valid subcommands", dest="command") @@ -752,11 +757,8 @@ def main(): """ args = parse_args() - if args.backend == "tensorflow": - from deepmd.tf.entrypoints.main import main as deepmd_main - elif args.backend == "pytorch": - from deepmd.pt.entrypoints.main import main as deepmd_main - else: + if args.backend not in BACKEND_TABLE: raise ValueError(f"Unknown backend {args.backend}") + deepmd_main = BACKENDS[args.backend]().entry_point_hook deepmd_main(args) diff --git a/source/tests/consistent/common.py b/source/tests/consistent/common.py index abf2507ef5..45c2114b24 100644 --- a/source/tests/consistent/common.py +++ b/source/tests/consistent/common.py @@ -9,9 +9,6 @@ from enum import ( Enum, ) -from importlib.util import ( - find_spec, -) from typing import ( Any, Callable, @@ -29,8 +26,12 @@ Argument, ) -INSTALLED_TF = find_spec("tensorflow") is not None -INSTALLED_PT = find_spec("torch") is not None +from deepmd.backend.tensorflow import ( + Backend, +) + +INSTALLED_TF = Backend.get_backend("tensorflow")().is_available() +INSTALLED_PT = Backend.get_backend("pytorch")().is_available() if os.environ.get("CI") and not (INSTALLED_TF and INSTALLED_PT): raise ImportError("TensorFlow or PyTorch should be tested in the CI") From 235ff24e40d5664f6e5d9f360a7181401b5bc2ee Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Mon, 19 Feb 2024 11:42:25 +0800 Subject: [PATCH 098/686] test the case of permuted index in different frames. (#3295) Co-authored-by: Han Wang --- .../dpmodel/case_single_frame_with_nlist.py | 20 ++++++++++++++++-- .../common/dpmodel/test_exclusion_mask.py | 3 +++ .../dpmodel/test_fitting_invar_fitting.py | 9 ++++++-- source/tests/pt/model/test_env_mat.py | 21 +++++++++++++++++-- source/tests/pt/model/test_exclusion_mask.py | 3 +++ source/tests/pt/model/test_se_e2_a.py | 7 +++++++ 6 files changed, 57 insertions(+), 6 deletions(-) diff --git a/source/tests/common/dpmodel/case_single_frame_with_nlist.py b/source/tests/common/dpmodel/case_single_frame_with_nlist.py index 93a85c2d66..df4f73efbd 100644 --- a/source/tests/common/dpmodel/case_single_frame_with_nlist.py +++ b/source/tests/common/dpmodel/case_single_frame_with_nlist.py @@ -7,7 +7,7 @@ def setUp(self): # nloc == 3, nall == 4 self.nloc = 3 self.nall = 4 - self.nf, self.nt = 1, 2 + self.nf, self.nt = 2, 2 self.coord_ext = np.array( [ [0, 0, 0], @@ -16,7 +16,7 @@ def setUp(self): [0, -2, 0], ], dtype=np.float64, - ).reshape([1, self.nall * 3]) + ).reshape([1, self.nall, 3]) self.atype_ext = np.array([0, 0, 1, 0], dtype=int).reshape([1, self.nall]) # sel = [5, 2] self.sel = [5, 2] @@ -30,3 +30,19 @@ def setUp(self): ).reshape([1, self.nloc, sum(self.sel)]) self.rcut = 0.4 self.rcut_smth = 2.2 + # permutations + self.perm = np.array([2, 0, 1, 3], dtype=np.int32) + inv_perm = np.array([1, 2, 0, 3], dtype=np.int32) + # permute the coord and atype + self.coord_ext = np.concatenate( + [self.coord_ext, self.coord_ext[:, self.perm, :]], axis=0 + ).reshape(self.nf, self.nall * 3) + self.atype_ext = np.concatenate( + [self.atype_ext, self.atype_ext[:, self.perm]], axis=0 + ) + # permute the nlist + nlist1 = self.nlist[:, self.perm[: self.nloc], :] + mask = nlist1 == -1 + nlist1 = inv_perm[nlist1] + nlist1 = np.where(mask, -1, nlist1) + self.nlist = np.concatenate([self.nlist, nlist1], axis=0) diff --git a/source/tests/common/dpmodel/test_exclusion_mask.py b/source/tests/common/dpmodel/test_exclusion_mask.py index 89727ec6c3..a6fdce317a 100644 --- a/source/tests/common/dpmodel/test_exclusion_mask.py +++ b/source/tests/common/dpmodel/test_exclusion_mask.py @@ -48,6 +48,9 @@ def test_build_type_exclude_mask(self): [1, 1, 1, 1, 1, 0, 1], [1, 1, 1, 1, 1, 0, 1], [0, 0, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 0, 1], + [1, 1, 1, 1, 1, 0, 1], ] ).reshape(self.nf, self.nloc, sum(self.sel)) des = PairExcludeMask(self.nt, exclude_types=exclude_types) diff --git a/source/tests/common/dpmodel/test_fitting_invar_fitting.py b/source/tests/common/dpmodel/test_fitting_invar_fitting.py index 77d3b429ec..85c90ef8ec 100644 --- a/source/tests/common/dpmodel/test_fitting_invar_fitting.py +++ b/source/tests/common/dpmodel/test_fitting_invar_fitting.py @@ -86,8 +86,13 @@ def test_mask(self): # atom index 2 is of type 1 that is excluded zero_idx = 2 np.testing.assert_allclose( - ret0["energy"][:, zero_idx, :], - np.zeros_like(ret0["energy"][:, zero_idx, :]), + ret0["energy"][0, zero_idx, :], + np.zeros_like(ret0["energy"][0, zero_idx, :]), + ) + zero_idx = 0 + np.testing.assert_allclose( + ret0["energy"][1, zero_idx, :], + np.zeros_like(ret0["energy"][1, zero_idx, :]), ) def test_self_exception( diff --git a/source/tests/pt/model/test_env_mat.py b/source/tests/pt/model/test_env_mat.py index 08890cc963..0746f9e8df 100644 --- a/source/tests/pt/model/test_env_mat.py +++ b/source/tests/pt/model/test_env_mat.py @@ -22,7 +22,7 @@ def setUp(self): # nloc == 3, nall == 4 self.nloc = 3 self.nall = 4 - self.nf, self.nt = 1, 2 + self.nf, self.nt = 2, 2 self.coord_ext = np.array( [ [0, 0, 0], @@ -31,7 +31,7 @@ def setUp(self): [0, -2, 0], ], dtype=np.float64, - ).reshape([1, self.nall * 3]) + ).reshape([1, self.nall, 3]) self.atype_ext = np.array([0, 0, 1, 0], dtype=int).reshape([1, self.nall]) # sel = [5, 2] self.sel = [5, 2] @@ -45,6 +45,22 @@ def setUp(self): ).reshape([1, self.nloc, sum(self.sel)]) self.rcut = 0.4 self.rcut_smth = 2.2 + # permutations + self.perm = np.array([2, 0, 1, 3], dtype=np.int32) + inv_perm = np.array([1, 2, 0, 3], dtype=np.int32) + # permute the coord and atype + self.coord_ext = np.concatenate( + [self.coord_ext, self.coord_ext[:, self.perm, :]], axis=0 + ).reshape(self.nf, self.nall * 3) + self.atype_ext = np.concatenate( + [self.atype_ext, self.atype_ext[:, self.perm]], axis=0 + ) + # permute the nlist + nlist1 = self.nlist[:, self.perm[: self.nloc], :] + mask = nlist1 == -1 + nlist1 = inv_perm[nlist1] + nlist1 = np.where(mask, -1, nlist1) + self.nlist = np.concatenate([self.nlist, nlist1], axis=0) class TestCaseSingleFrameWithoutNlist: @@ -94,3 +110,4 @@ def test_consistency( ) np.testing.assert_allclose(mm0, mm1) np.testing.assert_allclose(ww0, ww1) + np.testing.assert_allclose(mm0[0][self.perm[: self.nloc]], mm0[1]) diff --git a/source/tests/pt/model/test_exclusion_mask.py b/source/tests/pt/model/test_exclusion_mask.py index 18ab56be49..b50f163eb6 100644 --- a/source/tests/pt/model/test_exclusion_mask.py +++ b/source/tests/pt/model/test_exclusion_mask.py @@ -57,6 +57,9 @@ def test_build_type_exclude_mask(self): [1, 1, 1, 1, 1, 0, 1], [1, 1, 1, 1, 1, 0, 1], [0, 0, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 0, 1], + [1, 1, 1, 1, 1, 0, 1], ] ).reshape(self.nf, self.nloc, sum(self.sel)) des = PairExcludeMask(self.nt, exclude_types=exclude_types).to(env.DEVICE) diff --git a/source/tests/pt/model/test_se_e2_a.py b/source/tests/pt/model/test_se_e2_a.py index bb15bb423d..214fdeb00f 100644 --- a/source/tests/pt/model/test_se_e2_a.py +++ b/source/tests/pt/model/test_se_e2_a.py @@ -79,6 +79,13 @@ def test_consistency( atol=atol, err_msg=err_msg, ) + np.testing.assert_allclose( + rd0.detach().cpu().numpy()[0][self.perm[: self.nloc]], + rd0.detach().cpu().numpy()[1], + rtol=rtol, + atol=atol, + err_msg=err_msg, + ) # dp impl dd2 = DPDescrptSeA.deserialize(dd0.serialize()) rd2, gr2, _, _, sw2 = dd2.call( From 9f6ff1e925ab00f2b0d40e549438009e3f3bb016 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Mon, 19 Feb 2024 14:46:54 +0800 Subject: [PATCH 099/686] fix: replace all distinguished types by mixed types. (#3289) Signed-off-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Co-authored-by: Han Wang Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../dpmodel/atomic_model/dp_atomic_model.py | 14 +++++-- .../atomic_model/linear_atomic_model.py | 24 +++++++----- .../atomic_model/make_base_atomic_model.py | 12 ++++-- .../atomic_model/pairtab_atomic_model.py | 13 ++++++- .../descriptor/make_base_descriptor.py | 2 +- deepmd/dpmodel/descriptor/se_e2_a.py | 4 +- deepmd/dpmodel/fitting/dipole_fitting.py | 11 ++++-- deepmd/dpmodel/fitting/ener_fitting.py | 4 +- deepmd/dpmodel/fitting/general_fitting.py | 19 ++++++---- deepmd/dpmodel/fitting/invar_fitting.py | 10 +++-- deepmd/dpmodel/model/make_model.py | 8 ++-- deepmd/dpmodel/utils/neighbor_stat.py | 10 ++--- .../pt/model/atomic_model/dp_atomic_model.py | 14 +++++-- .../model/atomic_model/linear_atomic_model.py | 24 +++++++----- .../atomic_model/pairtab_atomic_model.py | 13 ++++++- deepmd/pt/model/descriptor/dpa1.py | 14 +++++-- deepmd/pt/model/descriptor/dpa2.py | 14 +++++-- deepmd/pt/model/descriptor/hybrid.py | 16 +++++--- deepmd/pt/model/descriptor/repformers.py | 14 +++++-- deepmd/pt/model/descriptor/se_a.py | 18 ++++++--- deepmd/pt/model/descriptor/se_atten.py | 18 ++++++--- deepmd/pt/model/model/__init__.py | 4 +- deepmd/pt/model/model/make_model.py | 8 ++-- deepmd/pt/model/task/ener.py | 20 ++++++---- deepmd/pt/model/task/fitting.py | 38 +++++++++---------- deepmd/pt/utils/env_mat_stat.py | 4 +- deepmd/pt/utils/neighbor_stat.py | 10 ++--- deepmd/pt/utils/nlist.py | 4 +- deepmd/tf/fit/ener.py | 2 +- deepmd/tf/utils/neighbor_stat.py | 12 +++--- .../common/dpmodel/test_dp_atomic_model.py | 2 +- source/tests/common/dpmodel/test_dp_model.py | 2 +- .../dpmodel/test_fitting_invar_fitting.py | 12 +++--- .../dpmodel/test_linear_atomic_model.py | 4 +- source/tests/common/dpmodel/test_nlist.py | 2 +- source/tests/consistent/fitting/test_ener.py | 24 ++++++------ source/tests/pt/model/test_descriptor.py | 2 +- source/tests/pt/model/test_descriptor_dpa1.py | 4 +- source/tests/pt/model/test_descriptor_dpa2.py | 4 +- source/tests/pt/model/test_dp_atomic_model.py | 6 +-- source/tests/pt/model/test_dp_model.py | 24 ++++++------ source/tests/pt/model/test_embedding_net.py | 2 +- source/tests/pt/model/test_ener_fitting.py | 15 ++++---- source/tests/pt/model/test_fitting_net.py | 2 +- .../pt/model/test_linear_atomic_model.py | 4 +- .../tests/pt/model/test_make_hessian_model.py | 2 +- source/tests/pt/model/test_model.py | 2 +- 47 files changed, 294 insertions(+), 197 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/dp_atomic_model.py b/deepmd/dpmodel/atomic_model/dp_atomic_model.py index 8c63ee40fc..abf66cc3fa 100644 --- a/deepmd/dpmodel/atomic_model/dp_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/dp_atomic_model.py @@ -66,11 +66,17 @@ def get_type_map(self) -> Optional[List[str]]: """Get the type map.""" return self.type_map - def distinguish_types(self) -> bool: - """Returns if model requires a neighbor list that distinguish different - atomic types or not. + def mixed_types(self) -> bool: + """If true, the model + 1. assumes total number of atoms aligned across frames; + 2. uses a neighbor list that does not distinguish different atomic types. + + If false, the model + 1. assumes total number of atoms of each atom type aligned across frames; + 2. uses a neighbor list that distinguishes different atomic types. + """ - return self.descriptor.distinguish_types() + return self.descriptor.mixed_types() def forward_atomic( self, diff --git a/deepmd/dpmodel/atomic_model/linear_atomic_model.py b/deepmd/dpmodel/atomic_model/linear_atomic_model.py index a9e57d6270..520ad9185e 100644 --- a/deepmd/dpmodel/atomic_model/linear_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/linear_atomic_model.py @@ -50,13 +50,19 @@ def __init__( ): super().__init__() self.models = models - self.distinguish_type_list = [ - model.distinguish_types() for model in self.models - ] + self.mixed_types_list = [model.mixed_types() for model in self.models] - def distinguish_types(self) -> bool: - """If distinguish different types by sorting.""" - return False + def mixed_types(self) -> bool: + """If true, the model + 1. assumes total number of atoms aligned across frames; + 2. uses a neighbor list that does not distinguish different atomic types. + + If false, the model + 1. assumes total number of atoms of each atom type aligned across frames; + 2. uses a neighbor list that distinguishes different atomic types. + + """ + return True def get_rcut(self) -> float: """Get the cut-off radius.""" @@ -134,9 +140,9 @@ def forward_atomic( for rcut, sel in zip(self.get_model_rcuts(), self.get_model_nsels()) ] nlists_ = [ - nl if not dt else nlist_distinguish_types(nl, extended_atype, sel) - for dt, nl, sel in zip( - self.distinguish_type_list, raw_nlists, self.get_model_sels() + nl if mt else nlist_distinguish_types(nl, extended_atype, sel) + for mt, nl, sel in zip( + self.mixed_types_list, raw_nlists, self.get_model_sels() ) ] ener_list = [ diff --git a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py index f1dea4615f..e262404762 100644 --- a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py @@ -86,9 +86,15 @@ def is_aparam_nall(self) -> bool: """ @abstractmethod - def distinguish_types(self) -> bool: - """Returns if the model requires a neighbor list that distinguish different - atomic types or not. + def mixed_types(self) -> bool: + """If true, the model + 1. assumes total number of atoms aligned across frames; + 2. uses a neighbor list that does not distinguish different atomic types. + + If false, the model + 1. assumes total number of atoms of each atom type aligned across frames; + 2. uses a neighbor list that distinguishes different atomic types. + """ pass diff --git a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py index d5f2bd2f1a..34f6514986 100644 --- a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py @@ -91,9 +91,18 @@ def get_sel(self) -> List[int]: def get_nsel(self) -> int: return self.sel - def distinguish_types(self) -> bool: + def mixed_types(self) -> bool: + """If true, the model + 1. assumes total number of atoms aligned across frames; + 2. uses a neighbor list that does not distinguish different atomic types. + + If false, the model + 1. assumes total number of atoms of each atom type aligned across frames; + 2. uses a neighbor list that distinguishes different atomic types. + + """ # to match DPA1 and DPA2. - return False + return True def serialize(self) -> dict: return {"tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel} diff --git a/deepmd/dpmodel/descriptor/make_base_descriptor.py b/deepmd/dpmodel/descriptor/make_base_descriptor.py index b7a8bfebcf..7bd553db9e 100644 --- a/deepmd/dpmodel/descriptor/make_base_descriptor.py +++ b/deepmd/dpmodel/descriptor/make_base_descriptor.py @@ -67,7 +67,7 @@ def get_dim_emb(self) -> int: pass @abstractmethod - def distinguish_types(self) -> bool: + def mixed_types(self) -> bool: """Returns if the descriptor requires a neighbor list that distinguish different atomic types or not. """ diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index 3b98f9dc67..1802b5bab6 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -223,11 +223,11 @@ def get_sel(self): """Returns cutoff radius.""" return self.sel - def distinguish_types(self): + def mixed_types(self): """Returns if the descriptor requires a neighbor list that distinguish different atomic types or not. """ - return True + return False def get_ntypes(self) -> int: """Returns the number of element types.""" diff --git a/deepmd/dpmodel/fitting/dipole_fitting.py b/deepmd/dpmodel/fitting/dipole_fitting.py index d40639b1cd..5a4952edb8 100644 --- a/deepmd/dpmodel/fitting/dipole_fitting.py +++ b/deepmd/dpmodel/fitting/dipole_fitting.py @@ -65,8 +65,11 @@ class DipoleFitting(GeneralFitting): use_aparam_as_mask: bool, optional If True, the atomic parameters will be used as a mask that determines the atom is real/virtual. And the aparam will not be used as the atomic parameters for embedding. - distinguish_types - Different atomic types uses different fitting net. + mixed_types + If true, use a uniform fitting net for all atom types, otherwise use + different fitting nets for different atom types. + exclude_types: List[int] + Atomic contributions of the excluded atom types are set zero. """ @@ -89,7 +92,7 @@ def __init__( layer_name: Optional[List[Optional[str]]] = None, use_aparam_as_mask: bool = False, spin: Any = None, - distinguish_types: bool = False, + mixed_types: bool = False, exclude_types: List[int] = [], old_impl=False, ): @@ -123,7 +126,7 @@ def __init__( layer_name=layer_name, use_aparam_as_mask=use_aparam_as_mask, spin=spin, - distinguish_types=distinguish_types, + mixed_types=mixed_types, exclude_types=exclude_types, ) self.old_impl = False diff --git a/deepmd/dpmodel/fitting/ener_fitting.py b/deepmd/dpmodel/fitting/ener_fitting.py index 11444f0942..4196d07926 100644 --- a/deepmd/dpmodel/fitting/ener_fitting.py +++ b/deepmd/dpmodel/fitting/ener_fitting.py @@ -38,7 +38,7 @@ def __init__( layer_name: Optional[List[Optional[str]]] = None, use_aparam_as_mask: bool = False, spin: Any = None, - distinguish_types: bool = False, + mixed_types: bool = False, exclude_types: List[int] = [], # not used seed: Optional[int] = None, @@ -61,7 +61,7 @@ def __init__( layer_name=layer_name, use_aparam_as_mask=use_aparam_as_mask, spin=spin, - distinguish_types=distinguish_types, + mixed_types=mixed_types, ) @classmethod diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index 3fdb124869..03f8f25237 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -67,8 +67,11 @@ class GeneralFitting(NativeOP, BaseFitting): use_aparam_as_mask: bool, optional If True, the atomic parameters will be used as a mask that determines the atom is real/virtual. And the aparam will not be used as the atomic parameters for embedding. - distinguish_types - Different atomic types uses different fitting net. + mixed_types + If true, use a uniform fitting net for all atom types, otherwise use + different fitting nets for different atom types. + exclude_types: List[int] + Atomic contributions of the excluded atom types are set zero. """ @@ -90,7 +93,7 @@ def __init__( layer_name: Optional[List[Optional[str]]] = None, use_aparam_as_mask: bool = False, spin: Any = None, - distinguish_types: bool = False, + mixed_types: bool = True, exclude_types: List[int] = [], ): self.var_name = var_name @@ -113,7 +116,7 @@ def __init__( self.layer_name = layer_name self.use_aparam_as_mask = use_aparam_as_mask self.spin = spin - self.distinguish_types = distinguish_types + self.mixed_types = mixed_types self.exclude_types = exclude_types if self.spin is not None: raise NotImplementedError("spin is not supported") @@ -136,7 +139,7 @@ def __init__( # init networks in_dim = self.dim_descrpt + self.numb_fparam + self.numb_aparam self.nets = NetworkCollection( - 1 if self.distinguish_types else 0, + 1 if not self.mixed_types else 0, self.ntypes, network_type="fitting_network", networks=[ @@ -149,7 +152,7 @@ def __init__( self.precision, bias_out=True, ) - for ii in range(self.ntypes if self.distinguish_types else 1) + for ii in range(self.ntypes if not self.mixed_types else 1) ], ) @@ -216,7 +219,7 @@ def serialize(self) -> dict: "rcond": self.rcond, "activation_function": self.activation_function, "precision": self.precision, - "distinguish_types": self.distinguish_types, + "mixed_types": self.mixed_types, "exclude_types": self.exclude_types, "nets": self.nets.serialize(), "@variables": { @@ -318,7 +321,7 @@ def _call_common( ) # calcualte the prediction - if self.distinguish_types: + if not self.mixed_types: outs = np.zeros([nf, nloc, net_dim_out]) for type_i in range(self.ntypes): mask = np.tile( diff --git a/deepmd/dpmodel/fitting/invar_fitting.py b/deepmd/dpmodel/fitting/invar_fitting.py index c065a615e6..429565d016 100644 --- a/deepmd/dpmodel/fitting/invar_fitting.py +++ b/deepmd/dpmodel/fitting/invar_fitting.py @@ -95,8 +95,10 @@ class InvarFitting(GeneralFitting): use_aparam_as_mask: bool, optional If True, the atomic parameters will be used as a mask that determines the atom is real/virtual. And the aparam will not be used as the atomic parameters for embedding. - distinguish_types - Different atomic types uses different fitting net. + mixed_types + If false, different atomic types uses different fitting net, otherwise different atom types share the same fitting net. + exclude_types: List[int] + Atomic contributions of the excluded atom types are set zero. """ @@ -119,7 +121,7 @@ def __init__( layer_name: Optional[List[Optional[str]]] = None, use_aparam_as_mask: bool = False, spin: Any = None, - distinguish_types: bool = False, + mixed_types: bool = True, exclude_types: List[int] = [], ): # seed, uniform_seed are not included @@ -154,7 +156,7 @@ def __init__( layer_name=layer_name, use_aparam_as_mask=use_aparam_as_mask, spin=spin, - distinguish_types=distinguish_types, + mixed_types=mixed_types, exclude_types=exclude_types, ) diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py index f507374ff2..e44c2e9701 100644 --- a/deepmd/dpmodel/model/make_model.py +++ b/deepmd/dpmodel/model/make_model.py @@ -111,7 +111,7 @@ def call( nloc, self.get_rcut(), self.get_sel(), - distinguish_types=self.distinguish_types(), + distinguish_types=not self.mixed_types(), ) extended_coord = extended_coord.reshape(nframes, -1, 3) model_predict_lower = self.call_lower( @@ -207,7 +207,7 @@ def format_nlist( Known limitations: - In the case of self.distinguish_types, the nlist is always formatted. + In the case of not self.mixed_types, the nlist is always formatted. May have side effact on the efficiency. Parameters @@ -226,9 +226,9 @@ def format_nlist( """ n_nf, n_nloc, n_nnei = nlist.shape - distinguish_types = self.distinguish_types() + mixed_types = self.mixed_types() ret = self._format_nlist(extended_coord, nlist, sum(self.get_sel())) - if distinguish_types: + if not mixed_types: ret = nlist_distinguish_types(ret, extended_atype, self.get_sel()) return ret diff --git a/deepmd/dpmodel/utils/neighbor_stat.py b/deepmd/dpmodel/utils/neighbor_stat.py index a0f3c02131..bf0c30c153 100644 --- a/deepmd/dpmodel/utils/neighbor_stat.py +++ b/deepmd/dpmodel/utils/neighbor_stat.py @@ -28,19 +28,19 @@ class NeighborStatOP(NativeOP): The num of atom types rcut The cut-off radius - distinguish_types : bool, optional - If False, treat all types as a single type. + mixed_types : bool, optional + If True, treat all types as a single type. """ def __init__( self, ntypes: int, rcut: float, - distinguish_types: bool, + mixed_types: bool, ) -> None: self.rcut = rcut self.ntypes = ntypes - self.distinguish_types = distinguish_types + self.mixed_types = mixed_types def call( self, @@ -89,7 +89,7 @@ def call( rr2 = np.sum(np.square(diff), axis=-1) min_rr2 = np.min(rr2, axis=-1) # count the number of neighbors - if self.distinguish_types: + if not self.mixed_types: mask = rr2 < self.rcut**2 nnei = np.zeros((nframes, nloc, self.ntypes), dtype=int) for ii in range(self.ntypes): diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index 5888b84b98..e6dc395500 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -79,9 +79,17 @@ def get_sel(self) -> List[int]: """Get the neighbor selection.""" return self.sel - def distinguish_types(self) -> bool: - """If distinguish different types by sorting.""" - return self.descriptor.distinguish_types() + def mixed_types(self) -> bool: + """If true, the model + 1. assumes total number of atoms aligned across frames; + 2. uses a neighbor list that does not distinguish different atomic types. + + If false, the model + 1. assumes total number of atoms of each atom type aligned across frames; + 2. uses a neighbor list that distinguishes different atomic types. + + """ + return self.descriptor.mixed_types() def serialize(self) -> dict: return { diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 7ccc5e225b..b688362b73 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -50,13 +50,19 @@ def __init__( super().__init__() self.models = torch.nn.ModuleList(models) self.atomic_bias = None - self.distinguish_type_list = [ - model.distinguish_types() for model in self.models - ] + self.mixed_types_list = [model.mixed_types() for model in self.models] - def distinguish_types(self) -> bool: - """If distinguish different types by sorting.""" - return False + def mixed_types(self) -> bool: + """If true, the model + 1. assumes total number of atoms aligned across frames; + 2. uses a neighbor list that does not distinguish different atomic types. + + If false, the model + 1. assumes total number of atoms of each atom type aligned across frames; + 2. uses a neighbor list that distinguishes different atomic types. + + """ + return True @torch.jit.export def get_rcut(self) -> float: @@ -143,9 +149,9 @@ def forward_atomic( for rcut, sel in zip(self.get_model_rcuts(), self.get_model_nsels()) ] nlists_ = [ - nl if not dt else nlist_distinguish_types(nl, extended_atype, sel) - for dt, nl, sel in zip( - self.distinguish_type_list, raw_nlists, self.get_model_sels() + nl if mt else nlist_distinguish_types(nl, extended_atype, sel) + for mt, nl, sel in zip( + self.mixed_types_list, raw_nlists, self.get_model_sels() ) ] ener_list = [] diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index cda9071cda..4019fda423 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -106,9 +106,18 @@ def get_sel(self) -> List[int]: def get_nsel(self) -> int: return self.sel - def distinguish_types(self) -> bool: + def mixed_types(self) -> bool: + """If true, the model + 1. assumes total number of atoms aligned across frames; + 2. uses a neighbor list that does not distinguish different atomic types. + + If false, the model + 1. assumes total number of atoms of each atom type aligned across frames; + 2. uses a neighbor list that distinguishes different atomic types. + + """ # to match DPA1 and DPA2. - return False + return True def serialize(self) -> dict: return {"tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel} diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index 6bdb5c2cb3..d948c7abf7 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -108,11 +108,17 @@ def get_dim_out(self) -> int: def get_dim_emb(self) -> int: return self.se_atten.dim_emb - def distinguish_types(self) -> bool: - """Returns if the descriptor requires a neighbor list that distinguish different - atomic types or not. + def mixed_types(self) -> bool: + """If true, the discriptor + 1. assumes total number of atoms aligned across frames; + 2. requires a neighbor list that does not distinguish different atomic types. + + If false, the discriptor + 1. assumes total number of atoms of each atom type aligned across frames; + 2. requires a neighbor list that distinguishes different atomic types. + """ - return self.se_atten.distinguish_types() + return self.se_atten.mixed_types() @property def dim_out(self): diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index 0122dcacb8..90ec56e0bf 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -274,11 +274,17 @@ def get_dim_emb(self) -> int: """Returns the embedding dimension of this descriptor.""" return self.repformers.dim_emb - def distinguish_types(self) -> bool: - """Returns if the descriptor requires a neighbor list that distinguish different - atomic types or not. + def mixed_types(self) -> bool: + """If true, the discriptor + 1. assumes total number of atoms aligned across frames; + 2. requires a neighbor list that does not distinguish different atomic types. + + If false, the discriptor + 1. assumes total number of atoms of each atom type aligned across frames; + 2. requires a neighbor list that distinguishes different atomic types. + """ - return False + return True @property def dim_out(self): diff --git a/deepmd/pt/model/descriptor/hybrid.py b/deepmd/pt/model/descriptor/hybrid.py index d6678f2a4b..688d448b81 100644 --- a/deepmd/pt/model/descriptor/hybrid.py +++ b/deepmd/pt/model/descriptor/hybrid.py @@ -106,13 +106,17 @@ def get_dim_in(self) -> int: def get_dim_emb(self): return self.dim_emb - def distinguish_types(self) -> bool: - """Returns if the descriptor requires a neighbor list that distinguish different - atomic types or not. + def mixed_types(self) -> bool: + """If true, the discriptor + 1. assumes total number of atoms aligned across frames; + 2. requires a neighbor list that does not distinguish different atomic types. + + If false, the discriptor + 1. assumes total number of atoms of each atom type aligned across frames; + 2. requires a neighbor list that distinguishes different atomic types. + """ - return any( - descriptor.distinguish_types() for descriptor in self.descriptor_list - ) + return all(descriptor.mixed_types() for descriptor in self.descriptor_list) @property def dim_out(self): diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py index 76051a52ed..8aa1114fdc 100644 --- a/deepmd/pt/model/descriptor/repformers.py +++ b/deepmd/pt/model/descriptor/repformers.py @@ -181,11 +181,17 @@ def get_dim_emb(self) -> int: """Returns the embedding dimension g2.""" return self.g2_dim - def distinguish_types(self) -> bool: - """Returns if the descriptor requires a neighbor list that distinguish different - atomic types or not. + def mixed_types(self) -> bool: + """If true, the discriptor + 1. assumes total number of atoms aligned across frames; + 2. requires a neighbor list that does not distinguish different atomic types. + + If false, the discriptor + 1. assumes total number of atoms of each atom type aligned across frames; + 2. requires a neighbor list that distinguishes different atomic types. + """ - return False + return True @property def dim_out(self): diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 33cc3ee9e2..0f8add1d8d 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -109,11 +109,11 @@ def get_dim_emb(self) -> int: """Returns the output dimension.""" return self.sea.get_dim_emb() - def distinguish_types(self): + def mixed_types(self): """Returns if the descriptor requires a neighbor list that distinguish different atomic types or not. """ - return self.sea.distinguish_types() + return self.sea.mixed_types() @property def dim_out(self): @@ -347,11 +347,17 @@ def get_dim_in(self) -> int: """Returns the input dimension.""" return self.dim_in - def distinguish_types(self) -> bool: - """Returns if the descriptor requires a neighbor list that distinguish different - atomic types or not. + def mixed_types(self) -> bool: + """If true, the discriptor + 1. assumes total number of atoms aligned across frames; + 2. requires a neighbor list that does not distinguish different atomic types. + + If false, the discriptor + 1. assumes total number of atoms of each atom type aligned across frames; + 2. requires a neighbor list that distinguishes different atomic types. + """ - return True + return False @property def dim_out(self): diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index 410a2039aa..0c15cae46c 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -171,11 +171,17 @@ def get_dim_emb(self) -> int: """Returns the output dimension of embedding.""" return self.filter_neuron[-1] - def distinguish_types(self) -> bool: - """Returns if the descriptor requires a neighbor list that distinguish different - atomic types or not. + def mixed_types(self) -> bool: + """If true, the discriptor + 1. assumes total number of atoms aligned across frames; + 2. requires a neighbor list that does not distinguish different atomic types. + + If false, the discriptor + 1. assumes total number of atoms of each atom type aligned across frames; + 2. requires a neighbor list that distinguishes different atomic types. + """ - return False + return True @property def dim_out(self): @@ -300,10 +306,10 @@ def forward( ) -def analyze_descrpt(matrix, ndescrpt, natoms, mixed_type=False, real_atype=None): +def analyze_descrpt(matrix, ndescrpt, natoms, mixed_types=False, real_atype=None): """Collect avg, square avg and count of descriptors in a batch.""" ntypes = natoms.shape[1] - 2 - if not mixed_type: + if not mixed_types: sysr = [] sysa = [] sysn = [] diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index 81a3d9ffa0..ad16741ee4 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -52,7 +52,7 @@ def get_zbl_model(model_params): fitting_net = model_params.get("fitting_net", None) fitting_net["type"] = fitting_net.get("type", "ener") fitting_net["ntypes"] = descriptor.get_ntypes() - fitting_net["distinguish_types"] = descriptor.distinguish_types() + fitting_net["mixed_types"] = descriptor.mixed_types() fitting_net["embedding_width"] = descriptor.get_dim_out() fitting_net["dim_descrpt"] = descriptor.get_dim_out() grad_force = "direct" not in fitting_net["type"] @@ -88,7 +88,7 @@ def get_model(model_params): fitting_net = model_params.get("fitting_net", None) fitting_net["type"] = fitting_net.get("type", "ener") fitting_net["ntypes"] = descriptor.get_ntypes() - fitting_net["distinguish_types"] = descriptor.distinguish_types() + fitting_net["mixed_types"] = descriptor.mixed_types() fitting_net["embedding_width"] = descriptor.get_dim_out() fitting_net["dim_descrpt"] = descriptor.get_dim_out() grad_force = "direct" not in fitting_net["type"] diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index a68ddd45a5..19bc514a2d 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -103,7 +103,7 @@ def forward_common( atype, self.get_rcut(), self.get_sel(), - distinguish_types=self.distinguish_types(), + mixed_types=self.mixed_types(), box=box, ) model_predict_lower = self.forward_common_lower( @@ -199,7 +199,7 @@ def format_nlist( Known limitations: - In the case of self.distinguish_types, the nlist is always formatted. + In the case of not self.mixed_types, the nlist is always formatted. May have side effact on the efficiency. Parameters @@ -217,9 +217,9 @@ def format_nlist( the formated nlist. """ - distinguish_types = self.distinguish_types() + mixed_types = self.mixed_types() nlist = self._format_nlist(extended_coord, nlist, sum(self.get_sel())) - if distinguish_types: + if not mixed_types: nlist = nlist_distinguish_types(nlist, extended_atype, self.get_sel()) return nlist diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index c67294c3cd..d57d460f6d 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -69,12 +69,16 @@ class InvarFitting(GeneralFitting): Activation function. precision : str Numerical precision. - distinguish_types : bool - Neighbor list that distinguish different atomic types or not. + mixed_types : bool + If true, use a uniform fitting net for all atom types, otherwise use + different fitting nets for different atom types. rcond : float, optional The condition number for the regression of atomic energy. seed : int, optional Random seed. + exclude_types: List[int] + Atomic contributions of the excluded atom types are set zero. + """ def __init__( @@ -90,7 +94,7 @@ def __init__( numb_aparam: int = 0, activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, - distinguish_types: bool = False, + mixed_types: bool = True, rcond: Optional[float] = None, seed: Optional[int] = None, exclude_types: List[int] = [], @@ -108,7 +112,7 @@ def __init__( numb_aparam=numb_aparam, activation_function=activation_function, precision=precision, - distinguish_types=distinguish_types, + mixed_types=mixed_types, rcond=rcond, seed=seed, exclude_types=exclude_types, @@ -134,8 +138,8 @@ def data_stat_key(self): def compute_output_stats(self, merged, stat_file_path: Optional[DPPath] = None): energy = [item["energy"] for item in merged] - mixed_type = "real_natoms_vec" in merged[0] - if mixed_type: + data_mixed_type = "real_natoms_vec" in merged[0] + if data_mixed_type: input_natoms = [item["real_natoms_vec"] for item in merged] else: input_natoms = [item["natoms"] for item in merged] @@ -203,7 +207,7 @@ def __init__( numb_aparam: int = 0, activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, - use_tebd: bool = True, + mixed_types: bool = True, **kwargs, ): super().__init__( @@ -218,7 +222,7 @@ def __init__( numb_aparam=numb_aparam, activation_function=activation_function, precision=precision, - use_tebd=use_tebd, + mixed_types=mixed_types, **kwargs, ) diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index c6b6959896..3a904a0696 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -168,12 +168,12 @@ def change_energy_bias( idx_type_map = sorter[ np.searchsorted(old_type_map, new_type_map, sorter=sorter) ] - mixed_type = np.all([i.mixed_type for i in finetune_data.systems]) + data_mixed_types = np.all([i.mixed_type for i in finetune_data.systems]) numb_type = len(old_type_map) type_numbs, energy_ground_truth, energy_predict = [], [], [] for test_data in sampled: nframes = test_data["energy"].shape[0] - if mixed_type: + if data_mixed_types: atype = test_data["atype"].detach().cpu().numpy() else: atype = test_data["atype"][0].detach().cpu().numpy() @@ -181,7 +181,7 @@ def change_energy_bias( [i.item() in idx_type_map for i in list(set(atype.reshape(-1)))] ).all(), "Some types are not in 'type_map'!" energy_ground_truth.append(test_data["energy"].cpu().numpy()) - if mixed_type: + if data_mixed_types: type_numbs.append( np.array( [(atype == i).sum(axis=-1) for i in idx_type_map], @@ -277,12 +277,16 @@ class GeneralFitting(Fitting): Activation function. precision : str Numerical precision. - distinguish_types : bool - Neighbor list that distinguish different atomic types or not. + mixed_types : bool + If true, use a uniform fitting net for all atom types, otherwise use + different fitting nets for different atom types. rcond : float, optional The condition number for the regression of atomic energy. seed : int, optional Random seed. + exclude_types: List[int] + Atomic contributions of the excluded atom types are set zero. + """ def __init__( @@ -297,7 +301,7 @@ def __init__( numb_aparam: int = 0, activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, - distinguish_types: bool = False, + mixed_types: bool = True, rcond: Optional[float] = None, seed: Optional[int] = None, exclude_types: List[int] = [], @@ -308,8 +312,7 @@ def __init__( self.ntypes = ntypes self.dim_descrpt = dim_descrpt self.neuron = neuron - self.distinguish_types = distinguish_types - self.use_tebd = not self.distinguish_types + self.mixed_types = mixed_types self.resnet_dt = resnet_dt self.numb_fparam = numb_fparam self.numb_aparam = numb_aparam @@ -327,7 +330,7 @@ def __init__( bias_atom_e = np.zeros([self.ntypes, net_dim_out], dtype=np.float64) bias_atom_e = torch.tensor(bias_atom_e, dtype=self.prec, device=device) bias_atom_e = bias_atom_e.view([self.ntypes, net_dim_out]) - if not self.use_tebd: + if not self.mixed_types: assert self.ntypes == bias_atom_e.shape[0], "Element count mismatches!" self.register_buffer("bias_atom_e", bias_atom_e) @@ -359,7 +362,7 @@ def __init__( self.old_impl = kwargs.get("old_impl", False) if self.old_impl: filter_layers = [] - for type_i in range(self.ntypes): + for type_i in range(self.ntypes if not self.mixed_types else 1): bias_type = 0.0 one = ResidualDeep( type_i, @@ -373,7 +376,7 @@ def __init__( self.filter_layers = None else: self.filter_layers = NetworkCollection( - 1 if self.distinguish_types else 0, + 1 if not self.mixed_types else 0, self.ntypes, network_type="fitting_network", networks=[ @@ -386,7 +389,7 @@ def __init__( self.precision, bias_out=True, ) - for ii in range(self.ntypes if self.distinguish_types else 1) + for ii in range(self.ntypes if not self.mixed_types else 1) ], ) self.filter_layers_old = None @@ -407,7 +410,7 @@ def serialize(self) -> dict: "numb_aparam": self.numb_aparam, "activation_function": self.activation_function, "precision": self.precision, - "distinguish_types": self.distinguish_types, + "mixed_types": self.mixed_types, "nets": self.filter_layers.serialize(), "rcond": self.rcond, "exclude_types": self.exclude_types, @@ -566,12 +569,9 @@ def _forward_common( device=env.DEVICE, ) # jit assertion if self.old_impl: - outs = torch.zeros_like(atype).unsqueeze(-1) # jit assertion assert self.filter_layers_old is not None - if self.use_tebd: - atom_property = self.filter_layers_old[0](xx) + self.bias_atom_e[ - atype - ].unsqueeze(-1) + if self.mixed_types: + atom_property = self.filter_layers_old[0](xx) + self.bias_atom_e[atype] outs = outs + atom_property # Shape is [nframes, natoms[0], 1] else: for type_i, filter_layer in enumerate(self.filter_layers_old): @@ -581,7 +581,7 @@ def _forward_common( atom_property = atom_property * mask.unsqueeze(-1) outs = outs + atom_property # Shape is [nframes, natoms[0], 1] else: - if self.use_tebd: + if self.mixed_types: atom_property = ( self.filter_layers.networks[0](xx) + self.bias_atom_e[atype] ) diff --git a/deepmd/pt/utils/env_mat_stat.py b/deepmd/pt/utils/env_mat_stat.py index 5247ce08ba..fa97203c89 100644 --- a/deepmd/pt/utils/env_mat_stat.py +++ b/deepmd/pt/utils/env_mat_stat.py @@ -115,7 +115,7 @@ def iter( atype, self.descriptor.get_rcut(), self.descriptor.get_sel(), - distinguish_types=self.descriptor.distinguish_types(), + mixed_types=self.descriptor.mixed_types(), box=box, ) env_mat, _, _ = prod_env_mat_se_a( @@ -177,7 +177,7 @@ def get_hash(self) -> str: "rcut_smth": round(self.descriptor.rcut_smth, 2), "nsel": self.descriptor.get_nsel(), "sel": self.descriptor.get_sel(), - "distinguish_types": self.descriptor.distinguish_types(), + "mixed_types": self.descriptor.mixed_types(), } ) diff --git a/deepmd/pt/utils/neighbor_stat.py b/deepmd/pt/utils/neighbor_stat.py index b85f3ebcd1..6361e7b9c7 100644 --- a/deepmd/pt/utils/neighbor_stat.py +++ b/deepmd/pt/utils/neighbor_stat.py @@ -32,20 +32,20 @@ class NeighborStatOP(torch.nn.Module): The num of atom types rcut The cut-off radius - distinguish_types : bool, optional - If False, treat all types as a single type. + mixed_types : bool, optional + If True, treat neighbors of all types as a single type. """ def __init__( self, ntypes: int, rcut: float, - distinguish_types: bool, + mixed_types: bool, ) -> None: super().__init__() self.rcut = rcut self.ntypes = ntypes - self.distinguish_types = distinguish_types + self.mixed_types = mixed_types def forward( self, @@ -94,7 +94,7 @@ def forward( rr2 = torch.sum(torch.square(diff), dim=-1) min_rr2, _ = torch.min(rr2, dim=-1) # count the number of neighbors - if self.distinguish_types: + if not self.mixed_types: mask = rr2 < self.rcut**2 nnei = torch.zeros((nframes, nloc, self.ntypes), dtype=torch.int32) for ii in range(self.ntypes): diff --git a/deepmd/pt/utils/nlist.py b/deepmd/pt/utils/nlist.py index 963c9bc9b6..43036604e2 100644 --- a/deepmd/pt/utils/nlist.py +++ b/deepmd/pt/utils/nlist.py @@ -22,7 +22,7 @@ def extend_input_and_build_neighbor_list( atype, rcut: float, sel: List[int], - distinguish_types: bool = False, + mixed_types: bool = False, box: Optional[torch.Tensor] = None, ): nframes, nloc = atype.shape[:2] @@ -42,7 +42,7 @@ def extend_input_and_build_neighbor_list( nloc, rcut, sel, - distinguish_types=distinguish_types, + distinguish_types=(not mixed_types), ) extended_coord = extended_coord.view(nframes, -1, 3) return extended_coord, extended_atype, mapping, nlist diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index 8b4a573a58..074856ea6c 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -970,7 +970,7 @@ def serialize(self, suffix: str) -> dict: "dim_descrpt": self.dim_descrpt, # very bad design: type embedding is not passed to the class # TODO: refactor the class - "distinguish_types": True, + "mixed_types": False, "dim_out": 1, "neuron": self.n_neuron, "resnet_dt": self.resnet_dt, diff --git a/deepmd/tf/utils/neighbor_stat.py b/deepmd/tf/utils/neighbor_stat.py index 2c063246e6..61531e7857 100644 --- a/deepmd/tf/utils/neighbor_stat.py +++ b/deepmd/tf/utils/neighbor_stat.py @@ -40,20 +40,20 @@ class NeighborStatOP: The num of atom types rcut The cut-off radius - distinguish_types : bool, optional - If False, treat all types as a single type. + mixed_types : bool, optional + If True, treat neighbors of all types as a single type. """ def __init__( self, ntypes: int, rcut: float, - distinguish_types: bool, + mixed_types: bool, ) -> None: super().__init__() self.rcut = rcut self.ntypes = ntypes - self.distinguish_types = distinguish_types + self.mixed_types = mixed_types def build( self, @@ -117,7 +117,7 @@ def build( rr2 = tf.where(mask, inf_mask, rr2) min_rr2 = tf.reduce_min(rr2, axis=(1, 2)) # count the number of neighbors - if self.distinguish_types: + if not self.mixed_types: mask = rr2 < self.rcut**2 nnei = [] for ii in range(self.ntypes): @@ -148,7 +148,7 @@ def build( nnei = tf.where( tf.tile( tf.less(atype, 0)[:, :, None], - [1, 1, self.ntypes if self.distinguish_types else 1], + [1, 1, self.ntypes if not self.mixed_types else 1], ), tf.zeros_like(nnei, dtype=tf.int32), nnei, diff --git a/source/tests/common/dpmodel/test_dp_atomic_model.py b/source/tests/common/dpmodel/test_dp_atomic_model.py index ea5d811b77..b32c8ae11a 100644 --- a/source/tests/common/dpmodel/test_dp_atomic_model.py +++ b/source/tests/common/dpmodel/test_dp_atomic_model.py @@ -35,7 +35,7 @@ def test_self_consistency( self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ) type_map = ["foo", "bar"] md0 = DPAtomicModel(ds, ft, type_map=type_map) diff --git a/source/tests/common/dpmodel/test_dp_model.py b/source/tests/common/dpmodel/test_dp_model.py index d22cd274c7..b982c9c2b5 100644 --- a/source/tests/common/dpmodel/test_dp_model.py +++ b/source/tests/common/dpmodel/test_dp_model.py @@ -36,7 +36,7 @@ def test_self_consistency( self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ) type_map = ["foo", "bar"] md0 = DPModel(ds, ft, type_map=type_map) diff --git a/source/tests/common/dpmodel/test_fitting_invar_fitting.py b/source/tests/common/dpmodel/test_fitting_invar_fitting.py index 85c90ef8ec..a31439d406 100644 --- a/source/tests/common/dpmodel/test_fitting_invar_fitting.py +++ b/source/tests/common/dpmodel/test_fitting_invar_fitting.py @@ -30,7 +30,7 @@ def test_self_consistency( atype = self.atype_ext[:, :nloc] for ( - distinguish_types, + mixed_types, od, nfp, nap, @@ -49,7 +49,7 @@ def test_self_consistency( od, numb_fparam=nfp, numb_aparam=nap, - distinguish_types=distinguish_types, + mixed_types=mixed_types, exclude_types=et, ) ifn1 = InvarFitting.deserialize(ifn0.serialize()) @@ -71,7 +71,7 @@ def test_mask(self): dd = ds.call(self.coord_ext, self.atype_ext, self.nlist) atype = self.atype_ext[:, :nloc] od = 2 - distinguish_types = False + mixed_types = True # exclude type 1 et = [1] ifn0 = InvarFitting( @@ -79,7 +79,7 @@ def test_mask(self): self.nt, ds.dim_out, od, - distinguish_types=distinguish_types, + mixed_types=mixed_types, exclude_types=et, ) ret0 = ifn0(dd[0], atype) @@ -105,7 +105,7 @@ def test_self_exception( atype = self.atype_ext[:, :nloc] for ( - distinguish_types, + mixed_types, od, nfp, nap, @@ -122,7 +122,7 @@ def test_self_exception( od, numb_fparam=nfp, numb_aparam=nap, - distinguish_types=distinguish_types, + mixed_types=mixed_types, ) if nfp > 0: diff --git a/source/tests/common/dpmodel/test_linear_atomic_model.py b/source/tests/common/dpmodel/test_linear_atomic_model.py index 74f2daf7be..79eef46b8a 100644 --- a/source/tests/common/dpmodel/test_linear_atomic_model.py +++ b/source/tests/common/dpmodel/test_linear_atomic_model.py @@ -49,7 +49,7 @@ def test_pairwise(self, mock_loadtxt): 2, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ) type_map = ["foo", "bar"] @@ -141,7 +141,7 @@ def setUp(self, mock_loadtxt): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ) type_map = ["foo", "bar"] dp_model = DPAtomicModel(ds, ft, type_map=type_map) diff --git a/source/tests/common/dpmodel/test_nlist.py b/source/tests/common/dpmodel/test_nlist.py index a36fa66d40..35145cde39 100644 --- a/source/tests/common/dpmodel/test_nlist.py +++ b/source/tests/common/dpmodel/test_nlist.py @@ -62,7 +62,7 @@ def setUp(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ) type_map = ["foo", "bar"] self.md = DPModel(ds, ft, type_map=type_map) diff --git a/source/tests/consistent/fitting/test_ener.py b/source/tests/consistent/fitting/test_ener.py index 222c4d84a5..e1f1c0fe84 100644 --- a/source/tests/consistent/fitting/test_ener.py +++ b/source/tests/consistent/fitting/test_ener.py @@ -41,7 +41,7 @@ @parameterized( (True, False), # resnet_dt ("float64", "float32"), # precision - (True, False), # distinguish_types + (True, False), # mixed_types (0, 1), # numb_fparam ) class TestEner(CommonTest, FittingTest, unittest.TestCase): @@ -50,7 +50,7 @@ def data(self) -> dict: ( resnet_dt, precision, - distinguish_types, + mixed_types, numb_fparam, ) = self.param return { @@ -66,18 +66,18 @@ def skip_tf(self) -> bool: ( resnet_dt, precision, - distinguish_types, + mixed_types, numb_fparam, ) = self.param - # TODO: distinguish_types - return not distinguish_types or CommonTest.skip_pt + # TODO: mixed_types + return mixed_types or CommonTest.skip_pt @property def skip_pt(self) -> bool: ( resnet_dt, precision, - distinguish_types, + mixed_types, numb_fparam, ) = self.param # TODO: float32 has bug @@ -104,20 +104,20 @@ def addtional_data(self) -> dict: ( resnet_dt, precision, - distinguish_types, + mixed_types, numb_fparam, ) = self.param return { "ntypes": self.ntypes, "dim_descrpt": self.inputs.shape[-1], - "distinguish_types": distinguish_types, + "mixed_types": mixed_types, } def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: ( resnet_dt, precision, - distinguish_types, + mixed_types, numb_fparam, ) = self.param return self.build_tf_fitting( @@ -133,7 +133,7 @@ def eval_pt(self, pt_obj: Any) -> Any: ( resnet_dt, precision, - distinguish_types, + mixed_types, numb_fparam, ) = self.param return ( @@ -153,7 +153,7 @@ def eval_dp(self, dp_obj: Any) -> Any: ( resnet_dt, precision, - distinguish_types, + mixed_types, numb_fparam, ) = self.param return dp_obj( @@ -174,7 +174,7 @@ def rtol(self) -> float: ( resnet_dt, precision, - distinguish_types, + mixed_types, numb_fparam, ) = self.param if precision == "float64": diff --git a/source/tests/pt/model/test_descriptor.py b/source/tests/pt/model/test_descriptor.py index a4493b5b51..cfc909b0e9 100644 --- a/source/tests/pt/model/test_descriptor.py +++ b/source/tests/pt/model/test_descriptor.py @@ -155,7 +155,7 @@ def test_consistency(self): self.pt_batch["atype"].to(env.DEVICE), self.rcut, self.sel, - distinguish_types=True, + mixed_types=False, box=self.pt_batch["box"].to(env.DEVICE), ) my_d, _, _ = prod_env_mat_se_a( diff --git a/source/tests/pt/model/test_descriptor_dpa1.py b/source/tests/pt/model/test_descriptor_dpa1.py index 07d4d34449..555d33429c 100644 --- a/source/tests/pt/model/test_descriptor_dpa1.py +++ b/source/tests/pt/model/test_descriptor_dpa1.py @@ -261,7 +261,7 @@ def test_descriptor_block(self): atype, des.get_rcut(), des.get_sel(), - distinguish_types=des.distinguish_types(), + mixed_types=des.mixed_types(), box=box, ) descriptor, env_mat, diff, rot_mat, sw = des( @@ -315,7 +315,7 @@ def test_descriptor(self): atype, des.get_rcut(), des.get_sel(), - distinguish_types=des.distinguish_types(), + mixed_types=des.mixed_types(), box=box, ) descriptor, env_mat, diff, rot_mat, sw = des( diff --git a/source/tests/pt/model/test_descriptor_dpa2.py b/source/tests/pt/model/test_descriptor_dpa2.py index 6b80eb89a2..d10c1a5a6e 100644 --- a/source/tests/pt/model/test_descriptor_dpa2.py +++ b/source/tests/pt/model/test_descriptor_dpa2.py @@ -152,7 +152,7 @@ def test_descriptor_hyb(self): atype, rcut_max, sel_max, - distinguish_types=des.distinguish_types(), + mixed_types=des.mixed_types(), box=box, ) nlist_dict = build_multiple_neighbor_list( @@ -215,7 +215,7 @@ def test_descriptor(self): atype, des.get_rcut(), des.get_sel(), - distinguish_types=des.distinguish_types(), + mixed_types=des.mixed_types(), box=box, ) descriptor, env_mat, diff, rot_mat, sw = des( diff --git a/source/tests/pt/model/test_dp_atomic_model.py b/source/tests/pt/model/test_dp_atomic_model.py index d56bec3bb2..bb0d20ab02 100644 --- a/source/tests/pt/model/test_dp_atomic_model.py +++ b/source/tests/pt/model/test_dp_atomic_model.py @@ -47,7 +47,7 @@ def test_self_consistency(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] md0 = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) @@ -75,7 +75,7 @@ def test_dp_consistency(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ) type_map = ["foo", "bar"] md0 = DPDPAtomicModel(ds, ft, type_map=type_map) @@ -103,7 +103,7 @@ def test_jit(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] md0 = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) diff --git a/source/tests/pt/model/test_dp_model.py b/source/tests/pt/model/test_dp_model.py index fb936e59cb..0a16d4672c 100644 --- a/source/tests/pt/model/test_dp_model.py +++ b/source/tests/pt/model/test_dp_model.py @@ -54,7 +54,7 @@ def test_self_consistency(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] md0 = DPModel(ds, ft, type_map=type_map).to(env.DEVICE) @@ -97,7 +97,7 @@ def test_self_consistency(self): self.nloc, self.rcut, self.sel, - distinguish_types=md0.distinguish_types(), + distinguish_types=(not md0.mixed_types()), ) args = [coord_ext, atype_ext, nlist] ret2 = md0.forward_common_lower(*args, do_atomic_virial=True) @@ -121,7 +121,7 @@ def test_dp_consistency(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), numb_fparam=nfp, numb_aparam=nap, ) @@ -160,7 +160,7 @@ def test_dp_consistency_nopbc(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), numb_fparam=nfp, numb_aparam=nap, ) @@ -203,7 +203,7 @@ def test_self_consistency(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] md0 = DPModel(ds, ft, type_map=type_map).to(env.DEVICE) @@ -249,7 +249,7 @@ def test_dp_consistency(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ) type_map = ["foo", "bar"] md0 = DPDPModel(ds, ft, type_map=type_map) @@ -281,7 +281,7 @@ def test_jit(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] md0 = DPModel(ds, ft, type_map=type_map).to(env.DEVICE) @@ -331,7 +331,7 @@ def setUp(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] self.md = DPModel(ds, ft, type_map=type_map).to(env.DEVICE) @@ -404,7 +404,7 @@ def test_self_consistency(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] md0 = EnergyModel(ds, ft, type_map=type_map).to(env.DEVICE) @@ -439,7 +439,7 @@ def test_self_consistency(self): to_torch_tensor(self.atype), self.rcut, self.sel, - distinguish_types=md0.distinguish_types(), + mixed_types=md0.mixed_types(), box=to_torch_tensor(self.cell), ) args = [coord_ext, atype_ext, nlist] @@ -468,7 +468,7 @@ def test_self_consistency(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] md0 = EnergyModel(ds, ft, type_map=type_map).to(env.DEVICE) @@ -513,7 +513,7 @@ def test_jit(self): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] md0 = EnergyModel(ds, ft, type_map=type_map).to(env.DEVICE) diff --git a/source/tests/pt/model/test_embedding_net.py b/source/tests/pt/model/test_embedding_net.py index 2621b5d135..02e3f9f70a 100644 --- a/source/tests/pt/model/test_embedding_net.py +++ b/source/tests/pt/model/test_embedding_net.py @@ -190,7 +190,7 @@ def test_consistency(self): self.torch_batch["atype"].to(env.DEVICE), self.rcut, self.sel, - distinguish_types=True, + mixed_types=False, box=self.torch_batch["box"].to(env.DEVICE), ) descriptor_out, _, _, _, _ = descriptor( diff --git a/source/tests/pt/model/test_ener_fitting.py b/source/tests/pt/model/test_ener_fitting.py index 9e5ec0b903..a41b4d6b9f 100644 --- a/source/tests/pt/model/test_ener_fitting.py +++ b/source/tests/pt/model/test_ener_fitting.py @@ -44,7 +44,7 @@ def test_consistency( ) atype = torch.tensor(self.atype_ext[:, :nloc], dtype=int, device=env.DEVICE) - for od, distinguish_types, nfp, nap, et in itertools.product( + for od, mixed_types, nfp, nap, et in itertools.product( [1, 3], [True, False], [0, 3], @@ -58,7 +58,7 @@ def test_consistency( od, numb_fparam=nfp, numb_aparam=nap, - use_tebd=(not distinguish_types), + mixed_types=mixed_types, exclude_types=et, ).to(env.DEVICE) ft1 = DPInvarFitting.deserialize(ft0.serialize()) @@ -110,18 +110,19 @@ def test_new_old( atype = torch.tensor(self.atype_ext[:, :nloc], dtype=int, device=env.DEVICE) od = 1 - for distinguish_types in itertools.product( + for foo, mixed_types in itertools.product( + [True], [True, False], ): ft0 = EnergyFittingNet( self.nt, dd.dim_out, - distinguish_types=distinguish_types, + mixed_types=mixed_types, ).to(env.DEVICE) ft1 = EnergyFittingNet( self.nt, dd.dim_out, - distinguish_types=distinguish_types, + mixed_types=mixed_types, old_impl=True, ).to(env.DEVICE) dd0 = ft0.state_dict() @@ -146,7 +147,7 @@ def test_new_old( def test_jit( self, ): - for od, distinguish_types, nfp, nap, et in itertools.product( + for od, mixed_types, nfp, nap, et in itertools.product( [1, 3], [True, False], [0, 3], @@ -160,7 +161,7 @@ def test_jit( od, numb_fparam=nfp, numb_aparam=nap, - use_tebd=(not distinguish_types), + mixed_types=mixed_types, exclude_types=et, ).to(env.DEVICE) torch.jit.script(ft0) diff --git a/source/tests/pt/model/test_fitting_net.py b/source/tests/pt/model/test_fitting_net.py index 2bcfb7b64c..c7e1723799 100644 --- a/source/tests/pt/model/test_fitting_net.py +++ b/source/tests/pt/model/test_fitting_net.py @@ -109,7 +109,7 @@ def test_consistency(self): self.embedding_width, neuron=self.n_neuron, bias_atom_e=self.dp_fn.bias_atom_e, - distinguish_types=True, + mixed_types=False, ).to(env.DEVICE) for name, param in my_fn.named_parameters(): matched = re.match( diff --git a/source/tests/pt/model/test_linear_atomic_model.py b/source/tests/pt/model/test_linear_atomic_model.py index bb836d4e46..dcfff0b387 100644 --- a/source/tests/pt/model/test_linear_atomic_model.py +++ b/source/tests/pt/model/test_linear_atomic_model.py @@ -65,7 +65,7 @@ def test_pairwise(self, mock_loadtxt): 2, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] @@ -133,7 +133,7 @@ def setUp(self, mock_loadtxt): self.nt, ds.get_dim_out(), 1, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] dp_model = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) diff --git a/source/tests/pt/model/test_make_hessian_model.py b/source/tests/pt/model/test_make_hessian_model.py index 6f321b6478..1fb7e6f53a 100644 --- a/source/tests/pt/model/test_make_hessian_model.py +++ b/source/tests/pt/model/test_make_hessian_model.py @@ -154,7 +154,7 @@ def setUp(self): self.nt, ds.get_dim_out(), self.nv, - distinguish_types=ds.distinguish_types(), + mixed_types=ds.mixed_types(), do_hessian=True, neuron=[4, 4, 4], ).to(env.DEVICE) diff --git a/source/tests/pt/model/test_model.py b/source/tests/pt/model/test_model.py index 856b48064b..938eb7545d 100644 --- a/source/tests/pt/model/test_model.py +++ b/source/tests/pt/model/test_model.py @@ -291,7 +291,7 @@ def test_consistency(self): "neuron": self.filter_neuron, "axis_neuron": self.axis_neuron, }, - "fitting_net": {"neuron": self.n_neuron, "distinguish_types": True}, + "fitting_net": {"neuron": self.n_neuron, "mixed_types": False}, "data_stat_nbatch": self.data_stat_nbatch, "type_map": self.type_map, }, From ab354686ac11308828bd0c95dd09014495c85ea2 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 19 Feb 2024 02:02:45 -0500 Subject: [PATCH 100/686] pt: process frames in parallel for env mat stat (#3293) Resolves https://github.com/deepmodeling/deepmd-kit/pull/3285#discussion_r1493338812. We don't even need to consider whether the data uses mixed type in this piece of code. The data can be reshaped to the atom level, and type masks can be used to get an environmental matrix with certain types. No frame-level things are involved here. --------- Signed-off-by: Jinzhe Zeng --- deepmd/pt/utils/env_mat_stat.py | 49 +++++++++++++-------------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/deepmd/pt/utils/env_mat_stat.py b/deepmd/pt/utils/env_mat_stat.py index fa97203c89..2f3c728c99 100644 --- a/deepmd/pt/utils/env_mat_stat.py +++ b/deepmd/pt/utils/env_mat_stat.py @@ -128,38 +128,27 @@ def iter( # TODO: export rcut_smth from DescriptorBlock self.descriptor.rcut_smth, ) + # reshape to nframes * nloc at the atom level, + # so nframes/mixed_type do not matter env_mat = env_mat.view( - coord.shape[0], coord.shape[1], self.descriptor.get_nsel(), 4 + coord.shape[0] * coord.shape[1], self.descriptor.get_nsel(), 4 ) - - if "real_natoms_vec" not in system: - end_indexes = torch.cumsum(natoms[0, 2:], 0) - start_indexes = torch.cat( - [ - torch.zeros(1, dtype=torch.int32, device=env.DEVICE), - end_indexes[:-1], - ] - ) - for type_i in range(self.descriptor.get_ntypes()): - dd = env_mat[ - :, start_indexes[type_i] : end_indexes[type_i], :, : - ] # all descriptors for this element - env_mats = {} - env_mats[f"r_{type_i}"] = dd[:, :, :, :1] - env_mats[f"a_{type_i}"] = dd[:, :, :, 1:] - yield self.compute_stat(env_mats) - else: - for frame_item in range(env_mat.shape[0]): - dd_ff = env_mat[frame_item] - atype_frame = atype[frame_item] - for type_i in range(self.descriptor.get_ntypes()): - type_idx = atype_frame == type_i - dd = dd_ff[type_idx] - dd = dd.reshape([-1, 4]) # typen_atoms * nnei, 4 - env_mats = {} - env_mats[f"r_{type_i}"] = dd[:, :1] - env_mats[f"a_{type_i}"] = dd[:, 1:] - yield self.compute_stat(env_mats) + atype = atype.view(coord.shape[0] * coord.shape[1]) + # (1, nloc) eq (ntypes, 1), so broadcast is possible + # shape: (ntypes, nloc) + type_idx = torch.eq( + atype.view(1, -1), + torch.arange( + self.descriptor.get_ntypes(), device=env.DEVICE, dtype=torch.int32 + ).view(-1, 1), + ) + for type_i in range(self.descriptor.get_ntypes()): + dd = env_mat[type_idx[type_i]] + dd = dd.reshape([-1, 4]) # typen_atoms * nnei, 4 + env_mats = {} + env_mats[f"r_{type_i}"] = dd[:, :1] + env_mats[f"a_{type_i}"] = dd[:, 1:] + yield self.compute_stat(env_mats) def get_hash(self) -> str: """Get the hash of the environment matrix. From ab2ed0eb6d566b0777a3155f8c7aec8746765410 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 19 Feb 2024 20:34:43 -0500 Subject: [PATCH 101/686] pt: avoid `set_default_dtype` in tests (#3303) Why it is bad: The default dtype in the producution code is still float32. When setting it to float64 during tests, the actual producation behavior may not be properly tested. (for example, if the production code misses dtype for pt.zeros, we are not able to find it in these tests) Signed-off-by: Jinzhe Zeng --- source/tests/pt/model/test_descriptor_dpa1.py | 20 ++++++++++++------- source/tests/pt/model/test_descriptor_dpa2.py | 20 ++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/source/tests/pt/model/test_descriptor_dpa1.py b/source/tests/pt/model/test_descriptor_dpa1.py index 555d33429c..07cb684afa 100644 --- a/source/tests/pt/model/test_descriptor_dpa1.py +++ b/source/tests/pt/model/test_descriptor_dpa1.py @@ -22,9 +22,6 @@ extend_input_and_build_neighbor_list, ) -dtype = torch.float64 -torch.set_default_dtype(dtype) - CUR_DIR = os.path.dirname(__file__) @@ -41,7 +38,11 @@ def setUp(self): 8.178091365465004481e-01, 6.159552512682983760e00, ] - self.cell = torch.Tensor(cell).view(1, 3, 3).to(env.DEVICE) + self.cell = ( + torch.tensor(cell, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + .view(1, 3, 3) + .to(env.DEVICE) + ) coord = [ 2.978060152121375648e00, 3.588469695887098077e00, @@ -59,9 +60,13 @@ def setUp(self): 6.575011420004332585e00, 6.825240650611076099e00, ] - self.coord = torch.Tensor(coord).view(1, -1, 3).to(env.DEVICE) + self.coord = ( + torch.tensor(coord, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + .view(1, -1, 3) + .to(env.DEVICE) + ) self.atype = torch.IntTensor([0, 0, 0, 1, 1]).view(1, -1).to(env.DEVICE) - self.ref_d = torch.Tensor( + self.ref_d = torch.tensor( [ 8.382518544113587780e-03, -3.390120566088597812e-03, @@ -223,7 +228,8 @@ def setUp(self): 1.038826463247002245e-03, -1.899910089750410976e-03, 1.518237240362583541e-03, - ] + ], + dtype=env.GLOBAL_PT_FLOAT_PRECISION, ).to(env.DEVICE) with open(Path(CUR_DIR) / "models" / "dpa1.json") as fp: self.model_json = json.load(fp) diff --git a/source/tests/pt/model/test_descriptor_dpa2.py b/source/tests/pt/model/test_descriptor_dpa2.py index d10c1a5a6e..d94686fee1 100644 --- a/source/tests/pt/model/test_descriptor_dpa2.py +++ b/source/tests/pt/model/test_descriptor_dpa2.py @@ -24,9 +24,6 @@ get_multiple_nlist_key, ) -dtype = torch.float64 -torch.set_default_dtype(dtype) - CUR_DIR = os.path.dirname(__file__) @@ -43,7 +40,11 @@ def setUp(self): 8.178091365465004481e-01, 6.159552512682983760e00, ] - self.cell = torch.Tensor(cell).view(1, 3, 3).to(env.DEVICE) + self.cell = ( + torch.tensor(cell, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + .view(1, 3, 3) + .to(env.DEVICE) + ) coord = [ 2.978060152121375648e00, 3.588469695887098077e00, @@ -61,9 +62,13 @@ def setUp(self): 6.575011420004332585e00, 6.825240650611076099e00, ] - self.coord = torch.Tensor(coord).view(1, -1, 3).to(env.DEVICE) + self.coord = ( + torch.tensor(coord, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + .view(1, -1, 3) + .to(env.DEVICE) + ) self.atype = torch.IntTensor([0, 0, 0, 1, 1]).view(1, -1).to(env.DEVICE) - self.ref_d = torch.Tensor( + self.ref_d = torch.tensor( [ 8.435412613327306630e-01, -4.717109614540972440e-01, @@ -105,7 +110,8 @@ def setUp(self): -5.384371277650709109e-01, -1.490368056268364549e00, -3.073744832541754762e-02, - ] + ], + dtype=env.GLOBAL_PT_FLOAT_PRECISION, ).to(env.DEVICE) with open(Path(CUR_DIR) / "models" / "dpa2_hyb.json") as fp: self.model_json = json.load(fp) From 33495d82162b8c82d8cbba12ede817878bda3751 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:03:59 +0800 Subject: [PATCH 102/686] Feat: add polarizability fitting net (#3296) This PR is to provide pytorch implementation and backend-independent numpy implementation of the polarizability fitting net. Note: - The shift_diag requires statistics, not implemented in this PR. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/dpmodel/fitting/__init__.py | 4 + deepmd/dpmodel/fitting/dipole_fitting.py | 14 +- deepmd/dpmodel/fitting/general_fitting.py | 12 +- .../dpmodel/fitting/polarizability_fitting.py | 241 ++++++++++++++ deepmd/pt/model/task/dipole.py | 25 +- deepmd/pt/model/task/fitting.py | 12 +- deepmd/pt/model/task/polarizability.py | 194 +++++++++++ source/tests/pt/model/test_dipole_fitting.py | 38 +-- .../pt/model/test_polarizability_fitting.py | 312 ++++++++++++++++++ 9 files changed, 807 insertions(+), 45 deletions(-) create mode 100644 deepmd/dpmodel/fitting/polarizability_fitting.py create mode 100644 deepmd/pt/model/task/polarizability.py create mode 100644 source/tests/pt/model/test_polarizability_fitting.py diff --git a/deepmd/dpmodel/fitting/__init__.py b/deepmd/dpmodel/fitting/__init__.py index 2da752eaa7..0b4fe001b3 100644 --- a/deepmd/dpmodel/fitting/__init__.py +++ b/deepmd/dpmodel/fitting/__init__.py @@ -8,9 +8,13 @@ from .make_base_fitting import ( make_base_fitting, ) +from .polarizability_fitting import ( + PolarFitting, +) __all__ = [ "InvarFitting", "make_base_fitting", "DipoleFitting", + "PolarFitting", ] diff --git a/deepmd/dpmodel/fitting/dipole_fitting.py b/deepmd/dpmodel/fitting/dipole_fitting.py index 5a4952edb8..c210945e76 100644 --- a/deepmd/dpmodel/fitting/dipole_fitting.py +++ b/deepmd/dpmodel/fitting/dipole_fitting.py @@ -24,7 +24,7 @@ @fitting_check_output class DipoleFitting(GeneralFitting): - r"""Fitting rotationally invariant diploe of the system. + r"""Fitting rotationally equivariant diploe of the system. Parameters ---------- @@ -34,7 +34,7 @@ class DipoleFitting(GeneralFitting): The number of atom types. dim_descrpt The dimension of the input descriptor. - dim_rot_mat : int + embedding_width : int The dimension of rotation matrix, m1. neuron Number of neurons :math:`N` in each hidden layer of the fitting net @@ -78,7 +78,7 @@ def __init__( var_name: str, ntypes: int, dim_descrpt: int, - dim_rot_mat: int, + embedding_width: int, neuron: List[int] = [120, 120, 120], resnet_dt: bool = True, numb_fparam: int = 0, @@ -108,7 +108,7 @@ def __init__( if atom_ener is not None and atom_ener != []: raise NotImplementedError("atom_ener is not implemented") - self.dim_rot_mat = dim_rot_mat + self.embedding_width = embedding_width super().__init__( var_name=var_name, ntypes=ntypes, @@ -133,11 +133,11 @@ def __init__( def _net_out_dim(self): """Set the FittingNet output dim.""" - return self.dim_rot_mat + return self.embedding_width def serialize(self) -> dict: data = super().serialize() - data["dim_rot_mat"] = self.dim_rot_mat + data["embedding_width"] = self.embedding_width data["old_impl"] = self.old_impl return data @@ -194,7 +194,7 @@ def call( self.var_name ] # (nframes * nloc, 1, m1) - out = out.reshape(-1, 1, self.dim_rot_mat) + out = out.reshape(-1, 1, self.embedding_width) # (nframes * nloc, m1, 3) gr = gr.reshape(nframes * nloc, -1, 3) # (nframes, nloc, 3) diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index 03f8f25237..890a065f15 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -189,6 +189,8 @@ def __setitem__(self, key, value): self.aparam_avg = value elif key in ["aparam_inv_std"]: self.aparam_inv_std = value + elif key in ["scale"]: + self.scale = value else: raise KeyError(key) @@ -203,6 +205,8 @@ def __getitem__(self, key): return self.aparam_avg elif key in ["aparam_inv_std"]: return self.aparam_inv_std + elif key in ["scale"]: + return self.scale else: raise KeyError(key) @@ -327,10 +331,10 @@ def _call_common( mask = np.tile( (atype == type_i).reshape([nf, nloc, 1]), [1, 1, net_dim_out] ) - atom_energy = self.nets[(type_i,)](xx) - atom_energy = atom_energy + self.bias_atom_e[type_i] - atom_energy = atom_energy * mask - outs = outs + atom_energy # Shape is [nframes, natoms[0], 1] + atom_property = self.nets[(type_i,)](xx) + atom_property = atom_property + self.bias_atom_e[type_i] + atom_property = atom_property * mask + outs = outs + atom_property # Shape is [nframes, natoms[0], 1] else: outs = self.nets[()](xx) + self.bias_atom_e[atype] # nf x nloc diff --git a/deepmd/dpmodel/fitting/polarizability_fitting.py b/deepmd/dpmodel/fitting/polarizability_fitting.py new file mode 100644 index 0000000000..d828693fe0 --- /dev/null +++ b/deepmd/dpmodel/fitting/polarizability_fitting.py @@ -0,0 +1,241 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Any, + Dict, + List, + Optional, +) + +import numpy as np + +from deepmd.common import ( + GLOBAL_NP_FLOAT_PRECISION, +) +from deepmd.dpmodel import ( + DEFAULT_PRECISION, +) +from deepmd.dpmodel.output_def import ( + FittingOutputDef, + OutputVariableDef, + fitting_check_output, +) + +from .general_fitting import ( + GeneralFitting, +) + + +@fitting_check_output +class PolarFitting(GeneralFitting): + r"""Fitting rotationally equivariant polarizability of the system. + + Parameters + ---------- + var_name + The name of the output variable. + ntypes + The number of atom types. + dim_descrpt + The dimension of the input descriptor. + embedding_width : int + The dimension of rotation matrix, m1. + neuron + Number of neurons :math:`N` in each hidden layer of the fitting net + resnet_dt + Time-step `dt` in the resnet construction: + :math:`y = x + dt * \phi (Wx + b)` + numb_fparam + Number of frame parameter + numb_aparam + Number of atomic parameter + rcond + The condition number for the regression of atomic energy. + tot_ener_zero + Force the total energy to zero. Useful for the charge fitting. + trainable + If the weights of fitting net are trainable. + Suppose that we have :math:`N_l` hidden layers in the fitting net, + this list is of length :math:`N_l + 1`, specifying if the hidden layers and the output layer are trainable. + atom_ener + Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. + activation_function + The activation function :math:`\boldsymbol{\phi}` in the embedding net. Supported options are |ACTIVATION_FN| + precision + The precision of the embedding net parameters. Supported options are |PRECISION| + layer_name : list[Optional[str]], optional + The name of the each layer. If two layers, either in the same fitting or different fittings, + have the same name, they will share the same neural network parameters. + use_aparam_as_mask: bool, optional + If True, the atomic parameters will be used as a mask that determines the atom is real/virtual. + And the aparam will not be used as the atomic parameters for embedding. + mixed_types + If true, use a uniform fitting net for all atom types, otherwise use + different fitting nets for different atom types. + fit_diag : bool + Fit the diagonal part of the rotational invariant polarizability matrix, which will be converted to + normal polarizability matrix by contracting with the rotation matrix. + scale : List[float] + The output of the fitting net (polarizability matrix) for type i atom will be scaled by scale[i] + shift_diag : bool + Whether to shift the diagonal part of the polarizability matrix. The shift operation is carried out after scale. + """ + + def __init__( + self, + var_name: str, + ntypes: int, + dim_descrpt: int, + embedding_width: int, + neuron: List[int] = [120, 120, 120], + resnet_dt: bool = True, + numb_fparam: int = 0, + numb_aparam: int = 0, + rcond: Optional[float] = None, + tot_ener_zero: bool = False, + trainable: Optional[List[bool]] = None, + atom_ener: Optional[List[Optional[float]]] = None, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + layer_name: Optional[List[Optional[str]]] = None, + use_aparam_as_mask: bool = False, + spin: Any = None, + mixed_types: bool = False, + exclude_types: List[int] = [], + old_impl: bool = False, + fit_diag: bool = True, + scale: Optional[List[float]] = None, + shift_diag: bool = True, + ): + # seed, uniform_seed are not included + if tot_ener_zero: + raise NotImplementedError("tot_ener_zero is not implemented") + if spin is not None: + raise NotImplementedError("spin is not implemented") + if use_aparam_as_mask: + raise NotImplementedError("use_aparam_as_mask is not implemented") + if layer_name is not None: + raise NotImplementedError("layer_name is not implemented") + if atom_ener is not None and atom_ener != []: + raise NotImplementedError("atom_ener is not implemented") + + self.embedding_width = embedding_width + self.fit_diag = fit_diag + self.scale = scale + if self.scale is None: + self.scale = [1.0 for _ in range(ntypes)] + else: + assert ( + isinstance(self.scale, list) and len(self.scale) == ntypes + ), "Scale should be a list of length ntypes." + self.scale = np.array(self.scale, dtype=GLOBAL_NP_FLOAT_PRECISION).reshape( + ntypes, 1 + ) + self.shift_diag = shift_diag + super().__init__( + var_name=var_name, + ntypes=ntypes, + dim_descrpt=dim_descrpt, + neuron=neuron, + resnet_dt=resnet_dt, + numb_fparam=numb_fparam, + numb_aparam=numb_aparam, + rcond=rcond, + tot_ener_zero=tot_ener_zero, + trainable=trainable, + atom_ener=atom_ener, + activation_function=activation_function, + precision=precision, + layer_name=layer_name, + use_aparam_as_mask=use_aparam_as_mask, + spin=spin, + mixed_types=mixed_types, + exclude_types=exclude_types, + ) + self.old_impl = False + + def _net_out_dim(self): + """Set the FittingNet output dim.""" + return ( + self.embedding_width + if self.fit_diag + else self.embedding_width * self.embedding_width + ) + + def serialize(self) -> dict: + data = super().serialize() + data["embedding_width"] = self.embedding_width + data["old_impl"] = self.old_impl + data["fit_diag"] = self.fit_diag + data["@variables"]["scale"] = self.scale + return data + + def output_def(self): + return FittingOutputDef( + [ + OutputVariableDef( + self.var_name, + [3, 3], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ), + ] + ) + + def call( + self, + descriptor: np.ndarray, + atype: np.ndarray, + gr: Optional[np.ndarray] = None, + g2: Optional[np.ndarray] = None, + h2: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + ) -> Dict[str, np.ndarray]: + """Calculate the fitting. + + Parameters + ---------- + descriptor + input descriptor. shape: nf x nloc x nd + atype + the atom type. shape: nf x nloc + gr + The rotationally equivariant and permutationally invariant single particle + representation. shape: nf x nloc x ng x 3 + g2 + The rotationally invariant pair-partical representation. + shape: nf x nloc x nnei x ng + h2 + The rotationally equivariant pair-partical representation. + shape: nf x nloc x nnei x 3 + fparam + The frame parameter. shape: nf x nfp. nfp being `numb_fparam` + aparam + The atomic parameter. shape: nf x nloc x nap. nap being `numb_aparam` + + """ + nframes, nloc, _ = descriptor.shape + assert ( + gr is not None + ), "Must provide the rotation matrix for polarizability fitting." + # (nframes, nloc, _net_out_dim) + out = self._call_common(descriptor, atype, gr, g2, h2, fparam, aparam)[ + self.var_name + ] + out = out * self.scale[atype] + # (nframes * nloc, m1, 3) + gr = gr.reshape(nframes * nloc, -1, 3) + + if self.fit_diag: + out = out.reshape(-1, self.embedding_width) + out = np.einsum("ij,ijk->ijk", out, gr) + else: + out = out.reshape(-1, self.embedding_width, self.embedding_width) + out = (out + np.transpose(out, axes=(0, 2, 1))) / 2 + out = np.einsum("bim,bmj->bij", out, gr) # (nframes * nloc, m1, 3) + out = np.einsum( + "bim,bmj->bij", np.transpose(gr, axes=(0, 2, 1)), out + ) # (nframes * nloc, 3, 3) + out = out.reshape(nframes, nloc, 3, 3) + return {self.var_name: out} diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py index fedf4386c0..4ea66e2636 100644 --- a/deepmd/pt/model/task/dipole.py +++ b/deepmd/pt/model/task/dipole.py @@ -25,7 +25,7 @@ class DipoleFittingNet(GeneralFitting): - """Construct a general fitting net. + """Construct a dipole fitting net. Parameters ---------- @@ -35,9 +35,7 @@ class DipoleFittingNet(GeneralFitting): Element count. dim_descrpt : int Embedding width per atom. - dim_out : int - The output dimension of the fitting net. - dim_rot_mat : int + embedding_width : int The dimension of rotation matrix, m1. neuron : List[int] Number of neurons in each hidden layers of the fitting net. @@ -51,8 +49,9 @@ class DipoleFittingNet(GeneralFitting): Activation function. precision : str Numerical precision. - distinguish_types : bool - Neighbor list that distinguish different atomic types or not. + mixed_types : bool + If true, use a uniform fitting net for all atom types, otherwise use + different fitting nets for different atom types. rcond : float, optional The condition number for the regression of atomic energy. seed : int, optional @@ -64,20 +63,20 @@ def __init__( var_name: str, ntypes: int, dim_descrpt: int, - dim_rot_mat: int, + embedding_width: int, neuron: List[int] = [128, 128, 128], resnet_dt: bool = True, numb_fparam: int = 0, numb_aparam: int = 0, activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, - distinguish_types: bool = False, + mixed_types: bool = True, rcond: Optional[float] = None, seed: Optional[int] = None, exclude_types: List[int] = [], **kwargs, ): - self.dim_rot_mat = dim_rot_mat + self.embedding_width = embedding_width super().__init__( var_name=var_name, ntypes=ntypes, @@ -88,7 +87,7 @@ def __init__( numb_aparam=numb_aparam, activation_function=activation_function, precision=precision, - distinguish_types=distinguish_types, + mixed_types=mixed_types, rcond=rcond, seed=seed, exclude_types=exclude_types, @@ -98,11 +97,11 @@ def __init__( def _net_out_dim(self): """Set the FittingNet output dim.""" - return self.dim_rot_mat + return self.embedding_width def serialize(self) -> dict: data = super().serialize() - data["dim_rot_mat"] = self.dim_rot_mat + data["embedding_width"] = self.embedding_width data["old_impl"] = self.old_impl return data @@ -144,7 +143,7 @@ def forward( self.var_name ] # (nframes * nloc, 1, m1) - out = out.view(-1, 1, self.dim_rot_mat) + out = out.view(-1, 1, self.embedding_width) # (nframes * nloc, m1, 3) gr = gr.view(nframes * nloc, -1, 3) # (nframes, nloc, 3) diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 3a904a0696..cade533f1a 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -476,6 +476,8 @@ def __setitem__(self, key, value): self.aparam_avg = value elif key in ["aparam_inv_std"]: self.aparam_inv_std = value + elif key in ["scale"]: + self.scale = value else: raise KeyError(key) @@ -490,6 +492,8 @@ def __getitem__(self, key): return self.aparam_avg elif key in ["aparam_inv_std"]: return self.aparam_inv_std + elif key in ["scale"]: + return self.scale else: raise KeyError(key) @@ -585,7 +589,9 @@ def _forward_common( atom_property = ( self.filter_layers.networks[0](xx) + self.bias_atom_e[atype] ) - outs = outs + atom_property # Shape is [nframes, natoms[0], 1] + outs = ( + outs + atom_property + ) # Shape is [nframes, natoms[0], net_dim_out] else: for type_i, ll in enumerate(self.filter_layers.networks): mask = (atype == type_i).unsqueeze(-1) @@ -593,7 +599,9 @@ def _forward_common( atom_property = ll(xx) atom_property = atom_property + self.bias_atom_e[type_i] atom_property = atom_property * mask - outs = outs + atom_property # Shape is [nframes, natoms[0], 1] + outs = ( + outs + atom_property + ) # Shape is [nframes, natoms[0], net_dim_out] # nf x nloc mask = self.emask(atype) # nf x nloc x nod diff --git a/deepmd/pt/model/task/polarizability.py b/deepmd/pt/model/task/polarizability.py new file mode 100644 index 0000000000..dc8d13ee84 --- /dev/null +++ b/deepmd/pt/model/task/polarizability.py @@ -0,0 +1,194 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging +from typing import ( + List, + Optional, +) + +import torch + +from deepmd.dpmodel import ( + FittingOutputDef, + OutputVariableDef, +) +from deepmd.pt.model.task.fitting import ( + GeneralFitting, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.env import ( + DEFAULT_PRECISION, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, +) + +log = logging.getLogger(__name__) + + +class PolarFittingNet(GeneralFitting): + """Construct a polar fitting net. + + Parameters + ---------- + var_name : str + The atomic property to fit, 'polar'. + ntypes : int + Element count. + dim_descrpt : int + Embedding width per atom. + embedding_width : int + The dimension of rotation matrix, m1. + neuron : List[int] + Number of neurons in each hidden layers of the fitting net. + resnet_dt : bool + Using time-step in the ResNet construction. + numb_fparam : int + Number of frame parameters. + numb_aparam : int + Number of atomic parameters. + activation_function : str + Activation function. + precision : str + Numerical precision. + mixed_types : bool + If true, use a uniform fitting net for all atom types, otherwise use + different fitting nets for different atom types. + rcond : float, optional + The condition number for the regression of atomic energy. + seed : int, optional + Random seed. + fit_diag : bool + Fit the diagonal part of the rotational invariant polarizability matrix, which will be converted to + normal polarizability matrix by contracting with the rotation matrix. + scale : List[float] + The output of the fitting net (polarizability matrix) for type i atom will be scaled by scale[i] + shift_diag : bool + Whether to shift the diagonal part of the polarizability matrix. The shift operation is carried out after scale. + """ + + def __init__( + self, + var_name: str, + ntypes: int, + dim_descrpt: int, + embedding_width: int, + neuron: List[int] = [128, 128, 128], + resnet_dt: bool = True, + numb_fparam: int = 0, + numb_aparam: int = 0, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + mixed_types: bool = True, + rcond: Optional[float] = None, + seed: Optional[int] = None, + exclude_types: List[int] = [], + fit_diag: bool = True, + scale: Optional[List[float]] = None, + shift_diag: bool = True, + **kwargs, + ): + self.embedding_width = embedding_width + self.fit_diag = fit_diag + self.scale = scale + if self.scale is None: + self.scale = [1.0 for _ in range(ntypes)] + else: + assert ( + isinstance(self.scale, list) and len(self.scale) == ntypes + ), "Scale should be a list of length ntypes." + self.scale = torch.tensor( + self.scale, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ).view(ntypes, 1) + self.shift_diag = shift_diag + super().__init__( + var_name=var_name, + ntypes=ntypes, + dim_descrpt=dim_descrpt, + neuron=neuron, + resnet_dt=resnet_dt, + numb_fparam=numb_fparam, + numb_aparam=numb_aparam, + activation_function=activation_function, + precision=precision, + mixed_types=mixed_types, + rcond=rcond, + seed=seed, + exclude_types=exclude_types, + **kwargs, + ) + self.old_impl = False # this only supports the new implementation. + + def _net_out_dim(self): + """Set the FittingNet output dim.""" + return ( + self.embedding_width + if self.fit_diag + else self.embedding_width * self.embedding_width + ) + + def serialize(self) -> dict: + data = super().serialize() + data["embedding_width"] = self.embedding_width + data["old_impl"] = self.old_impl + data["fit_diag"] = self.fit_diag + data["fit_diag"] = self.fit_diag + data["@variables"]["scale"] = to_numpy_array(self.scale) + return data + + def output_def(self) -> FittingOutputDef: + return FittingOutputDef( + [ + OutputVariableDef( + self.var_name, + [3, 3], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ), + ] + ) + + @property + def data_stat_key(self): + """ + Get the keys for the data statistic of the fitting. + Return a list of statistic names needed, such as "bias_atom_e". + """ + return [] + + def forward( + self, + descriptor: torch.Tensor, + atype: torch.Tensor, + gr: Optional[torch.Tensor] = None, + g2: Optional[torch.Tensor] = None, + h2: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + ): + nframes, nloc, _ = descriptor.shape + assert ( + gr is not None + ), "Must provide the rotation matrix for polarizability fitting." + # (nframes, nloc, _net_out_dim) + out = self._forward_common(descriptor, atype, gr, g2, h2, fparam, aparam)[ + self.var_name + ] + out = out * self.scale[atype] + gr = gr.view(nframes * nloc, -1, 3) # (nframes * nloc, m1, 3) + + if self.fit_diag: + out = out.reshape(-1, self.embedding_width) + out = torch.einsum("ij,ijk->ijk", out, gr) + else: + out = out.reshape(-1, self.embedding_width, self.embedding_width) + out = (out + out.transpose(1, 2)) / 2 + out = torch.einsum("bim,bmj->bij", out, gr) # (nframes * nloc, m1, 3) + out = torch.einsum( + "bim,bmj->bij", gr.transpose(1, 2), out + ) # (nframes * nloc, 3, 3) + out = out.view(nframes, nloc, 3, 3) + + return {self.var_name: out.to(env.GLOBAL_PT_FLOAT_PRECISION)} diff --git a/source/tests/pt/model/test_dipole_fitting.py b/source/tests/pt/model/test_dipole_fitting.py index fffed123e0..3f67043767 100644 --- a/source/tests/pt/model/test_dipole_fitting.py +++ b/source/tests/pt/model/test_dipole_fitting.py @@ -36,7 +36,7 @@ class TestDipoleFitting(unittest.TestCase, TestCaseSingleFrameWithNlist): def setUp(self): TestCaseSingleFrameWithNlist.setUp(self) self.rng = np.random.default_rng() - self.nf, self.nloc, nnei = self.nlist.shape + self.nf, self.nloc, _ = self.nlist.shape self.dd0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) def test_consistency( @@ -51,7 +51,7 @@ def test_consistency( self.atype_ext[:, : self.nloc], dtype=int, device=env.DEVICE ) - for distinguish_types, nfp, nap in itertools.product( + for mixed_types, nfp, nap in itertools.product( [True, False], [0, 3], [0, 4], @@ -60,10 +60,10 @@ def test_consistency( "foo", self.nt, self.dd0.dim_out, - dim_rot_mat=self.dd0.get_dim_emb(), + embedding_width=self.dd0.get_dim_emb(), numb_fparam=nfp, numb_aparam=nap, - use_tebd=(not distinguish_types), + mixed_types=mixed_types, ).to(env.DEVICE) ft1 = DPDipoleFitting.deserialize(ft0.serialize()) ft2 = DipoleFittingNet.deserialize(ft1.serialize()) @@ -104,7 +104,7 @@ def test_consistency( def test_jit( self, ): - for distinguish_types, nfp, nap in itertools.product( + for mixed_types, nfp, nap in itertools.product( [True, False], [0, 3], [0, 4], @@ -113,10 +113,10 @@ def test_jit( "foo", self.nt, self.dd0.dim_out, - dim_rot_mat=self.dd0.get_dim_emb(), + embedding_width=self.dd0.get_dim_emb(), numb_fparam=nfp, numb_aparam=nap, - use_tebd=(not distinguish_types), + mixed_types=mixed_types, ).to(env.DEVICE) torch.jit.script(ft0) @@ -140,7 +140,7 @@ def test_rot(self): rmat = torch.tensor(special_ortho_group.rvs(3), dtype=dtype).to(env.DEVICE) coord_rot = torch.matmul(self.coord, rmat) rng = np.random.default_rng() - for distinguish_types, nfp, nap in itertools.product( + for mixed_types, nfp, nap in itertools.product( [True, False], [0, 3], [0, 4], @@ -149,10 +149,10 @@ def test_rot(self): "foo", 3, # ntype self.dd0.dim_out, # dim_descrpt - dim_rot_mat=self.dd0.get_dim_emb(), + embedding_width=self.dd0.get_dim_emb(), numb_fparam=nfp, numb_aparam=nap, - use_tebd=False, + mixed_types=mixed_types, ).to(env.DEVICE) if nfp > 0: ifp = torch.tensor( @@ -174,10 +174,10 @@ def test_rot(self): ( extended_coord, extended_atype, - mapping, + _, nlist, ) = extend_input_and_build_neighbor_list( - xyz + self.shift, atype, self.rcut, self.sel, distinguish_types + xyz + self.shift, atype, self.rcut, self.sel, not mixed_types ) rd0, gr0, _, _, _ = self.dd0( @@ -199,10 +199,10 @@ def test_permu(self): "foo", 3, # ntype self.dd0.dim_out, - dim_rot_mat=self.dd0.get_dim_emb(), + embedding_width=self.dd0.get_dim_emb(), numb_fparam=0, numb_aparam=0, - use_tebd=False, + mixed_types=False, ).to(env.DEVICE) res = [] for idx_perm in [[0, 1, 2, 3, 4], [1, 0, 4, 3, 2]]: @@ -210,10 +210,10 @@ def test_permu(self): ( extended_coord, extended_atype, - mapping, + _, nlist, ) = extend_input_and_build_neighbor_list( - coord[idx_perm], atype, self.rcut, self.sel, False + coord[idx_perm], atype, self.rcut, self.sel, True ) rd0, gr0, _, _, _ = self.dd0( @@ -241,17 +241,17 @@ def test_trans(self): "foo", 3, # ntype self.dd0.dim_out, - dim_rot_mat=self.dd0.get_dim_emb(), + embedding_width=self.dd0.get_dim_emb(), numb_fparam=0, numb_aparam=0, - use_tebd=False, + mixed_types=True, ).to(env.DEVICE) res = [] for xyz in [self.coord, coord_s]: ( extended_coord, extended_atype, - mapping, + _, nlist, ) = extend_input_and_build_neighbor_list( xyz, atype, self.rcut, self.sel, False diff --git a/source/tests/pt/model/test_polarizability_fitting.py b/source/tests/pt/model/test_polarizability_fitting.py new file mode 100644 index 0000000000..de43c57b8b --- /dev/null +++ b/source/tests/pt/model/test_polarizability_fitting.py @@ -0,0 +1,312 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import itertools +import unittest + +import numpy as np +import torch +from scipy.stats import ( + special_ortho_group, +) + +from deepmd.dpmodel.fitting import PolarFitting as DPPolarFitting +from deepmd.pt.model.descriptor.se_a import ( + DescrptSeA, +) +from deepmd.pt.model.task.polarizability import ( + PolarFittingNet, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.nlist import ( + extend_input_and_build_neighbor_list, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, +) + +from .test_env_mat import ( + TestCaseSingleFrameWithNlist, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION + + +class TestDipoleFitting(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + self.rng = np.random.default_rng() + self.nf, self.nloc, _ = self.nlist.shape + self.dd0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) + self.scale = self.rng.uniform(0, 1, self.nt).tolist() + + def test_consistency( + self, + ): + rd0, gr, _, _, _ = self.dd0( + torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), + torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), + torch.tensor(self.nlist, dtype=int, device=env.DEVICE), + ) + atype = torch.tensor( + self.atype_ext[:, : self.nloc], dtype=int, device=env.DEVICE + ) + + for mixed_types, nfp, nap, fit_diag, scale in itertools.product( + [True, False], + [0, 3], + [0, 4], + [True, False], + [None, self.scale], + ): + ft0 = PolarFittingNet( + "foo", + self.nt, + self.dd0.dim_out, + embedding_width=self.dd0.get_dim_emb(), + numb_fparam=nfp, + numb_aparam=nap, + mixed_types=mixed_types, + fit_diag=fit_diag, + scale=scale, + ).to(env.DEVICE) + ft1 = DPPolarFitting.deserialize(ft0.serialize()) + ft2 = PolarFittingNet.deserialize(ft0.serialize()) + ft3 = DPPolarFitting.deserialize(ft1.serialize()) + + if nfp > 0: + ifp = torch.tensor( + self.rng.normal(size=(self.nf, nfp)), dtype=dtype, device=env.DEVICE + ) + else: + ifp = None + if nap > 0: + iap = torch.tensor( + self.rng.normal(size=(self.nf, self.nloc, nap)), + dtype=dtype, + device=env.DEVICE, + ) + else: + iap = None + + ret0 = ft0(rd0, atype, gr, fparam=ifp, aparam=iap) + ret1 = ft1( + rd0.detach().cpu().numpy(), + atype.detach().cpu().numpy(), + gr.detach().cpu().numpy(), + fparam=to_numpy_array(ifp), + aparam=to_numpy_array(iap), + ) + ret2 = ft2(rd0, atype, gr, fparam=ifp, aparam=iap) + ret3 = ft3( + rd0.detach().cpu().numpy(), + atype.detach().cpu().numpy(), + gr.detach().cpu().numpy(), + fparam=to_numpy_array(ifp), + aparam=to_numpy_array(iap), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["foo"]), + ret1["foo"], + ) + np.testing.assert_allclose( + to_numpy_array(ret0["foo"]), + to_numpy_array(ret2["foo"]), + ) + np.testing.assert_allclose( + to_numpy_array(ret0["foo"]), + ret3["foo"], + ) + + def test_jit( + self, + ): + for mixed_types, nfp, nap, fit_diag in itertools.product( + [True, False], + [0, 3], + [0, 4], + [True, False], + ): + ft0 = PolarFittingNet( + "foo", + self.nt, + self.dd0.dim_out, + embedding_width=self.dd0.get_dim_emb(), + numb_fparam=nfp, + numb_aparam=nap, + mixed_types=mixed_types, + fit_diag=fit_diag, + ).to(env.DEVICE) + torch.jit.script(ft0) + + +class TestEquivalence(unittest.TestCase): + def setUp(self) -> None: + self.natoms = 5 + self.rcut = 4 + self.rcut_smth = 0.5 + self.sel = [46, 92, 4] + self.nf = 1 + self.nt = 3 + self.rng = np.random.default_rng() + self.coord = 2 * torch.rand([self.natoms, 3], dtype=dtype).to(env.DEVICE) + self.shift = torch.tensor([4, 4, 4], dtype=dtype).to(env.DEVICE) + self.atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + self.dd0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) + self.cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) + self.cell = (self.cell + self.cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) + self.scale = self.rng.uniform(0, 1, self.nt).tolist() + + def test_rot(self): + atype = self.atype.reshape(1, 5) + rmat = torch.tensor(special_ortho_group.rvs(3), dtype=dtype).to(env.DEVICE) + coord_rot = torch.matmul(self.coord, rmat) + + for mixed_types, nfp, nap, fit_diag, scale in itertools.product( + [True, False], + [0, 3], + [0, 4], + [True, False], + [None, self.scale], + ): + ft0 = PolarFittingNet( + "foo", + self.nt, + self.dd0.dim_out, # dim_descrpt + embedding_width=self.dd0.get_dim_emb(), + numb_fparam=nfp, + numb_aparam=nap, + mixed_types=True, + fit_diag=fit_diag, + scale=scale, + ).to(env.DEVICE) + if nfp > 0: + ifp = torch.tensor( + self.rng.normal(size=(self.nf, nfp)), dtype=dtype, device=env.DEVICE + ) + else: + ifp = None + if nap > 0: + iap = torch.tensor( + self.rng.normal(size=(self.nf, self.natoms, nap)), + dtype=dtype, + device=env.DEVICE, + ) + else: + iap = None + + res = [] + for xyz in [self.coord, coord_rot]: + ( + extended_coord, + extended_atype, + _, + nlist, + ) = extend_input_and_build_neighbor_list( + xyz + self.shift, atype, self.rcut, self.sel, mixed_types + ) + + rd0, gr0, _, _, _ = self.dd0( + extended_coord, + extended_atype, + nlist, + ) + + ret0 = ft0(rd0, extended_atype, gr0, fparam=ifp, aparam=iap) + res.append(ret0["foo"]) + print(res[1].shape) + np.testing.assert_allclose( + to_numpy_array(res[1]), + to_numpy_array( + torch.matmul( + rmat.T, + torch.matmul(res[0], rmat), + ) + ), + ) + + def test_permu(self): + coord = torch.matmul(self.coord, self.cell) + for fit_diag, scale in itertools.product([True, False], [None, self.scale]): + ft0 = PolarFittingNet( + "foo", + self.nt, + self.dd0.dim_out, + embedding_width=self.dd0.get_dim_emb(), + numb_fparam=0, + numb_aparam=0, + mixed_types=True, + fit_diag=fit_diag, + scale=scale, + ).to(env.DEVICE) + res = [] + for idx_perm in [[0, 1, 2, 3, 4], [1, 0, 4, 3, 2]]: + atype = self.atype[idx_perm].reshape(1, 5) + ( + extended_coord, + extended_atype, + _, + nlist, + ) = extend_input_and_build_neighbor_list( + coord[idx_perm], atype, self.rcut, self.sel, False + ) + + rd0, gr0, _, _, _ = self.dd0( + extended_coord, + extended_atype, + nlist, + ) + + ret0 = ft0(rd0, extended_atype, gr0, fparam=None, aparam=None) + res.append(ret0["foo"]) + + np.testing.assert_allclose( + to_numpy_array(res[0][:, idx_perm]), + to_numpy_array(res[1]), + ) + + def test_trans(self): + atype = self.atype.reshape(1, 5) + coord_s = torch.matmul( + torch.remainder( + torch.matmul(self.coord + self.shift, torch.linalg.inv(self.cell)), 1.0 + ), + self.cell, + ) + for fit_diag, scale in itertools.product([True, False], [None, self.scale]): + ft0 = PolarFittingNet( + "foo", + self.nt, + self.dd0.dim_out, + embedding_width=self.dd0.get_dim_emb(), + numb_fparam=0, + numb_aparam=0, + mixed_types=True, + fit_diag=fit_diag, + scale=scale, + ).to(env.DEVICE) + res = [] + for xyz in [self.coord, coord_s]: + ( + extended_coord, + extended_atype, + _, + nlist, + ) = extend_input_and_build_neighbor_list( + xyz, atype, self.rcut, self.sel, False + ) + + rd0, gr0, _, _, _ = self.dd0( + extended_coord, + extended_atype, + nlist, + ) + + ret0 = ft0(rd0, extended_atype, gr0, fparam=0, aparam=0) + res.append(ret0["foo"]) + + np.testing.assert_allclose(to_numpy_array(res[0]), to_numpy_array(res[1])) + + +if __name__ == "__main__": + unittest.main() From fe14402b9ecc8f7a52bf7a6b072321c8d0bbce81 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 19 Feb 2024 22:07:53 -0500 Subject: [PATCH 103/686] fix neighbor stat mixed_types input (#3304) It was broken by #3289, which replaced distinguished types by mixed types but didn't change the input. Add tests for two types. --------- Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/utils/neighbor_stat.py | 8 ++-- deepmd/entrypoints/neighbor_stat.py | 6 +-- deepmd/main.py | 1 + deepmd/pt/utils/neighbor_stat.py | 14 ++++--- deepmd/tf/entrypoints/train.py | 14 +++---- deepmd/tf/utils/neighbor_stat.py | 8 ++-- deepmd/utils/neighbor_stat.py | 8 ++-- .../common/dpmodel/test_neighbor_stat.py | 13 +++--- source/tests/pt/test_neighbor_stat.py | 13 +++--- source/tests/tf/test_neighbor_stat.py | 41 +++++++++++-------- source/tests/tf/test_virtual_type.py | 2 +- 11 files changed, 71 insertions(+), 57 deletions(-) diff --git a/deepmd/dpmodel/utils/neighbor_stat.py b/deepmd/dpmodel/utils/neighbor_stat.py index bf0c30c153..96b39d20ad 100644 --- a/deepmd/dpmodel/utils/neighbor_stat.py +++ b/deepmd/dpmodel/utils/neighbor_stat.py @@ -115,7 +115,7 @@ class NeighborStat(BaseNeighborStat): The num of atom types rcut : float The cut-off radius - one_type : bool, optional, default=False + mixed_type : bool, optional, default=False Treat all types as a single type. """ @@ -123,10 +123,10 @@ def __init__( self, ntypes: int, rcut: float, - one_type: bool = False, + mixed_type: bool = False, ) -> None: - super().__init__(ntypes, rcut, one_type) - self.op = NeighborStatOP(ntypes, rcut, not one_type) + super().__init__(ntypes, rcut, mixed_type) + self.op = NeighborStatOP(ntypes, rcut, mixed_type) def iterator( self, data: DeepmdDataSystem diff --git a/deepmd/entrypoints/neighbor_stat.py b/deepmd/entrypoints/neighbor_stat.py index 8a496fb6f0..a68a3fd3bb 100644 --- a/deepmd/entrypoints/neighbor_stat.py +++ b/deepmd/entrypoints/neighbor_stat.py @@ -22,7 +22,7 @@ def neighbor_stat( system: str, rcut: float, type_map: List[str], - one_type: bool = False, + mixed_type: bool = False, backend: str = "tensorflow", **kwargs, ): @@ -36,7 +36,7 @@ def neighbor_stat( cutoff radius type_map : list[str] type map - one_type : bool, optional, default=False + mixed_type : bool, optional, default=False treat all types as a single type backend : str, optional, default="tensorflow" backend to use @@ -89,7 +89,7 @@ def neighbor_stat( type_map=type_map, ) data.get_batch() - nei = NeighborStat(data.get_ntypes(), rcut, one_type=one_type) + nei = NeighborStat(data.get_ntypes(), rcut, mixed_type=mixed_type) min_nbor_dist, max_nbor_size = nei.get_stat(data) log.info("min_nbor_dist: %f" % min_nbor_dist) log.info("max_nbor_size: %s" % str(max_nbor_size)) diff --git a/deepmd/main.py b/deepmd/main.py index d31cab30c2..2bde6376f2 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -640,6 +640,7 @@ def main_parser() -> argparse.ArgumentParser: help="type map", ) parser_neighbor_stat.add_argument( + "--mixed-type", "--one-type", action="store_true", default=False, diff --git a/deepmd/pt/utils/neighbor_stat.py b/deepmd/pt/utils/neighbor_stat.py index 6361e7b9c7..d5b5c74bdc 100644 --- a/deepmd/pt/utils/neighbor_stat.py +++ b/deepmd/pt/utils/neighbor_stat.py @@ -89,14 +89,16 @@ def forward( ) assert list(diff.shape) == [nframes, nloc, nall, 3] # remove the diagonal elements - mask = torch.eye(nloc, nall, dtype=torch.bool) + mask = torch.eye(nloc, nall, dtype=torch.bool, device=diff.device) diff[:, mask] = torch.inf rr2 = torch.sum(torch.square(diff), dim=-1) min_rr2, _ = torch.min(rr2, dim=-1) # count the number of neighbors if not self.mixed_types: mask = rr2 < self.rcut**2 - nnei = torch.zeros((nframes, nloc, self.ntypes), dtype=torch.int32) + nnei = torch.zeros( + (nframes, nloc, self.ntypes), dtype=torch.int32, device=mask.device + ) for ii in range(self.ntypes): nnei[:, :, ii] = torch.sum( mask & extend_atype.eq(ii)[:, None, :], dim=-1 @@ -120,7 +122,7 @@ class NeighborStat(BaseNeighborStat): The num of atom types rcut : float The cut-off radius - one_type : bool, optional, default=False + mixed_type : bool, optional, default=False Treat all types as a single type. """ @@ -128,10 +130,10 @@ def __init__( self, ntypes: int, rcut: float, - one_type: bool = False, + mixed_type: bool = False, ) -> None: - super().__init__(ntypes, rcut, one_type) - op = NeighborStatOP(ntypes, rcut, not one_type) + super().__init__(ntypes, rcut, mixed_type) + op = NeighborStatOP(ntypes, rcut, mixed_type) self.op = torch.jit.script(op) self.auto_batch_size = AutoBatchSize() diff --git a/deepmd/tf/entrypoints/train.py b/deepmd/tf/entrypoints/train.py index f0b9481d99..3759ff9331 100755 --- a/deepmd/tf/entrypoints/train.py +++ b/deepmd/tf/entrypoints/train.py @@ -370,7 +370,7 @@ def get_type_map(jdata): return jdata["model"].get("type_map", None) -def get_nbor_stat(jdata, rcut, one_type: bool = False): +def get_nbor_stat(jdata, rcut, mixed_type: bool = False): # it seems that DeepmdDataSystem does not need rcut # it's not clear why there is an argument... # max_rcut = get_rcut(jdata) @@ -414,7 +414,7 @@ def get_nbor_stat(jdata, rcut, one_type: bool = False): map_ntypes = data_ntypes ntypes = max([map_ntypes, data_ntypes]) - neistat = NeighborStat(ntypes, rcut, one_type=one_type) + neistat = NeighborStat(ntypes, rcut, mixed_type=mixed_type) min_nbor_dist, max_nbor_size = neistat.get_stat(train_data) @@ -430,8 +430,8 @@ def get_nbor_stat(jdata, rcut, one_type: bool = False): return min_nbor_dist, max_nbor_size -def get_sel(jdata, rcut, one_type: bool = False): - _, max_nbor_size = get_nbor_stat(jdata, rcut, one_type=one_type) +def get_sel(jdata, rcut, mixed_type: bool = False): + _, max_nbor_size = get_nbor_stat(jdata, rcut, mixed_type=mixed_type) return max_nbor_size @@ -468,12 +468,12 @@ def wrap_up_4(xx): return 4 * ((int(xx) + 3) // 4) -def update_one_sel(jdata, descriptor, one_type: bool = False): +def update_one_sel(jdata, descriptor, mixed_type: bool = False): rcut = descriptor["rcut"] tmp_sel = get_sel( jdata, rcut, - one_type=one_type, + mixed_type=mixed_type, ) sel = descriptor["sel"] if isinstance(sel, int): @@ -493,7 +493,7 @@ def update_one_sel(jdata, descriptor, one_type: bool = False): "not less than %d, but you set it to %d. The accuracy" " of your model may get worse." % (ii, tt, dd) ) - if one_type: + if mixed_type: descriptor["sel"] = sel = sum(sel) return descriptor diff --git a/deepmd/tf/utils/neighbor_stat.py b/deepmd/tf/utils/neighbor_stat.py index 61531e7857..f668d4a4da 100644 --- a/deepmd/tf/utils/neighbor_stat.py +++ b/deepmd/tf/utils/neighbor_stat.py @@ -168,7 +168,7 @@ class NeighborStat(BaseNeighborStat): The num of atom types rcut The cut-off radius - one_type : bool, optional, default=False + mixed_type : bool, optional, default=False Treat all types as a single type. """ @@ -176,12 +176,12 @@ def __init__( self, ntypes: int, rcut: float, - one_type: bool = False, + mixed_type: bool = False, ) -> None: """Constructor.""" - super().__init__(ntypes, rcut, one_type) + super().__init__(ntypes, rcut, mixed_type) self.auto_batch_size = AutoBatchSize() - self.neighbor_stat = NeighborStatOP(ntypes, rcut, not one_type) + self.neighbor_stat = NeighborStatOP(ntypes, rcut, mixed_type) self.place_holders = {} with tf.Graph().as_default() as sub_graph: self.op = self.build() diff --git a/deepmd/utils/neighbor_stat.py b/deepmd/utils/neighbor_stat.py index c6327a705e..34200df007 100644 --- a/deepmd/utils/neighbor_stat.py +++ b/deepmd/utils/neighbor_stat.py @@ -32,7 +32,7 @@ class NeighborStat(ABC): The num of atom types rcut : float The cut-off radius - one_type : bool, optional, default=False + mixed_type : bool, optional, default=False Treat all types as a single type. """ @@ -40,11 +40,11 @@ def __init__( self, ntypes: int, rcut: float, - one_type: bool = False, + mixed_type: bool = False, ) -> None: self.rcut = rcut self.ntypes = ntypes - self.one_type = one_type + self.mixed_type = mixed_type def get_stat(self, data: DeepmdDataSystem) -> Tuple[float, np.ndarray]: """Get the data statistics of the training data, including nearest nbor distance between atoms, max nbor size of atoms. @@ -62,7 +62,7 @@ def get_stat(self, data: DeepmdDataSystem) -> Tuple[float, np.ndarray]: An array with ntypes integers, denotes the actual achieved max sel """ min_nbor_dist = 100.0 - max_nbor_size = np.zeros(1 if self.one_type else self.ntypes, dtype=int) + max_nbor_size = np.zeros(1 if self.mixed_type else self.ntypes, dtype=int) for mn, dt, jj in self.iterator(data): if np.isinf(dt): diff --git a/source/tests/common/dpmodel/test_neighbor_stat.py b/source/tests/common/dpmodel/test_neighbor_stat.py index aa2ed5b3db..90764a049c 100644 --- a/source/tests/common/dpmodel/test_neighbor_stat.py +++ b/source/tests/common/dpmodel/test_neighbor_stat.py @@ -39,14 +39,14 @@ def tearDown(self): def test_neighbor_stat(self): for rcut in (0.0, 1.0, 2.0, 4.0): - for one_type in (True, False): - with self.subTest(rcut=rcut, one_type=one_type): + for mixed_type in (True, False): + with self.subTest(rcut=rcut, mixed_type=mixed_type): rcut += 1e-3 # prevent numerical errors min_nbor_dist, max_nbor_size = neighbor_stat( system="system_0", rcut=rcut, - type_map=["TYPE"], - one_type=one_type, + type_map=["TYPE", "NO_THIS_TYPE"], + mixed_type=mixed_type, backend="numpy", ) upper = np.ceil(rcut) + 1 @@ -58,4 +58,7 @@ def test_neighbor_stat(self): np.logical_and(distance > 0, distance <= rcut) ) self.assertAlmostEqual(min_nbor_dist, 1.0, 6) - self.assertEqual(max_nbor_size, [expected_neighbors]) + ret = [expected_neighbors] + if not mixed_type: + ret.append(0) + np.testing.assert_array_equal(max_nbor_size, ret) diff --git a/source/tests/pt/test_neighbor_stat.py b/source/tests/pt/test_neighbor_stat.py index 7e4be0909e..63ec684792 100644 --- a/source/tests/pt/test_neighbor_stat.py +++ b/source/tests/pt/test_neighbor_stat.py @@ -39,14 +39,14 @@ def tearDown(self): def test_neighbor_stat(self): for rcut in (0.0, 1.0, 2.0, 4.0): - for one_type in (True, False): - with self.subTest(rcut=rcut, one_type=one_type): + for mixed_type in (True, False): + with self.subTest(rcut=rcut, mixed_type=mixed_type): rcut += 1e-3 # prevent numerical errors min_nbor_dist, max_nbor_size = neighbor_stat( system="system_0", rcut=rcut, - type_map=["TYPE"], - one_type=one_type, + type_map=["TYPE", "NO_THIS_TYPE"], + mixed_type=mixed_type, backend="pytorch", ) upper = np.ceil(rcut) + 1 @@ -58,4 +58,7 @@ def test_neighbor_stat(self): np.logical_and(distance > 0, distance <= rcut) ) self.assertAlmostEqual(min_nbor_dist, 1.0, 6) - self.assertEqual(max_nbor_size, [expected_neighbors]) + ret = [expected_neighbors] + if not mixed_type: + ret.append(0) + np.testing.assert_array_equal(max_nbor_size, ret) diff --git a/source/tests/tf/test_neighbor_stat.py b/source/tests/tf/test_neighbor_stat.py index 9806e2053a..6fcba36914 100644 --- a/source/tests/tf/test_neighbor_stat.py +++ b/source/tests/tf/test_neighbor_stat.py @@ -38,21 +38,26 @@ def tearDown(self): shutil.rmtree("system_0") def test_neighbor_stat(self): - # set rcut to 0. will cause a core dumped - # TODO: check what is wrong - for rcut in (1.0, 2.0, 4.0): - with self.subTest(): - rcut += 1e-3 # prevent numerical errors - min_nbor_dist, max_nbor_size = neighbor_stat( - system="system_0", rcut=rcut, type_map=["TYPE"] - ) - upper = np.ceil(rcut) + 1 - X, Y, Z = np.mgrid[-upper:upper, -upper:upper, -upper:upper] - positions = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T - # distance to (0,0,0) - distance = np.linalg.norm(positions, axis=1) - expected_neighbors = np.count_nonzero( - np.logical_and(distance > 0, distance <= rcut) - ) - self.assertAlmostEqual(min_nbor_dist, 1.0, 6) - self.assertEqual(max_nbor_size, [expected_neighbors]) + for rcut in (0.0, 1.0, 2.0, 4.0): + for mixed_type in (True, False): + with self.subTest(rcut=rcut, mixed_type=mixed_type): + rcut += 1e-3 # prevent numerical errors + min_nbor_dist, max_nbor_size = neighbor_stat( + system="system_0", + rcut=rcut, + type_map=["TYPE", "NO_THIS_TYPE"], + mixed_type=mixed_type, + ) + upper = np.ceil(rcut) + 1 + X, Y, Z = np.mgrid[-upper:upper, -upper:upper, -upper:upper] + positions = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T + # distance to (0,0,0) + distance = np.linalg.norm(positions, axis=1) + expected_neighbors = np.count_nonzero( + np.logical_and(distance > 0, distance <= rcut) + ) + self.assertAlmostEqual(min_nbor_dist, 1.0, 6) + ret = [expected_neighbors] + if not mixed_type: + ret.append(0) + np.testing.assert_array_equal(max_nbor_size, ret) diff --git a/source/tests/tf/test_virtual_type.py b/source/tests/tf/test_virtual_type.py index 5ceb1c7637..e9c675fe3a 100644 --- a/source/tests/tf/test_virtual_type.py +++ b/source/tests/tf/test_virtual_type.py @@ -138,5 +138,5 @@ def test_data_mixed_type(self): data = DeepmdDataSystem(systems, batch_size, test_size, rcut, type_map=type_map) data.get_batch() # neighbor stat - nei_stat = NeighborStat(len(type_map), rcut, one_type=True) + nei_stat = NeighborStat(len(type_map), rcut, mixed_type=True) min_nbor_dist, max_nbor_size = nei_stat.get_stat(data) From 4b994df1d4a99b78efd9f00a32e4d4b66d84d6d3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 22:58:29 -0500 Subject: [PATCH 104/686] [pre-commit.ci] pre-commit autoupdate (#3305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.1 → v0.2.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.1...v0.2.2) - [github.com/Lucas-C/pre-commit-hooks: v1.5.4 → v1.5.5](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.5.4...v1.5.5) --------- Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jinzhe Zeng --- .pre-commit-config.yaml | 4 ++-- .../common/dpmodel/test_neighbor_stat.py | 4 ++-- source/tests/pt/model/test_force_grad.py | 2 +- source/tests/pt/test_neighbor_stat.py | 4 ++-- source/tests/tf/test_data_modifier_shuffle.py | 6 +++-- source/tests/tf/test_deepmd_data.py | 24 ++++++++++--------- source/tests/tf/test_deepmd_data_sys.py | 8 ++++--- source/tests/tf/test_descrpt_sea_ef_rot.py | 4 ++-- source/tests/tf/test_ewald.py | 6 ++--- source/tests/tf/test_gen_stat_data.py | 8 +++---- source/tests/tf/test_neighbor_stat.py | 4 ++-- 11 files changed, 40 insertions(+), 34 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3b16a4cc67..041d47f0da 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^source/3rdparty - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.2.1 + rev: v0.2.2 hooks: - id: ruff args: ["--fix"] @@ -75,7 +75,7 @@ repos: #- id: cmake-lint # license header - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.4 + rev: v1.5.5 hooks: # C++, js - id: insert-license diff --git a/source/tests/common/dpmodel/test_neighbor_stat.py b/source/tests/common/dpmodel/test_neighbor_stat.py index 90764a049c..2a9296057b 100644 --- a/source/tests/common/dpmodel/test_neighbor_stat.py +++ b/source/tests/common/dpmodel/test_neighbor_stat.py @@ -16,11 +16,11 @@ def gen_sys(nframes): X, Y, Z = np.mgrid[0:2:3j, 0:2:3j, 0:2:3j] positions = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T # + 0.1 data["coords"] = np.repeat(positions[np.newaxis, :, :], nframes, axis=0) - data["forces"] = np.random.random([nframes, natoms, 3]) + data["forces"] = np.random.default_rng().random([nframes, natoms, 3]) data["cells"] = np.array([3.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 3.0]).reshape( 1, 3, 3 ) - data["energies"] = np.random.random([nframes, 1]) + data["energies"] = np.random.default_rng().random([nframes, 1]) data["atom_names"] = ["TYPE"] data["atom_numbs"] = [27] data["atom_types"] = np.repeat(0, 27) diff --git a/source/tests/pt/model/test_force_grad.py b/source/tests/pt/model/test_force_grad.py index 0a4dc32d9f..d695c821e5 100644 --- a/source/tests/pt/model/test_force_grad.py +++ b/source/tests/pt/model/test_force_grad.py @@ -89,7 +89,7 @@ def test_force_grad(self, threshold=1e-2, delta0=1e-6, seed=20): errors = np.zeros((self.dpdatasystem._natoms, 3)) for atom_index in range(self.dpdatasystem._natoms): for axis_index in range(3): - delta = np.random.random() * delta0 + delta = np.random.default_rng().random() * delta0 disturb_batch = self.dpdatasystem.get_disturb( self.batch_index, atom_index, axis_index, delta ) diff --git a/source/tests/pt/test_neighbor_stat.py b/source/tests/pt/test_neighbor_stat.py index 63ec684792..4cbb46f66b 100644 --- a/source/tests/pt/test_neighbor_stat.py +++ b/source/tests/pt/test_neighbor_stat.py @@ -16,11 +16,11 @@ def gen_sys(nframes): X, Y, Z = np.mgrid[0:2:3j, 0:2:3j, 0:2:3j] positions = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T # + 0.1 data["coords"] = np.repeat(positions[np.newaxis, :, :], nframes, axis=0) - data["forces"] = np.random.random([nframes, natoms, 3]) + data["forces"] = np.random.default_rng().random([nframes, natoms, 3]) data["cells"] = np.array([3.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 3.0]).reshape( 1, 3, 3 ) - data["energies"] = np.random.random([nframes, 1]) + data["energies"] = np.random.default_rng().random([nframes, 1]) data["atom_names"] = ["TYPE"] data["atom_numbs"] = [27] data["atom_types"] = np.repeat(0, 27) diff --git a/source/tests/tf/test_data_modifier_shuffle.py b/source/tests/tf/test_data_modifier_shuffle.py index e6985b9e0f..fb9da948f1 100644 --- a/source/tests/tf/test_data_modifier_shuffle.py +++ b/source/tests/tf/test_data_modifier_shuffle.py @@ -105,8 +105,10 @@ def _setUp_data(self): self.nsel = 0 for ii in self.sel_type: self.nsel += np.sum(self.atom_types0 == ii) - self.coords0 = np.random.random([self.nframes, self.natoms * 3]) * scale - self.dipoles0 = np.random.random([self.nframes, self.nsel * 3]) + self.coords0 = ( + np.random.default_rng().random([self.nframes, self.natoms * 3]) * scale + ) + self.dipoles0 = np.random.default_rng().random([self.nframes, self.nsel * 3]) self.box0 = np.reshape(np.eye(3) * scale, [-1, 9]) self.box0 = np.tile(self.box0, [self.nframes, 1]) self._write_sys_data( diff --git a/source/tests/tf/test_deepmd_data.py b/source/tests/tf/test_deepmd_data.py index b1a0147771..3998e0f3e3 100644 --- a/source/tests/tf/test_deepmd_data.py +++ b/source/tests/tf/test_deepmd_data.py @@ -37,13 +37,13 @@ def setUp(self): self.natoms = 6 # coord path = os.path.join(self.data_name, "set.foo", "coord.npy") - self.coord = np.random.random([self.nframes, self.natoms, 3]) + self.coord = np.random.default_rng().random([self.nframes, self.natoms, 3]) np.save(path, np.reshape(self.coord, [self.nframes, -1])) self.coord = self.coord[:, [0, 3, 1, 2, 4, 5], :] self.coord = self.coord.reshape([self.nframes, -1]) # box path = os.path.join(self.data_name, "set.foo", "box.npy") - self.box = np.random.random([self.nframes, 9]) + self.box = np.random.default_rng().random([self.nframes, 9]) np.save(path, self.box) # value path = os.path.join(self.data_name, "set.foo", "value_1.npy") @@ -93,49 +93,51 @@ def setUp(self): self.natoms = 2 # coord path = os.path.join(self.data_name, "set.foo", "coord.npy") - self.coord = np.random.random([self.nframes, self.natoms, 3]) + self.coord = np.random.default_rng().random([self.nframes, self.natoms, 3]) np.save(path, np.reshape(self.coord, [self.nframes, -1])) self.coord = self.coord[:, [1, 0], :] self.coord = self.coord.reshape([self.nframes, -1]) # coord bar path = os.path.join(self.data_name, "set.bar", "coord.npy") - self.coord_bar = np.random.random([self.nframes, 3 * self.natoms]) + self.coord_bar = np.random.default_rng().random([self.nframes, 3 * self.natoms]) np.save(path, self.coord_bar) self.coord_bar = self.coord_bar.reshape([self.nframes, self.natoms, 3]) self.coord_bar = self.coord_bar[:, [1, 0], :] self.coord_bar = self.coord_bar.reshape([self.nframes, -1]) # coord tar path = os.path.join(self.data_name, "set.tar", "coord.npy") - self.coord_tar = np.random.random([2, 3 * self.natoms]) + self.coord_tar = np.random.default_rng().random([2, 3 * self.natoms]) np.save(path, self.coord_tar) self.coord_tar = self.coord_tar.reshape([2, self.natoms, 3]) self.coord_tar = self.coord_tar[:, [1, 0], :] self.coord_tar = self.coord_tar.reshape([2, -1]) # box path = os.path.join(self.data_name, "set.foo", "box.npy") - self.box = np.random.random([self.nframes, 9]) + self.box = np.random.default_rng().random([self.nframes, 9]) np.save(path, self.box) # box bar path = os.path.join(self.data_name, "set.bar", "box.npy") - self.box_bar = np.random.random([self.nframes, 9]) + self.box_bar = np.random.default_rng().random([self.nframes, 9]) np.save(path, self.box_bar) # box tar path = os.path.join(self.data_name, "set.tar", "box.npy") - self.box_tar = np.random.random([2, 9]) + self.box_tar = np.random.default_rng().random([2, 9]) np.save(path, self.box_tar) # t a path = os.path.join(self.data_name, "set.foo", "test_atomic.npy") - self.test_atomic = np.random.random([self.nframes, self.natoms, 7]) + self.test_atomic = np.random.default_rng().random( + [self.nframes, self.natoms, 7] + ) self.redu_atomic = np.sum(self.test_atomic, axis=1) np.save(path, np.reshape(self.test_atomic, [self.nframes, -1])) self.test_atomic = self.test_atomic[:, [1, 0], :] self.test_atomic = self.test_atomic.reshape([self.nframes, -1]) # t f path = os.path.join(self.data_name, "set.foo", "test_frame.npy") - self.test_frame = np.random.random([self.nframes, 5]) + self.test_frame = np.random.default_rng().random([self.nframes, 5]) np.save(path, self.test_frame) path = os.path.join(self.data_name, "set.bar", "test_frame.npy") - self.test_frame_bar = np.random.random([self.nframes, 5]) + self.test_frame_bar = np.random.default_rng().random([self.nframes, 5]) np.save(path, self.test_frame_bar) # t n self.test_null = np.zeros([self.nframes, 2 * self.natoms]) diff --git a/source/tests/tf/test_deepmd_data_sys.py b/source/tests/tf/test_deepmd_data_sys.py index 49ad8f501c..84b5d39a05 100644 --- a/source/tests/tf/test_deepmd_data_sys.py +++ b/source/tests/tf/test_deepmd_data_sys.py @@ -40,13 +40,15 @@ def setUp(self): set_name = os.path.join(sys_name, "set.%03d" % jj) os.makedirs(set_name, exist_ok=True) path = os.path.join(set_name, "coord.npy") - val = np.random.random([self.nframes[ii] + jj, self.natoms[ii] * 3]) + val = np.random.default_rng().random( + [self.nframes[ii] + jj, self.natoms[ii] * 3] + ) np.save(path, val) path = os.path.join(set_name, "box.npy") - val = np.random.random([self.nframes[ii] + jj, 9]) * 10 + val = np.random.default_rng().random([self.nframes[ii] + jj, 9]) * 10 np.save(path, val) path = os.path.join(set_name, "test.npy") - val = np.random.random( + val = np.random.default_rng().random( [self.nframes[ii] + jj, self.natoms[ii] * self.test_ndof] ) np.save(path, val) diff --git a/source/tests/tf/test_descrpt_sea_ef_rot.py b/source/tests/tf/test_descrpt_sea_ef_rot.py index 8cdbc19ca2..6ebc067211 100644 --- a/source/tests/tf/test_descrpt_sea_ef_rot.py +++ b/source/tests/tf/test_descrpt_sea_ef_rot.py @@ -97,7 +97,7 @@ def build_efv(self, dcoord, dbox, dtype, tnatoms, name, op, reuse=None): return energy, force, virial, atom_ener, atom_vir def make_test_data(self, nframes): - dcoord = np.random.random([nframes, self.natoms[0], 3]) + dcoord = np.random.default_rng().random([nframes, self.natoms[0], 3]) for ii in range(nframes): dcoord[ii, :, :] = dcoord[ii, :, :] - np.tile( dcoord[ii, 0, :], [self.natoms[0], 1] @@ -111,7 +111,7 @@ def make_test_data(self, nframes): np.random.shuffle(one_type) # noqa: NPY002 one_type = np.array(one_type, dtype=int).reshape([1, -1]) dtype = np.tile(one_type, [nframes, 1]) - defield = np.random.random(dcoord.shape) + defield = np.random.default_rng().random(dcoord.shape) return dcoord, dbox, dtype, defield def rotate_mat(self, axis_, theta): diff --git a/source/tests/tf/test_ewald.py b/source/tests/tf/test_ewald.py index 74b65e9be3..c68a0c84ee 100644 --- a/source/tests/tf/test_ewald.py +++ b/source/tests/tf/test_ewald.py @@ -38,16 +38,16 @@ def setUp(self): box = np.eye(3) * boxl box[1][1] += 1 box[2][2] += 2 - box += np.random.random([3, 3]) * box_pert + box += np.random.default_rng().random([3, 3]) * box_pert box = 0.5 * (box + box.T) self.dbox.append(box) # scaled - coord = np.random.random([self.natoms, 3]) + coord = np.random.default_rng().random([self.natoms, 3]) self.rcoord.append(coord) # real coords self.dcoord.append(np.matmul(coord, box)) # charge - dcharge = np.random.random([self.natoms]) + dcharge = np.random.default_rng().random([self.natoms]) dcharge -= np.average(dcharge) assert np.abs(np.sum(self.dcharge) - 0) < 1e-12 self.dcharge.append(dcharge) diff --git a/source/tests/tf/test_gen_stat_data.py b/source/tests/tf/test_gen_stat_data.py index fe4ec36b24..5442fded75 100644 --- a/source/tests/tf/test_gen_stat_data.py +++ b/source/tests/tf/test_gen_stat_data.py @@ -25,10 +25,10 @@ def gen_sys(nframes, atom_types): natoms = len(atom_types) data = {} - data["coords"] = np.random.random([nframes, natoms, 3]) - data["forces"] = np.random.random([nframes, natoms, 3]) - data["cells"] = np.random.random([nframes, 9]) - data["energies"] = np.random.random([nframes, 1]) + data["coords"] = np.random.default_rng().random([nframes, natoms, 3]) + data["forces"] = np.random.default_rng().random([nframes, natoms, 3]) + data["cells"] = np.random.default_rng().random([nframes, 9]) + data["energies"] = np.random.default_rng().random([nframes, 1]) types = list(set(atom_types)) types.sort() data["atom_names"] = [] diff --git a/source/tests/tf/test_neighbor_stat.py b/source/tests/tf/test_neighbor_stat.py index 6fcba36914..653634d674 100644 --- a/source/tests/tf/test_neighbor_stat.py +++ b/source/tests/tf/test_neighbor_stat.py @@ -16,11 +16,11 @@ def gen_sys(nframes): X, Y, Z = np.mgrid[0:2:3j, 0:2:3j, 0:2:3j] positions = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T # + 0.1 data["coords"] = np.repeat(positions[np.newaxis, :, :], nframes, axis=0) - data["forces"] = np.random.random([nframes, natoms, 3]) + data["forces"] = np.random.default_rng().random([nframes, natoms, 3]) data["cells"] = np.array([3.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 3.0]).reshape( 1, 3, 3 ) - data["energies"] = np.random.random([nframes, 1]) + data["energies"] = np.random.default_rng().random([nframes, 1]) data["atom_names"] = ["TYPE"] data["atom_numbs"] = [27] data["atom_types"] = np.repeat(0, 27) From f5c2f5792ae8bf411173a8dfc2277aea79813ae6 Mon Sep 17 00:00:00 2001 From: Lysithea <52808607+CaRoLZhangxy@users.noreply.github.com> Date: Wed, 21 Feb 2024 09:44:57 +0800 Subject: [PATCH 105/686] pt:Removed the data system implemented in PyTorch (#3287) The old code has been retained for reference by @Chengqian-Zhang for de-noise implementation. --------- Signed-off-by: Lysithea <52808607+CaRoLZhangxy@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jinzhe Zeng Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- deepmd/pt/entrypoints/main.py | 7 - deepmd/pt/infer/inference.py | 2 - deepmd/pt/model/task/fitting.py | 4 +- deepmd/pt/utils/dataloader.py | 63 +-- deepmd/pt/utils/dataset.py | 593 +------------------- deepmd/utils/data.py | 52 ++ source/tests/pt/model/test_descriptor.py | 3 - source/tests/pt/model/test_embedding_net.py | 18 +- source/tests/pt/model/test_force_grad.py | 29 +- source/tests/pt/model/test_model.py | 6 +- source/tests/pt/model/test_rotation.py | 33 +- source/tests/pt/test_loss.py | 4 +- 12 files changed, 114 insertions(+), 700 deletions(-) diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index b260000d87..8ed9c51634 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -147,16 +147,12 @@ def prepare_trainer_input_single( validation_systems, validation_dataset_params["batch_size"], model_params_single, - type_split=type_split, - noise_settings=noise_settings, ) if ckpt or finetune_model: train_data_single = DpLoaderSet( training_systems, training_dataset_params["batch_size"], model_params_single, - type_split=type_split, - noise_settings=noise_settings, ) sampled_single = None else: @@ -164,7 +160,6 @@ def prepare_trainer_input_single( training_systems, training_dataset_params["batch_size"], model_params_single, - type_split=type_split, ) data_stat_nbatch = model_params_single.get("data_stat_nbatch", 10) sampled_single = make_stat_input( @@ -177,8 +172,6 @@ def prepare_trainer_input_single( training_systems, training_dataset_params["batch_size"], model_params_single, - type_split=type_split, - noise_settings=noise_settings, ) return ( train_data_single, diff --git a/deepmd/pt/infer/inference.py b/deepmd/pt/infer/inference.py index 6a9f0d99d2..cce9291e3b 100644 --- a/deepmd/pt/infer/inference.py +++ b/deepmd/pt/infer/inference.py @@ -217,8 +217,6 @@ def run(self): [system], self.batchsize, self.model_params, - type_split=self.type_split, - noise_settings=self.noise_settings, shuffle=self.shuffle_test, ) sampler = RandomSampler( diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index cade533f1a..be20aa9496 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -159,9 +159,7 @@ def change_energy_bias( ) # data systems = config["training"]["training_data"]["systems"] - finetune_data = DpLoaderSet( - systems, ntest, config["model"], type_split=False, noise_settings=None - ) + finetune_data = DpLoaderSet(systems, ntest, config["model"]) sampled = make_stat_input(finetune_data.systems, finetune_data.dataloaders, 1) # map sorter = np.argsort(old_type_map) diff --git a/deepmd/pt/utils/dataloader.py b/deepmd/pt/utils/dataloader.py index 7f8cf4eb3c..7844d6d741 100644 --- a/deepmd/pt/utils/dataloader.py +++ b/deepmd/pt/utils/dataloader.py @@ -22,13 +22,13 @@ Dataset, WeightedRandomSampler, ) +from torch.utils.data._utils.collate import ( + collate_tensor_fn, +) from torch.utils.data.distributed import ( DistributedSampler, ) -from deepmd.pt.model.descriptor import ( - Descriptor, -) from deepmd.pt.utils import ( env, ) @@ -59,8 +59,6 @@ def __init__( batch_size, model_params, seed=10, - type_split=True, - noise_settings=None, shuffle=True, ): setup_seed(seed) @@ -73,26 +71,9 @@ def __init__( log.info(f"Constructing DataLoaders from {len(systems)} systems") def construct_dataset(system): - ### this design requires "rcut" and "sel" in the descriptor - ### VERY BAD DESIGN!!!! - ### not all descriptors provides these parameter in their constructor - if model_params["descriptor"].get("type") != "hybrid": - info_dict = Descriptor.get_data_process_key(model_params["descriptor"]) - rcut = info_dict["rcut"] - sel = info_dict["sel"] - else: ### need to remove this - rcut = [] - sel = [] - for ii in model_params["descriptor"]["list"]: - rcut.append(ii["rcut"]) - sel.append(ii["sel"]) return DeepmdDataSetForLoader( system=system, type_map=model_params["type_map"], - rcut=rcut, - sel=sel, - type_split=type_split, - noise_settings=noise_settings, shuffle=shuffle, ) @@ -233,39 +214,9 @@ def __next__(self): return item -def collate_tensor_fn(batch): - elem = batch[0] - if not isinstance(elem, list): - out = None - if torch.utils.data.get_worker_info() is not None: - # If we're in a background process, concatenate directly into a - # shared memory tensor to avoid an extra copy - numel = sum(x.numel() for x in batch) - storage = elem._typed_storage()._new_shared(numel, device=elem.device) - out = elem.new(storage).resize_(len(batch), *list(elem.size())) - return torch.stack(batch, 0, out=out) - else: - out_hybrid = [] - for ii, hybrid_item in enumerate(elem): - out = None - tmp_batch = [x[ii] for x in batch] - if torch.utils.data.get_worker_info() is not None: - # If we're in a background process, concatenate directly into a - # shared memory tensor to avoid an extra copy - numel = sum(x.numel() for x in tmp_batch) - storage = hybrid_item._typed_storage()._new_shared( - numel, device=hybrid_item.device - ) - out = hybrid_item.new(storage).resize_( - len(tmp_batch), *list(hybrid_item.size()) - ) - out_hybrid.append(torch.stack(tmp_batch, 0, out=out)) - return out_hybrid - - def collate_batch(batch): example = batch[0] - result = example.copy() + result = {} for key in example.keys(): if "find_" in key: result[key] = batch[0][key] @@ -274,8 +225,12 @@ def collate_batch(batch): result[key] = None elif key == "fid": result[key] = [d[key] for d in batch] + elif key == "type": + continue else: - result[key] = collate_tensor_fn([d[key] for d in batch]) + result[key] = collate_tensor_fn( + [torch.as_tensor(d[key]) for d in batch] + ) return result diff --git a/deepmd/pt/utils/dataset.py b/deepmd/pt/utils/dataset.py index 60055ebda9..4619b6417f 100644 --- a/deepmd/pt/utils/dataset.py +++ b/deepmd/pt/utils/dataset.py @@ -1,558 +1,12 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import glob -import os -from typing import ( - List, - Optional, -) -import h5py -import numpy as np -import torch from torch.utils.data import ( Dataset, ) -from deepmd.pt.utils import ( - dp_random, - env, -) -from deepmd.pt.utils.cache import ( - lru_cache, +from deepmd.utils.data import ( + DeepmdData, ) -from deepmd.pt.utils.preprocess import ( - Region3D, - make_env_mat, - normalize_coord, -) - - -class DeepmdDataSystem: - def __init__( - self, - sys_path: str, - rcut, - sec, - type_map: Optional[List[str]] = None, - type_split=True, - noise_settings=None, - shuffle=True, - ): - """Construct DeePMD-style frame collection of one system. - - Args: - - sys_path: Paths to the system. - - type_map: Atom types. - """ - sys_path = sys_path.replace("#", "") - if ".hdf5" in sys_path: - tmp = sys_path.split("/") - path = "/".join(tmp[:-1]) - sys = tmp[-1] - self.file = h5py.File(path)[sys] - self._dirs = [] - for item in self.file.keys(): - if "set." in item: - self._dirs.append(item) - self._dirs.sort() - else: - self.file = None - self._dirs = glob.glob(os.path.join(sys_path, "set.*")) - self._dirs.sort() - self.type_split = type_split - self.noise_settings = noise_settings - self._check_pbc(sys_path) - self.shuffle = shuffle - if noise_settings is not None: - self.noise_type = noise_settings.get("noise_type", "uniform") - self.noise = float(noise_settings.get("noise", 1.0)) - self.noise_mode = noise_settings.get("noise_mode", "fix_num") - self.mask_num = int(noise_settings.get("mask_num", 1)) - self.mask_prob = float(noise_settings.get("mask_prob", 0.15)) - self.same_mask = noise_settings.get("same_mask", False) - self.mask_coord = noise_settings.get("mask_coord", False) - self.mask_type = noise_settings.get("mask_type", False) - self.mask_type_idx = int(noise_settings.get("mask_type_idx", 0)) - self.max_fail_num = int(noise_settings.get("max_fail_num", 10)) - - # check mixed type - error_format_msg = ( - "if one of the set is of mixed_type format, " - "then all of the sets in this system should be of mixed_type format!" - ) - if len(self._dirs) == 0: - raise RuntimeError(f"No set found in system {sys_path}.") - - self.mixed_type = self._check_mode(self._dirs[0]) - for set_item in self._dirs[1:]: - assert self._check_mode(set_item) == self.mixed_type, error_format_msg - - self._atom_type = self._load_type(sys_path) - self._natoms = len(self._atom_type) - - self._type_map = self._load_type_map(sys_path) - self.enforce_type_map = False - if type_map is not None and self._type_map is not None: - if not self.mixed_type: - atom_type = [ - type_map.index(self._type_map[ii]) for ii in self._atom_type - ] - self._atom_type = np.array(atom_type, dtype=np.int32) - - else: - self.enforce_type_map = True - sorter = np.argsort(type_map) - self.type_idx_map = np.array( - sorter[np.searchsorted(type_map, self._type_map, sorter=sorter)] - ) - # padding for virtual atom - self.type_idx_map = np.append( - self.type_idx_map, np.array([-1], dtype=np.int32) - ) - self._type_map = type_map - if type_map is None and self.type_map is None and self.mixed_type: - raise RuntimeError("mixed_type format must have type_map!") - self._idx_map = _make_idx_map(self._atom_type) - - self._data_dict = {} - self.add("box", 9, must=self.pbc) - self.add("coord", 3, atomic=True, must=True) - self.add("energy", 1, atomic=False, must=False, high_prec=True) - self.add("force", 3, atomic=True, must=False, high_prec=False) - self.add("virial", 9, atomic=False, must=False, high_prec=False) - - self._sys_path = sys_path - self.rcut = rcut - self.sec = sec - if isinstance(rcut, float): - self.hybrid = False - elif isinstance(rcut, list): - self.hybrid = True - else: - RuntimeError("Unkown rcut type!") - self.sets = [None for i in range(len(self._sys_path))] - - self.nframes = 0 - i = 1 - self.prefix_sum = [0] * (len(self._dirs) + 1) - for item in self._dirs: - frames = self._load_set(item, fast=True) - self.prefix_sum[i] = self.prefix_sum[i - 1] + frames - i += 1 - self.nframes += frames - - def _check_pbc(self, sys_path): - pbc = True - if os.path.isfile(os.path.join(sys_path, "nopbc")): - pbc = False - self.pbc = pbc - - def set_noise(self, noise_settings): - # noise_settings['noise_type'] # "trunc_normal", "normal", "uniform" - # noise_settings['noise'] # float, default 1.0 - # noise_settings['noise_mode'] # "prob", "fix_num" - # noise_settings['mask_num'] # if "fix_num", int - # noise_settings['mask_prob'] # if "prob", float - # noise_settings['same_mask'] # coord and type same mask? - self.noise_settings = noise_settings - self.noise_type = noise_settings.get("noise_type", "uniform") - self.noise = float(noise_settings.get("noise", 1.0)) - self.noise_mode = noise_settings.get("noise_mode", "fix_num") - self.mask_num = int(noise_settings.get("mask_num", 1)) - self.mask_coord = noise_settings.get("mask_coord", False) - self.mask_type = noise_settings.get("mask_type", False) - self.mask_prob = float(noise_settings.get("mask_prob", 0.15)) - self.same_mask = noise_settings.get("noise_type", False) - - def add( - self, - key: str, - ndof: int, - atomic: bool = False, - must: bool = False, - high_prec: bool = False, - ): - """Add a data item that to be loaded. - - Args: - - key: The key of the item. The corresponding data is stored in `sys_path/set.*/key.npy` - - ndof: The number of dof - - atomic: The item is an atomic property. - - must: The data file `sys_path/set.*/key.npy` must exist. Otherwise, value is set to zero. - - high_prec: Load the data and store in float64, otherwise in float32. - """ - self._data_dict[key] = { - "ndof": ndof, - "atomic": atomic, - "must": must, - "high_prec": high_prec, - } - - def get_ntypes(self): - """Number of atom types in the system.""" - if self._type_map is not None: - return len(self._type_map) - else: - return max(self._atom_type) + 1 - - def get_natoms_vec(self, ntypes: int): - """Get number of atoms and number of atoms in different types. - - Args: - - ntypes: Number of types (may be larger than the actual number of types in the system). - """ - natoms = len(self._atom_type) - natoms_vec = np.zeros(ntypes).astype(int) - for ii in range(ntypes): - natoms_vec[ii] = np.count_nonzero(self._atom_type == ii) - tmp = [natoms, natoms] - tmp = np.append(tmp, natoms_vec) - return tmp.astype(np.int32) - - def _load_type(self, sys_path): - if self.file is not None: - return self.file["type.raw"][:] - else: - return np.loadtxt( - os.path.join(sys_path, "type.raw"), dtype=np.int32, ndmin=1 - ) - - def _load_type_map(self, sys_path): - if self.file is not None: - tmp = self.file["type_map.raw"][:].tolist() - tmp = [item.decode("ascii") for item in tmp] - return tmp - else: - fname = os.path.join(sys_path, "type_map.raw") - if os.path.isfile(fname): - with open(fname) as fin: - content = fin.read() - return content.split() - else: - return None - - def _check_mode(self, sys_path): - return os.path.isfile(sys_path + "/real_atom_types.npy") - - def _load_type_mix(self, set_name): - type_path = set_name + "/real_atom_types.npy" - real_type = np.load(type_path).astype(np.int32).reshape([-1, self._natoms]) - return real_type - - @lru_cache(maxsize=16, copy=True) - def _load_set(self, set_name, fast=False): - if self.file is None: - path = os.path.join(set_name, "coord.npy") - if self._data_dict["coord"]["high_prec"]: - coord = np.load(path).astype(env.GLOBAL_ENER_FLOAT_PRECISION) - else: - coord = np.load(path).astype(env.GLOBAL_NP_FLOAT_PRECISION) - if coord.ndim == 1: - coord = coord.reshape([1, -1]) - assert coord.shape[1] == self._data_dict["coord"]["ndof"] * self._natoms - nframes = coord.shape[0] - if fast: - return nframes - data = {"type": np.tile(self._atom_type[self._idx_map], (nframes, 1))} - for kk in self._data_dict.keys(): - data["find_" + kk], data[kk] = self._load_data( - set_name, - kk, - nframes, - self._data_dict[kk]["ndof"], - atomic=self._data_dict[kk]["atomic"], - high_prec=self._data_dict[kk]["high_prec"], - must=self._data_dict[kk]["must"], - ) - if self.mixed_type: - # nframes x natoms - atom_type_mix = self._load_type_mix(set_name) - if self.enforce_type_map: - try: - atom_type_mix_ = self.type_idx_map[atom_type_mix].astype( - np.int32 - ) - except IndexError as e: - raise IndexError( - "some types in 'real_atom_types.npy' of set {} are not contained in {} types!".format( - set_name, self.get_ntypes() - ) - ) from e - atom_type_mix = atom_type_mix_ - real_type = atom_type_mix.reshape([nframes, self._natoms]) - data["type"] = real_type - natoms = data["type"].shape[1] - # nframes x ntypes - atom_type_nums = np.array( - [(real_type == i).sum(axis=-1) for i in range(self.get_ntypes())], - dtype=np.int32, - ).T - ghost_nums = np.array( - [(real_type == -1).sum(axis=-1)], - dtype=np.int32, - ).T - assert ( - atom_type_nums.sum(axis=-1) + ghost_nums.sum(axis=-1) == natoms - ).all(), "some types in 'real_atom_types.npy' of set {} are not contained in {} types!".format( - set_name, self.get_ntypes() - ) - data["real_natoms_vec"] = np.concatenate( - ( - np.tile( - np.array([natoms, natoms], dtype=np.int32), (nframes, 1) - ), - atom_type_nums, - ), - axis=-1, - ) - - return data - else: - data = {} - nframes = self.file[set_name]["coord.npy"].shape[0] - if fast: - return nframes - for key in ["coord", "energy", "force", "box"]: - data[key] = self.file[set_name][f"{key}.npy"][:] - if self._data_dict[key]["atomic"]: - data[key] = data[key].reshape(nframes, self._natoms, -1)[ - :, self._idx_map, : - ] - if self.mixed_type: - # nframes x natoms - atom_type_mix = self._load_type_mix(set_name) - if self.enforce_type_map: - try: - atom_type_mix_ = self.type_idx_map[atom_type_mix].astype( - np.int32 - ) - except IndexError as e: - raise IndexError( - "some types in 'real_atom_types.npy' of set {} are not contained in {} types!".format( - set_name, self.get_ntypes() - ) - ) from e - atom_type_mix = atom_type_mix_ - real_type = atom_type_mix.reshape([nframes, self._natoms]) - data["type"] = real_type - natoms = data["type"].shape[1] - # nframes x ntypes - atom_type_nums = np.array( - [(real_type == i).sum(axis=-1) for i in range(self.get_ntypes())], - dtype=np.int32, - ).T - ghost_nums = np.array( - [(real_type == -1).sum(axis=-1)], - dtype=np.int32, - ).T - assert ( - atom_type_nums.sum(axis=-1) + ghost_nums.sum(axis=-1) == natoms - ).all(), "some types in 'real_atom_types.npy' of set {} are not contained in {} types!".format( - set_name, self.get_ntypes() - ) - data["real_natoms_vec"] = np.concatenate( - ( - np.tile( - np.array([natoms, natoms], dtype=np.int32), (nframes, 1) - ), - atom_type_nums, - ), - axis=-1, - ) - else: - data["type"] = np.tile(self._atom_type[self._idx_map], (nframes, 1)) - return data - - def _load_data( - self, set_name, key, nframes, ndof, atomic=False, must=True, high_prec=False - ): - if atomic: - ndof *= self._natoms - path = os.path.join(set_name, key + ".npy") - # log.info('Loading data from: %s', path) - if os.path.isfile(path): - if high_prec: - data = np.load(path).astype(env.GLOBAL_ENER_FLOAT_PRECISION) - else: - data = np.load(path).astype(env.GLOBAL_NP_FLOAT_PRECISION) - if atomic: - data = data.reshape([nframes, self._natoms, -1]) - data = data[:, self._idx_map, :] - data = data.reshape([nframes, -1]) - data = np.reshape(data, [nframes, ndof]) - return np.float32(1.0), data - elif must: - raise RuntimeError("%s not found!" % path) - else: - if high_prec: - data = np.zeros([nframes, ndof]).astype(env.GLOBAL_ENER_FLOAT_PRECISION) - else: - data = np.zeros([nframes, ndof]).astype(env.GLOBAL_NP_FLOAT_PRECISION) - return np.float32(0.0), data - - def _shuffle_data(self): - nframes = self._frames["coord"].shape[0] - idx = np.arange(nframes) - if self.shuffle: - dp_random.shuffle(idx) - self.idx_mapping = idx - - def _get_subdata(self, idx=None): - data = self._frames - idx = self.idx_mapping[idx] - new_data = {} - for ii in data: - dd = data[ii] - if "find_" in ii: - new_data[ii] = dd - else: - if idx is not None: - new_data[ii] = dd[idx] - else: - new_data[ii] = dd - return new_data - - # note: this function needs to be optimized for single frame process - def single_preprocess(self, batch, sid): - for kk in self._data_dict.keys(): - if "find_" in kk: - pass - else: - batch[kk] = torch.tensor( - batch[kk][sid], dtype=env.GLOBAL_PT_FLOAT_PRECISION - ) - if self._data_dict[kk]["atomic"]: - batch[kk] = batch[kk].view(-1, self._data_dict[kk]["ndof"]) - for kk in ["type", "real_natoms_vec"]: - if kk in batch.keys(): - batch[kk] = torch.tensor(batch[kk][sid], dtype=torch.long) - batch["atype"] = batch["type"] - rcut = self.rcut - sec = self.sec - if not self.pbc: - batch["box"] = None - if self.noise_settings is None: - return batch - else: # TODO need to clean up this method! - if self.pbc: - region = Region3D(batch["box"]) - else: - region = None - clean_coord = batch.pop("coord") - clean_type = batch.pop("type") - nloc = clean_type.shape[0] - batch["clean_type"] = clean_type - if self.pbc: - _clean_coord = normalize_coord(clean_coord, region, nloc) - else: - _clean_coord = clean_coord.clone() - batch["clean_coord"] = _clean_coord - # add noise - for i in range(self.max_fail_num): - mask_num = 0 - if self.noise_mode == "fix_num": - mask_num = self.mask_num - if len(batch["clean_type"]) < mask_num: - mask_num = len(batch["clean_type"]) - elif self.noise_mode == "prob": - mask_num = int(self.mask_prob * nloc) - if mask_num == 0: - mask_num = 1 - else: - NotImplementedError(f"Unknown noise mode {self.noise_mode}!") - rng = np.random.default_rng() - coord_mask_res = rng.choice( - range(nloc), mask_num, replace=False - ).tolist() - coord_mask = np.isin(range(nloc), coord_mask_res) - if self.same_mask: - type_mask = coord_mask.copy() - else: - rng = np.random.default_rng() - type_mask_res = rng.choice( - range(nloc), mask_num, replace=False - ).tolist() - type_mask = np.isin(range(nloc), type_mask_res) - - # add noise for coord - if self.mask_coord: - noise_on_coord = 0.0 - rng = np.random.default_rng() - if self.noise_type == "trunc_normal": - noise_on_coord = np.clip( - rng.standard_normal((mask_num, 3)) * self.noise, - a_min=-self.noise * 2.0, - a_max=self.noise * 2.0, - ) - elif self.noise_type == "normal": - noise_on_coord = rng.standard_normal((mask_num, 3)) * self.noise - elif self.noise_type == "uniform": - noise_on_coord = rng.uniform( - low=-self.noise, high=self.noise, size=(mask_num, 3) - ) - else: - NotImplementedError(f"Unknown noise type {self.noise_type}!") - noised_coord = _clean_coord.clone().detach() - noised_coord[coord_mask] += noise_on_coord - batch["coord_mask"] = torch.tensor(coord_mask, dtype=torch.bool) - else: - noised_coord = _clean_coord - batch["coord_mask"] = torch.tensor( - np.zeros_like(coord_mask, dtype=bool), dtype=torch.bool - ) - - # add mask for type - if self.mask_type: - masked_type = clean_type.clone().detach() - masked_type[type_mask] = self.mask_type_idx - batch["type_mask"] = torch.tensor(type_mask, dtype=torch.bool) - else: - masked_type = clean_type - batch["type_mask"] = torch.tensor( - np.zeros_like(type_mask, dtype=bool), dtype=torch.bool - ) - if self.pbc: - _coord = normalize_coord(noised_coord, region, nloc) - else: - _coord = noised_coord.clone() - try: - _ = make_env_mat( - _coord, - masked_type, - region, - rcut, - sec, - pbc=self.pbc, - type_split=self.type_split, - min_check=True, - ) - except RuntimeError as e: - if i == self.max_fail_num - 1: - RuntimeError( - f"Add noise times beyond max tries {self.max_fail_num}!" - ) - continue - batch["type"] = masked_type - batch["coord"] = noised_coord - return batch - - def _get_item(self, index): - for i in range( - 0, len(self._dirs) + 1 - ): # note: if different sets can be merged, prefix sum is unused to calculate - if index < self.prefix_sum[i]: - break - frames = self._load_set(self._dirs[i - 1]) - frame = self.single_preprocess(frames, index - self.prefix_sum[i - 1]) - frame["fid"] = index - return frame - - -def _make_idx_map(atom_type): - natoms = atom_type.shape[0] - idx = np.arange(natoms) - idx_map = np.lexsort((idx, atom_type)) - return idx_map class DeepmdDataSetForLoader(Dataset): @@ -560,11 +14,6 @@ def __init__( self, system: str, type_map: str, - rcut, - sel, - weight=None, - type_split=True, - noise_settings=None, shuffle=True, ): """Construct DeePMD-style dataset containing frames cross different systems. @@ -575,44 +24,22 @@ def __init__( - type_map: Atom types. """ self._type_map = type_map - if not isinstance(rcut, list): - if isinstance(sel, int): - sel = [sel] - sec = torch.cumsum(torch.tensor(sel), dim=0) - else: - sec = [] - for sel_item in sel: - if isinstance(sel_item, int): - sel_item = [sel_item] - sec.append(torch.cumsum(torch.tensor(sel_item), dim=0)) - self._data_system = DeepmdDataSystem( - system, - rcut, - sec, - type_map=self._type_map, - type_split=type_split, - noise_settings=noise_settings, - shuffle=shuffle, + self._data_system = DeepmdData( + sys_path=system, shuffle_test=shuffle, type_map=self._type_map ) + self._data_system.add("energy", 1, atomic=False, must=False, high_prec=True) + self._data_system.add("force", 3, atomic=True, must=False, high_prec=False) + self._data_system.add("virial", 9, atomic=False, must=False, high_prec=False) self.mixed_type = self._data_system.mixed_type self._ntypes = self._data_system.get_ntypes() - self._natoms = self._data_system._natoms + self._natoms = self._data_system.get_natoms() self._natoms_vec = self._data_system.get_natoms_vec(self._ntypes) - def set_noise(self, noise_settings): - # noise_settings['noise_type'] # "trunc_normal", "normal", "uniform" - # noise_settings['noise'] # float, default 1.0 - # noise_settings['noise_mode'] # "prob", "fix_num" - # noise_settings['mask_num'] # if "fix_num", int - # noise_settings['mask_prob'] # if "prob", float - # noise_settings['same_mask'] # coord and type same mask? - self._data_system.set_noise(noise_settings) - def __len__(self): return self._data_system.nframes def __getitem__(self, index): """Get a frame from the selected system.""" - b_data = self._data_system._get_item(index) - b_data["natoms"] = torch.tensor(self._natoms_vec) + b_data = self._data_system.get_item_torch(index) + b_data["natoms"] = self._natoms_vec return b_data diff --git a/deepmd/utils/data.py b/deepmd/utils/data.py index 423745cddf..6e0c47881f 100644 --- a/deepmd/utils/data.py +++ b/deepmd/utils/data.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: LGPL-3.0-or-later +import bisect import logging from typing import ( List, @@ -129,6 +130,11 @@ def __init__( self.shuffle_test = shuffle_test # set modifier self.modifier = modifier + # calculate prefix sum for get_item method + frames_list = [self._get_nframes(item) for item in self.dirs] + self.nframes = np.sum(frames_list) + # The prefix sum stores the range of indices contained in each directory, which is needed by get_item method + self.prefix_sum = np.cumsum(frames_list).tolist() def add( self, @@ -249,6 +255,21 @@ def check_test_size(self, test_size): else: return None + def get_item_torch(self, index: int) -> dict: + """Get a single frame data . The frame is picked from the data system by index. The index is coded across all the sets. + + Parameters + ---------- + index + index of the frame + """ + i = bisect.bisect_right(self.prefix_sum, index) + frames = self._load_set(self.dirs[i]) + frame = self._get_subdata(frames, index - self.prefix_sum[i]) + frame = self.reformat_data_torch(frame) + frame["fid"] = index + return frame + def get_batch(self, batch_size: int) -> dict: """Get a batch of data with `batch_size` frames. The frames are randomly picked from the data system. @@ -440,6 +461,37 @@ def _shuffle_data(self, data): ret[kk] = data[kk] return ret, idx + def _get_nframes(self, set_name: DPPath): + # get nframes + if not isinstance(set_name, DPPath): + set_name = DPPath(set_name) + path = set_name / "coord.npy" + if self.data_dict["coord"]["high_prec"]: + coord = path.load_numpy().astype(GLOBAL_ENER_FLOAT_PRECISION) + else: + coord = path.load_numpy().astype(GLOBAL_NP_FLOAT_PRECISION) + if coord.ndim == 1: + coord = coord.reshape([1, -1]) + nframes = coord.shape[0] + return nframes + + def reformat_data_torch(self, data): + """Modify the data format for the requirements of Torch backend. + + Parameters + ---------- + data + original data + """ + for kk in self.data_dict.keys(): + if "find_" in kk: + pass + else: + if self.data_dict[kk]["atomic"]: + data[kk] = data[kk].reshape(-1, self.data_dict[kk]["ndof"]) + data["atype"] = data["type"] + return data + def _load_set(self, set_name: DPPath): # get nframes if not isinstance(set_name, DPPath): diff --git a/source/tests/pt/model/test_descriptor.py b/source/tests/pt/model/test_descriptor.py index cfc909b0e9..529a83ac6d 100644 --- a/source/tests/pt/model/test_descriptor.py +++ b/source/tests/pt/model/test_descriptor.py @@ -113,9 +113,6 @@ def setUp(self): ds = DeepmdDataSetForLoader( self.systems[0], model_config["type_map"], - self.rcut, - self.sel, - type_split=True, ) self.np_batch, self.pt_batch = get_single_batch(ds) self.sec = np.cumsum(self.sel) diff --git a/source/tests/pt/model/test_embedding_net.py b/source/tests/pt/model/test_embedding_net.py index 02e3f9f70a..c7782e61f2 100644 --- a/source/tests/pt/model/test_embedding_net.py +++ b/source/tests/pt/model/test_embedding_net.py @@ -49,16 +49,13 @@ def gen_key(worb, depth, elemid): def get_single_batch(dataset, index=None): if index is None: index = dp_random.choice(np.arange(len(dataset))) - pt_batch = dataset[index] - np_batch = {} - # TODO deprecated - for key in ["mapping", "shift", "nlist"]: - if key in pt_batch.keys(): - pt_batch[key] = pt_batch[key].unsqueeze(0) + np_batch = dataset[index] + pt_batch = {} + for key in ["coord", "box", "force", "energy", "virial", "atype", "natoms"]: - if key in pt_batch.keys(): - pt_batch[key] = pt_batch[key].unsqueeze(0) - np_batch[key] = pt_batch[key].cpu().numpy() + if key in np_batch.keys(): + np_batch[key] = np.expand_dims(np_batch[key], axis=0) + pt_batch[key] = torch.as_tensor(np_batch[key]) np_batch["coord"] = np_batch["coord"].reshape(1, -1) np_batch["natoms"] = np_batch["natoms"][0] np_batch["force"] = np_batch["force"].reshape(1, -1) @@ -130,9 +127,6 @@ def setUp(self): ds = DeepmdDataSetForLoader( self.systems[0], model_config["type_map"], - self.rcut, - self.sel, - type_split=True, ) self.filter_neuron = model_config["descriptor"]["neuron"] self.axis_neuron = model_config["descriptor"]["axis_neuron"] diff --git a/source/tests/pt/model/test_force_grad.py b/source/tests/pt/model/test_force_grad.py index d695c821e5..80a72cc176 100644 --- a/source/tests/pt/model/test_force_grad.py +++ b/source/tests/pt/model/test_force_grad.py @@ -19,33 +19,34 @@ from deepmd.pt.utils import ( env, ) -from deepmd.pt.utils.dataset import ( - DeepmdDataSystem, +from deepmd.utils.data import ( + DeepmdData, ) -class CheckSymmetry(DeepmdDataSystem): +class CheckSymmetry(DeepmdData): def __init__( self, sys_path: str, - rcut, - sec, type_map: Optional[List[str]] = None, - type_split=True, ): - super().__init__(sys_path, rcut, sec, type_map, type_split) + super().__init__(sys_path=sys_path, type_map=type_map) + self.add("energy", 1, atomic=False, must=False, high_prec=True) + self.add("force", 3, atomic=True, must=False, high_prec=False) + self.add("virial", 9, atomic=False, must=False, high_prec=False) def get_disturb(self, index, atom_index, axis_index, delta): for i in range( - 0, len(self._dirs) + 1 + 0, len(self.dirs) + 1 ): # note: if different sets can be merged, prefix sum is unused to calculate if index < self.prefix_sum[i]: break - frames = self._load_set(self._dirs[i - 1]) + frames = self._load_set(self.dirs[i - 1]) tmp = copy.deepcopy(frames["coord"].reshape(self.nframes, -1, 3)) tmp[:, atom_index, axis_index] += delta frames["coord"] = tmp - frame = self.single_preprocess(frames, index - self.prefix_sum[i - 1]) + frame = self._get_subdata(frames, index - self.prefix_sum[i - 1]) + frame = self.reformat_data_torch(frame) return frame @@ -78,16 +79,16 @@ def get_dataset(self, system_index=0, batch_index=0): sec = torch.cumsum(torch.tensor(sel), dim=0) type_map = self.config["model"]["type_map"] self.dpdatasystem = CheckSymmetry( - sys_path=systems[system_index], rcut=rcut, sec=sec, type_map=type_map + sys_path=systems[system_index], type_map=type_map ) - self.origin_batch = self.dpdatasystem._get_item(batch_index) + self.origin_batch = self.dpdatasystem.get_item_torch(batch_index) @unittest.skip("it can be replaced by autodiff") def test_force_grad(self, threshold=1e-2, delta0=1e-6, seed=20): result0 = self.model(**get_data(self.origin_batch)) np.random.default_rng(seed) - errors = np.zeros((self.dpdatasystem._natoms, 3)) - for atom_index in range(self.dpdatasystem._natoms): + errors = np.zeros((self.dpdatasystem.natoms, 3)) + for atom_index in range(self.dpdatasystem.natoms): for axis_index in range(3): delta = np.random.default_rng().random() * delta0 disturb_batch = self.dpdatasystem.get_disturb( diff --git a/source/tests/pt/model/test_model.py b/source/tests/pt/model/test_model.py index 938eb7545d..789ad80ada 100644 --- a/source/tests/pt/model/test_model.py +++ b/source/tests/pt/model/test_model.py @@ -333,10 +333,14 @@ def test_consistency(self): # print(dst.mean(), dst.std()) dst.copy_(src) # Start forward computing - batch = my_ds.systems[0]._data_system.single_preprocess(batch, 0) + tmp = np.copy(batch["natoms_vec"]) + batch = my_ds.systems[0]._data_system._get_subdata(batch, 0) + batch = my_ds.systems[0]._data_system.reformat_data_torch(batch) for key in ["coord", "atype", "box", "energy", "force"]: + batch[key] = torch.as_tensor(batch[key]) batch[key] = batch[key].unsqueeze(0) batch["coord"].requires_grad_(True) + batch["natoms_vec"] = tmp batch["natoms"] = torch.tensor( batch["natoms_vec"], device=batch["coord"].device ).unsqueeze(0) diff --git a/source/tests/pt/model/test_rotation.py b/source/tests/pt/model/test_rotation.py index 5314959673..18d7bb8553 100644 --- a/source/tests/pt/model/test_rotation.py +++ b/source/tests/pt/model/test_rotation.py @@ -21,29 +21,29 @@ from deepmd.pt.utils import ( env, ) -from deepmd.pt.utils.dataset import ( - DeepmdDataSystem, +from deepmd.utils.data import ( + DeepmdData, ) -class CheckSymmetry(DeepmdDataSystem): +class CheckSymmetry(DeepmdData): def __init__( self, sys_path: str, - rcut, - sec, type_map: Optional[List[str]] = None, - type_split=True, ): - super().__init__(sys_path, rcut, sec, type_map, type_split) + super().__init__(sys_path=sys_path, type_map=type_map) + self.add("energy", 1, atomic=False, must=False, high_prec=True) + self.add("force", 3, atomic=True, must=False, high_prec=False) + self.add("virial", 9, atomic=False, must=False, high_prec=False) def get_rotation(self, index, rotation_matrix): for i in range( - 0, len(self._dirs) + 1 + 0, len(self.dirs) + 1 ): # note: if different sets can be merged, prefix sum is unused to calculate if index < self.prefix_sum[i]: break - frames = self._load_set(self._dirs[i - 1]) + frames = self._load_set(self.dirs[i - 1]) frames["coord"] = np.dot( rotation_matrix, frames["coord"].reshape(-1, 3).T ).T.reshape(self.nframes, -1) @@ -53,14 +53,16 @@ def get_rotation(self, index, rotation_matrix): frames["force"] = np.dot( rotation_matrix, frames["force"].reshape(-1, 3).T ).T.reshape(self.nframes, -1) - frame = self.single_preprocess(frames, index - self.prefix_sum[i - 1]) + frame = self._get_subdata(frames, index - self.prefix_sum[i - 1]) + frame = self.reformat_data_torch(frame) return frame def get_data(batch): inputs = {} for key in ["coord", "atype", "box"]: - inputs[key] = batch[key].unsqueeze(0).to(env.DEVICE) + inputs[key] = torch.as_tensor(batch[key]) + inputs[key] = inputs[key].unsqueeze(0).to(env.DEVICE) return inputs @@ -80,14 +82,9 @@ def get_model(self): def get_dataset(self, system_index=0, batch_index=0): systems = self.config["training"]["training_data"]["systems"] - rcut = self.config["model"]["descriptor"]["rcut"] - sel = self.config["model"]["descriptor"]["sel"] - sec = torch.cumsum(torch.tensor(sel), dim=0) type_map = self.config["model"]["type_map"] - dpdatasystem = CheckSymmetry( - sys_path=systems[system_index], rcut=rcut, sec=sec, type_map=type_map - ) - self.origin_batch = dpdatasystem._get_item(batch_index) + dpdatasystem = CheckSymmetry(sys_path=systems[system_index], type_map=type_map) + self.origin_batch = dpdatasystem.get_item_torch(batch_index) self.rotated_batch = dpdatasystem.get_rotation(batch_index, self.rotation) def test_rotation(self): diff --git a/source/tests/pt/test_loss.py b/source/tests/pt/test_loss.py index f0c75ef288..e117c7f05a 100644 --- a/source/tests/pt/test_loss.py +++ b/source/tests/pt/test_loss.py @@ -46,9 +46,7 @@ def get_batch(): systems = config["training"]["validation_data"]["systems"] if isinstance(systems, str): systems = expand_sys_str(systems) - dataset = DeepmdDataSetForLoader( - systems[0], model_config["type_map"], rcut, sel, type_split=True - ) + dataset = DeepmdDataSetForLoader(systems[0], model_config["type_map"]) np_batch, pt_batch = get_single_batch(dataset) return np_batch, pt_batch From 8b1ed143520f89e95d8bcd1faa89a6d94a7536c0 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 20 Feb 2024 20:47:11 -0500 Subject: [PATCH 106/686] allow both absulute and relative tolerance when testing consistency (#3308) Signed-off-by: Jinzhe Zeng --- source/tests/consistent/common.py | 14 ++++++++------ source/tests/consistent/fitting/test_ener.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/source/tests/consistent/common.py b/source/tests/consistent/common.py index 45c2114b24..7cec3da006 100644 --- a/source/tests/consistent/common.py +++ b/source/tests/consistent/common.py @@ -78,6 +78,8 @@ class CommonTest(ABC): """Whether to skip the PyTorch model.""" rtol = 1e-10 """Relative tolerance for comparing the return value. Override for float32.""" + atol = 1e-10 + """Absolute tolerance for comparing the return value. Override for float32.""" def setUp(self): self.unique_id = uuid4().hex @@ -242,7 +244,7 @@ def test_tf_consistent_with_ref(self): ret2 = self.extract_ret(ret2, self.RefBackend.TF) np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): - np.testing.assert_allclose(rr1, rr2, rtol=self.rtol) + np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) def test_tf_self_consistent(self): """Test whether TF is self consistent.""" @@ -256,7 +258,7 @@ def test_tf_self_consistent(self): ret2, data2 = self.get_tf_ret_serialization_from_cls(obj2) np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): - np.testing.assert_allclose(rr1, rr2, rtol=self.rtol) + np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) def test_dp_consistent_with_ref(self): """Test whether DP and reference are consistent.""" @@ -273,7 +275,7 @@ def test_dp_consistent_with_ref(self): data2 = dp_obj.serialize() np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): - np.testing.assert_allclose(rr1, rr2, rtol=self.rtol) + np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) def test_dp_self_consistent(self): """Test whether DP is self consistent.""" @@ -286,7 +288,7 @@ def test_dp_self_consistent(self): np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): if isinstance(rr1, np.ndarray) and isinstance(rr2, np.ndarray): - np.testing.assert_allclose(rr1, rr2, rtol=self.rtol) + np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) else: self.assertEqual(rr1, rr2) @@ -305,7 +307,7 @@ def test_pt_consistent_with_ref(self): data2 = obj.serialize() np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): - np.testing.assert_allclose(rr1, rr2, rtol=self.rtol) + np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) def test_pt_self_consistent(self): """Test whether PT is self consistent.""" @@ -318,7 +320,7 @@ def test_pt_self_consistent(self): np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): if isinstance(rr1, np.ndarray) and isinstance(rr2, np.ndarray): - np.testing.assert_allclose(rr1, rr2, rtol=self.rtol) + np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) else: self.assertEqual(rr1, rr2) diff --git a/source/tests/consistent/fitting/test_ener.py b/source/tests/consistent/fitting/test_ener.py index e1f1c0fe84..3b3255d683 100644 --- a/source/tests/consistent/fitting/test_ener.py +++ b/source/tests/consistent/fitting/test_ener.py @@ -183,3 +183,19 @@ def rtol(self) -> float: return 1e-4 else: raise ValueError(f"Unknown precision: {precision}") + + @property + def atol(self) -> float: + """Absolute tolerance for comparing the return value.""" + ( + resnet_dt, + precision, + mixed_types, + numb_fparam, + ) = self.param + if precision == "float64": + return 1e-10 + elif precision == "float32": + return 1e-4 + else: + raise ValueError(f"Unknown precision: {precision}") From 139721fe11275d8bc47142a6cefe861be5ad3381 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 20 Feb 2024 22:27:09 -0500 Subject: [PATCH 107/686] pt: explicitly set device (#3307) Forcely requires setting the device to `env.DEVICE` or `cpu` explicitly for functions like `torch.tensor`, `torch.zeros`, `torch.ones`, `torch.rand`, `torch.eye`, `LayerNorm`, and data loader. This ensures that no OP runs on the wrong device. The trick here is `torch.set_default_device("cuda:9999999")` in the tests, so errors will be thrown if the default device is used. Tips: (1) Avoid `torch.zeros(...).to(device=...)`. This first initializes memory on CPUs and copies it to GPUs. (2) Use `with torch.device(...)` for a module that cannot set a device (i.e., the data loader). --------- Signed-off-by: Jinzhe Zeng --- deepmd/pt/infer/deep_eval.py | 46 +++++++++++-------- deepmd/pt/infer/inference.py | 6 ++- .../model/atomic_model/linear_atomic_model.py | 19 ++++++-- .../atomic_model/pairtab_atomic_model.py | 8 +++- deepmd/pt/model/descriptor/repformer_layer.py | 2 +- deepmd/pt/model/descriptor/se_a.py | 1 + deepmd/pt/model/model/make_model.py | 6 ++- deepmd/pt/model/network/network.py | 13 ++++-- deepmd/pt/model/task/ener.py | 2 +- deepmd/pt/train/training.py | 9 ++-- deepmd/pt/utils/dataloader.py | 8 ++-- deepmd/pt/utils/nlist.py | 8 ++-- deepmd/pt/utils/stat.py | 21 +++++---- source/tests/pt/__init__.py | 2 + source/tests/pt/model/test_autodiff.py | 23 ++++++---- source/tests/pt/model/test_deeppot.py | 4 +- source/tests/pt/model/test_descriptor_dpa1.py | 23 +++++----- source/tests/pt/model/test_descriptor_dpa2.py | 23 +++++----- source/tests/pt/model/test_dipole_fitting.py | 12 ++--- source/tests/pt/model/test_embedding_net.py | 2 +- source/tests/pt/model/test_env_mat.py | 14 +++--- .../pt/model/test_linear_atomic_model.py | 12 +++-- source/tests/pt/model/test_model.py | 4 +- source/tests/pt/model/test_nlist.py | 27 +++++------ .../pt/model/test_pairtab_atomic_model.py | 46 +++++++++++++------ source/tests/pt/model/test_permutation.py | 8 ++-- .../pt/model/test_polarizability_fitting.py | 12 ++--- source/tests/pt/model/test_region.py | 15 +++--- source/tests/pt/model/test_rot.py | 18 ++++---- source/tests/pt/model/test_rotation.py | 5 +- source/tests/pt/model/test_saveload_dpa1.py | 3 +- .../tests/pt/model/test_saveload_se_e2_a.py | 3 +- source/tests/pt/model/test_smooth.py | 39 ++++++++-------- source/tests/pt/model/test_trans.py | 10 ++-- source/tests/pt/model/test_unused_params.py | 6 +-- source/tests/pt/test_calculator.py | 16 ++++--- source/tests/pt/test_dp_test.py | 4 +- source/tests/pt/test_sampler.py | 4 +- source/tests/pt/test_stat.py | 4 +- 39 files changed, 284 insertions(+), 204 deletions(-) diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index 9679bbb1e6..601bd6f755 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -343,13 +343,17 @@ def _eval_model( natoms = len(atom_types[0]) coord_input = torch.tensor( - coords.reshape([-1, natoms, 3]), dtype=GLOBAL_PT_FLOAT_PRECISION - ).to(DEVICE) - type_input = torch.tensor(atom_types, dtype=torch.long).to(DEVICE) + coords.reshape([-1, natoms, 3]), + dtype=GLOBAL_PT_FLOAT_PRECISION, + device=DEVICE, + ) + type_input = torch.tensor(atom_types, dtype=torch.long, device=DEVICE) if cells is not None: box_input = torch.tensor( - cells.reshape([-1, 3, 3]), dtype=GLOBAL_PT_FLOAT_PRECISION - ).to(DEVICE) + cells.reshape([-1, 3, 3]), + dtype=GLOBAL_PT_FLOAT_PRECISION, + device=DEVICE, + ) else: box_input = None @@ -420,7 +424,7 @@ def eval_model( if cells is not None: assert isinstance(cells, torch.Tensor), err_msg assert isinstance(atom_types, torch.Tensor) or isinstance(atom_types, list) - atom_types = torch.tensor(atom_types, dtype=torch.long).to(DEVICE) + atom_types = torch.tensor(atom_types, dtype=torch.long, device=DEVICE) elif isinstance(coords, np.ndarray): if cells is not None: assert isinstance(cells, np.ndarray), err_msg @@ -441,17 +445,17 @@ def eval_model( natoms = len(atom_types[0]) coord_input = torch.tensor( - coords.reshape([-1, natoms, 3]), dtype=GLOBAL_PT_FLOAT_PRECISION - ).to(DEVICE) - type_input = torch.tensor(atom_types, dtype=torch.long).to(DEVICE) + coords.reshape([-1, natoms, 3]), dtype=GLOBAL_PT_FLOAT_PRECISION, device=DEVICE + ) + type_input = torch.tensor(atom_types, dtype=torch.long, device=DEVICE) box_input = None if cells is None: pbc = False else: pbc = True box_input = torch.tensor( - cells.reshape([-1, 3, 3]), dtype=GLOBAL_PT_FLOAT_PRECISION - ).to(DEVICE) + cells.reshape([-1, 3, 3]), dtype=GLOBAL_PT_FLOAT_PRECISION, device=DEVICE + ) num_iter = int((nframes + infer_batch_size - 1) / infer_batch_size) for ii in range(num_iter): @@ -527,35 +531,37 @@ def eval_model( energy_out = ( torch.cat(energy_out) if energy_out - else torch.zeros([nframes, 1], dtype=GLOBAL_PT_FLOAT_PRECISION).to(DEVICE) + else torch.zeros( + [nframes, 1], dtype=GLOBAL_PT_FLOAT_PRECISION, device=DEVICE + ) ) atomic_energy_out = ( torch.cat(atomic_energy_out) if atomic_energy_out - else torch.zeros([nframes, natoms, 1], dtype=GLOBAL_PT_FLOAT_PRECISION).to( - DEVICE + else torch.zeros( + [nframes, natoms, 1], dtype=GLOBAL_PT_FLOAT_PRECISION, device=DEVICE ) ) force_out = ( torch.cat(force_out) if force_out - else torch.zeros([nframes, natoms, 3], dtype=GLOBAL_PT_FLOAT_PRECISION).to( - DEVICE + else torch.zeros( + [nframes, natoms, 3], dtype=GLOBAL_PT_FLOAT_PRECISION, device=DEVICE ) ) virial_out = ( torch.cat(virial_out) if virial_out - else torch.zeros([nframes, 3, 3], dtype=GLOBAL_PT_FLOAT_PRECISION).to( - DEVICE + else torch.zeros( + [nframes, 3, 3], dtype=GLOBAL_PT_FLOAT_PRECISION, device=DEVICE ) ) atomic_virial_out = ( torch.cat(atomic_virial_out) if atomic_virial_out else torch.zeros( - [nframes, natoms, 3, 3], dtype=GLOBAL_PT_FLOAT_PRECISION - ).to(DEVICE) + [nframes, natoms, 3, 3], dtype=GLOBAL_PT_FLOAT_PRECISION, device=DEVICE + ) ) updated_coord_out = torch.cat(updated_coord_out) if updated_coord_out else None logits_out = torch.cat(logits_out) if logits_out else None diff --git a/deepmd/pt/infer/inference.py b/deepmd/pt/infer/inference.py index cce9291e3b..e97623dd24 100644 --- a/deepmd/pt/infer/inference.py +++ b/deepmd/pt/infer/inference.py @@ -171,7 +171,8 @@ def __init__( @staticmethod def get_data(data): - batch_data = next(iter(data)) + with torch.device("cpu"): + batch_data = next(iter(data)) for key in batch_data.keys(): if key == "sid" or key == "fid": continue @@ -235,7 +236,8 @@ def run(self): ), # setting to 0 diverges the behavior of its iterator; should be >=1 drop_last=False, ) - data = iter(dataloader) + with torch.device("cpu"): + data = iter(dataloader) single_results = {} sum_natoms = 0 diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index b688362b73..29ca9c8f96 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -16,6 +16,9 @@ FittingOutputDef, OutputVariableDef, ) +from deepmd.pt.utils import ( + env, +) from deepmd.pt.utils.nlist import ( build_multiple_neighbor_list, get_multiple_nlist_key, @@ -91,9 +94,17 @@ def get_model_sels(self) -> List[List[int]]: def _sort_rcuts_sels(self) -> Tuple[List[float], List[int]]: # sort the pair of rcut and sels in ascending order, first based on sel, then on rcut. - rcuts = torch.tensor(self.get_model_rcuts(), dtype=torch.float64) - nsels = torch.tensor(self.get_model_nsels()) - zipped = torch.stack([torch.tensor(rcuts), torch.tensor(nsels)], dim=0).T + rcuts = torch.tensor( + self.get_model_rcuts(), dtype=torch.float64, device=env.DEVICE + ) + nsels = torch.tensor(self.get_model_nsels(), device=env.DEVICE) + zipped = torch.stack( + [ + torch.tensor(rcuts, device=env.DEVICE), + torch.tensor(nsels, device=env.DEVICE), + ], + dim=0, + ).T inner_sorting = torch.argsort(zipped[:, 1], dim=0) inner_sorted = zipped[inner_sorting] outer_sorting = torch.argsort(inner_sorted[:, 0], stable=True) @@ -285,7 +296,7 @@ def __init__( self.smin_alpha = smin_alpha # this is a placeholder being updated in _compute_weight, to handle Jit attribute init error. - self.zbl_weight = torch.empty(0, dtype=torch.float64) + self.zbl_weight = torch.empty(0, dtype=torch.float64, device=env.DEVICE) def serialize(self) -> dict: return { diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index 4019fda423..c79e742d63 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -12,6 +12,9 @@ FittingOutputDef, OutputVariableDef, ) +from deepmd.pt.utils import ( + env, +) from deepmd.utils.pair_tab import ( PairTab, ) @@ -156,7 +159,7 @@ def forward_atomic( pairwise_rr = self._get_pairwise_dist( extended_coord, masked_nlist ) # (nframes, nloc, nnei) - self.tab_data = self.tab_data.view( + self.tab_data = self.tab_data.to(device=env.DEVICE).view( int(self.tab_info[-1]), int(self.tab_info[-1]), int(self.tab_info[2]), 4 ) @@ -164,7 +167,8 @@ def forward_atomic( # i_type : (nframes, nloc), this is atype. # j_type : (nframes, nloc, nnei) j_type = extended_atype[ - torch.arange(extended_atype.size(0))[:, None, None], masked_nlist + torch.arange(extended_atype.size(0), device=env.DEVICE)[:, None, None], + masked_nlist, ] raw_atomic_energy = self._pair_tabulated_inter( diff --git a/deepmd/pt/model/descriptor/repformer_layer.py b/deepmd/pt/model/descriptor/repformer_layer.py index 21ae0ff6f3..66ce38c0f7 100644 --- a/deepmd/pt/model/descriptor/repformer_layer.py +++ b/deepmd/pt/model/descriptor/repformer_layer.py @@ -326,7 +326,7 @@ def __init__( sel = [sel] if isinstance(sel, int) else sel self.nnei = sum(sel) assert len(sel) == 1 - self.sel = torch.tensor(sel) + self.sel = torch.tensor(sel, device=env.DEVICE) self.sec = self.sel self.axis_dim = axis_dim self.set_davg_zero = set_davg_zero diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 0f8add1d8d..30682a4605 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -448,6 +448,7 @@ def forward( ) # shape is [nframes*nall, self.ndescrpt] xyz_scatter = torch.empty( 1, + device=env.DEVICE, ) ret = self.filter_layers_old[0](dmatrix) xyz_scatter = ret diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 19bc514a2d..9f0fd81842 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -240,8 +240,10 @@ def _format_nlist( nlist, -1 * torch.ones( - [n_nf, n_nloc, nnei - n_nnei], dtype=nlist.dtype - ).to(nlist.device), + [n_nf, n_nloc, nnei - n_nnei], + dtype=nlist.dtype, + device=nlist.device, + ), ], dim=-1, ) diff --git a/deepmd/pt/model/network/network.py b/deepmd/pt/model/network/network.py index 8b5b3cf998..9ef7b3366a 100644 --- a/deepmd/pt/model/network/network.py +++ b/deepmd/pt/model/network/network.py @@ -32,7 +32,7 @@ def Tensor(*shape): - return torch.empty(shape, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + return torch.empty(shape, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE) class Dropout(nn.Module): @@ -332,7 +332,13 @@ def __init__( bias: bool = True, init: str = "default", ): - super().__init__(d_in, d_out, bias=bias, dtype=env.GLOBAL_PT_FLOAT_PRECISION) + super().__init__( + d_in, + d_out, + bias=bias, + dtype=env.GLOBAL_PT_FLOAT_PRECISION, + device=env.DEVICE, + ) self.use_bias = bias @@ -552,6 +558,7 @@ def __init__(self, type_nums, embed_dim, bavg=0.0, stddev=1.0): embed_dim, padding_idx=type_nums, dtype=env.GLOBAL_PT_FLOAT_PRECISION, + device=env.DEVICE, ) # nn.init.normal_(self.embedding.weight[:-1], mean=bavg, std=stddev) @@ -799,7 +806,7 @@ def __init__( temperature=temperature, ) self.attn_layer_norm = nn.LayerNorm( - self.embed_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION + self.embed_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE ) if self.ffn: self.ffn_embed_dim = ffn_embed_dim diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index d57d460f6d..ed2dfbc02b 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -268,7 +268,7 @@ def __init__( bias_atom_e = np.zeros([self.ntypes]) if not use_tebd: assert self.ntypes == len(bias_atom_e), "Element count mismatches!" - bias_atom_e = torch.tensor(bias_atom_e) + bias_atom_e = torch.tensor(bias_atom_e, device=env.DEVICE) self.register_buffer("bias_atom_e", bias_atom_e) filter_layers_dipole = [] diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 8537be6e12..5a783e412b 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -156,7 +156,8 @@ def get_data_loader(_training_data, _validation_data, _training_params): drop_last=False, pin_memory=True, ) - training_data_buffered = BufferedIterator(iter(training_dataloader)) + with torch.device("cpu"): + training_data_buffered = BufferedIterator(iter(training_dataloader)) validation_dataloader = DataLoader( _validation_data, sampler=valid_sampler, @@ -166,7 +167,8 @@ def get_data_loader(_training_data, _validation_data, _training_params): pin_memory=True, ) - validation_data_buffered = BufferedIterator(iter(validation_dataloader)) + with torch.device("cpu"): + validation_data_buffered = BufferedIterator(iter(validation_dataloader)) if _training_params.get("validation_data", None) is not None: valid_numb_batch = _training_params["validation_data"].get( "numb_btch", 1 @@ -519,7 +521,8 @@ def step(_step_id, task_key="Default"): if not torch.isfinite(grad_norm).all(): # check local gradnorm single GPU case, trigger NanDetector raise FloatingPointError("gradients are Nan/Inf") - self.optimizer.step() + with torch.device("cpu"): + self.optimizer.step() self.scheduler.step() elif self.opt_type == "LKF": if isinstance(self.loss, EnergyStdLoss): diff --git a/deepmd/pt/utils/dataloader.py b/deepmd/pt/utils/dataloader.py index 7844d6d741..2125f9cdee 100644 --- a/deepmd/pt/utils/dataloader.py +++ b/deepmd/pt/utils/dataloader.py @@ -120,8 +120,9 @@ def construct_dataset(system): self.total_batch += len(system_dataloader) # Initialize iterator instances for DataLoader self.iters = [] - for item in self.dataloaders: - self.iters.append(iter(item)) + with torch.device("cpu"): + for item in self.dataloaders: + self.iters.append(iter(item)) def set_noise(self, noise_settings): # noise_settings['noise_type'] # "trunc_normal", "normal", "uniform" @@ -250,5 +251,6 @@ def get_weighted_sampler(training_data, prob_style, sys_prob=False): log.info("Generated weighted sampler with prob array: " + str(probs)) # training_data.total_batch is the size of one epoch, you can increase it to avoid too many rebuilding of iteraters len_sampler = training_data.total_batch * max(env.NUM_WORKERS, 1) - sampler = WeightedRandomSampler(probs, len_sampler, replacement=True) + with torch.device("cpu"): + sampler = WeightedRandomSampler(probs, len_sampler, replacement=True) return sampler diff --git a/deepmd/pt/utils/nlist.py b/deepmd/pt/utils/nlist.py index 43036604e2..0e2d9785f8 100644 --- a/deepmd/pt/utils/nlist.py +++ b/deepmd/pt/utils/nlist.py @@ -116,14 +116,14 @@ def build_neighbor_list( nlist = nlist[:, :, :nsel] else: rr = torch.cat( - [rr, torch.ones([batch_size, nloc, nsel - nnei]).to(rr.device) + rcut], + [rr, torch.ones([batch_size, nloc, nsel - nnei], device=rr.device) + rcut], dim=-1, ) nlist = torch.cat( [ nlist, - torch.ones([batch_size, nloc, nsel - nnei], dtype=nlist.dtype).to( - rr.device + torch.ones( + [batch_size, nloc, nsel - nnei], dtype=nlist.dtype, device=rr.device ), ], dim=-1, @@ -289,7 +289,7 @@ def extend_coord_with_ghosts( """ nf, nloc = atype.shape - aidx = torch.tile(torch.arange(nloc).unsqueeze(0), [nf, 1]) + aidx = torch.tile(torch.arange(nloc, device=env.DEVICE).unsqueeze(0), [nf, 1]) if cell is None: nall = nloc extend_coord = coord.clone() diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index 051fddd14b..38f71d6994 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -32,16 +32,17 @@ def make_stat_input(datasets, dataloaders, nbatches): log.info(f"Packing data for statistics from {len(datasets)} systems") for i in range(len(datasets)): sys_stat = {key: [] for key in keys} - iterator = iter(dataloaders[i]) - for _ in range(nbatches): - try: - stat_data = next(iterator) - except StopIteration: - iterator = iter(dataloaders[i]) - stat_data = next(iterator) - for dd in stat_data: - if dd in keys: - sys_stat[dd].append(stat_data[dd]) + with torch.device("cpu"): + iterator = iter(dataloaders[i]) + for _ in range(nbatches): + try: + stat_data = next(iterator) + except StopIteration: + iterator = iter(dataloaders[i]) + stat_data = next(iterator) + for dd in stat_data: + if dd in keys: + sys_stat[dd].append(stat_data[dd]) for key in keys: if not isinstance(sys_stat[key][0], list): if sys_stat[key][0] is None: diff --git a/source/tests/pt/__init__.py b/source/tests/pt/__init__.py index fdbdd73f79..1a6de0591a 100644 --- a/source/tests/pt/__init__.py +++ b/source/tests/pt/__init__.py @@ -3,3 +3,5 @@ torch.set_num_threads(1) torch.set_num_interop_threads(1) +# testing purposes; device should always be set explicitly +torch.set_default_device("cuda:9999999") diff --git a/source/tests/pt/model/test_autodiff.py b/source/tests/pt/model/test_autodiff.py index e69e894af6..b1745b384e 100644 --- a/source/tests/pt/model/test_autodiff.py +++ b/source/tests/pt/model/test_autodiff.py @@ -54,9 +54,9 @@ def test( places = 8 delta = 1e-5 natoms = 5 - cell = torch.rand([3, 3], dtype=dtype) - cell = (cell + cell.T) + 5.0 * torch.eye(3) - coord = torch.rand([natoms, 3], dtype=dtype) + cell = torch.rand([3, 3], dtype=dtype, device="cpu") + cell = (cell + cell.T) + 5.0 * torch.eye(3, device="cpu") + coord = torch.rand([natoms, 3], dtype=dtype, device="cpu") coord = torch.matmul(coord, cell) atype = torch.IntTensor([0, 0, 0, 1, 1]) # assumes input to be numpy tensor @@ -66,7 +66,10 @@ def np_infer( coord, ): e0, f0, v0 = eval_model( - self.model, torch.tensor(coord).unsqueeze(0), cell.unsqueeze(0), atype + self.model, + torch.tensor(coord, device=env.DEVICE).unsqueeze(0), + cell.unsqueeze(0), + atype, ) ret = { "energy": e0.squeeze(0), @@ -92,9 +95,9 @@ def test( places = 8 delta = 1e-4 natoms = 5 - cell = torch.rand([3, 3], dtype=dtype) - cell = (cell) + 5.0 * torch.eye(3) - coord = torch.rand([natoms, 3], dtype=dtype) + cell = torch.rand([3, 3], dtype=dtype, device="cpu") + cell = (cell) + 5.0 * torch.eye(3, device="cpu") + coord = torch.rand([natoms, 3], dtype=dtype, device="cpu") coord = torch.matmul(coord, cell) atype = torch.IntTensor([0, 0, 0, 1, 1]) # assumes input to be numpy tensor @@ -106,8 +109,10 @@ def np_infer( ): e0, f0, v0 = eval_model( self.model, - torch.tensor(stretch_box(coord, cell, new_cell)).unsqueeze(0), - torch.tensor(new_cell).unsqueeze(0), + torch.tensor( + stretch_box(coord, cell, new_cell), device="cpu" + ).unsqueeze(0), + torch.tensor(new_cell, device="cpu").unsqueeze(0), atype, ) ret = { diff --git a/source/tests/pt/model/test_deeppot.py b/source/tests/pt/model/test_deeppot.py index 3ef901d0e3..da2e554704 100644 --- a/source/tests/pt/model/test_deeppot.py +++ b/source/tests/pt/model/test_deeppot.py @@ -12,6 +12,7 @@ ) import numpy as np +import torch from deepmd.infer.deep_pot import DeepPot as DeepPotUni from deepmd.pt.entrypoints.main import ( @@ -43,7 +44,8 @@ def setUp(self): trainer = get_trainer(deepcopy(self.config)) trainer.run() - input_dict, label_dict, _ = trainer.get_data(is_train=False) + with torch.device("cpu"): + input_dict, label_dict, _ = trainer.get_data(is_train=False) trainer.wrapper(**input_dict, label=label_dict, cur_lr=1.0) self.model = "model.pt" diff --git a/source/tests/pt/model/test_descriptor_dpa1.py b/source/tests/pt/model/test_descriptor_dpa1.py index 07cb684afa..ced3bbac57 100644 --- a/source/tests/pt/model/test_descriptor_dpa1.py +++ b/source/tests/pt/model/test_descriptor_dpa1.py @@ -38,11 +38,9 @@ def setUp(self): 8.178091365465004481e-01, 6.159552512682983760e00, ] - self.cell = ( - torch.tensor(cell, dtype=env.GLOBAL_PT_FLOAT_PRECISION) - .view(1, 3, 3) - .to(env.DEVICE) - ) + self.cell = torch.tensor( + cell, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ).view(1, 3, 3) coord = [ 2.978060152121375648e00, 3.588469695887098077e00, @@ -60,12 +58,12 @@ def setUp(self): 6.575011420004332585e00, 6.825240650611076099e00, ] - self.coord = ( - torch.tensor(coord, dtype=env.GLOBAL_PT_FLOAT_PRECISION) - .view(1, -1, 3) - .to(env.DEVICE) - ) - self.atype = torch.IntTensor([0, 0, 0, 1, 1]).view(1, -1).to(env.DEVICE) + self.coord = torch.tensor( + coord, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ).view(1, -1, 3) + self.atype = torch.tensor( + [0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE + ).view(1, -1) self.ref_d = torch.tensor( [ 8.382518544113587780e-03, @@ -230,7 +228,8 @@ def setUp(self): 1.518237240362583541e-03, ], dtype=env.GLOBAL_PT_FLOAT_PRECISION, - ).to(env.DEVICE) + device=env.DEVICE, + ) with open(Path(CUR_DIR) / "models" / "dpa1.json") as fp: self.model_json = json.load(fp) self.file_model_param = Path(CUR_DIR) / "models" / "dpa1.pth" diff --git a/source/tests/pt/model/test_descriptor_dpa2.py b/source/tests/pt/model/test_descriptor_dpa2.py index d94686fee1..96f734a7d8 100644 --- a/source/tests/pt/model/test_descriptor_dpa2.py +++ b/source/tests/pt/model/test_descriptor_dpa2.py @@ -40,11 +40,9 @@ def setUp(self): 8.178091365465004481e-01, 6.159552512682983760e00, ] - self.cell = ( - torch.tensor(cell, dtype=env.GLOBAL_PT_FLOAT_PRECISION) - .view(1, 3, 3) - .to(env.DEVICE) - ) + self.cell = torch.tensor( + cell, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ).view(1, 3, 3) coord = [ 2.978060152121375648e00, 3.588469695887098077e00, @@ -62,12 +60,12 @@ def setUp(self): 6.575011420004332585e00, 6.825240650611076099e00, ] - self.coord = ( - torch.tensor(coord, dtype=env.GLOBAL_PT_FLOAT_PRECISION) - .view(1, -1, 3) - .to(env.DEVICE) - ) - self.atype = torch.IntTensor([0, 0, 0, 1, 1]).view(1, -1).to(env.DEVICE) + self.coord = torch.tensor( + coord, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ).view(1, -1, 3) + self.atype = torch.tensor( + [0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE + ).view(1, -1) self.ref_d = torch.tensor( [ 8.435412613327306630e-01, @@ -112,7 +110,8 @@ def setUp(self): -3.073744832541754762e-02, ], dtype=env.GLOBAL_PT_FLOAT_PRECISION, - ).to(env.DEVICE) + device=env.DEVICE, + ) with open(Path(CUR_DIR) / "models" / "dpa2_hyb.json") as fp: self.model_json = json.load(fp) self.file_model_param = Path(CUR_DIR) / "models" / "dpa2.pth" diff --git a/source/tests/pt/model/test_dipole_fitting.py b/source/tests/pt/model/test_dipole_fitting.py index 3f67043767..5ecfb72481 100644 --- a/source/tests/pt/model/test_dipole_fitting.py +++ b/source/tests/pt/model/test_dipole_fitting.py @@ -128,16 +128,16 @@ def setUp(self) -> None: self.rcut_smth = 0.5 self.sel = [46, 92, 4] self.nf = 1 - self.coord = 2 * torch.rand([self.natoms, 3], dtype=dtype).to(env.DEVICE) - self.shift = torch.tensor([4, 4, 4], dtype=dtype).to(env.DEVICE) - self.atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + self.coord = 2 * torch.rand([self.natoms, 3], dtype=dtype, device=env.DEVICE) + self.shift = torch.tensor([4, 4, 4], dtype=dtype, device=env.DEVICE) + self.atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) self.dd0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) - self.cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) - self.cell = (self.cell + self.cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) + self.cell = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) + self.cell = (self.cell + self.cell.T) + 5.0 * torch.eye(3, device=env.DEVICE) def test_rot(self): atype = self.atype.reshape(1, 5) - rmat = torch.tensor(special_ortho_group.rvs(3), dtype=dtype).to(env.DEVICE) + rmat = torch.tensor(special_ortho_group.rvs(3), dtype=dtype, device=env.DEVICE) coord_rot = torch.matmul(self.coord, rmat) rng = np.random.default_rng() for mixed_types, nfp, nap in itertools.product( diff --git a/source/tests/pt/model/test_embedding_net.py b/source/tests/pt/model/test_embedding_net.py index c7782e61f2..87e8a97444 100644 --- a/source/tests/pt/model/test_embedding_net.py +++ b/source/tests/pt/model/test_embedding_net.py @@ -55,7 +55,7 @@ def get_single_batch(dataset, index=None): for key in ["coord", "box", "force", "energy", "virial", "atype", "natoms"]: if key in np_batch.keys(): np_batch[key] = np.expand_dims(np_batch[key], axis=0) - pt_batch[key] = torch.as_tensor(np_batch[key]) + pt_batch[key] = torch.as_tensor(np_batch[key], device=env.DEVICE) np_batch["coord"] = np_batch["coord"].reshape(1, -1) np_batch["natoms"] = np_batch["natoms"][0] np_batch["force"] = np_batch["force"].reshape(1, -1) diff --git a/source/tests/pt/model/test_env_mat.py b/source/tests/pt/model/test_env_mat.py index 0746f9e8df..ee262e7ee5 100644 --- a/source/tests/pt/model/test_env_mat.py +++ b/source/tests/pt/model/test_env_mat.py @@ -100,14 +100,14 @@ def test_consistency( em0 = EnvMat(self.rcut, self.rcut_smth) mm0, ww0 = em0.call(self.coord_ext, self.atype_ext, self.nlist, davg, dstd) mm1, _, ww1 = prod_env_mat_se_a( - torch.tensor(self.coord_ext, dtype=dtype), - torch.tensor(self.nlist, dtype=int), - torch.tensor(self.atype_ext[:, :nloc], dtype=int), - davg, - dstd, + torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), + torch.tensor(self.nlist, dtype=int, device=env.DEVICE), + torch.tensor(self.atype_ext[:, :nloc], dtype=int, device=env.DEVICE), + torch.tensor(davg, device=env.DEVICE), + torch.tensor(dstd, device=env.DEVICE), self.rcut, self.rcut_smth, ) - np.testing.assert_allclose(mm0, mm1) - np.testing.assert_allclose(ww0, ww1) + np.testing.assert_allclose(mm0, mm1.detach().cpu().numpy()) + np.testing.assert_allclose(ww0, ww1.detach().cpu().numpy()) np.testing.assert_allclose(mm0[0][self.perm[: self.nloc]], mm0[1]) diff --git a/source/tests/pt/model/test_linear_atomic_model.py b/source/tests/pt/model/test_linear_atomic_model.py index dcfff0b387..aae1a66618 100644 --- a/source/tests/pt/model/test_linear_atomic_model.py +++ b/source/tests/pt/model/test_linear_atomic_model.py @@ -52,8 +52,8 @@ def test_pairwise(self, mock_loadtxt): [0.25, 0.0, 0.0, 0.0], ] ) - extended_atype = torch.tensor([[0, 0]]).to(env.DEVICE) - nlist = torch.tensor([[[1], [-1]]]).to(env.DEVICE) + extended_atype = torch.tensor([[0, 0]], device=env.DEVICE) + nlist = torch.tensor([[[1], [-1]]], device=env.DEVICE) ds = DescrptSeA( rcut=0.3, @@ -85,8 +85,9 @@ def test_pairwise(self, mock_loadtxt): [0.0, 0.0, 0.0], [0.0, dist, 0.0], ], - ] - ).to(env.DEVICE) + ], + device=env.DEVICE, + ) wgt_model.forward_atomic(extended_coord, extended_atype, nlist) @@ -106,7 +107,8 @@ def test_pairwise(self, mock_loadtxt): [0.0, 0.0], ], dtype=torch.float64, - ).to(env.DEVICE) + device=env.DEVICE, + ) torch.testing.assert_close(results, excepted_res, rtol=0.0001, atol=0.0001) diff --git a/source/tests/pt/model/test_model.py b/source/tests/pt/model/test_model.py index 789ad80ada..d8c7de39c3 100644 --- a/source/tests/pt/model/test_model.py +++ b/source/tests/pt/model/test_model.py @@ -337,7 +337,7 @@ def test_consistency(self): batch = my_ds.systems[0]._data_system._get_subdata(batch, 0) batch = my_ds.systems[0]._data_system.reformat_data_torch(batch) for key in ["coord", "atype", "box", "energy", "force"]: - batch[key] = torch.as_tensor(batch[key]) + batch[key] = torch.as_tensor(batch[key], device=env.DEVICE) batch[key] = batch[key].unsqueeze(0) batch["coord"].requires_grad_(True) batch["natoms_vec"] = tmp @@ -419,7 +419,7 @@ def step(step_id): var_name = torch2tf(name, last_layer_id=len(self.n_neuron)) var_grad = vs_dict[var_name].gradient param_grad = param.grad.cpu() - var_grad = torch.tensor(var_grad) + var_grad = torch.tensor(var_grad, device="cpu") assert np.allclose(var_grad, param_grad, rtol=rtol, atol=atol) diff --git a/source/tests/pt/model/test_nlist.py b/source/tests/pt/model/test_nlist.py index 27c03acfaa..616af93081 100644 --- a/source/tests/pt/model/test_nlist.py +++ b/source/tests/pt/model/test_nlist.py @@ -26,12 +26,12 @@ def setUp(self): self.ns = 5 * 5 * 3 self.nall = self.ns * self.nloc self.cell = torch.tensor( - [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype - ).to(env.DEVICE) - self.icoord = torch.tensor([[0, 0, 0], [0.5, 0.5, 0.1]], dtype=dtype).to( - env.DEVICE + [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype, device=env.DEVICE ) - self.atype = torch.tensor([0, 1], dtype=torch.int).to(env.DEVICE) + self.icoord = torch.tensor( + [[0, 0, 0], [0.5, 0.5, 0.1]], dtype=dtype, device=env.DEVICE + ) + self.atype = torch.tensor([0, 1], dtype=torch.int, device=env.DEVICE) [self.cell, self.icoord, self.atype] = [ ii.unsqueeze(0) for ii in [self.cell, self.icoord, self.atype] ] @@ -53,8 +53,9 @@ def setUp(self): [ [0, 0, 0, 0, 0, 0, -1, -1, -1, -1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1], [0, 0, 0, 0, -1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1], - ] - ).to(env.DEVICE) + ], + device=env.DEVICE, + ) def test_build_notype(self): ecoord, eatype, mapping = extend_coord_with_ghosts( @@ -174,39 +175,39 @@ def test_extend_coord(self): mm, cc = torch.unique(shift_vec[0][:, 0], dim=-1, return_counts=True) torch.testing.assert_close( mm, - torch.tensor([-2, -1, 0, 1, 2], dtype=dtype).to(env.DEVICE), + torch.tensor([-2, -1, 0, 1, 2], dtype=dtype, device=env.DEVICE), rtol=self.prec, atol=self.prec, ) torch.testing.assert_close( cc, - torch.tensor([30, 30, 30, 30, 30], dtype=torch.long).to(env.DEVICE), + torch.tensor([30, 30, 30, 30, 30], dtype=torch.long, device=env.DEVICE), rtol=self.prec, atol=self.prec, ) mm, cc = torch.unique(shift_vec[1][:, 1], dim=-1, return_counts=True) torch.testing.assert_close( mm, - torch.tensor([-2, -1, 0, 1, 2], dtype=dtype).to(env.DEVICE), + torch.tensor([-2, -1, 0, 1, 2], dtype=dtype, device=env.DEVICE), rtol=self.prec, atol=self.prec, ) torch.testing.assert_close( cc, - torch.tensor([30, 30, 30, 30, 30], dtype=torch.long).to(env.DEVICE), + torch.tensor([30, 30, 30, 30, 30], dtype=torch.long, device=env.DEVICE), rtol=self.prec, atol=self.prec, ) mm, cc = torch.unique(shift_vec[1][:, 2], dim=-1, return_counts=True) torch.testing.assert_close( mm, - torch.tensor([-1, 0, 1], dtype=dtype).to(env.DEVICE), + torch.tensor([-1, 0, 1], dtype=dtype, device=env.DEVICE), rtol=self.prec, atol=self.prec, ) torch.testing.assert_close( cc, - torch.tensor([50, 50, 50], dtype=torch.long).to(env.DEVICE), + torch.tensor([50, 50, 50], dtype=torch.long, device=env.DEVICE), rtol=self.prec, atol=self.prec, ) diff --git a/source/tests/pt/model/test_pairtab_atomic_model.py b/source/tests/pt/model/test_pairtab_atomic_model.py index 69e305b40b..021035e3de 100644 --- a/source/tests/pt/model/test_pairtab_atomic_model.py +++ b/source/tests/pt/model/test_pairtab_atomic_model.py @@ -11,6 +11,9 @@ from deepmd.pt.model.atomic_model import ( PairTabAtomicModel, ) +from deepmd.pt.utils import ( + env, +) from deepmd.pt.utils.utils import ( to_numpy_array, ) @@ -45,21 +48,28 @@ def setUp(self, mock_loadtxt) -> None: [0.01, 0.01, 0.02], [0.05, 0.01, 0.01], ], - ] + ], + device=env.DEVICE, ) # nframes=2, nall=4 - self.extended_atype = torch.tensor([[0, 1, 0, 1], [0, 0, 1, 1]]) + self.extended_atype = torch.tensor( + [[0, 1, 0, 1], [0, 0, 1, 1]], device=env.DEVICE + ) # nframes=2, nloc=2, nnei=2 - self.nlist = torch.tensor([[[1, 2], [0, 2]], [[1, 2], [0, 3]]]) + self.nlist = torch.tensor( + [[[1, 2], [0, 2]], [[1, 2], [0, 3]]], device=env.DEVICE + ) def test_without_mask(self): result = self.model.forward_atomic( self.extended_coord, self.extended_atype, self.nlist ) expected_result = torch.tensor( - [[[1.2000], [1.3614]], [[1.2000], [0.4000]]], dtype=torch.float64 + [[[1.2000], [1.3614]], [[1.2000], [0.4000]]], + dtype=torch.float64, + device=env.DEVICE, ) torch.testing.assert_close( @@ -67,13 +77,17 @@ def test_without_mask(self): ) def test_with_mask(self): - self.nlist = torch.tensor([[[1, -1], [0, 2]], [[1, 2], [0, 3]]]) + self.nlist = torch.tensor( + [[[1, -1], [0, 2]], [[1, 2], [0, 3]]], device=env.DEVICE + ) result = self.model.forward_atomic( self.extended_coord, self.extended_atype, self.nlist ) expected_result = torch.tensor( - [[[0.8000], [1.3614]], [[1.2000], [0.4000]]], dtype=torch.float64 + [[[0.8000], [1.3614]], [[1.2000], [0.4000]]], + dtype=torch.float64, + device=env.DEVICE, ) torch.testing.assert_close( @@ -91,7 +105,9 @@ def test_deserialize(self): torch.testing.assert_close(self.model.tab_data, model1.tab_data) torch.testing.assert_close(self.model.tab_info, model1.tab_info) - self.nlist = torch.tensor([[[1, -1], [0, 2]], [[1, 2], [0, 3]]]) + self.nlist = torch.tensor( + [[[1, -1], [0, 2]], [[1, 2], [0, 3]]], device=env.DEVICE + ) result = model1.forward_atomic( self.extended_coord, self.extended_atype, self.nlist ) @@ -116,12 +132,14 @@ def test_cross_deserialize(self): self.nlist = np.array([[[1, -1], [0, 2]], [[1, 2], [0, 3]]]) result = model1.forward_atomic( - self.extended_coord.numpy(), - self.extended_atype.numpy(), + self.extended_coord.cpu().numpy(), + self.extended_atype.cpu().numpy(), self.nlist, ) expected_result = self.model.forward_atomic( - self.extended_coord, self.extended_atype, torch.from_numpy(self.nlist) + self.extended_coord, + self.extended_atype, + torch.from_numpy(self.nlist).to(device=env.DEVICE), ) np.testing.assert_allclose( result["energy"], to_numpy_array(expected_result["energy"]), 0.0001, 0.0001 @@ -159,10 +177,10 @@ def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: ) # nframes=1, nall=2 - extended_atype = torch.tensor([[0, 0]]) + extended_atype = torch.tensor([[0, 0]], device=env.DEVICE) # nframes=1, nloc=2, nnei=1 - nlist = torch.tensor([[[1], [-1]]]) + nlist = torch.tensor([[[1], [-1]]], device=env.DEVICE) results = [] @@ -206,7 +224,8 @@ def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: [0.0, 0.0, 0.0], [0.0, dist, 0.0], ], - ] + ], + device=env.DEVICE, ) model = PairTabAtomicModel(tab_file=file_path, rcut=rcut, sel=2) @@ -236,6 +255,7 @@ def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: ] ], dtype=torch.float64, + device=env.DEVICE, ) ] ).reshape(14, 2) diff --git a/source/tests/pt/model/test_permutation.py b/source/tests/pt/model/test_permutation.py index 15359f873a..b97cb349ad 100644 --- a/source/tests/pt/model/test_permutation.py +++ b/source/tests/pt/model/test_permutation.py @@ -201,11 +201,11 @@ def test( self, ): natoms = 5 - cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) - cell = (cell + cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) - coord = torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) + cell = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) + cell = (cell + cell.T) + 5.0 * torch.eye(3, device=env.DEVICE) + coord = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) coord = torch.matmul(coord, cell) - atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) idx_perm = [1, 0, 4, 3, 2] e0, f0, v0 = eval_model( self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype diff --git a/source/tests/pt/model/test_polarizability_fitting.py b/source/tests/pt/model/test_polarizability_fitting.py index de43c57b8b..548219627b 100644 --- a/source/tests/pt/model/test_polarizability_fitting.py +++ b/source/tests/pt/model/test_polarizability_fitting.py @@ -149,17 +149,17 @@ def setUp(self) -> None: self.nf = 1 self.nt = 3 self.rng = np.random.default_rng() - self.coord = 2 * torch.rand([self.natoms, 3], dtype=dtype).to(env.DEVICE) - self.shift = torch.tensor([4, 4, 4], dtype=dtype).to(env.DEVICE) - self.atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + self.coord = 2 * torch.rand([self.natoms, 3], dtype=dtype, device=env.DEVICE) + self.shift = torch.tensor([4, 4, 4], dtype=dtype, device=env.DEVICE) + self.atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) self.dd0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) - self.cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) - self.cell = (self.cell + self.cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) + self.cell = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) + self.cell = (self.cell + self.cell.T) + 5.0 * torch.eye(3, device=env.DEVICE) self.scale = self.rng.uniform(0, 1, self.nt).tolist() def test_rot(self): atype = self.atype.reshape(1, 5) - rmat = torch.tensor(special_ortho_group.rvs(3), dtype=dtype).to(env.DEVICE) + rmat = torch.tensor(special_ortho_group.rvs(3), dtype=dtype, device=env.DEVICE) coord_rot = torch.matmul(self.coord, rmat) for mixed_types, nfp, nap, fit_diag, scale in itertools.product( diff --git a/source/tests/pt/model/test_region.py b/source/tests/pt/model/test_region.py index e8a3346562..b06f4221fd 100644 --- a/source/tests/pt/model/test_region.py +++ b/source/tests/pt/model/test_region.py @@ -4,6 +4,9 @@ import numpy as np import torch +from deepmd.pt.utils import ( + env, +) from deepmd.pt.utils.preprocess import ( Region3D, ) @@ -18,14 +21,14 @@ class TestRegion(unittest.TestCase): def setUp(self): self.cell = torch.tensor( - [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype + [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype, device="cpu" ) self.cell = self.cell.unsqueeze(0).unsqueeze(0) self.cell = torch.tile(self.cell, [4, 5, 1, 1]) self.prec = 1e-8 def test_inter_to_phys(self): - inter = torch.rand([4, 5, 3, 3], dtype=dtype) + inter = torch.rand([4, 5, 3, 3], dtype=dtype, device="cpu") phys = inter2phys(inter, self.cell) for ii in range(4): for jj in range(5): @@ -45,7 +48,7 @@ def test_to_face_dist(self): dz = vol / sxy dy = vol / sxz dx = vol / syz - expected = torch.tensor([dx, dy, dz]) + expected = torch.tensor([dx, dy, dz], device="cpu") dists = to_face_distance(self.cell) for ii in range(4): for jj in range(5): @@ -57,19 +60,19 @@ def test_to_face_dist(self): class TestLegacyRegion(unittest.TestCase): def setUp(self): self.cell = torch.tensor( - [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype + [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype, device=env.DEVICE ) self.prec = 1e-6 def test_inter_to_phys(self): - inter = torch.rand([3, 3], dtype=dtype) + inter = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) reg = Region3D(self.cell) phys = reg.inter2phys(inter) expected_phys = torch.matmul(inter, self.cell) torch.testing.assert_close(phys, expected_phys, rtol=self.prec, atol=self.prec) def test_inter_to_inter(self): - inter = torch.rand([3, 3], dtype=dtype) + inter = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) reg = Region3D(self.cell) new_inter = reg.phys2inter(reg.inter2phys(inter)) torch.testing.assert_close(inter, new_inter, rtol=self.prec, atol=self.prec) diff --git a/source/tests/pt/model/test_rot.py b/source/tests/pt/model/test_rot.py index 780d193ebd..0c3a34e2d5 100644 --- a/source/tests/pt/model/test_rot.py +++ b/source/tests/pt/model/test_rot.py @@ -32,15 +32,15 @@ def test( ): prec = 1e-10 natoms = 5 - cell = 10.0 * torch.eye(3, dtype=dtype).to(env.DEVICE) - coord = 2 * torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) - shift = torch.tensor([4, 4, 4], dtype=dtype).to(env.DEVICE) - atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + cell = 10.0 * torch.eye(3, dtype=dtype, device=env.DEVICE) + coord = 2 * torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) + shift = torch.tensor([4, 4, 4], dtype=dtype, device=env.DEVICE) + atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) from scipy.stats import ( special_ortho_group, ) - rmat = torch.tensor(special_ortho_group.rvs(3), dtype=dtype).to(env.DEVICE) + rmat = torch.tensor(special_ortho_group.rvs(3), dtype=dtype, device=env.DEVICE) # rotate only coord and shift to the center of cell coord_rot = torch.matmul(coord, rmat) @@ -74,11 +74,11 @@ def test( # rotate coord and cell torch.manual_seed(0) - cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) - cell = (cell + cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) - coord = torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) + cell = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) + cell = (cell + cell.T) + 5.0 * torch.eye(3, device=env.DEVICE) + coord = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) coord = torch.matmul(coord, cell) - atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) + atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) coord_rot = torch.matmul(coord, rmat) cell_rot = torch.matmul(cell, rmat) e0, f0, v0 = eval_model( diff --git a/source/tests/pt/model/test_rotation.py b/source/tests/pt/model/test_rotation.py index 18d7bb8553..caa6385c80 100644 --- a/source/tests/pt/model/test_rotation.py +++ b/source/tests/pt/model/test_rotation.py @@ -61,7 +61,7 @@ def get_rotation(self, index, rotation_matrix): def get_data(batch): inputs = {} for key in ["coord", "atype", "box"]: - inputs[key] = torch.as_tensor(batch[key]) + inputs[key] = torch.as_tensor(batch[key], device=env.DEVICE) inputs[key] = inputs[key].unsqueeze(0).to(env.DEVICE) return inputs @@ -74,7 +74,8 @@ def setUp(self): self.config["training"]["training_data"]["systems"] = data_file self.config["training"]["validation_data"]["systems"] = data_file self.rotation = special_ortho_group.rvs(3) - self.get_dataset(0) + with torch.device("cpu"): + self.get_dataset(0) self.get_model() def get_model(self): diff --git a/source/tests/pt/model/test_saveload_dpa1.py b/source/tests/pt/model/test_saveload_dpa1.py index 64229b8e9e..408afbef43 100644 --- a/source/tests/pt/model/test_saveload_dpa1.py +++ b/source/tests/pt/model/test_saveload_dpa1.py @@ -83,7 +83,8 @@ def setUp(self): drop_last=False, pin_memory=True, ) - self.training_data = BufferedIterator(iter(self.training_dataloader)) + with torch.device("cpu"): + self.training_data = BufferedIterator(iter(self.training_dataloader)) self.loss = EnergyStdLoss(**self.config["loss"]) self.cur_lr = 1 self.task_key = "Default" diff --git a/source/tests/pt/model/test_saveload_se_e2_a.py b/source/tests/pt/model/test_saveload_se_e2_a.py index 0632e30b5b..382f119c30 100644 --- a/source/tests/pt/model/test_saveload_se_e2_a.py +++ b/source/tests/pt/model/test_saveload_se_e2_a.py @@ -83,7 +83,8 @@ def setUp(self): drop_last=False, pin_memory=True, ) - self.training_data = BufferedIterator(iter(self.training_dataloader)) + with torch.device("cpu"): + self.training_data = BufferedIterator(iter(self.training_dataloader)) self.loss = EnergyStdLoss(**self.config["loss"]) self.cur_lr = 1 self.task_key = "Default" diff --git a/source/tests/pt/model/test_smooth.py b/source/tests/pt/model/test_smooth.py index fa9042a932..88d75a040c 100644 --- a/source/tests/pt/model/test_smooth.py +++ b/source/tests/pt/model/test_smooth.py @@ -37,27 +37,26 @@ def test( aprec = 1e-5 if self.aprec is None else self.aprec natoms = 10 - cell = 8.6 * torch.eye(3, dtype=dtype).to(env.DEVICE) - atype = torch.randint(0, 3, [natoms]) - coord0 = ( - torch.tensor( - [ - 0.0, - 0.0, - 0.0, - 4.0 - 0.5 * epsilon, - 0.0, - 0.0, - 0.0, - 4.0 - 0.5 * epsilon, - 0.0, - ], - dtype=dtype, - ) - .view([-1, 3]) - .to(env.DEVICE) + cell = 8.6 * torch.eye(3, dtype=dtype, device=env.DEVICE) + atype = torch.randint(0, 3, [natoms], device=env.DEVICE) + coord0 = torch.tensor( + [ + 0.0, + 0.0, + 0.0, + 4.0 - 0.5 * epsilon, + 0.0, + 0.0, + 0.0, + 4.0 - 0.5 * epsilon, + 0.0, + ], + dtype=dtype, + device=env.DEVICE, + ).view([-1, 3]) + coord1 = torch.rand( + [natoms - coord0.shape[0], 3], dtype=dtype, device=env.DEVICE ) - coord1 = torch.rand([natoms - coord0.shape[0], 3], dtype=dtype).to(env.DEVICE) coord1 = torch.matmul(coord1, cell) coord = torch.concat([coord0, coord1], dim=0) diff --git a/source/tests/pt/model/test_trans.py b/source/tests/pt/model/test_trans.py index a99d6c893f..23365f3c9a 100644 --- a/source/tests/pt/model/test_trans.py +++ b/source/tests/pt/model/test_trans.py @@ -31,12 +31,12 @@ def test( self, ): natoms = 5 - cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) - cell = (cell + cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) - coord = torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) + cell = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) + cell = (cell + cell.T) + 5.0 * torch.eye(3, device=env.DEVICE) + coord = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) coord = torch.matmul(coord, cell) - atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) - shift = (torch.rand([3], dtype=dtype) - 0.5).to(env.DEVICE) * 2.0 + atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) + shift = (torch.rand([3], dtype=dtype, device=env.DEVICE) - 0.5) * 2.0 coord_s = torch.matmul( torch.remainder(torch.matmul(coord + shift, torch.linalg.inv(cell)), 1.0), cell, diff --git a/source/tests/pt/model/test_unused_params.py b/source/tests/pt/model/test_unused_params.py index f69d8ac835..c20a5f1dc5 100644 --- a/source/tests/pt/model/test_unused_params.py +++ b/source/tests/pt/model/test_unused_params.py @@ -58,9 +58,9 @@ def test_unused(self): def _test_unused(self, model_params): self.model = get_model(model_params).to(env.DEVICE) natoms = 5 - cell = torch.rand([3, 3], dtype=dtype).to(env.DEVICE) - cell = (cell + cell.T) + 5.0 * torch.eye(3).to(env.DEVICE) - coord = torch.rand([natoms, 3], dtype=dtype).to(env.DEVICE) + cell = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) + cell = (cell + cell.T) + 5.0 * torch.eye(3, device=env.DEVICE) + coord = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) coord = torch.matmul(coord, cell) atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) idx_perm = [1, 0, 4, 3, 2] diff --git a/source/tests/pt/test_calculator.py b/source/tests/pt/test_calculator.py index a35538250b..52b4b6cbbe 100644 --- a/source/tests/pt/test_calculator.py +++ b/source/tests/pt/test_calculator.py @@ -8,6 +8,7 @@ Path, ) +import numpy as np import torch from deepmd.pt.entrypoints.main import ( @@ -39,7 +40,8 @@ def setUp(self): trainer = get_trainer(deepcopy(self.config)) trainer.run() - input_dict, label_dict, _ = trainer.get_data(is_train=False) + with torch.device("cpu"): + input_dict, label_dict, _ = trainer.get_data(is_train=False) _, _, more_loss = trainer.wrapper(**input_dict, label=label_dict, cur_lr=1.0) self.calculator = DPCalculator("model.pt") @@ -50,8 +52,8 @@ def test_calculator(self): ) natoms = 5 - cell = torch.eye(3, dtype=dtype) * 10 - coord = torch.rand([natoms, 3], dtype=dtype) + cell = torch.eye(3, dtype=dtype, device="cpu") * 10 + coord = torch.rand([natoms, 3], dtype=dtype, device="cpu") coord = torch.matmul(coord, cell) atype = torch.IntTensor([0, 0, 0, 1, 1]) atomic_numbers = [1, 1, 1, 8, 8] @@ -91,7 +93,7 @@ def test_calculator(self): assert isinstance(e0, float) assert f0.shape == (natoms, 3) assert v0.shape == (3, 3) - torch.testing.assert_close(e0, e1, rtol=low_prec, atol=prec) - torch.testing.assert_close(f0[idx_perm, :], f1, rtol=low_prec, atol=prec) - torch.testing.assert_close(s0, s1, rtol=low_prec, atol=prec) - torch.testing.assert_close(v0, v1, rtol=low_prec, atol=prec) + np.testing.assert_allclose(e0, e1, rtol=low_prec, atol=prec) + np.testing.assert_allclose(f0[idx_perm, :], f1, rtol=low_prec, atol=prec) + np.testing.assert_allclose(s0, s1, rtol=low_prec, atol=prec) + np.testing.assert_allclose(v0, v1, rtol=low_prec, atol=prec) diff --git a/source/tests/pt/test_dp_test.py b/source/tests/pt/test_dp_test.py index 3db66f073f..8d7dc9cd58 100644 --- a/source/tests/pt/test_dp_test.py +++ b/source/tests/pt/test_dp_test.py @@ -11,6 +11,7 @@ ) import numpy as np +import torch from deepmd.pt.entrypoints.main import ( get_trainer, @@ -40,7 +41,8 @@ def test_dp_test(self): trainer = get_trainer(deepcopy(self.config)) trainer.run() - input_dict, label_dict, _ = trainer.get_data(is_train=False) + with torch.device("cpu"): + input_dict, label_dict, _ = trainer.get_data(is_train=False) _, _, more_loss = trainer.wrapper(**input_dict, label=label_dict, cur_lr=1.0) tester = inference.Tester("model.pt", input_script=self.input_json) diff --git a/source/tests/pt/test_sampler.py b/source/tests/pt/test_sampler.py index 0ff16ed7c7..25980cc144 100644 --- a/source/tests/pt/test_sampler.py +++ b/source/tests/pt/test_sampler.py @@ -7,6 +7,7 @@ ) import numpy as np +import torch from torch.utils.data import ( DataLoader, ) @@ -69,7 +70,8 @@ def test_sampler_debug_info(self): drop_last=False, pin_memory=True, ) - batch_data = next(iter(dataloader)) + with torch.device("cpu"): + batch_data = next(iter(dataloader)) sid = batch_data["sid"] fid = batch_data["fid"][0] coord = batch_data["coord"].squeeze(0) diff --git a/source/tests/pt/test_stat.py b/source/tests/pt/test_stat.py index 5cf6a953cc..1e3c707d6f 100644 --- a/source/tests/pt/test_stat.py +++ b/source/tests/pt/test_stat.py @@ -131,9 +131,9 @@ def my_merge(energy, natoms): natoms_lst = [] for i in range(len(energy)): for j in range(len(energy[i])): - energy_lst.append(torch.tensor(energy[i][j])) + energy_lst.append(torch.tensor(energy[i][j], device="cpu")) natoms_lst.append( - torch.tensor(natoms[i][j]) + torch.tensor(natoms[i][j], device="cpu") .unsqueeze(0) .expand(energy[i][j].shape[0], -1) ) From 49568640a208799365728f0181d536c6898c8b2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 11:45:02 +0800 Subject: [PATCH 108/686] build(deps): bump the npm_and_yarn group across 1 directories with 1 update (#3312) Bumps the npm_and_yarn group with 1 update in the /source/nodejs directory: [ip](https://github.com/indutny/node-ip). Updates `ip` from 2.0.0 to 2.0.1
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ip&package-manager=npm_and_yarn&previous-version=2.0.0&new-version=2.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/deepmodeling/deepmd-kit/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- source/nodejs/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/nodejs/yarn.lock b/source/nodejs/yarn.lock index 864cd77297..fb0c093f11 100644 --- a/source/nodejs/yarn.lock +++ b/source/nodejs/yarn.lock @@ -307,9 +307,9 @@ inherits@2, inherits@^2.0.3: integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== ip@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" - integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105" + integrity sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ== is-fullwidth-code-point@^3.0.0: version "3.0.0" From af14ba4ac5f1496fc34e08647e81aa18d9225e13 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:54:28 +0800 Subject: [PATCH 109/686] Feat: add DipoleModel and PolarModel (#3309) This PR is to provide model wrappers for `DipoleFittingNet` and `PolarFittingNet`, such that the saved model can be used directly in inference with `DeepDiole` and `DeepPolar`. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../atomic_model/make_base_atomic_model.py | 36 +++++--- deepmd/dpmodel/fitting/dipole_fitting.py | 19 +++- .../dpmodel/fitting/polarizability_fitting.py | 4 +- deepmd/infer/deep_tensor.py | 18 ++-- deepmd/pt/infer/deep_eval.py | 3 + .../pt/model/atomic_model/dp_atomic_model.py | 2 +- .../model/atomic_model/linear_atomic_model.py | 2 +- .../atomic_model/pairtab_atomic_model.py | 2 +- deepmd/pt/model/model/dipole_model.py | 91 +++++++++++++++++++ deepmd/pt/model/model/dp_zbl_model.py | 14 +-- deepmd/pt/model/model/ener_model.py | 10 +- deepmd/pt/model/model/polar_model.py | 75 +++++++++++++++ deepmd/pt/model/task/dipole.py | 16 +++- deepmd/pt/model/task/polarizability.py | 4 +- source/tests/pt/model/test_dipole_fitting.py | 77 ++++++++++++++++ .../pt/model/test_polarizability_fitting.py | 48 ++++++++++ 16 files changed, 379 insertions(+), 42 deletions(-) create mode 100644 deepmd/pt/model/model/dipole_model.py create mode 100644 deepmd/pt/model/model/polar_model.py diff --git a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py index e262404762..b6c6b8460f 100644 --- a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py @@ -118,33 +118,47 @@ def serialize(self) -> dict: def deserialize(cls): pass - def do_grad( + def do_grad_r( self, var_name: Optional[str] = None, ) -> bool: - """Tell if the output variable `var_name` is differentiable. - if var_name is None, returns if any of the variable is differentiable. + """Tell if the output variable `var_name` is r_differentiable. + if var_name is None, returns if any of the variable is r_differentiable. """ odef = self.fitting_output_def() if var_name is None: require: List[bool] = [] for vv in odef.keys(): - require.append(self.do_grad_(vv)) + require.append(self.do_grad_(vv, "r")) return any(require) else: - return self.do_grad_(var_name) + return self.do_grad_(var_name, "r") - def do_grad_( + def do_grad_c( self, - var_name: str, + var_name: Optional[str] = None, ) -> bool: + """Tell if the output variable `var_name` is c_differentiable. + if var_name is None, returns if any of the variable is c_differentiable. + + """ + odef = self.fitting_output_def() + if var_name is None: + require: List[bool] = [] + for vv in odef.keys(): + require.append(self.do_grad_(vv, "c")) + return any(require) + else: + return self.do_grad_(var_name, "c") + + def do_grad_(self, var_name: str, base: str) -> bool: """Tell if the output variable `var_name` is differentiable.""" assert var_name is not None - return ( - self.fitting_output_def()[var_name].r_differentiable - or self.fitting_output_def()[var_name].c_differentiable - ) + assert base in ["c", "r"] + if base == "c": + return self.fitting_output_def()[var_name].c_differentiable + return self.fitting_output_def()[var_name].r_differentiable setattr(BAM, fwd_method_name, BAM.fwd) delattr(BAM, "fwd") diff --git a/deepmd/dpmodel/fitting/dipole_fitting.py b/deepmd/dpmodel/fitting/dipole_fitting.py index c210945e76..f5acabf7b1 100644 --- a/deepmd/dpmodel/fitting/dipole_fitting.py +++ b/deepmd/dpmodel/fitting/dipole_fitting.py @@ -68,9 +68,14 @@ class DipoleFitting(GeneralFitting): mixed_types If true, use a uniform fitting net for all atom types, otherwise use different fitting nets for different atom types. - exclude_types: List[int] + exclude_types Atomic contributions of the excluded atom types are set zero. - + r_differentiable + If the variable is differentiated with respect to coordinates of atoms. + Only reduciable variable are differentiable. + c_differentiable + If the variable is differentiated with respect to the cell tensor (pbc case). + Only reduciable variable are differentiable. """ def __init__( @@ -94,6 +99,8 @@ def __init__( spin: Any = None, mixed_types: bool = False, exclude_types: List[int] = [], + r_differentiable: bool = True, + c_differentiable: bool = True, old_impl=False, ): # seed, uniform_seed are not included @@ -109,6 +116,8 @@ def __init__( raise NotImplementedError("atom_ener is not implemented") self.embedding_width = embedding_width + self.r_differentiable = r_differentiable + self.c_differentiable = c_differentiable super().__init__( var_name=var_name, ntypes=ntypes, @@ -139,6 +148,8 @@ def serialize(self) -> dict: data = super().serialize() data["embedding_width"] = self.embedding_width data["old_impl"] = self.old_impl + data["r_differentiable"] = self.r_differentiable + data["c_differentiable"] = self.c_differentiable return data def output_def(self): @@ -148,8 +159,8 @@ def output_def(self): self.var_name, [3], reduciable=True, - r_differentiable=True, - c_differentiable=True, + r_differentiable=self.r_differentiable, + c_differentiable=self.c_differentiable, ), ] ) diff --git a/deepmd/dpmodel/fitting/polarizability_fitting.py b/deepmd/dpmodel/fitting/polarizability_fitting.py index d828693fe0..c3cbe7bd1a 100644 --- a/deepmd/dpmodel/fitting/polarizability_fitting.py +++ b/deepmd/dpmodel/fitting/polarizability_fitting.py @@ -176,8 +176,8 @@ def output_def(self): self.var_name, [3, 3], reduciable=True, - r_differentiable=True, - c_differentiable=True, + r_differentiable=False, + c_differentiable=False, ), ] ) diff --git a/deepmd/infer/deep_tensor.py b/deepmd/infer/deep_tensor.py index a6cefa63c1..1bdc459920 100644 --- a/deepmd/infer/deep_tensor.py +++ b/deepmd/infer/deep_tensor.py @@ -105,6 +105,8 @@ def eval( **kwargs, ) sel_natoms = self._get_sel_natoms(atom_types[0]) + if sel_natoms == 0: + sel_natoms = atom_types.shape[-1] # set to natoms if atomic: return results[self.output_tensor_name].reshape(nframes, sel_natoms, -1) else: @@ -184,7 +186,10 @@ def eval_full( aparam=aparam, **kwargs, ) + sel_natoms = self._get_sel_natoms(atom_types[0]) + if sel_natoms == 0: + sel_natoms = atom_types.shape[-1] # set to natoms energy = results[f"{self.output_tensor_name}_redu"].reshape(nframes, -1) force = results[f"{self.output_tensor_name}_derv_r"].reshape( nframes, -1, natoms, 3 @@ -192,14 +197,13 @@ def eval_full( virial = results[f"{self.output_tensor_name}_derv_c_redu"].reshape( nframes, -1, 9 ) - atomic_energy = results[self.output_tensor_name].reshape( - nframes, sel_natoms, -1 - ) - atomic_virial = results[f"{self.output_tensor_name}_derv_c"].reshape( - nframes, -1, natoms, 9 - ) - if atomic: + atomic_energy = results[self.output_tensor_name].reshape( + nframes, sel_natoms, -1 + ) + atomic_virial = results[f"{self.output_tensor_name}_derv_c"].reshape( + nframes, -1, natoms, 9 + ) return ( energy, force, diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index 601bd6f755..f642d34d61 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -373,6 +373,9 @@ def _eval_model( shape = self._get_output_shape(odef, nframes, natoms) out = batch_output[pt_name].reshape(shape).detach().cpu().numpy() results.append(out) + else: + shape = self._get_output_shape(odef, nframes, natoms) + results.append(np.full(np.abs(shape), np.nan)) # this is kinda hacky return tuple(results) def _get_output_shape(self, odef, nframes, natoms): diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index e6dc395500..ecac50737b 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -146,7 +146,7 @@ def forward_atomic( """ nframes, nloc, nnei = nlist.shape atype = extended_atype[:, :nloc] - if self.do_grad(): + if self.do_grad_r() or self.do_grad_c(): extended_coord.requires_grad_(True) descriptor, rot_mat, g2, h2, sw = self.descriptor( extended_coord, diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 29ca9c8f96..16b06b2211 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -145,7 +145,7 @@ def forward_atomic( the result dict, defined by the fitting net output def. """ nframes, nloc, nnei = nlist.shape - if self.do_grad(): + if self.do_grad_r() or self.do_grad_c(): extended_coord.requires_grad_(True) extended_coord = extended_coord.view(nframes, -1, 3) sorted_rcuts, sorted_sels = self._sort_rcuts_sels() diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index c79e742d63..d8b830d1eb 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -148,7 +148,7 @@ def forward_atomic( ) -> Dict[str, torch.Tensor]: nframes, nloc, nnei = nlist.shape extended_coord = extended_coord.view(nframes, -1, 3) - if self.do_grad(): + if self.do_grad_r() or self.do_grad_c(): extended_coord.requires_grad_(True) # this will mask all -1 in the nlist diff --git a/deepmd/pt/model/model/dipole_model.py b/deepmd/pt/model/model/dipole_model.py new file mode 100644 index 0000000000..220fdbb273 --- /dev/null +++ b/deepmd/pt/model/model/dipole_model.py @@ -0,0 +1,91 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + Optional, +) + +import torch + +from .dp_model import ( + DPModel, +) + + +class DipoleModel(DPModel): + model_type = "dipole" + + def __init__( + self, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) + + def forward( + self, + coord, + atype, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + model_ret = self.forward_common( + coord, + atype, + box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + if self.fitting_net is not None: + model_predict = {} + model_predict["dipole"] = model_ret["dipole"] + model_predict["global_dipole"] = model_ret["dipole_redu"] + if self.do_grad_r("dipole"): + model_predict["force"] = model_ret["dipole_derv_r"].squeeze(-2) + if self.do_grad_c("dipole"): + model_predict["virial"] = model_ret["dipole_derv_c_redu"].squeeze(-2) + if do_atomic_virial: + model_predict["atom_virial"] = model_ret["dipole_derv_c"].squeeze( + -3 + ) + else: + model_predict = model_ret + model_predict["updated_coord"] += coord + return model_predict + + def forward_lower( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ): + model_ret = self.forward_common_lower( + extended_coord, + extended_atype, + nlist, + mapping, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + if self.fitting_net is not None: + model_predict = {} + model_predict["dipole"] = model_ret["dipole"] + model_predict["global_dipole"] = model_ret["dipole_redu"] + if self.do_grad_r("dipole"): + model_predict["force"] = model_ret["dipole_derv_r"].squeeze(-2) + if self.do_grad_c("dipole"): + model_predict["virial"] = model_ret["dipole_derv_c_redu"].squeeze(-2) + if do_atomic_virial: + model_predict["atom_virial"] = model_ret["dipole_derv_c"].squeeze( + -3 + ) + else: + model_predict = model_ret + return model_predict diff --git a/deepmd/pt/model/model/dp_zbl_model.py b/deepmd/pt/model/model/dp_zbl_model.py index 8d71157b60..4683f62466 100644 --- a/deepmd/pt/model/model/dp_zbl_model.py +++ b/deepmd/pt/model/model/dp_zbl_model.py @@ -48,11 +48,12 @@ def forward( model_predict = {} model_predict["atom_energy"] = model_ret["energy"] model_predict["energy"] = model_ret["energy_redu"] - if self.do_grad("energy"): + if self.do_grad_r("energy"): model_predict["force"] = model_ret["energy_derv_r"].squeeze(-2) + if self.do_grad_c("energy"): + model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) if do_atomic_virial: model_predict["atom_virial"] = model_ret["energy_derv_c"].squeeze(-3) - model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) else: model_predict["force"] = model_ret["dforce"] return model_predict @@ -80,13 +81,12 @@ def forward_lower( model_predict = {} model_predict["atom_energy"] = model_ret["energy"] model_predict["energy"] = model_ret["energy_redu"] - if self.do_grad("energy"): - model_predict["extended_force"] = model_ret["energy_derv_r"].squeeze(-2) + if self.do_grad_r("energy"): + model_predict["force"] = model_ret["energy_derv_r"].squeeze(-2) + if self.do_grad_c("energy"): model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) if do_atomic_virial: - model_predict["extended_virial"] = model_ret["energy_derv_c"].squeeze( - -2 - ) + model_predict["atom_virial"] = model_ret["energy_derv_c"].squeeze(-3) else: assert model_ret["dforce"] is not None model_predict["dforce"] = model_ret["dforce"] diff --git a/deepmd/pt/model/model/ener_model.py b/deepmd/pt/model/model/ener_model.py index 2afeb2762b..946cfd20f8 100644 --- a/deepmd/pt/model/model/ener_model.py +++ b/deepmd/pt/model/model/ener_model.py @@ -42,13 +42,14 @@ def forward( model_predict = {} model_predict["atom_energy"] = model_ret["energy"] model_predict["energy"] = model_ret["energy_redu"] - if self.do_grad("energy"): + if self.do_grad_r("energy"): model_predict["force"] = model_ret["energy_derv_r"].squeeze(-2) + if self.do_grad_c("energy"): + model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) if do_atomic_virial: model_predict["atom_virial"] = model_ret["energy_derv_c"].squeeze( -3 ) - model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) else: model_predict["force"] = model_ret["dforce"] else: @@ -79,13 +80,14 @@ def forward_lower( model_predict = {} model_predict["atom_energy"] = model_ret["energy"] model_predict["energy"] = model_ret["energy_redu"] - if self.do_grad("energy"): + if self.do_grad_r("energy"): model_predict["extended_force"] = model_ret["energy_derv_r"].squeeze(-2) + if self.do_grad_c("energy"): model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) if do_atomic_virial: model_predict["extended_virial"] = model_ret[ "energy_derv_c" - ].squeeze(-2) + ].squeeze(-3) else: assert model_ret["dforce"] is not None model_predict["dforce"] = model_ret["dforce"] diff --git a/deepmd/pt/model/model/polar_model.py b/deepmd/pt/model/model/polar_model.py new file mode 100644 index 0000000000..85aeebc0f5 --- /dev/null +++ b/deepmd/pt/model/model/polar_model.py @@ -0,0 +1,75 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + Optional, +) + +import torch + +from .dp_model import ( + DPModel, +) + + +class PolarModel(DPModel): + model_type = "polar" + + def __init__( + self, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) + + def forward( + self, + coord, + atype, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + model_ret = self.forward_common( + coord, + atype, + box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + if self.fitting_net is not None: + model_predict = {} + model_predict["polar"] = model_ret["polar"] + model_predict["global_polar"] = model_ret["polar_redu"] + else: + model_predict = model_ret + model_predict["updated_coord"] += coord + return model_predict + + def forward_lower( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ): + model_ret = self.forward_common_lower( + extended_coord, + extended_atype, + nlist, + mapping, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + if self.fitting_net is not None: + model_predict = {} + model_predict["polar"] = model_ret["polar"] + model_predict["global_polar"] = model_ret["polar_redu"] + else: + model_predict = model_ret + return model_predict diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py index 4ea66e2636..88391b1922 100644 --- a/deepmd/pt/model/task/dipole.py +++ b/deepmd/pt/model/task/dipole.py @@ -56,6 +56,12 @@ class DipoleFittingNet(GeneralFitting): The condition number for the regression of atomic energy. seed : int, optional Random seed. + r_differentiable + If the variable is differentiated with respect to coordinates of atoms. + Only reduciable variable are differentiable. + c_differentiable + If the variable is differentiated with respect to the cell tensor (pbc case). + Only reduciable variable are differentiable. """ def __init__( @@ -74,9 +80,13 @@ def __init__( rcond: Optional[float] = None, seed: Optional[int] = None, exclude_types: List[int] = [], + r_differentiable: bool = True, + c_differentiable: bool = True, **kwargs, ): self.embedding_width = embedding_width + self.r_differentiable = r_differentiable + self.c_differentiable = c_differentiable super().__init__( var_name=var_name, ntypes=ntypes, @@ -103,6 +113,8 @@ def serialize(self) -> dict: data = super().serialize() data["embedding_width"] = self.embedding_width data["old_impl"] = self.old_impl + data["r_differentiable"] = self.r_differentiable + data["c_differentiable"] = self.c_differentiable return data def output_def(self) -> FittingOutputDef: @@ -112,8 +124,8 @@ def output_def(self) -> FittingOutputDef: self.var_name, [3], reduciable=True, - r_differentiable=True, - c_differentiable=True, + r_differentiable=self.r_differentiable, + c_differentiable=self.c_differentiable, ), ] ) diff --git a/deepmd/pt/model/task/polarizability.py b/deepmd/pt/model/task/polarizability.py index dc8d13ee84..c240567903 100644 --- a/deepmd/pt/model/task/polarizability.py +++ b/deepmd/pt/model/task/polarizability.py @@ -144,8 +144,8 @@ def output_def(self) -> FittingOutputDef: self.var_name, [3, 3], reduciable=True, - r_differentiable=True, - c_differentiable=True, + r_differentiable=False, + c_differentiable=False, ), ] ) diff --git a/source/tests/pt/model/test_dipole_fitting.py b/source/tests/pt/model/test_dipole_fitting.py index 5ecfb72481..fb04e49484 100644 --- a/source/tests/pt/model/test_dipole_fitting.py +++ b/source/tests/pt/model/test_dipole_fitting.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import itertools +import os import unittest import numpy as np @@ -9,9 +10,15 @@ ) from deepmd.dpmodel.fitting import DipoleFitting as DPDipoleFitting +from deepmd.infer.deep_dipole import ( + DeepDipole, +) from deepmd.pt.model.descriptor.se_a import ( DescrptSeA, ) +from deepmd.pt.model.model.dipole_model import ( + DipoleModel, +) from deepmd.pt.model.task.dipole import ( DipoleFittingNet, ) @@ -32,6 +39,20 @@ dtype = env.GLOBAL_PT_FLOAT_PRECISION +def finite_difference(f, x, a, delta=1e-6): + in_shape = x.shape + y0 = f(x, a) + out_shape = y0.shape + res = np.empty(out_shape + in_shape) + for idx in np.ndindex(*in_shape): + diff = np.zeros(in_shape) + diff[idx] += delta + y1p = f(x + diff, a) + y1n = f(x - diff, a) + res[(Ellipsis, *idx)] = (y1p - y1n) / (2 * delta) + return res + + class TestDipoleFitting(unittest.TestCase, TestCaseSingleFrameWithNlist): def setUp(self): TestCaseSingleFrameWithNlist.setUp(self) @@ -269,5 +290,61 @@ def test_trans(self): np.testing.assert_allclose(to_numpy_array(res[0]), to_numpy_array(res[1])) +class TestDipoleModel(unittest.TestCase): + def setUp(self): + self.natoms = 5 + self.rcut = 4.0 + self.nt = 3 + self.rcut_smth = 0.5 + self.sel = [46, 92, 4] + self.nf = 1 + self.coord = 2 * torch.rand([self.natoms, 3], dtype=dtype, device="cpu") + cell = torch.rand([3, 3], dtype=dtype, device="cpu") + self.cell = (cell + cell.T) + 5.0 * torch.eye(3, device="cpu") + self.atype = torch.IntTensor([0, 0, 0, 1, 1], device="cpu") + self.dd0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) + self.ft0 = DipoleFittingNet( + "dipole", + self.nt, + self.dd0.dim_out, + embedding_width=self.dd0.get_dim_emb(), + numb_fparam=0, + numb_aparam=0, + mixed_types=True, + ).to(env.DEVICE) + self.type_mapping = ["O", "H", "B"] + self.model = DipoleModel(self.dd0, self.ft0, self.type_mapping) + self.file_path = "model_output.pth" + + def test_auto_diff(self): + places = 5 + delta = 1e-5 + atype = self.atype.view(self.nf, self.natoms) + + def ff(coord, atype): + return self.model(coord, atype)["global_dipole"].detach().cpu().numpy() + + fdf = -finite_difference(ff, self.coord, atype, delta=delta) + rff = self.model(self.coord, atype)["force"].detach().cpu().numpy() + + np.testing.assert_almost_equal(fdf, rff.transpose(0, 2, 1, 3), decimal=places) + + def test_deepdipole_infer(self): + atype = self.atype.view(self.nf, self.natoms) + coord = self.coord.reshape(1, 5, 3) + cell = self.cell.reshape(1, 9) + jit_md = torch.jit.script(self.model) + torch.jit.save(jit_md, self.file_path) + load_md = DeepDipole(self.file_path) + load_md.eval(coords=coord, atom_types=atype, cells=cell, atomic=True) + load_md.eval(coords=coord, atom_types=atype, cells=cell, atomic=False) + load_md.eval_full(coords=coord, atom_types=atype, cells=cell, atomic=True) + load_md.eval_full(coords=coord, atom_types=atype, cells=cell, atomic=False) + + def tearDown(self) -> None: + if os.path.exists(self.file_path): + os.remove(self.file_path) + + if __name__ == "__main__": unittest.main() diff --git a/source/tests/pt/model/test_polarizability_fitting.py b/source/tests/pt/model/test_polarizability_fitting.py index 548219627b..3f154383b2 100644 --- a/source/tests/pt/model/test_polarizability_fitting.py +++ b/source/tests/pt/model/test_polarizability_fitting.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import itertools +import os import unittest import numpy as np @@ -9,9 +10,15 @@ ) from deepmd.dpmodel.fitting import PolarFitting as DPPolarFitting +from deepmd.infer.deep_polar import ( + DeepPolar, +) from deepmd.pt.model.descriptor.se_a import ( DescrptSeA, ) +from deepmd.pt.model.model.polar_model import ( + PolarModel, +) from deepmd.pt.model.task.polarizability import ( PolarFittingNet, ) @@ -308,5 +315,46 @@ def test_trans(self): np.testing.assert_allclose(to_numpy_array(res[0]), to_numpy_array(res[1])) +class TestDipoleModel(unittest.TestCase): + def setUp(self): + self.natoms = 5 + self.rcut = 4.0 + self.nt = 3 + self.rcut_smth = 0.5 + self.sel = [46, 92, 4] + self.nf = 1 + self.coord = 2 * torch.rand([self.natoms, 3], dtype=dtype, device="cpu") + cell = torch.rand([3, 3], dtype=dtype, device="cpu") + self.cell = (cell + cell.T) + 5.0 * torch.eye(3, device="cpu") + self.atype = torch.IntTensor([0, 0, 0, 1, 1], device="cpu") + self.dd0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) + self.ft0 = PolarFittingNet( + "polar", + self.nt, + self.dd0.dim_out, + embedding_width=self.dd0.get_dim_emb(), + numb_fparam=0, + numb_aparam=0, + mixed_types=True, + ).to(env.DEVICE) + self.type_mapping = ["O", "H", "B"] + self.model = PolarModel(self.dd0, self.ft0, self.type_mapping) + self.file_path = "model_output.pth" + + def test_deepdipole_infer(self): + atype = self.atype.view(self.nf, self.natoms) + coord = self.coord.reshape(1, 5, 3) + cell = self.cell.reshape(1, 9) + jit_md = torch.jit.script(self.model) + torch.jit.save(jit_md, self.file_path) + load_md = DeepPolar(self.file_path) + load_md.eval(coords=coord, atom_types=atype, cells=cell, atomic=True) + load_md.eval(coords=coord, atom_types=atype, cells=cell, atomic=False) + + def tearDown(self) -> None: + if os.path.exists(self.file_path): + os.remove(self.file_path) + + if __name__ == "__main__": unittest.main() From e1c0564edcd7b583197cb79aa4457c5256fee816 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 21 Feb 2024 02:58:07 -0500 Subject: [PATCH 110/686] consistent energy model (#3306) Many hacking things... --------- Signed-off-by: Jinzhe Zeng --- .../dpmodel/atomic_model/dp_atomic_model.py | 1 + deepmd/dpmodel/fitting/__init__.py | 4 + deepmd/dpmodel/model/dp_model.py | 5 +- deepmd/dpmodel/model/make_model.py | 5 +- deepmd/dpmodel/model/model.py | 41 +++++ .../pt/model/atomic_model/dp_atomic_model.py | 1 + deepmd/pt/utils/utils.py | 29 +++- deepmd/tf/descriptor/descriptor.py | 2 +- deepmd/tf/fit/ener.py | 4 +- deepmd/tf/fit/fitting.py | 72 +++++++- deepmd/tf/model/model.py | 129 +++++++++++++- source/tests/consistent/common.py | 14 +- source/tests/consistent/model/__init__.py | 1 + source/tests/consistent/model/common.py | 64 +++++++ source/tests/consistent/model/test_ener.py | 160 ++++++++++++++++++ 15 files changed, 508 insertions(+), 24 deletions(-) create mode 100644 deepmd/dpmodel/model/model.py create mode 100644 source/tests/consistent/model/__init__.py create mode 100644 source/tests/consistent/model/common.py create mode 100644 source/tests/consistent/model/test_ener.py diff --git a/deepmd/dpmodel/atomic_model/dp_atomic_model.py b/deepmd/dpmodel/atomic_model/dp_atomic_model.py index abf66cc3fa..220c072765 100644 --- a/deepmd/dpmodel/atomic_model/dp_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/dp_atomic_model.py @@ -13,6 +13,7 @@ DescrptSeA, ) from deepmd.dpmodel.fitting import ( # noqa # TODO: should import all fittings! + EnergyFittingNet, InvarFitting, ) from deepmd.dpmodel.output_def import ( diff --git a/deepmd/dpmodel/fitting/__init__.py b/deepmd/dpmodel/fitting/__init__.py index 0b4fe001b3..929a63fda7 100644 --- a/deepmd/dpmodel/fitting/__init__.py +++ b/deepmd/dpmodel/fitting/__init__.py @@ -2,6 +2,9 @@ from .dipole_fitting import ( DipoleFitting, ) +from .ener_fitting import ( + EnergyFittingNet, +) from .invar_fitting import ( InvarFitting, ) @@ -16,5 +19,6 @@ "InvarFitting", "make_base_fitting", "DipoleFitting", + "EnergyFittingNet", "PolarFitting", ] diff --git a/deepmd/dpmodel/model/dp_model.py b/deepmd/dpmodel/model/dp_model.py index 6196bcfe87..c2c40b40ba 100644 --- a/deepmd/dpmodel/model/dp_model.py +++ b/deepmd/dpmodel/model/dp_model.py @@ -7,4 +7,7 @@ make_model, ) -DPModel = make_model(DPAtomicModel) + +# use "class" to resolve "Variable not allowed in type expression" +class DPModel(make_model(DPAtomicModel)): + pass diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py index e44c2e9701..eb7e57747d 100644 --- a/deepmd/dpmodel/model/make_model.py +++ b/deepmd/dpmodel/model/make_model.py @@ -6,6 +6,9 @@ import numpy as np +from deepmd.dpmodel.common import ( + NativeOP, +) from deepmd.dpmodel.output_def import ( ModelOutputDef, ) @@ -45,7 +48,7 @@ def make_model(T_AtomicModel): """ - class CM(T_AtomicModel): + class CM(T_AtomicModel, NativeOP): def __init__( self, *args, diff --git a/deepmd/dpmodel/model/model.py b/deepmd/dpmodel/model/model.py new file mode 100644 index 0000000000..4a6e269f25 --- /dev/null +++ b/deepmd/dpmodel/model/model.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from deepmd.dpmodel.descriptor.se_e2_a import ( + DescrptSeA, +) +from deepmd.dpmodel.fitting.ener_fitting import ( + EnergyFittingNet, +) +from deepmd.dpmodel.model.dp_model import ( + DPModel, +) + + +def get_model(data: dict) -> DPModel: + """Get a DPModel from a dictionary. + + Parameters + ---------- + data : dict + The data to construct the model. + """ + descriptor_type = data["descriptor"].pop("type") + fitting_type = data["fitting_net"].pop("type") + if descriptor_type == "se_e2_a": + descriptor = DescrptSeA( + **data["descriptor"], + ) + else: + raise ValueError(f"Unknown descriptor type {descriptor_type}") + if fitting_type == "ener": + fitting = EnergyFittingNet( + ntypes=descriptor.get_ntypes(), + dim_descrpt=descriptor.get_dim_out(), + **data["fitting_net"], + ) + else: + raise ValueError(f"Unknown fitting type {fitting_type}") + return DPModel( + descriptor=descriptor, + fitting=fitting, + type_map=data["type_map"], + ) diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index ecac50737b..2d8836c0e1 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -17,6 +17,7 @@ DescrptSeA, ) from deepmd.pt.model.task.ener import ( # noqa # TODO: should import all fittings! + EnergyFittingNet, InvarFitting, ) from deepmd.pt.utils.utils import ( diff --git a/deepmd/pt/utils/utils.py b/deepmd/pt/utils/utils.py index d6621f7b4c..852c42cd0c 100644 --- a/deepmd/pt/utils/utils.py +++ b/deepmd/pt/utils/utils.py @@ -2,6 +2,7 @@ from typing import ( Callable, Optional, + overload, ) import numpy as np @@ -51,9 +52,19 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: raise RuntimeError(f"activation function {self.activation} not supported") +@overload +def to_numpy_array(xx: torch.Tensor) -> np.ndarray: + ... + + +@overload +def to_numpy_array(xx: None) -> None: + ... + + def to_numpy_array( - xx: torch.Tensor, -) -> np.ndarray: + xx, +): if xx is None: return None assert xx is not None @@ -67,9 +78,19 @@ def to_numpy_array( return xx.detach().cpu().numpy().astype(prec) +@overload +def to_torch_tensor(xx: np.ndarray) -> torch.Tensor: + ... + + +@overload +def to_torch_tensor(xx: None) -> None: + ... + + def to_torch_tensor( - xx: np.ndarray, -) -> torch.Tensor: + xx, +): if xx is None: return None assert xx is not None diff --git a/deepmd/tf/descriptor/descriptor.py b/deepmd/tf/descriptor/descriptor.py index 1a73d3c273..768e233245 100644 --- a/deepmd/tf/descriptor/descriptor.py +++ b/deepmd/tf/descriptor/descriptor.py @@ -530,7 +530,7 @@ def deserialize(cls, data: dict, suffix: str = "") -> "Descriptor": The deserialized descriptor """ if cls is Descriptor: - return Descriptor.get_class_by_input(data).deserialize(data) + return Descriptor.get_class_by_input(data).deserialize(data, suffix=suffix) raise NotImplementedError("Not implemented in class %s" % cls.__name__) def serialize(self, suffix: str = "") -> dict: diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index 074856ea6c..53eeda5f7f 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -929,7 +929,7 @@ def get_loss(self, loss: dict, lr) -> Loss: raise RuntimeError("unknown loss type") @classmethod - def deserialize(cls, data: dict, suffix: str): + def deserialize(cls, data: dict, suffix: str = ""): """Deserialize the model. Parameters @@ -956,7 +956,7 @@ def deserialize(cls, data: dict, suffix: str): fitting.aparam_inv_std = data["@variables"]["aparam_inv_std"] return fitting - def serialize(self, suffix: str) -> dict: + def serialize(self, suffix: str = "") -> dict: """Serialize the model. Returns diff --git a/deepmd/tf/fit/fitting.py b/deepmd/tf/fit/fitting.py index 2307fb957d..458765f7c1 100644 --- a/deepmd/tf/fit/fitting.py +++ b/deepmd/tf/fit/fitting.py @@ -6,6 +6,7 @@ from typing import ( Callable, List, + Type, ) from deepmd.dpmodel.utils.network import ( @@ -50,16 +51,33 @@ class SomeFitting(Fitting): """ return Fitting.__plugins.register(key) + @classmethod + def get_class_by_input(cls, data: dict) -> Type["Fitting"]: + """Get the fitting class by the input data. + + Parameters + ---------- + data : dict + The input data + + Returns + ------- + Fitting + The fitting class + """ + try: + fitting_type = data["type"] + except KeyError: + raise KeyError("the type of fitting should be set by `type`") + if fitting_type in Fitting.__plugins.plugins: + cls = Fitting.__plugins.plugins[fitting_type] + else: + raise RuntimeError("Unknown descriptor type: " + fitting_type) + return cls + def __new__(cls, *args, **kwargs): if cls is Fitting: - try: - fitting_type = kwargs["type"] - except KeyError: - raise KeyError("the type of fitting should be set by `type`") - if fitting_type in Fitting.__plugins.plugins: - cls = Fitting.__plugins.plugins[fitting_type] - else: - raise RuntimeError("Unknown descriptor type: " + fitting_type) + cls = cls.get_class_by_input(kwargs) return super().__new__(cls) @property @@ -110,6 +128,44 @@ def get_loss(self, loss: dict, lr) -> Loss: the loss function """ + @classmethod + def deserialize(cls, data: dict, suffix: str = "") -> "Fitting": + """Deserialize the fitting. + + There is no suffix in a native DP model, but it is important + for the TF backend. + + Parameters + ---------- + data : dict + The serialized data + suffix : str, optional + Name suffix to identify this fitting + + Returns + ------- + Fitting + The deserialized fitting + """ + if cls is Fitting: + return Fitting.get_class_by_input(data).deserialize(data, suffix=suffix) + raise NotImplementedError("Not implemented in class %s" % cls.__name__) + + def serialize(self, suffix: str = "") -> dict: + """Serialize the fitting. + + There is no suffix in a native DP model, but it is important + for the TF backend. + + Returns + ------- + dict + The serialized data + suffix : str, optional + Name suffix to identify this fitting + """ + raise NotImplementedError("Not implemented in class %s" % self.__name__) + def serialize_network( self, ntypes: int, diff --git a/deepmd/tf/model/model.py b/deepmd/tf/model/model.py index ac970e0b53..c2def1677f 100644 --- a/deepmd/tf/model/model.py +++ b/deepmd/tf/model/model.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy from abc import ( ABC, abstractmethod, @@ -20,9 +21,21 @@ GLOBAL_TF_FLOAT_PRECISION, tf, ) +from deepmd.tf.fit.dipole import ( + DipoleFittingSeA, +) +from deepmd.tf.fit.dos import ( + DOSFitting, +) +from deepmd.tf.fit.ener import ( + EnerFitting, +) from deepmd.tf.fit.fitting import ( Fitting, ) +from deepmd.tf.fit.polar import ( + PolarFittingSeA, +) from deepmd.tf.loss.loss import ( Loss, ) @@ -565,6 +578,44 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict) -> dict: cls = cls.get_class_by_input(local_jdata) return cls.update_sel(global_jdata, local_jdata) + @classmethod + def deserialize(cls, data: dict, suffix: str = "") -> "Descriptor": + """Deserialize the model. + + There is no suffix in a native DP model, but it is important + for the TF backend. + + Parameters + ---------- + data : dict + The serialized data + suffix : str, optional + Name suffix to identify this descriptor + + Returns + ------- + Descriptor + The deserialized descriptor + """ + if cls is Descriptor: + return Descriptor.get_class_by_input(data).deserialize(data) + raise NotImplementedError("Not implemented in class %s" % cls.__name__) + + def serialize(self, suffix: str = "") -> dict: + """Serialize the model. + + There is no suffix in a native DP model, but it is important + for the TF backend. + + Returns + ------- + dict + The serialized data + suffix : str, optional + Name suffix to identify this descriptor + """ + raise NotImplementedError("Not implemented in class %s" % self.__name__) + class StandardModel(Model): """Standard model, which must contain a descriptor and a fitting. @@ -594,16 +645,22 @@ def __new__(cls, *args, **kwargs): ) if cls is StandardModel: - fitting_type = kwargs["fitting_net"]["type"] + if isinstance(kwargs["fitting_net"], dict): + fitting_type = Fitting.get_class_by_input(kwargs["fitting_net"]) + elif isinstance(kwargs["fitting_net"], Fitting): + fitting_type = type(kwargs["fitting_net"]) + else: + raise RuntimeError("get unknown fitting type when building model") + print(fitting_type) # init model # infer model type by fitting_type - if fitting_type == "ener": + if issubclass(fitting_type, EnerFitting): cls = EnerModel - elif fitting_type == "dos": + elif issubclass(fitting_type, DOSFitting): cls = DOSModel - elif fitting_type == "dipole": + elif issubclass(fitting_type, DipoleFittingSeA): cls = DipoleModel - elif fitting_type == "polar": + elif issubclass(fitting_type, PolarFittingSeA): cls = PolarModel else: raise RuntimeError("get unknown fitting type when building model") @@ -730,3 +787,65 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): global_jdata, local_jdata["descriptor"] ) return local_jdata_cpy + + @classmethod + def deserialize(cls, data: dict, suffix: str = "") -> "Descriptor": + """Deserialize the model. + + There is no suffix in a native DP model, but it is important + for the TF backend. + + Parameters + ---------- + data : dict + The serialized data + suffix : str, optional + Name suffix to identify this descriptor + + Returns + ------- + Descriptor + The deserialized descriptor + """ + data = copy.deepcopy(data) + + data["descriptor"]["type"] = { + "DescrptSeA": "se_e2_a", + }[data.pop("descriptor_name")] + data["fitting"]["type"] = { + "EnergyFittingNet": "ener", + }[data.pop("fitting_name")] + descriptor = Descriptor.deserialize(data.pop("descriptor"), suffix=suffix) + fitting = Fitting.deserialize(data.pop("fitting"), suffix=suffix) + return cls( + descriptor=descriptor, + fitting_net=fitting, + **data, + ) + + def serialize(self, suffix: str = "") -> dict: + """Serialize the model. + + There is no suffix in a native DP model, but it is important + for the TF backend. + + Returns + ------- + dict + The serialized data + suffix : str, optional + Name suffix to identify this descriptor + """ + if self.typeebd is not None: + raise NotImplementedError("type embedding is not supported") + if self.spin is not None: + raise NotImplementedError("spin is not supported") + return { + "type_map": self.type_map, + "descriptor": self.descrpt.serialize(suffix=suffix), + "fitting": self.fitting.serialize(suffix=suffix), + "descriptor_name": self.descrpt.__class__.__name__, + "fitting_name": {"EnerFitting": "EnergyFittingNet"}[ + self.fitting.__class__.__name__ + ], + } diff --git a/source/tests/consistent/common.py b/source/tests/consistent/common.py index 7cec3da006..66ca2477fa 100644 --- a/source/tests/consistent/common.py +++ b/source/tests/consistent/common.py @@ -16,6 +16,7 @@ List, Optional, Tuple, + Union, ) from uuid import ( uuid4, @@ -68,7 +69,7 @@ class CommonTest(ABC): """Native DP model class.""" pt_class: ClassVar[Optional[type]] """PyTorch model class.""" - args: ClassVar[Optional[List[Argument]]] + args: ClassVar[Optional[Union[Argument, List[Argument]]]] """Arguments that maps to the `data`.""" skip_dp: ClassVar[bool] = False """Whether to skip the native DP model.""" @@ -93,9 +94,18 @@ def init_backend_cls(self, cls) -> Any: if self.args is None: data = self.data else: - base = Argument("arg", dict, sub_fields=self.args) + if isinstance(self.args, list): + base = Argument("arg", dict, sub_fields=self.args) + elif isinstance(self.args, Argument): + base = self.args + else: + raise ValueError("Invalid type for args") data = base.normalize_value(self.data, trim_pattern="_*") base.check_value(data, strict=True) + return self.pass_data_to_cls(cls, data) + + def pass_data_to_cls(self, cls, data) -> Any: + """Pass data to the class.""" return cls(**data, **self.addtional_data) @abstractmethod diff --git a/source/tests/consistent/model/__init__.py b/source/tests/consistent/model/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/source/tests/consistent/model/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/source/tests/consistent/model/common.py b/source/tests/consistent/model/common.py new file mode 100644 index 0000000000..294edec1d6 --- /dev/null +++ b/source/tests/consistent/model/common.py @@ -0,0 +1,64 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Any, +) + +from deepmd.common import ( + make_default_mesh, +) + +from ..common import ( + INSTALLED_PT, + INSTALLED_TF, +) + +if INSTALLED_PT: + from deepmd.pt.utils.utils import to_numpy_array as torch_to_numpy + from deepmd.pt.utils.utils import to_torch_tensor as numpy_to_torch +if INSTALLED_TF: + from deepmd.tf.env import ( + GLOBAL_TF_FLOAT_PRECISION, + tf, + ) + + +class ModelTest: + """Useful utilities for model tests.""" + + def build_tf_model(self, obj, natoms, coords, atype, box, suffix): + t_coord = tf.placeholder( + GLOBAL_TF_FLOAT_PRECISION, [None, None, None], name="i_coord" + ) + t_type = tf.placeholder(tf.int32, [None, None], name="i_type") + t_natoms = tf.placeholder(tf.int32, natoms.shape, name="i_natoms") + t_box = tf.placeholder(GLOBAL_TF_FLOAT_PRECISION, [None, 9], name="i_box") + t_mesh = tf.placeholder(tf.int32, [None], name="i_mesh") + ret = obj.build( + t_coord, + t_type, + t_natoms, + t_box, + t_mesh, + {}, + suffix=suffix, + ) + return [ret["energy"], ret["atom_ener"]], { + t_coord: coords, + t_type: atype, + t_natoms: natoms, + t_box: box, + t_mesh: make_default_mesh(True, False), + } + + def eval_dp_model(self, dp_obj: Any, natoms, coords, atype, box) -> Any: + return dp_obj(coords, atype, box=box) + + def eval_pt_model(self, pt_obj: Any, natoms, coords, atype, box) -> Any: + return { + kk: torch_to_numpy(vv) + for kk, vv in pt_obj( + numpy_to_torch(coords), + numpy_to_torch(atype), + box=numpy_to_torch(box), + ).items() + } diff --git a/source/tests/consistent/model/test_ener.py b/source/tests/consistent/model/test_ener.py new file mode 100644 index 0000000000..b3aa778ca0 --- /dev/null +++ b/source/tests/consistent/model/test_ener.py @@ -0,0 +1,160 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest +from typing import ( + Any, + Tuple, +) + +import numpy as np + +from deepmd.dpmodel.model.dp_model import DPModel as EnergyModelDP +from deepmd.dpmodel.model.model import get_model as get_model_dp +from deepmd.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) + +from ..common import ( + INSTALLED_PT, + INSTALLED_TF, + CommonTest, +) +from .common import ( + ModelTest, +) + +if INSTALLED_PT: + from deepmd.pt.model.model import get_model as get_model_pt + from deepmd.pt.model.model.ener_model import EnergyModel as EnergyModelPT + +else: + EnergyModelPT = None +if INSTALLED_TF: + from deepmd.tf.model.ener import EnerModel as EnergyModelTF +else: + EnergyModelTF = None +from deepmd.utils.argcheck import ( + model_args, +) + + +class TestEner(CommonTest, ModelTest, unittest.TestCase): + @property + def data(self) -> dict: + return { + "type_map": ["O", "H"], + "descriptor": { + "type": "se_e2_a", + "sel": [20, 20], + "rcut_smth": 0.50, + "rcut": 6.00, + "neuron": [ + 3, + 6, + ], + "resnet_dt": False, + "axis_neuron": 2, + "precision": "float64", + "type_one_side": True, + "seed": 1, + }, + "fitting_net": { + "neuron": [ + 5, + 5, + ], + "resnet_dt": True, + "precision": "float64", + "seed": 1, + }, + } + + tf_class = EnergyModelTF + dp_class = EnergyModelDP + pt_class = EnergyModelPT + args = model_args() + + def pass_data_to_cls(self, cls, data) -> Any: + """Pass data to the class.""" + data = data.copy() + if cls is EnergyModelDP: + return get_model_dp(data) + elif cls is EnergyModelPT: + return get_model_pt(data) + return cls(**data, **self.addtional_data) + + def setUp(self): + CommonTest.setUp(self) + + self.ntypes = 2 + self.coords = np.array( + [ + 12.83, + 2.56, + 2.18, + 12.09, + 2.87, + 2.74, + 00.25, + 3.32, + 1.68, + 3.36, + 3.00, + 1.81, + 3.51, + 2.51, + 2.60, + 4.27, + 3.22, + 1.56, + ], + dtype=GLOBAL_NP_FLOAT_PRECISION, + ).reshape(1, -1, 3) + self.atype = np.array([0, 1, 1, 0, 1, 1], dtype=np.int32).reshape(1, -1) + self.box = np.array( + [13.0, 0.0, 0.0, 0.0, 13.0, 0.0, 0.0, 0.0, 13.0], + dtype=GLOBAL_NP_FLOAT_PRECISION, + ).reshape(1, 9) + self.natoms = np.array([6, 6, 2, 4], dtype=np.int32) + + # TF requires the atype to be sort + idx_map = np.argsort(self.atype.ravel()) + self.atype = self.atype[:, idx_map] + self.coords = self.coords[:, idx_map] + + def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: + return self.build_tf_model( + obj, + self.natoms, + self.coords, + self.atype, + self.box, + suffix, + ) + + def eval_dp(self, dp_obj: Any) -> Any: + return self.eval_dp_model( + dp_obj, + self.natoms, + self.coords, + self.atype, + self.box, + ) + + def eval_pt(self, pt_obj: Any) -> Any: + return self.eval_pt_model( + pt_obj, + self.natoms, + self.coords, + self.atype, + self.box, + ) + + def extract_ret(self, ret: Any, backend) -> Tuple[np.ndarray, ...]: + # shape not matched. ravel... + if backend is self.RefBackend.DP: + return (ret["energy_redu"].ravel(), ret["energy"].ravel()) + elif backend is self.RefBackend.PT: + return (ret["energy"].ravel(), ret["atom_energy"].ravel()) + elif backend is self.RefBackend.TF: + return (ret[0].ravel(), ret[1].ravel()) + raise ValueError(f"Unknown backend: {backend}") From d629616305f8754e8a4115dc244a7057bd2d6b1e Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 21 Feb 2024 02:58:51 -0500 Subject: [PATCH 111/686] tf: add fparam/aparam support for finetune (#3313) Fix #3256. Signed-off-by: Jinzhe Zeng --- deepmd/tf/fit/ener.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index 53eeda5f7f..6be63f907c 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -856,7 +856,22 @@ def change_energy_bias( box = test_data["box"][:numb_test] else: box = None - ret = dp.eval(coord, box, atype, mixed_type=mixed_type) + if dp.get_dim_fparam() > 0: + fparam = test_data["fparam"][:numb_test] + else: + fparam = None + if dp.get_dim_aparam() > 0: + aparam = test_data["aparam"][:numb_test] + else: + aparam = None + ret = dp.eval( + coord, + box, + atype, + mixed_type=mixed_type, + fparam=fparam, + aparam=aparam, + ) energy_predict.append(ret[0].reshape([numb_test, 1])) type_numbs = np.concatenate(type_numbs) energy_ground_truth = np.concatenate(energy_ground_truth) From e5653911176fe658ad3a75914f922e1a558f5e87 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Thu, 22 Feb 2024 09:04:35 +0800 Subject: [PATCH 112/686] Fix: float32 bug in UTs (#3314) --- deepmd/pt/model/network/mlp.py | 3 +++ source/tests/consistent/fitting/test_ener.py | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/deepmd/pt/model/network/mlp.py b/deepmd/pt/model/network/mlp.py index 251150f945..4af1d00df8 100644 --- a/deepmd/pt/model/network/mlp.py +++ b/deepmd/pt/model/network/mlp.py @@ -114,6 +114,8 @@ def forward( yy: torch.Tensor The output. """ + ori_prec = xx.dtype + xx = xx.to(self.prec) yy = ( torch.matmul(xx, self.matrix) + self.bias if self.bias is not None @@ -128,6 +130,7 @@ def forward( yy += torch.concat([xx, xx], dim=-1) else: yy = yy + yy = yy.to(ori_prec) return yy def serialize(self) -> dict: diff --git a/source/tests/consistent/fitting/test_ener.py b/source/tests/consistent/fitting/test_ener.py index 3b3255d683..994d967bc8 100644 --- a/source/tests/consistent/fitting/test_ener.py +++ b/source/tests/consistent/fitting/test_ener.py @@ -80,8 +80,7 @@ def skip_pt(self) -> bool: mixed_types, numb_fparam, ) = self.param - # TODO: float32 has bug - return precision == "float32" or CommonTest.skip_pt + return CommonTest.skip_pt tf_class = EnerFittingTF dp_class = EnerFittingDP From cf21b7abb5b652dfce8682ad88e978fae11d9017 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 21 Feb 2024 20:07:51 -0500 Subject: [PATCH 113/686] merge common subcommands in cli (#3316) Signed-off-by: Jinzhe Zeng --- deepmd/backend/suffix.py | 76 +++++++++++++++++++++++++++++++++ deepmd/entrypoints/main.py | 80 +++++++++++++++++++++++++++++++++++ deepmd/main.py | 24 ++++++++++- deepmd/pt/entrypoints/main.py | 38 ----------------- deepmd/tf/entrypoints/main.py | 29 +++---------- deepmd/tf/model/model.py | 1 - 6 files changed, 185 insertions(+), 63 deletions(-) create mode 100644 deepmd/backend/suffix.py create mode 100644 deepmd/entrypoints/main.py diff --git a/deepmd/backend/suffix.py b/deepmd/backend/suffix.py new file mode 100644 index 0000000000..273fbc0951 --- /dev/null +++ b/deepmd/backend/suffix.py @@ -0,0 +1,76 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import functools +import operator +from pathlib import ( + Path, +) +from typing import ( + Optional, + Type, + Union, +) + +from deepmd.backend.backend import ( + Backend, +) + + +def format_model_suffix( + filename: str, + feature: Optional[Backend.Feature] = None, + preferred_backend: Optional[Union[str, Type["Backend"]]] = None, + strict_prefer: Optional[bool] = None, +) -> str: + """Check and format the suffixes of a filename. + + When preferred_backend is not given, this method checks the suffix of the filename + is within the suffixes of the any backends (with the given feature) and doesn't do formating. + When preferred_backend is given, strict_prefer must be given. + If strict_prefer is True and the suffix is not within the suffixes of the preferred backend, + or strict_prefer is False and the suffix is not within the suffixes of the any backend with the given feature, + the filename will be formatted with the preferred suffix of the preferred backend. + + Parameters + ---------- + filename : str + The filename to be formatted. + feature : Backend.Feature, optional + The feature of the backend, by default None + preferred_backend : str or type of Backend, optional + The preferred backend, by default None + strict_prefer : bool, optional + Whether to strictly prefer the preferred backend, by default None + + Returns + ------- + str + The formatted filename with the correct suffix. + + Raises + ------ + ValueError + When preferred_backend is not given and the filename is not supported by any backend. + """ + if preferred_backend is not None and strict_prefer is None: + raise ValueError("strict_prefer must be given when preferred_backend is given.") + if isinstance(preferred_backend, str): + preferred_backend = Backend.get_backend(preferred_backend) + if preferred_backend is not None and strict_prefer: + all_backends = [preferred_backend] + elif feature is None: + all_backends = list(Backend.get_backends().values()) + else: + all_backends = list(Backend.get_backends_by_feature(feature).values()) + + all_suffixes = set( + functools.reduce( + operator.iconcat, [backend.suffixes for backend in all_backends], [] + ) + ) + pp = Path(filename) + current_suffix = pp.suffix + if current_suffix not in all_suffixes: + if preferred_backend is not None: + return str(pp) + preferred_backend.suffixes[0] + raise ValueError(f"Unsupported model file format: {filename}") + return filename diff --git a/deepmd/entrypoints/main.py b/deepmd/entrypoints/main.py new file mode 100644 index 0000000000..9a03ac5e45 --- /dev/null +++ b/deepmd/entrypoints/main.py @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Common entrypoints.""" + +import argparse +from pathlib import ( + Path, +) + +from deepmd.backend.backend import ( + Backend, +) +from deepmd.backend.suffix import ( + format_model_suffix, +) +from deepmd.entrypoints.doc import ( + doc_train_input, +) +from deepmd.entrypoints.gui import ( + start_dpgui, +) +from deepmd.entrypoints.neighbor_stat import ( + neighbor_stat, +) +from deepmd.entrypoints.test import ( + test, +) +from deepmd.infer.model_devi import ( + make_model_devi, +) +from deepmd.loggers.loggers import ( + set_log_handles, +) + + +def main(args: argparse.Namespace): + """DeePMD-Kit entry point. + + Parameters + ---------- + args : List[str] or argparse.Namespace, optional + list of command line arguments, used to avoid calling from the subprocess, + as it is quite slow to import tensorflow; if Namespace is given, it will + be used directly + + Raises + ------ + RuntimeError + if no command was input + """ + set_log_handles(args.log_level, Path(args.log_path) if args.log_path else None) + + dict_args = vars(args) + + if args.command == "test": + dict_args["model"] = format_model_suffix( + dict_args["model"], + feature=Backend.Feature.DEEP_EVAL, + preferred_backend=args.backend, + strict_prefer=False, + ) + test(**dict_args) + elif args.command == "doc-train-input": + doc_train_input(**dict_args) + elif args.command == "model-devi": + dict_args["models"] = [ + format_model_suffix( + mm, + feature=Backend.Feature.DEEP_EVAL, + preferred_backend=args.backend, + strict_prefer=False, + ) + for mm in dict_args["models"] + ] + make_model_devi(**dict_args) + elif args.command == "neighbor-stat": + neighbor_stat(**dict_args) + elif args.command == "gui": + start_dpgui(**dict_args) + else: + raise ValueError(f"Unknown command: {args.command}") diff --git a/deepmd/main.py b/deepmd/main.py index 2bde6376f2..98f5ab0c6b 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -760,6 +760,28 @@ def main(): if args.backend not in BACKEND_TABLE: raise ValueError(f"Unknown backend {args.backend}") - deepmd_main = BACKENDS[args.backend]().entry_point_hook + + if args.command in ( + "test", + "doc-train-input", + "model-devi", + "neighbor-stat", + "gui", + ): + # common entrypoints + from deepmd.entrypoints.main import main as deepmd_main + elif args.command in ( + "train", + "freeze", + "transfer", + "compress", + "convert-from", + "train-nvnmd", + ): + deepmd_main = BACKENDS[args.backend]().entry_point_hook + elif args.command is None: + pass + else: + raise RuntimeError(f"unknown command {args.command}") deepmd_main(args) diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 8ed9c51634..212a6824e7 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -23,21 +23,6 @@ from deepmd import ( __version__, ) -from deepmd.entrypoints.doc import ( - doc_train_input, -) -from deepmd.entrypoints.gui import ( - start_dpgui, -) -from deepmd.entrypoints.neighbor_stat import ( - neighbor_stat, -) -from deepmd.entrypoints.test import ( - test, -) -from deepmd.infer.model_devi import ( - make_model_devi, -) from deepmd.loggers.loggers import ( set_log_handles, ) @@ -281,22 +266,13 @@ def main(args: Optional[Union[List[str], argparse.Namespace]] = None): FLAGS = parse_args(args=args) else: FLAGS = args - dict_args = vars(FLAGS) set_log_handles(FLAGS.log_level, FLAGS.log_path, mpi_log=None) log.debug("Log handles were successfully set") - log.info("DeepMD version: %s", __version__) if FLAGS.command == "train": train(FLAGS) - elif FLAGS.command == "test": - dict_args["output"] = ( - str(Path(FLAGS.model).with_suffix(".pth")) - if Path(FLAGS.model).suffix not in (".pt", ".pth") - else FLAGS.model - ) - test(**dict_args) elif FLAGS.command == "freeze": if Path(FLAGS.checkpoint_folder).is_dir(): checkpoint_path = Path(FLAGS.checkpoint_folder) @@ -306,20 +282,6 @@ def main(args: Optional[Union[List[str], argparse.Namespace]] = None): FLAGS.model = FLAGS.checkpoint_folder FLAGS.output = str(Path(FLAGS.output).with_suffix(".pth")) freeze(FLAGS) - elif FLAGS.command == "doc-train-input": - doc_train_input(**dict_args) - elif FLAGS.command == "model-devi": - dict_args["models"] = [ - str(Path(mm).with_suffix(".pth")) - if Path(mm).suffix not in (".pb", ".pt", ".pth") - else mm - for mm in dict_args["models"] - ] - make_model_devi(**dict_args) - elif FLAGS.command == "gui": - start_dpgui(**dict_args) - elif FLAGS.command == "neighbor-stat": - neighbor_stat(**dict_args) else: raise RuntimeError(f"Invalid command {FLAGS.command}!") diff --git a/deepmd/tf/entrypoints/main.py b/deepmd/tf/entrypoints/main.py index d57b43fc7c..493e5b7aa4 100644 --- a/deepmd/tf/entrypoints/main.py +++ b/deepmd/tf/entrypoints/main.py @@ -11,6 +11,9 @@ Union, ) +from deepmd.backend.suffix import ( + format_model_suffix, +) from deepmd.main import ( get_ll, main_parser, @@ -22,12 +25,7 @@ from deepmd.tf.entrypoints import ( compress, convert, - doc_train_input, freeze, - make_model_devi, - neighbor_stat, - start_dpgui, - test, train_dp, transfer, ) @@ -73,33 +71,18 @@ def main(args: Optional[Union[List[str], argparse.Namespace]] = None): if args.command == "train": train_dp(**dict_args) elif args.command == "freeze": - dict_args["output"] = str(Path(dict_args["output"]).with_suffix(".pb")) + dict_args["output"] = format_model_suffix( + dict_args["output"], preferred_backend=args.backend, strict_prefer=True + ) freeze(**dict_args) - elif args.command == "test": - dict_args["model"] = str(Path(dict_args["model"]).with_suffix(".pb")) - test(**dict_args) elif args.command == "transfer": transfer(**dict_args) elif args.command == "compress": compress(**dict_args) - elif args.command == "doc-train-input": - doc_train_input(**dict_args) - elif args.command == "model-devi": - dict_args["models"] = [ - str(Path(mm).with_suffix(".pb")) - if Path(mm).suffix not in (".pb", ".pt") - else mm - for mm in dict_args["models"] - ] - make_model_devi(**dict_args) elif args.command == "convert-from": convert(**dict_args) - elif args.command == "neighbor-stat": - neighbor_stat(**dict_args) elif args.command == "train-nvnmd": # nvnmd train_nvnmd(**dict_args) - elif args.command == "gui": - start_dpgui(**dict_args) elif args.command is None: pass else: diff --git a/deepmd/tf/model/model.py b/deepmd/tf/model/model.py index c2def1677f..7ca2b6f4ab 100644 --- a/deepmd/tf/model/model.py +++ b/deepmd/tf/model/model.py @@ -651,7 +651,6 @@ def __new__(cls, *args, **kwargs): fitting_type = type(kwargs["fitting_net"]) else: raise RuntimeError("get unknown fitting type when building model") - print(fitting_type) # init model # infer model type by fitting_type if issubclass(fitting_type, EnerFitting): From d7a4f724ac036bd6cddbbaed7420ec1bfe99d874 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 22 Feb 2024 21:34:59 -0500 Subject: [PATCH 114/686] fix AlmaLinux GPG key error (#3326) This seems to fix the GPG key error posted at https://github.com/deepmodeling/deepmd-kit/pull/3325#issuecomment-1960484188. See https://almalinux.org/blog/2023-12-20-almalinux-8-key-update/ Signed-off-by: Jinzhe Zeng --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 26dad4fe1a..8615cd12be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,6 +159,8 @@ environment-pass = [ environment = { PIP_PREFER_BINARY="1", DP_LAMMPS_VERSION="stable_2Aug2023_update2", DP_ENABLE_IPI="1", MPI_HOME="/usr/lib64/mpich", PATH="/usr/lib64/mpich/bin:$PATH" } before-all = [ """if [ ! -z "${DP_PKG_NAME}" ]; then sed -i "s/name = \\"deepmd-kit\\"/name = \\"${DP_PKG_NAME}\\"/g" pyproject.toml; fi""", + # https://almalinux.org/blog/2023-12-20-almalinux-8-key-update/ + """rpm --import https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux""", """{ if [ "$(uname -m)" = "x86_64" ] ; then yum config-manager --add-repo http://developer.download.nvidia.com/compute/cuda/repos/rhel8/x86_64/cuda-rhel8.repo && yum install -y cuda-nvcc-${CUDA_VERSION/./-} cuda-cudart-devel-${CUDA_VERSION/./-}; fi }""", "yum install -y mpich-devel", ] From 94f0ad1d193565aa978c0809a4d04b57ca9f2791 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 22 Feb 2024 22:23:43 -0500 Subject: [PATCH 115/686] pt: fix se_e2_a precision cast (#3315) The output should cast back to the global precision. --------- Signed-off-by: Jinzhe Zeng --- deepmd/pt/model/descriptor/se_a.py | 5 ++- source/tests/consistent/common.py | 6 +++ .../consistent/descriptor/test_se_e2_a.py | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 30682a4605..4134c963da 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -459,6 +459,7 @@ def forward( else: assert self.filter_layers is not None dmatrix = dmatrix.view(-1, self.nnei, 4) + dmatrix = dmatrix.to(dtype=self.prec) nfnl = dmatrix.shape[0] # pre-allocate a shape to pass jit xyz_scatter = torch.zeros( @@ -489,8 +490,8 @@ def forward( result = result.view(-1, nloc, self.filter_neuron[-1] * self.axis_neuron) rot_mat = rot_mat.view([-1, nloc] + list(rot_mat.shape[1:])) # noqa:RUF005 return ( - result, - rot_mat, + result.to(dtype=env.GLOBAL_PT_FLOAT_PRECISION), + rot_mat.to(dtype=env.GLOBAL_PT_FLOAT_PRECISION), None, None, sw, diff --git a/source/tests/consistent/common.py b/source/tests/consistent/common.py index 66ca2477fa..aa1bfe6d9a 100644 --- a/source/tests/consistent/common.py +++ b/source/tests/consistent/common.py @@ -255,6 +255,7 @@ def test_tf_consistent_with_ref(self): np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) + assert rr1.dtype == rr2.dtype, f"{rr1.dtype} != {rr2.dtype}" def test_tf_self_consistent(self): """Test whether TF is self consistent.""" @@ -269,6 +270,7 @@ def test_tf_self_consistent(self): np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) + assert rr1.dtype == rr2.dtype, f"{rr1.dtype} != {rr2.dtype}" def test_dp_consistent_with_ref(self): """Test whether DP and reference are consistent.""" @@ -286,6 +288,7 @@ def test_dp_consistent_with_ref(self): np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) + assert rr1.dtype == rr2.dtype, f"{rr1.dtype} != {rr2.dtype}" def test_dp_self_consistent(self): """Test whether DP is self consistent.""" @@ -299,6 +302,7 @@ def test_dp_self_consistent(self): for rr1, rr2 in zip(ret1, ret2): if isinstance(rr1, np.ndarray) and isinstance(rr2, np.ndarray): np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) + assert rr1.dtype == rr2.dtype, f"{rr1.dtype} != {rr2.dtype}" else: self.assertEqual(rr1, rr2) @@ -318,6 +322,7 @@ def test_pt_consistent_with_ref(self): np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) + assert rr1.dtype == rr2.dtype, f"{rr1.dtype} != {rr2.dtype}" def test_pt_self_consistent(self): """Test whether PT is self consistent.""" @@ -331,6 +336,7 @@ def test_pt_self_consistent(self): for rr1, rr2 in zip(ret1, ret2): if isinstance(rr1, np.ndarray) and isinstance(rr2, np.ndarray): np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) + assert rr1.dtype == rr2.dtype, f"{rr1.dtype} != {rr2.dtype}" else: self.assertEqual(rr1, rr2) diff --git a/source/tests/consistent/descriptor/test_se_e2_a.py b/source/tests/consistent/descriptor/test_se_e2_a.py index a1f829aeea..fe20278e6f 100644 --- a/source/tests/consistent/descriptor/test_se_e2_a.py +++ b/source/tests/consistent/descriptor/test_se_e2_a.py @@ -39,6 +39,7 @@ (True, False), # resnet_dt (True, False), # type_one_side ([], [[0, 1]]), # excluded_types + ("float32", "float64"), # precision ) class TestSeA(CommonTest, DescriptorTest, unittest.TestCase): @property @@ -47,6 +48,7 @@ def data(self) -> dict: resnet_dt, type_one_side, excluded_types, + precision, ) = self.param return { "sel": [10, 10], @@ -57,6 +59,7 @@ def data(self) -> dict: "resnet_dt": resnet_dt, "type_one_side": type_one_side, "exclude_types": excluded_types, + "precision": precision, "seed": 1145141919810, } @@ -66,6 +69,7 @@ def skip_pt(self) -> bool: resnet_dt, type_one_side, excluded_types, + precision, ) = self.param return not type_one_side or CommonTest.skip_pt @@ -75,6 +79,7 @@ def skip_dp(self) -> bool: resnet_dt, type_one_side, excluded_types, + precision, ) = self.param return not type_one_side or CommonTest.skip_dp @@ -147,3 +152,35 @@ def eval_pt(self, pt_obj: Any) -> Any: def extract_ret(self, ret: Any, backend) -> Tuple[np.ndarray, ...]: return (ret[0],) + + @property + def rtol(self) -> float: + """Relative tolerance for comparing the return value.""" + ( + resnet_dt, + type_one_side, + excluded_types, + precision, + ) = self.param + if precision == "float64": + return 1e-10 + elif precision == "float32": + return 1e-4 + else: + raise ValueError(f"Unknown precision: {precision}") + + @property + def atol(self) -> float: + """Absolute tolerance for comparing the return value.""" + ( + resnet_dt, + type_one_side, + excluded_types, + precision, + ) = self.param + if precision == "float64": + return 1e-10 + elif precision == "float32": + return 1e-4 + else: + raise ValueError(f"Unknown precision: {precision}") From 63452077c8a9d00047b7a91e6691d40f6264d6df Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 22 Feb 2024 23:07:58 -0500 Subject: [PATCH 116/686] pt: export `model_output_type` instead of `model_output_def` (#3318) Fix #3317. --------- Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/model/make_model.py | 17 +++++++++++++++++ deepmd/pt/infer/deep_eval.py | 15 +++++++-------- deepmd/pt/model/model/make_model.py | 24 +++++++++++++++++++++++- source/tests/pt/model/test_deeppot.py | 1 + 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py index eb7e57747d..7928644061 100644 --- a/deepmd/dpmodel/model/make_model.py +++ b/deepmd/dpmodel/model/make_model.py @@ -11,6 +11,7 @@ ) from deepmd.dpmodel.output_def import ( ModelOutputDef, + OutputVariableCategory, ) from deepmd.dpmodel.utils import ( build_neighbor_list, @@ -63,6 +64,22 @@ def model_output_def(self): """Get the output def for the model.""" return ModelOutputDef(self.fitting_output_def()) + def model_output_type(self) -> str: + """Get the output type for the model.""" + output_def = self.model_output_def() + var_defs = output_def.var_defs + vars = [ + kk + for kk, vv in var_defs.items() + if vv.category == OutputVariableCategory.OUT + ] + if len(vars) == 1: + return vars[0] + elif len(vars) == 0: + raise ValueError("No valid output type found") + else: + raise ValueError(f"Multiple valid output types found: {vars}") + def call( self, coord, diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index f642d34d61..b13a968a61 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -145,19 +145,18 @@ def get_dim_aparam(self) -> int: @property def model_type(self) -> "DeepEvalWrapper": """The the evaluator of the model type.""" - output_def = self.dp.model["Default"].model_output_def() - var_defs = output_def.var_defs - if "energy" in var_defs: + model_type = self.dp.model["Default"].model_output_type() + if model_type == "energy": return DeepPot - elif "dos" in var_defs: + elif model_type == "dos": return DeepDOS - elif "dipole" in var_defs: + elif model_type == "dipole": return DeepDipole - elif "polar" in var_defs: + elif model_type == "polar": return DeepPolar - elif "global_polar" in var_defs: + elif model_type == "global_polar": return DeepGlobalPolar - elif "wfc" in var_defs: + elif model_type == "wfc": return DeepWFC else: raise RuntimeError("Unknown model type") diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 9f0fd81842..5c2cb3d298 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( Dict, + List, Optional, ) @@ -9,6 +10,9 @@ from deepmd.dpmodel import ( ModelOutputDef, ) +from deepmd.dpmodel.output_def import ( + OutputVariableCategory, +) from deepmd.pt.model.model.transform_output import ( communicate_extended_output, fit_output_to_model_output, @@ -53,11 +57,29 @@ def __init__( **kwargs, ) - @torch.jit.export def model_output_def(self): """Get the output def for the model.""" return ModelOutputDef(self.fitting_output_def()) + @torch.jit.export + def model_output_type(self) -> str: + """Get the output type for the model.""" + output_def = self.model_output_def() + var_defs = output_def.var_defs + # jit: Comprehension ifs are not supported yet + # type hint is critical for JIT + vars: List[str] = [] + for kk, vv in var_defs.items(): + # .value is critical for JIT + if vv.category == OutputVariableCategory.OUT.value: + vars.append(kk) + if len(vars) == 1: + return vars[0] + elif len(vars) == 0: + raise ValueError("No valid output type found") + else: + raise ValueError(f"Multiple valid output types found: {vars}") + # cannot use the name forward. torch script does not work def forward_common( self, diff --git a/source/tests/pt/model/test_deeppot.py b/source/tests/pt/model/test_deeppot.py index da2e554704..ee04942ae7 100644 --- a/source/tests/pt/model/test_deeppot.py +++ b/source/tests/pt/model/test_deeppot.py @@ -96,6 +96,7 @@ def test_dp_test(self): self.assertEqual(dp.get_ntypes(), 2) self.assertEqual(dp.get_dim_fparam(), 0) self.assertEqual(dp.get_dim_aparam(), 0) + self.assertEqual(dp.deep_eval.model_type, DeepPot) def test_uni(self): dp = DeepPotUni("model.pt") From d949bc895b248f0eabd862cc878255c97d9870fb Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 22 Feb 2024 23:50:26 -0500 Subject: [PATCH 117/686] feat: convert model files between backends (#3323) ```sh dp convert-backend model.pb model.pth dp convert-backend model.pb model.dp ``` --------- Signed-off-by: Jinzhe Zeng --- deepmd/backend/backend.py | 26 +++ deepmd/backend/dpmodel.py | 34 +++- deepmd/backend/pytorch.py | 31 ++++ deepmd/backend/tensorflow.py | 31 ++++ deepmd/dpmodel/utils/network.py | 19 +- deepmd/entrypoints/convert_backend.py | 27 +++ deepmd/entrypoints/main.py | 5 + deepmd/main.py | 18 ++ .../model/atomic_model/base_atomic_model.py | 4 + .../pt/model/atomic_model/dp_atomic_model.py | 1 + .../model/atomic_model/linear_atomic_model.py | 1 + .../atomic_model/pairtab_atomic_model.py | 1 + deepmd/pt/model/descriptor/se_atten.py | 2 + deepmd/pt/model/model/__init__.py | 5 +- deepmd/pt/utils/serialization.py | 76 ++++++++ deepmd/tf/fit/ener.py | 2 +- deepmd/tf/model/model.py | 12 +- deepmd/tf/utils/serialization.py | 132 ++++++++++++++ source/tests/common/dpmodel/test_network.py | 2 +- source/tests/consistent/io/__init__.py | 1 + source/tests/consistent/io/test_io.py | 172 ++++++++++++++++++ 21 files changed, 583 insertions(+), 19 deletions(-) create mode 100644 deepmd/entrypoints/convert_backend.py create mode 100644 deepmd/pt/utils/serialization.py create mode 100644 deepmd/tf/utils/serialization.py create mode 100644 source/tests/consistent/io/__init__.py create mode 100644 source/tests/consistent/io/test_io.py diff --git a/deepmd/backend/backend.py b/deepmd/backend/backend.py index 179b2e556a..f1ef4cb52a 100644 --- a/deepmd/backend/backend.py +++ b/deepmd/backend/backend.py @@ -141,6 +141,8 @@ class Feature(Flag): """Support Deep Eval backend.""" NEIGHBOR_STAT = auto() """Support neighbor statistics.""" + IO = auto() + """Support IO hook.""" name: ClassVar[str] = "Unknown" """The formal name of the backend. @@ -199,3 +201,27 @@ def neighbor_stat(self) -> Type["NeighborStat"]: The neighbor statistics of the backend. """ pass + + @property + @abstractmethod + def serialize_hook(self) -> Callable[[str], dict]: + """The serialize hook to convert the model file to a dictionary. + + Returns + ------- + Callable[[str], dict] + The serialize hook of the backend. + """ + pass + + @property + @abstractmethod + def deserialize_hook(self) -> Callable[[str, dict], None]: + """The deserialize hook to convert the dictionary to a model file. + + Returns + ------- + Callable[[str, dict], None] + The deserialize hook of the backend. + """ + pass diff --git a/deepmd/backend/dpmodel.py b/deepmd/backend/dpmodel.py index 8745ca6d5a..e5c09349cf 100644 --- a/deepmd/backend/dpmodel.py +++ b/deepmd/backend/dpmodel.py @@ -33,7 +33,9 @@ class DPModelBackend(Backend): name = "DPModel" """The formal name of the backend.""" - features: ClassVar[Backend.Feature] = Backend.Feature.NEIGHBOR_STAT + features: ClassVar[Backend.Feature] = ( + Backend.Feature.NEIGHBOR_STAT | Backend.Feature.IO + ) """The features of the backend.""" suffixes: ClassVar[List[str]] = [".dp"] """The suffixes of the backend.""" @@ -84,3 +86,33 @@ def neighbor_stat(self) -> Type["NeighborStat"]: ) return NeighborStat + + @property + def serialize_hook(self) -> Callable[[str], dict]: + """The serialize hook to convert the model file to a dictionary. + + Returns + ------- + Callable[[str], dict] + The serialize hook of the backend. + """ + from deepmd.dpmodel.utils.network import ( + load_dp_model, + ) + + return load_dp_model + + @property + def deserialize_hook(self) -> Callable[[str, dict], None]: + """The deserialize hook to convert the dictionary to a model file. + + Returns + ------- + Callable[[str, dict], None] + The deserialize hook of the backend. + """ + from deepmd.dpmodel.utils.network import ( + save_dp_model, + ) + + return save_dp_model diff --git a/deepmd/backend/pytorch.py b/deepmd/backend/pytorch.py index 4c0b0699f9..676694172b 100644 --- a/deepmd/backend/pytorch.py +++ b/deepmd/backend/pytorch.py @@ -38,6 +38,7 @@ class TensorFlowBackend(Backend): Backend.Feature.ENTRY_POINT | Backend.Feature.DEEP_EVAL | Backend.Feature.NEIGHBOR_STAT + | Backend.Feature.IO ) """The features of the backend.""" suffixes: ClassVar[List[str]] = [".pth", ".pt"] @@ -93,3 +94,33 @@ def neighbor_stat(self) -> Type["NeighborStat"]: ) return NeighborStat + + @property + def serialize_hook(self) -> Callable[[str], dict]: + """The serialize hook to convert the model file to a dictionary. + + Returns + ------- + Callable[[str], dict] + The serialize hook of the backend. + """ + from deepmd.pt.utils.serialization import ( + serialize_from_file, + ) + + return serialize_from_file + + @property + def deserialize_hook(self) -> Callable[[str, dict], None]: + """The deserialize hook to convert the dictionary to a model file. + + Returns + ------- + Callable[[str, dict], None] + The deserialize hook of the backend. + """ + from deepmd.pt.utils.serialization import ( + deserialize_to_file, + ) + + return deserialize_to_file diff --git a/deepmd/backend/tensorflow.py b/deepmd/backend/tensorflow.py index 80569afa97..15b03ee7c8 100644 --- a/deepmd/backend/tensorflow.py +++ b/deepmd/backend/tensorflow.py @@ -38,6 +38,7 @@ class TensorFlowBackend(Backend): Backend.Feature.ENTRY_POINT | Backend.Feature.DEEP_EVAL | Backend.Feature.NEIGHBOR_STAT + | Backend.Feature.IO ) """The features of the backend.""" suffixes: ClassVar[List[str]] = [".pb"] @@ -102,3 +103,33 @@ def neighbor_stat(self) -> Type["NeighborStat"]: ) return NeighborStat + + @property + def serialize_hook(self) -> Callable[[str], dict]: + """The serialize hook to convert the model file to a dictionary. + + Returns + ------- + Callable[[str], dict] + The serialize hook of the backend. + """ + from deepmd.tf.utils.serialization import ( + serialize_from_file, + ) + + return serialize_from_file + + @property + def deserialize_hook(self) -> Callable[[str, dict], None]: + """The deserialize hook to convert the dictionary to a model file. + + Returns + ------- + Callable[[str, dict], None] + The deserialize hook of the backend. + """ + from deepmd.tf.utils.serialization import ( + deserialize_to_file, + ) + + return deserialize_to_file diff --git a/deepmd/dpmodel/utils/network.py b/deepmd/dpmodel/utils/network.py index 8c826c8771..c0a62c9a3e 100644 --- a/deepmd/dpmodel/utils/network.py +++ b/deepmd/dpmodel/utils/network.py @@ -6,6 +6,9 @@ import copy import itertools import json +from datetime import ( + datetime, +) from typing import ( ClassVar, Dict, @@ -54,6 +57,8 @@ def traverse_model_dict(model_obj, callback: callable, is_variable: bool = False elif isinstance(model_obj, list): for ii, vv in enumerate(model_obj): model_obj[ii] = traverse_model_dict(vv, callback, is_variable=is_variable) + elif model_obj is None: + return model_obj elif is_variable: model_obj = callback(model_obj) return model_obj @@ -79,7 +84,8 @@ def __call__(self): return self.count -def save_dp_model(filename: str, model_dict: dict, extra_info: Optional[dict] = None): +# TODO: should be moved to otherwhere... +def save_dp_model(filename: str, model_dict: dict) -> None: """Save a DP model to a file in the native format. Parameters @@ -88,15 +94,9 @@ def save_dp_model(filename: str, model_dict: dict, extra_info: Optional[dict] = The filename to save to. model_dict : dict The model dict to save. - extra_info : dict, optional - Extra meta information to save. """ model_dict = model_dict.copy() variable_counter = Counter() - if extra_info is not None: - extra_info = extra_info.copy() - else: - extra_info = {} with h5py.File(filename, "w") as f: model_dict = traverse_model_dict( model_dict, @@ -105,10 +105,11 @@ def save_dp_model(filename: str, model_dict: dict, extra_info: Optional[dict] = ).name, ) save_dict = { - "model": model_dict, "software": "deepmd-kit", "version": __version__, - **extra_info, + # use UTC+0 time + "time": str(datetime.utcnow()), + **model_dict, } f.attrs["json"] = json.dumps(save_dict, separators=(",", ":")) diff --git a/deepmd/entrypoints/convert_backend.py b/deepmd/entrypoints/convert_backend.py new file mode 100644 index 0000000000..39967d565c --- /dev/null +++ b/deepmd/entrypoints/convert_backend.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from deepmd.backend.backend import ( + Backend, +) + + +def convert_backend( + *, # Enforce keyword-only arguments + INPUT: str, + OUTPUT: str, + **kwargs, +) -> None: + """Convert a model file from one backend to another. + + Parameters + ---------- + INPUT : str + The input model file. + INPUT : str + The output model file. + """ + inp_backend: Backend = Backend.detect_backend_by_model(INPUT)() + out_backend: Backend = Backend.detect_backend_by_model(OUTPUT)() + inp_hook = inp_backend.serialize_hook + out_hook = out_backend.deserialize_hook + data = inp_hook(INPUT) + out_hook(OUTPUT, data) diff --git a/deepmd/entrypoints/main.py b/deepmd/entrypoints/main.py index 9a03ac5e45..9f05b9a530 100644 --- a/deepmd/entrypoints/main.py +++ b/deepmd/entrypoints/main.py @@ -12,6 +12,9 @@ from deepmd.backend.suffix import ( format_model_suffix, ) +from deepmd.entrypoints.convert_backend import ( + convert_backend, +) from deepmd.entrypoints.doc import ( doc_train_input, ) @@ -76,5 +79,7 @@ def main(args: argparse.Namespace): neighbor_stat(**dict_args) elif args.command == "gui": start_dpgui(**dict_args) + elif args.command == "convert-backend": + convert_backend(**dict_args) else: raise ValueError(f"Unknown command: {args.command}") diff --git a/deepmd/main.py b/deepmd/main.py index 98f5ab0c6b..4d2d62ed14 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -721,6 +721,23 @@ def main_parser() -> argparse.ArgumentParser: "to the network on both IPv4 and IPv6 (where available)." ), ) + + # convert_backend + parser_convert_backend = subparsers.add_parser( + "convert-backend", + parents=[parser_log], + help="Convert model to another backend.", + formatter_class=RawTextArgumentDefaultsHelpFormatter, + epilog=textwrap.dedent( + """\ + examples: + dp convert-backend model.pb model.pth + dp convert-backend model.pb model.dp + """ + ), + ) + parser_convert_backend.add_argument("INPUT", help="The input model file.") + parser_convert_backend.add_argument("OUTPUT", help="The output model file.") return parser @@ -767,6 +784,7 @@ def main(): "model-devi", "neighbor-stat", "gui", + "convert-backend", ): # common entrypoints from deepmd.entrypoints.main import main as deepmd_main diff --git a/deepmd/pt/model/atomic_model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py index 73b2d76a6d..1e5f976baf 100644 --- a/deepmd/pt/model/atomic_model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -14,3 +14,7 @@ class BaseAtomicModel(BaseAtomicModel_): # export public methods that are not abstract get_nsel = torch.jit.export(BaseAtomicModel_.get_nsel) get_nnei = torch.jit.export(BaseAtomicModel_.get_nnei) + + @torch.jit.export + def get_model_def_script(self) -> str: + return self.model_def_script diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index 2d8836c0e1..dafc9e109e 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -50,6 +50,7 @@ class DPAtomicModel(torch.nn.Module, BaseAtomicModel): def __init__(self, descriptor, fitting, type_map: Optional[List[str]]): super().__init__() + self.model_def_script = "" ntypes = len(type_map) self.type_map = type_map self.ntypes = ntypes diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 16b06b2211..70afbcb0bc 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -288,6 +288,7 @@ def __init__( ): models = [dp_model, zbl_model] super().__init__(models, **kwargs) + self.model_def_script = "" self.dp_model = dp_model self.zbl_model = zbl_model diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index d8b830d1eb..cf5a70eb88 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -52,6 +52,7 @@ def __init__( self, tab_file: str, rcut: float, sel: Union[int, List[int]], **kwargs ): super().__init__() + self.model_def_script = "" self.tab_file = tab_file self.rcut = rcut self.tab = self._set_pairtab(tab_file, rcut) diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index 0c15cae46c..4a7469a804 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -87,6 +87,8 @@ def __init__( self.ffn = ffn self.ffn_embed_dim = ffn_embed_dim self.activation = activation + # TODO: To be fixed: precision should be given from inputs + self.prec = torch.float64 self.scaling_factor = scaling_factor self.head_num = head_num self.normalize = normalize diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index ad16741ee4..974c42ee41 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -10,6 +10,7 @@ """ import copy +import json from deepmd.pt.model.atomic_model import ( DPAtomicModel, @@ -98,7 +99,9 @@ def get_model(model_params): fitting_net["return_energy"] = True fitting = Fitting(**fitting_net) - return EnergyModel(descriptor, fitting, type_map=model_params["type_map"]) + model = EnergyModel(descriptor, fitting, type_map=model_params["type_map"]) + model.model_def_script = json.dumps(model_params) + return model __all__ = [ diff --git a/deepmd/pt/utils/serialization.py b/deepmd/pt/utils/serialization.py new file mode 100644 index 0000000000..91d1a3c76f --- /dev/null +++ b/deepmd/pt/utils/serialization.py @@ -0,0 +1,76 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json + +import torch + +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.model.model.ener_model import ( + EnergyModel, +) +from deepmd.pt.train.wrapper import ( + ModelWrapper, +) + + +def serialize_from_file(model_file: str) -> dict: + """Serialize the model file to a dictionary. + + Parameters + ---------- + model_file : str + The model file to be serialized. + + Returns + ------- + dict + The serialized model data. + """ + if model_file.endswith(".pth"): + saved_model = torch.jit.load(model_file, map_location="cpu") + model_def_script = json.loads(saved_model.model_def_script) + model = get_model(model_def_script) + model.load_state_dict(saved_model.state_dict()) + elif model_file.endswith(".pt"): + state_dict = torch.load(model_file, map_location="cpu") + if "model" in state_dict: + state_dict = state_dict["model"] + model_def_script = state_dict["_extra_state"]["model_params"] + model = get_model(model_def_script) + modelwrapper = ModelWrapper(model) + modelwrapper.load_state_dict(state_dict) + model = modelwrapper.model["Default"] + else: + raise ValueError("PyTorch backend only supports converting .pth or .pt file") + + model_dict = model.serialize() + data = { + "backend": "PyTorch", + "pt_version": torch.__version__, + "model": model_dict, + "model_def_script": model_def_script, + # TODO + "@variables": {}, + } + return data + + +def deserialize_to_file(model_file: str, data: dict) -> None: + """Deserialize the dictionary to a model file. + + Parameters + ---------- + model_file : str + The model file to be saved. + data : dict + The dictionary to be deserialized. + """ + if not model_file.endswith(".pth"): + raise ValueError("PyTorch backend only supports converting .pth file") + # TODO: read class type from data; see #3319 + model = EnergyModel.deserialize(data["model"]) + # JIT will happy in this way... + model.model_def_script = json.dumps(data["model_def_script"]) + model = torch.jit.script(model) + torch.jit.save(model, model_file) diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index 6be63f907c..19bec5cec0 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -194,7 +194,7 @@ def __init__( ), "length of trainable should be that of n_neuron + 1" self.atom_ener = [] self.atom_ener_v = atom_ener - for at, ae in enumerate(atom_ener): + for at, ae in enumerate(atom_ener if atom_ener is not None else []): if ae is not None: self.atom_ener.append( tf.constant(ae, GLOBAL_TF_FLOAT_PRECISION, name="atom_%d_ener" % at) diff --git a/deepmd/tf/model/model.py b/deepmd/tf/model/model.py index 7ca2b6f4ab..65413a87c1 100644 --- a/deepmd/tf/model/model.py +++ b/deepmd/tf/model/model.py @@ -579,7 +579,7 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict) -> dict: return cls.update_sel(global_jdata, local_jdata) @classmethod - def deserialize(cls, data: dict, suffix: str = "") -> "Descriptor": + def deserialize(cls, data: dict, suffix: str = "") -> "Model": """Deserialize the model. There is no suffix in a native DP model, but it is important @@ -590,15 +590,15 @@ def deserialize(cls, data: dict, suffix: str = "") -> "Descriptor": data : dict The serialized data suffix : str, optional - Name suffix to identify this descriptor + Name suffix to identify this model Returns ------- - Descriptor - The deserialized descriptor + Model + The deserialized Model """ - if cls is Descriptor: - return Descriptor.get_class_by_input(data).deserialize(data) + if cls is Model: + return Model.get_class_by_input(data).deserialize(data) raise NotImplementedError("Not implemented in class %s" % cls.__name__) def serialize(self, suffix: str = "") -> dict: diff --git a/deepmd/tf/utils/serialization.py b/deepmd/tf/utils/serialization.py new file mode 100644 index 0000000000..7cf596f5bd --- /dev/null +++ b/deepmd/tf/utils/serialization.py @@ -0,0 +1,132 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import os +import tempfile + +from deepmd.tf.entrypoints import ( + freeze, +) +from deepmd.tf.env import ( + GLOBAL_TF_FLOAT_PRECISION, + tf, +) +from deepmd.tf.model.model import ( + Model, +) +from deepmd.tf.utils.errors import ( + GraphWithoutTensorError, +) +from deepmd.tf.utils.graph import ( + get_tensor_by_name_from_graph, + load_graph_def, +) +from deepmd.tf.utils.sess import ( + run_sess, +) + + +def serialize_from_file(model_file: str) -> dict: + """Serialize the model file to a dictionary. + + Parameters + ---------- + model_file : str + The model file to be serialized. + + Returns + ------- + dict + The serialized model data. + """ + graph, graph_def = load_graph_def(model_file) + t_jdata = get_tensor_by_name_from_graph(graph, "train_attr/training_script") + jdata = json.loads(t_jdata) + model = Model(**jdata["model"]) + # important! must be called before serialize + model.init_variables(graph=graph, graph_def=graph_def) + model_dict = model.serialize() + data = { + "backend": "TensorFlow", + "tf_version": tf.__version__, + "model": model_dict, + "model_def_script": jdata["model"], + } + # neighbor stat information + try: + t_min_nbor_dist = get_tensor_by_name_from_graph( + graph, "train_attr/min_nbor_dist" + ) + except GraphWithoutTensorError as e: + pass + else: + data.setdefault("@variables", {}) + data["@variables"]["min_nbor_dist"] = t_min_nbor_dist + return data + + +def deserialize_to_file(model_file: str, data: dict) -> None: + """Deserialize the dictionary to a model file. + + Parameters + ---------- + model_file : str + The model file to be saved. + data : dict + The dictionary to be deserialized. + """ + model = Model.deserialize(data["model"]) + with tf.Graph().as_default() as graph, tf.Session(graph=graph) as sess: + place_holders = {} + for ii in ["coord", "box"]: + place_holders[ii] = tf.placeholder( + GLOBAL_TF_FLOAT_PRECISION, [None], name="t_" + ii + ) + place_holders["type"] = tf.placeholder(tf.int32, [None], name="t_type") + place_holders["natoms_vec"] = tf.placeholder( + tf.int32, [model.get_ntypes() + 2], name="t_natoms" + ) + place_holders["default_mesh"] = tf.placeholder(tf.int32, [None], name="t_mesh") + inputs = {} + # fparam, aparam + if model.get_numb_fparam() > 0: + inputs["fparam"] = tf.placeholder( + GLOBAL_TF_FLOAT_PRECISION, + [None, model.get_numb_fparam()], + name="t_fparam", + ) + if model.get_numb_aparam() > 0: + inputs["aparam"] = tf.placeholder( + GLOBAL_TF_FLOAT_PRECISION, + [None, model.get_numb_aparam()], + name="t_aparam", + ) + model.build( + place_holders["coord"], + place_holders["type"], + place_holders["natoms_vec"], + place_holders["box"], + place_holders["default_mesh"], + inputs, + reuse=False, + ) + init = tf.global_variables_initializer() + tf.constant( + json.dumps({"model": data["model_def_script"]}, separators=(",", ":")), + name="train_attr/training_script", + dtype=tf.string, + ) + if "min_nbor_dist" in data.get("@variables", {}): + tf.constant( + data["@variables"]["min_nbor_dist"], + name="train_attr/min_nbor_dist", + dtype=GLOBAL_TF_FLOAT_PRECISION, + ) + run_sess(sess, init) + saver = tf.train.Saver() + with tempfile.TemporaryDirectory() as nt: + saver.save( + sess, + os.path.join(nt, "model.ckpt"), + global_step=0, + ) + freeze(checkpoint_folder=nt, output=model_file, node_names=None) diff --git a/source/tests/common/dpmodel/test_network.py b/source/tests/common/dpmodel/test_network.py index bfed8da45a..047eee501c 100644 --- a/source/tests/common/dpmodel/test_network.py +++ b/source/tests/common/dpmodel/test_network.py @@ -285,7 +285,7 @@ def setUp(self) -> None: self.filename = "test_dp_dpmodel.dp" def test_save_load_model(self): - save_dp_model(self.filename, deepcopy(self.model_dict)) + save_dp_model(self.filename, {"model": deepcopy(self.model_dict)}) model = load_dp_model(self.filename) np.testing.assert_equal(model["model"], self.model_dict) assert "software" in model diff --git a/source/tests/consistent/io/__init__.py b/source/tests/consistent/io/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/source/tests/consistent/io/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/source/tests/consistent/io/test_io.py b/source/tests/consistent/io/test_io.py new file mode 100644 index 0000000000..b8fae40cda --- /dev/null +++ b/source/tests/consistent/io/test_io.py @@ -0,0 +1,172 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest +from pathlib import ( + Path, +) + +import numpy as np + +from deepmd.backend.backend import ( + Backend, +) +from deepmd.dpmodel.model.model import ( + get_model, +) +from deepmd.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) +from deepmd.infer.deep_pot import ( + DeepPot, +) + +infer_path = Path(__file__).parent.parent.parent / "infer" + + +class IOTest: + data: dict + + def get_data_from_model(self, model_file: str) -> dict: + """Get data from a model file. + + Parameters + ---------- + model_file : str + The model file. + + Returns + ------- + dict + The data from the model file. + """ + inp_backend: Backend = Backend.detect_backend_by_model(model_file)() + inp_hook = inp_backend.serialize_hook + return inp_hook(model_file) + + def save_data_to_model(self, model_file: str, data: dict) -> None: + """Save data to a model file. + + Parameters + ---------- + model_file : str + The model file. + data : dict + The data to save. + """ + out_backend: Backend = Backend.detect_backend_by_model(model_file)() + out_hook = out_backend.deserialize_hook + out_hook(model_file, data) + + def test_data_equal(self): + prefix = "test_consistent_io_" + self.__class__.__name__.lower() + for backend_name in ("tensorflow", "pytorch", "dpmodel"): + with self.subTest(backend_name=backend_name): + backend = Backend.get_backend(backend_name)() + if not backend.is_available: + continue + reference_data = copy.deepcopy(self.data) + self.save_data_to_model(prefix + backend.suffixes[0], reference_data) + data = self.get_data_from_model(prefix + backend.suffixes[0]) + data = copy.deepcopy(data) + reference_data = copy.deepcopy(self.data) + # some keys are not expected to be not the same + for kk in [ + "backend", + "tf_version", + "pt_version", + "@variables", + # dpmodel only + "software", + "version", + "time", + ]: + data.pop(kk, None) + reference_data.pop(kk, None) + np.testing.assert_equal(data, reference_data) + + def test_deep_eval(self): + self.coords = np.array( + [ + 12.83, + 2.56, + 2.18, + 12.09, + 2.87, + 2.74, + 00.25, + 3.32, + 1.68, + 3.36, + 3.00, + 1.81, + 3.51, + 2.51, + 2.60, + 4.27, + 3.22, + 1.56, + ], + dtype=GLOBAL_NP_FLOAT_PRECISION, + ).reshape(1, -1, 3) + self.atype = np.array([0, 1, 1, 0, 1, 1], dtype=np.int32).reshape(1, -1) + self.box = np.array( + [13.0, 0.0, 0.0, 0.0, 13.0, 0.0, 0.0, 0.0, 13.0], + dtype=GLOBAL_NP_FLOAT_PRECISION, + ).reshape(1, 9) + prefix = "test_consistent_io_" + self.__class__.__name__.lower() + rets = [] + for backend_name in ("tensorflow", "pytorch"): + backend = Backend.get_backend(backend_name)() + if not backend.is_available: + continue + reference_data = copy.deepcopy(self.data) + self.save_data_to_model(prefix + backend.suffixes[0], reference_data) + deep_eval = DeepPot(prefix + backend.suffixes[0]) + ret = deep_eval.eval( + self.coords, + self.box, + self.atype, + ) + rets.append(ret) + for ret in rets[1:]: + for vv1, vv2 in zip(rets[0], ret): + np.testing.assert_allclose(vv1, vv2, rtol=1e-12, atol=1e-12) + + +class TestDeepPot(unittest.TestCase, IOTest): + def setUp(self): + model_def_script = { + "type_map": ["O", "H"], + "descriptor": { + "type": "se_e2_a", + "sel": [20, 20], + "rcut_smth": 0.50, + "rcut": 6.00, + "neuron": [ + 3, + 6, + ], + "resnet_dt": False, + "axis_neuron": 2, + "precision": "float64", + "type_one_side": True, + "seed": 1, + }, + "fitting_net": { + "type": "ener", + "neuron": [ + 5, + 5, + ], + "resnet_dt": True, + "precision": "float64", + "atom_ener": [], + "seed": 1, + }, + } + model = get_model(copy.deepcopy(model_def_script)) + self.data = { + "model": model.serialize(), + "backend": "test", + "model_def_script": model_def_script, + } From 260ef21cf2fcf41850b95baa6073df364c618841 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Fri, 23 Feb 2024 13:18:54 +0800 Subject: [PATCH 118/686] Feat: add dipole consistency test (#3321) This PR is to add cross framework consistency test on DipoleFittingNet. Known Limitations: 1. There are some mismatched keys in the serialized model, only common keys are tested. --------- Signed-off-by: Anyang Peng <137014849+anyangml@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/dpmodel/fitting/dipole_fitting.py | 8 +- deepmd/dpmodel/fitting/general_fitting.py | 5 - deepmd/dpmodel/fitting/invar_fitting.py | 5 +- .../dpmodel/fitting/polarizability_fitting.py | 6 - deepmd/pt/model/task/ener.py | 5 + deepmd/pt/model/task/fitting.py | 1 - deepmd/tf/fit/dipole.py | 77 ++++++- deepmd/tf/fit/ener.py | 6 +- deepmd/tf/fit/fitting.py | 6 +- deepmd/tf/model/model.py | 2 + deepmd/tf/model/multi.py | 4 + source/tests/consistent/common.py | 14 +- source/tests/consistent/fitting/common.py | 34 ++++ .../tests/consistent/fitting/test_dipole.py | 192 ++++++++++++++++++ source/tests/tf/test_data_large_batch.py | 3 + source/tests/tf/test_data_modifier.py | 1 - source/tests/tf/test_dipole_se_a.py | 4 +- source/tests/tf/test_dipole_se_a_tebd.py | 4 +- source/tests/tf/test_fitting_ener_type.py | 1 + source/tests/tf/test_model_multi.py | 1 + source/tests/tf/test_model_se_a.py | 3 + source/tests/tf/test_model_se_a_aparam.py | 1 + source/tests/tf/test_model_se_a_ebd.py | 1 + source/tests/tf/test_model_se_a_ebd_v2.py | 1 + source/tests/tf/test_model_se_a_fparam.py | 1 + source/tests/tf/test_model_se_a_srtab.py | 1 + source/tests/tf/test_model_se_a_type.py | 1 + source/tests/tf/test_model_se_atten.py | 4 + 28 files changed, 359 insertions(+), 33 deletions(-) create mode 100644 source/tests/consistent/fitting/test_dipole.py diff --git a/deepmd/dpmodel/fitting/dipole_fitting.py b/deepmd/dpmodel/fitting/dipole_fitting.py index f5acabf7b1..55b237e0fe 100644 --- a/deepmd/dpmodel/fitting/dipole_fitting.py +++ b/deepmd/dpmodel/fitting/dipole_fitting.py @@ -53,8 +53,6 @@ class DipoleFitting(GeneralFitting): If the weights of fitting net are trainable. Suppose that we have :math:`N_l` hidden layers in the fitting net, this list is of length :math:`N_l + 1`, specifying if the hidden layers and the output layer are trainable. - atom_ener - Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. activation_function The activation function :math:`\boldsymbol{\phi}` in the embedding net. Supported options are |ACTIVATION_FN| precision @@ -91,7 +89,6 @@ def __init__( rcond: Optional[float] = None, tot_ener_zero: bool = False, trainable: Optional[List[bool]] = None, - atom_ener: Optional[List[Optional[float]]] = None, activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, layer_name: Optional[List[Optional[str]]] = None, @@ -102,6 +99,8 @@ def __init__( r_differentiable: bool = True, c_differentiable: bool = True, old_impl=False, + # not used + seed: Optional[int] = None, ): # seed, uniform_seed are not included if tot_ener_zero: @@ -112,8 +111,6 @@ def __init__( raise NotImplementedError("use_aparam_as_mask is not implemented") if layer_name is not None: raise NotImplementedError("layer_name is not implemented") - if atom_ener is not None and atom_ener != []: - raise NotImplementedError("atom_ener is not implemented") self.embedding_width = embedding_width self.r_differentiable = r_differentiable @@ -129,7 +126,6 @@ def __init__( rcond=rcond, tot_ener_zero=tot_ener_zero, trainable=trainable, - atom_ener=atom_ener, activation_function=activation_function, precision=precision, layer_name=layer_name, diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index 890a065f15..a64fa283ec 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -55,8 +55,6 @@ class GeneralFitting(NativeOP, BaseFitting): If the weights of fitting net are trainable. Suppose that we have :math:`N_l` hidden layers in the fitting net, this list is of length :math:`N_l + 1`, specifying if the hidden layers and the output layer are trainable. - atom_ener - Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. activation_function The activation function :math:`\boldsymbol{\phi}` in the embedding net. Supported options are |ACTIVATION_FN| precision @@ -87,7 +85,6 @@ def __init__( rcond: Optional[float] = None, tot_ener_zero: bool = False, trainable: Optional[List[bool]] = None, - atom_ener: Optional[List[float]] = None, activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, layer_name: Optional[List[Optional[str]]] = None, @@ -110,7 +107,6 @@ def __init__( self.trainable = [True for ii in range(len(self.neuron) + 1)] if isinstance(self.trainable, bool): self.trainable = [self.trainable] * (len(self.neuron) + 1) - self.atom_ener = atom_ener self.activation_function = activation_function self.precision = precision self.layer_name = layer_name @@ -236,7 +232,6 @@ def serialize(self) -> dict: # not supported "tot_ener_zero": self.tot_ener_zero, "trainable": self.trainable, - "atom_ener": self.atom_ener, "layer_name": self.layer_name, "use_aparam_as_mask": self.use_aparam_as_mask, "spin": self.spin, diff --git a/deepmd/dpmodel/fitting/invar_fitting.py b/deepmd/dpmodel/fitting/invar_fitting.py index 429565d016..3d958301ff 100644 --- a/deepmd/dpmodel/fitting/invar_fitting.py +++ b/deepmd/dpmodel/fitting/invar_fitting.py @@ -115,7 +115,7 @@ def __init__( rcond: Optional[float] = None, tot_ener_zero: bool = False, trainable: Optional[List[bool]] = None, - atom_ener: Optional[List[float]] = None, + atom_ener: Optional[List[float]] = [], activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, layer_name: Optional[List[Optional[str]]] = None, @@ -139,6 +139,7 @@ def __init__( raise NotImplementedError("atom_ener is not implemented") self.dim_out = dim_out + self.atom_ener = atom_ener super().__init__( var_name=var_name, ntypes=ntypes, @@ -150,7 +151,6 @@ def __init__( rcond=rcond, tot_ener_zero=tot_ener_zero, trainable=trainable, - atom_ener=atom_ener, activation_function=activation_function, precision=precision, layer_name=layer_name, @@ -163,6 +163,7 @@ def __init__( def serialize(self) -> dict: data = super().serialize() data["dim_out"] = self.dim_out + data["atom_ener"] = self.atom_ener return data def _net_out_dim(self): diff --git a/deepmd/dpmodel/fitting/polarizability_fitting.py b/deepmd/dpmodel/fitting/polarizability_fitting.py index c3cbe7bd1a..9811e8e1c8 100644 --- a/deepmd/dpmodel/fitting/polarizability_fitting.py +++ b/deepmd/dpmodel/fitting/polarizability_fitting.py @@ -56,8 +56,6 @@ class PolarFitting(GeneralFitting): If the weights of fitting net are trainable. Suppose that we have :math:`N_l` hidden layers in the fitting net, this list is of length :math:`N_l + 1`, specifying if the hidden layers and the output layer are trainable. - atom_ener - Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. activation_function The activation function :math:`\boldsymbol{\phi}` in the embedding net. Supported options are |ACTIVATION_FN| precision @@ -93,7 +91,6 @@ def __init__( rcond: Optional[float] = None, tot_ener_zero: bool = False, trainable: Optional[List[bool]] = None, - atom_ener: Optional[List[Optional[float]]] = None, activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, layer_name: Optional[List[Optional[str]]] = None, @@ -115,8 +112,6 @@ def __init__( raise NotImplementedError("use_aparam_as_mask is not implemented") if layer_name is not None: raise NotImplementedError("layer_name is not implemented") - if atom_ener is not None and atom_ener != []: - raise NotImplementedError("atom_ener is not implemented") self.embedding_width = embedding_width self.fit_diag = fit_diag @@ -142,7 +137,6 @@ def __init__( rcond=rcond, tot_ener_zero=tot_ener_zero, trainable=trainable, - atom_ener=atom_ener, activation_function=activation_function, precision=precision, layer_name=layer_name, diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index ed2dfbc02b..d0acc6fe2b 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -78,6 +78,8 @@ class InvarFitting(GeneralFitting): Random seed. exclude_types: List[int] Atomic contributions of the excluded atom types are set zero. + atom_ener + Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. """ @@ -98,9 +100,11 @@ def __init__( rcond: Optional[float] = None, seed: Optional[int] = None, exclude_types: List[int] = [], + atom_ener: Optional[List[float]] = None, **kwargs, ): self.dim_out = dim_out + self.atom_ener = atom_ener super().__init__( var_name=var_name, ntypes=ntypes, @@ -126,6 +130,7 @@ def _net_out_dim(self): def serialize(self) -> dict: data = super().serialize() data["dim_out"] = self.dim_out + data["atom_ener"] = self.atom_ener return data @property diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index be20aa9496..080bfb5172 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -428,7 +428,6 @@ def serialize(self) -> dict: ## NOTICE: not supported by far "tot_ener_zero": False, "trainable": [True] * (len(self.neuron) + 1), - "atom_ener": [], "layer_name": None, "use_aparam_as_mask": False, "spin": None, diff --git a/deepmd/tf/fit/dipole.py b/deepmd/tf/fit/dipole.py index 55da62d69b..efb94a85c0 100644 --- a/deepmd/tf/fit/dipole.py +++ b/deepmd/tf/fit/dipole.py @@ -38,8 +38,12 @@ class DipoleFittingSeA(Fitting): Parameters ---------- - descrpt : tf.Tensor - The descrptor + ntypes + The ntypes of the descrptor :math:`\mathcal{D}` + dim_descrpt + The dimension of the descrptor :math:`\mathcal{D}` + embedding_width + The rotation matrix dimension of the descrptor :math:`\mathcal{D}` neuron : List[int] Number of neurons in each hidden layer of the fitting net resnet_dt : bool @@ -59,7 +63,9 @@ class DipoleFittingSeA(Fitting): def __init__( self, - descrpt: tf.Tensor, + ntypes: int, + dim_descrpt: int, + embedding_width: int, neuron: List[int] = [120, 120, 120], resnet_dt: bool = True, sel_type: Optional[List[int]] = None, @@ -70,8 +76,8 @@ def __init__( **kwargs, ) -> None: """Constructor.""" - self.ntypes = descrpt.get_ntypes() - self.dim_descrpt = descrpt.get_dim_out() + self.ntypes = ntypes + self.dim_descrpt = dim_descrpt self.n_neuron = neuron self.resnet_dt = resnet_dt self.sel_type = sel_type @@ -83,9 +89,10 @@ def __init__( self.seed = seed self.uniform_seed = uniform_seed self.seed_shift = one_layer_rand_seed_shift() + self.activation_function_name = activation_function self.fitting_activation_fn = get_activation_func(activation_function) self.fitting_precision = get_precision(precision) - self.dim_rot_mat_1 = descrpt.get_dim_rot_mat_1() + self.dim_rot_mat_1 = embedding_width self.dim_rot_mat = self.dim_rot_mat_1 * 3 self.useBN = False self.fitting_net_variables = None @@ -327,3 +334,61 @@ def get_loss(self, loss: dict, lr) -> Loss: tensor_size=3, label_name="dipole", ) + + def serialize(self, suffix: str) -> dict: + """Serialize the model. + + Returns + ------- + dict + The serialized data + """ + data = { + "var_name": "dipole", + "ntypes": self.ntypes, + "dim_descrpt": self.dim_descrpt, + "embedding_width": self.dim_rot_mat_1, + # very bad design: type embedding is not passed to the class + # TODO: refactor the class + "mixed_types": False, + "dim_out": 3, + "neuron": self.n_neuron, + "resnet_dt": self.resnet_dt, + "activation_function": self.activation_function_name, + "precision": self.fitting_precision.name, + "exclude_types": [], + "nets": self.serialize_network( + ntypes=self.ntypes, + # TODO: consider type embeddings + ndim=1, + in_dim=self.dim_descrpt, + out_dim=self.dim_rot_mat_1, + neuron=self.n_neuron, + activation_function=self.activation_function_name, + resnet_dt=self.resnet_dt, + variables=self.fitting_net_variables, + suffix=suffix, + ), + } + return data + + @classmethod + def deserialize(cls, data: dict, suffix: str): + """Deserialize the model. + + Parameters + ---------- + data : dict + The serialized data + + Returns + ------- + Model + The deserialized model + """ + fitting = cls(**data) + fitting.fitting_net_variables = cls.deserialize_network( + data["nets"], + suffix=suffix, + ) + return fitting diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index 19bec5cec0..59a1bfe0bd 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -95,8 +95,10 @@ class EnerFitting(Fitting): Parameters ---------- - descrpt - The descrptor :math:`\mathcal{D}` + ntypes + The ntypes of the descrptor :math:`\mathcal{D}` + dim_descrpt + The dimension of the descrptor :math:`\mathcal{D}` neuron Number of neurons :math:`N` in each hidden layer of the fitting net resnet_dt diff --git a/deepmd/tf/fit/fitting.py b/deepmd/tf/fit/fitting.py index 458765f7c1..598d020fe9 100644 --- a/deepmd/tf/fit/fitting.py +++ b/deepmd/tf/fit/fitting.py @@ -6,6 +6,7 @@ from typing import ( Callable, List, + Optional, Type, ) @@ -175,6 +176,7 @@ def serialize_network( activation_function: str, resnet_dt: bool, variables: dict, + out_dim: Optional[int] = 1, suffix: str = "", ) -> dict: """Serialize network. @@ -197,6 +199,8 @@ def serialize_network( The input variables suffix : str, optional The suffix of the scope + out_dim : int, optional + The output dimension Returns ------- @@ -231,7 +235,7 @@ def serialize_network( # initialize the network if it is not initialized fittings[network_idx] = FittingNet( in_dim=in_dim, - out_dim=1, + out_dim=out_dim, neuron=neuron, activation_function=activation_function, resnet_dt=resnet_dt, diff --git a/deepmd/tf/model/model.py b/deepmd/tf/model/model.py index 65413a87c1..09b55f4a04 100644 --- a/deepmd/tf/model/model.py +++ b/deepmd/tf/model/model.py @@ -687,6 +687,8 @@ def __init__( if isinstance(fitting_net, Fitting): self.fitting = fitting_net else: + if fitting_net["type"] in ["dipole", "polar"]: + fitting_net["embedding_width"] = self.descrpt.get_dim_rot_mat_1() self.fitting = Fitting( **fitting_net, descrpt=self.descrpt, diff --git a/deepmd/tf/model/multi.py b/deepmd/tf/model/multi.py index 833a700ebc..2acf00fd52 100644 --- a/deepmd/tf/model/multi.py +++ b/deepmd/tf/model/multi.py @@ -133,6 +133,10 @@ def __init__( if isinstance(item_fitting_param, Fitting): fitting_dict[item] = item_fitting_param else: + if item_fitting_param["type"] in ["dipole", "polar"]: + item_fitting_param[ + "embedding_width" + ] = self.descrpt.get_dim_rot_mat_1() fitting_dict[item] = Fitting( **item_fitting_param, descrpt=self.descrpt, diff --git a/source/tests/consistent/common.py b/source/tests/consistent/common.py index aa1bfe6d9a..622e2ed3cf 100644 --- a/source/tests/consistent/common.py +++ b/source/tests/consistent/common.py @@ -252,9 +252,16 @@ def test_tf_consistent_with_ref(self): tf_obj = self.tf_class.deserialize(data1, suffix=self.unique_id) ret2, data2 = self.get_tf_ret_serialization_from_cls(tf_obj) ret2 = self.extract_ret(ret2, self.RefBackend.TF) + if tf_obj.__class__.__name__.startswith(("Polar", "Dipole")): + # tf, pt serialization mismatch + common_keys = set(data1.keys()) & set(data2.keys()) + data1 = {k: data1[k] for k in common_keys} + data2 = {k: data2[k] for k in common_keys} np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): - np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) + np.testing.assert_allclose( + rr1.ravel(), rr2.ravel(), rtol=self.rtol, atol=self.atol + ) assert rr1.dtype == rr2.dtype, f"{rr1.dtype} != {rr2.dtype}" def test_tf_self_consistent(self): @@ -319,6 +326,11 @@ def test_pt_consistent_with_ref(self): ret2 = self.eval_pt(obj) ret2 = self.extract_ret(ret2, self.RefBackend.PT) data2 = obj.serialize() + if obj.__class__.__name__.startswith(("Polar", "Dipole")): + # tf, pt serialization mismatch + common_keys = set(data1.keys()) & set(data2.keys()) + data1 = {k: data1[k] for k in common_keys} + data2 = {k: data2[k] for k in common_keys} np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): np.testing.assert_allclose(rr1, rr2, rtol=self.rtol, atol=self.atol) diff --git a/source/tests/consistent/fitting/common.py b/source/tests/consistent/fitting/common.py index 276e81dbc6..bdd4b7cf81 100644 --- a/source/tests/consistent/fitting/common.py +++ b/source/tests/consistent/fitting/common.py @@ -42,3 +42,37 @@ def build_tf_fitting(self, obj, inputs, natoms, atype, fparam, suffix): t_atype: atype, **feed_dict, } + + +class DipoleFittingTest: + """Useful utilities for descriptor tests.""" + + def build_tf_fitting(self, obj, inputs, rot_mat, natoms, atype, fparam, suffix): + t_inputs = tf.placeholder(GLOBAL_TF_FLOAT_PRECISION, [None], name="i_inputs") + t_rot_mat = tf.placeholder( + GLOBAL_TF_FLOAT_PRECISION, rot_mat.shape, name="i_rot_mat" + ) + t_natoms = tf.placeholder(tf.int32, natoms.shape, name="i_natoms") + t_atype = tf.placeholder(tf.int32, [None], name="i_atype") + extras = {} + feed_dict = {} + if fparam is not None: + t_fparam = tf.placeholder( + GLOBAL_TF_FLOAT_PRECISION, [None], name="i_fparam" + ) + extras["fparam"] = t_fparam + feed_dict[t_fparam] = fparam + t_out = obj.build( + t_inputs, + t_rot_mat, + t_natoms, + {"atype": t_atype, **extras}, + suffix=suffix, + ) + return [t_out], { + t_inputs: inputs, + t_rot_mat: rot_mat, + t_natoms: natoms, + t_atype: atype, + **feed_dict, + } diff --git a/source/tests/consistent/fitting/test_dipole.py b/source/tests/consistent/fitting/test_dipole.py new file mode 100644 index 0000000000..7b5d4d59e8 --- /dev/null +++ b/source/tests/consistent/fitting/test_dipole.py @@ -0,0 +1,192 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest +from typing import ( + Any, + Tuple, +) + +import numpy as np + +from deepmd.dpmodel.fitting.dipole_fitting import DipoleFitting as DipoleFittingDP +from deepmd.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) + +from ..common import ( + INSTALLED_PT, + INSTALLED_TF, + CommonTest, + parameterized, +) +from .common import ( + DipoleFittingTest, +) + +if INSTALLED_PT: + import torch + + from deepmd.pt.model.task.dipole import DipoleFittingNet as DipoleFittingPT + from deepmd.pt.utils.env import DEVICE as PT_DEVICE +else: + DipoleFittingPT = object +if INSTALLED_TF: + from deepmd.tf.fit.dipole import DipoleFittingSeA as DipoleFittingTF +else: + DipoleFittingTF = object +from deepmd.utils.argcheck import ( + fitting_dipole, +) + + +@parameterized( + (True, False), # resnet_dt + ("float64", "float32"), # precision + (True, False), # mixed_types +) +class TestDipole(CommonTest, DipoleFittingTest, unittest.TestCase): + @property + def data(self) -> dict: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + return { + "neuron": [5, 5, 5], + "resnet_dt": resnet_dt, + "precision": precision, + "seed": 20240217, + } + + @property + def skip_tf(self) -> bool: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + # TODO: mixed_types + return mixed_types or CommonTest.skip_pt + + @property + def skip_pt(self) -> bool: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + return CommonTest.skip_pt + + tf_class = DipoleFittingTF + dp_class = DipoleFittingDP + pt_class = DipoleFittingPT + args = fitting_dipole() + + def setUp(self): + CommonTest.setUp(self) + + self.ntypes = 2 + self.natoms = np.array([6, 6, 2, 4], dtype=np.int32) + self.inputs = np.ones((1, 6, 20), dtype=GLOBAL_NP_FLOAT_PRECISION) + self.gr = np.ones((1, 6, 30, 3), dtype=GLOBAL_NP_FLOAT_PRECISION) + self.atype = np.array([0, 1, 1, 0, 1, 1], dtype=np.int32) + # inconsistent if not sorted + self.atype.sort() + + @property + def addtional_data(self) -> dict: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + return { + "ntypes": self.ntypes, + "dim_descrpt": self.inputs.shape[-1], + "mixed_types": mixed_types, + "var_name": "dipole", + "embedding_width": 30, + } + + def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + return self.build_tf_fitting( + obj, + self.inputs.ravel(), + self.gr, + self.natoms, + self.atype, + None, + suffix, + ) + + def eval_pt(self, pt_obj: Any) -> Any: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + return ( + pt_obj( + torch.from_numpy(self.inputs).to(device=PT_DEVICE), + torch.from_numpy(self.atype.reshape(1, -1)).to(device=PT_DEVICE), + torch.from_numpy(self.gr).to(device=PT_DEVICE), + None, + )["dipole"] + .detach() + .cpu() + .numpy() + ) + + def eval_dp(self, dp_obj: Any) -> Any: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + return dp_obj( + self.inputs, + self.atype.reshape(1, -1), + self.gr, + None, + )["dipole"] + + def extract_ret(self, ret: Any, backend) -> Tuple[np.ndarray, ...]: + if backend == self.RefBackend.TF: + # shape is not same + ret = ret[0].reshape(-1, self.natoms[0], 1) + return (ret,) + + @property + def rtol(self) -> float: + """Relative tolerance for comparing the return value.""" + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + if precision == "float64": + return 1e-10 + elif precision == "float32": + return 1e-4 + else: + raise ValueError(f"Unknown precision: {precision}") + + @property + def atol(self) -> float: + """Absolute tolerance for comparing the return value.""" + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + if precision == "float64": + return 1e-10 + elif precision == "float32": + return 1e-4 + else: + raise ValueError(f"Unknown precision: {precision}") diff --git a/source/tests/tf/test_data_large_batch.py b/source/tests/tf/test_data_large_batch.py index 53991fa7f2..4a142192a1 100644 --- a/source/tests/tf/test_data_large_batch.py +++ b/source/tests/tf/test_data_large_batch.py @@ -114,6 +114,7 @@ def test_data_mixed_type(self): descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( @@ -311,6 +312,7 @@ def test_stripped_data_mixed_type(self): descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( @@ -508,6 +510,7 @@ def test_compressible_data_mixed_type(self): descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( diff --git a/source/tests/tf/test_data_modifier.py b/source/tests/tf/test_data_modifier.py index 98b6c41427..cf2c50b761 100644 --- a/source/tests/tf/test_data_modifier.py +++ b/source/tests/tf/test_data_modifier.py @@ -57,7 +57,6 @@ def _setUp(self): restart=None, init_model=None, log_path=None, log_level=30, mpi_log="master" ) jdata = j_loader(INPUT) - # init model model = DPTrainer(jdata, run_opt=run_opt) rcut = model.model.get_rcut() diff --git a/source/tests/tf/test_dipole_se_a.py b/source/tests/tf/test_dipole_se_a.py index f0e495ef21..6905d94371 100644 --- a/source/tests/tf/test_dipole_se_a.py +++ b/source/tests/tf/test_dipole_se_a.py @@ -56,7 +56,9 @@ def test_model(self): descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"].pop("type", None) jdata["model"]["fitting_net"].pop("fit_diag", None) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["embedding_width"] = descrpt.get_dim_rot_mat_1() fitting = DipoleFittingSeA(**jdata["model"]["fitting_net"], uniform_seed=True) model = DipoleModel(descrpt, fitting) diff --git a/source/tests/tf/test_dipole_se_a_tebd.py b/source/tests/tf/test_dipole_se_a_tebd.py index ed403bd047..57e681ff42 100644 --- a/source/tests/tf/test_dipole_se_a_tebd.py +++ b/source/tests/tf/test_dipole_se_a_tebd.py @@ -66,7 +66,9 @@ def test_model(self): descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"].pop("type", None) jdata["model"]["fitting_net"].pop("fit_diag", None) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["embedding_width"] = descrpt.get_dim_rot_mat_1() fitting = DipoleFittingSeA(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( diff --git a/source/tests/tf/test_fitting_ener_type.py b/source/tests/tf/test_fitting_ener_type.py index f88692be74..c1c1698d4f 100644 --- a/source/tests/tf/test_fitting_ener_type.py +++ b/source/tests/tf/test_fitting_ener_type.py @@ -56,6 +56,7 @@ def test_fitting(self): descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) # model._compute_dstats([test_data['coord']], [test_data['box']], [test_data['type']], [test_data['natoms_vec']], [test_data['default_mesh']]) diff --git a/source/tests/tf/test_model_multi.py b/source/tests/tf/test_model_multi.py index b978dff1ab..66b0cce000 100644 --- a/source/tests/tf/test_model_multi.py +++ b/source/tests/tf/test_model_multi.py @@ -69,6 +69,7 @@ def test_model(self): item_fitting_param.pop("type", None) item_fitting_param.pop("fit_diag", None) item_fitting_param["descrpt"] = descrpt + item_fitting_param["embedding_width"] = descrpt.get_dim_rot_mat_1() item_fitting_param["ntypes"] = descrpt.get_ntypes() item_fitting_param["dim_descrpt"] = descrpt.get_dim_out() if item_fitting_type == "ener": diff --git a/source/tests/tf/test_model_se_a.py b/source/tests/tf/test_model_se_a.py index e60cb2307f..414bee2b83 100644 --- a/source/tests/tf/test_model_se_a.py +++ b/source/tests/tf/test_model_se_a.py @@ -76,6 +76,7 @@ def test_model_atom_ener(self): descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) model = EnerModel(descrpt, fitting) @@ -157,6 +158,7 @@ def test_model(self): descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) model = EnerModel(descrpt, fitting) @@ -302,6 +304,7 @@ def test_model_atom_ener_type_embedding(self): descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) model = EnerModel(descrpt, fitting, typeebd=typeebd) diff --git a/source/tests/tf/test_model_se_a_aparam.py b/source/tests/tf/test_model_se_a_aparam.py index 6bf059f8fa..00a71f9136 100644 --- a/source/tests/tf/test_model_se_a_aparam.py +++ b/source/tests/tf/test_model_se_a_aparam.py @@ -55,6 +55,7 @@ def test_model(self): descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) model = EnerModel(descrpt, fitting) diff --git a/source/tests/tf/test_model_se_a_ebd.py b/source/tests/tf/test_model_se_a_ebd.py index 599cce6386..e4a6d78d65 100644 --- a/source/tests/tf/test_model_se_a_ebd.py +++ b/source/tests/tf/test_model_se_a_ebd.py @@ -56,6 +56,7 @@ def test_model(self): ) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting( **jdata["model"]["fitting_net"], ) diff --git a/source/tests/tf/test_model_se_a_ebd_v2.py b/source/tests/tf/test_model_se_a_ebd_v2.py index 22b3c3389d..cab5146312 100644 --- a/source/tests/tf/test_model_se_a_ebd_v2.py +++ b/source/tests/tf/test_model_se_a_ebd_v2.py @@ -72,6 +72,7 @@ def test_model(self): ) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting( **jdata["model"]["fitting_net"], ) diff --git a/source/tests/tf/test_model_se_a_fparam.py b/source/tests/tf/test_model_se_a_fparam.py index 4f94fc1655..3045948480 100644 --- a/source/tests/tf/test_model_se_a_fparam.py +++ b/source/tests/tf/test_model_se_a_fparam.py @@ -54,6 +54,7 @@ def test_model(self): descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) # descrpt = DescrptSeA(jdata['model']['descriptor']) # fitting = EnerFitting(jdata['model']['fitting_net'], descrpt) diff --git a/source/tests/tf/test_model_se_a_srtab.py b/source/tests/tf/test_model_se_a_srtab.py index 00f59668a0..2c4d5d70f9 100644 --- a/source/tests/tf/test_model_se_a_srtab.py +++ b/source/tests/tf/test_model_se_a_srtab.py @@ -71,6 +71,7 @@ def test_model(self): descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) # descrpt = DescrptSeA(jdata['model']['descriptor']) # fitting = EnerFitting(jdata['model']['fitting_net'], descrpt) diff --git a/source/tests/tf/test_model_se_a_type.py b/source/tests/tf/test_model_se_a_type.py index 9c0a07cc98..3432bebf2a 100644 --- a/source/tests/tf/test_model_se_a_type.py +++ b/source/tests/tf/test_model_se_a_type.py @@ -57,6 +57,7 @@ def test_model(self): descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( diff --git a/source/tests/tf/test_model_se_atten.py b/source/tests/tf/test_model_se_atten.py index 13e4c554ca..874931cb40 100644 --- a/source/tests/tf/test_model_se_atten.py +++ b/source/tests/tf/test_model_se_atten.py @@ -69,6 +69,7 @@ def test_model(self): descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( @@ -295,6 +296,7 @@ def test_compressible_model(self): descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( @@ -523,6 +525,7 @@ def test_stripped_type_embedding_model(self): descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( @@ -762,6 +765,7 @@ def test_smoothness_of_stripped_type_embedding_smooth_model(self): descrpt = DescrptSeAtten(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["dim_rot_mat_1"] = descrpt.get_dim_rot_mat_1() fitting = EnerFitting(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( From 543276a5601848075a442cad4d7feaeed6aa6457 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Fri, 23 Feb 2024 17:17:16 +0800 Subject: [PATCH 119/686] Feat: add polar consistency test (#3327) This PR is to add cross framework consistency test on PolarFittingNet. Note: `shift_diag` not yet implemented in PT. --------- Signed-off-by: Anyang Peng <137014849+anyangml@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../dpmodel/fitting/polarizability_fitting.py | 15 +- deepmd/pt/model/task/polarizability.py | 16 +- deepmd/tf/fit/polar.py | 97 ++++++++- source/tests/consistent/fitting/test_polar.py | 192 ++++++++++++++++++ source/tests/tf/test_polar_se_a.py | 4 +- source/tests/tf/test_polar_se_a_tebd.py | 4 +- 6 files changed, 309 insertions(+), 19 deletions(-) create mode 100644 source/tests/consistent/fitting/test_polar.py diff --git a/deepmd/dpmodel/fitting/polarizability_fitting.py b/deepmd/dpmodel/fitting/polarizability_fitting.py index 9811e8e1c8..0b22fa03f8 100644 --- a/deepmd/dpmodel/fitting/polarizability_fitting.py +++ b/deepmd/dpmodel/fitting/polarizability_fitting.py @@ -102,6 +102,8 @@ def __init__( fit_diag: bool = True, scale: Optional[List[float]] = None, shift_diag: bool = True, + # not used + seed: Optional[int] = None, ): # seed, uniform_seed are not included if tot_ener_zero: @@ -119,9 +121,16 @@ def __init__( if self.scale is None: self.scale = [1.0 for _ in range(ntypes)] else: - assert ( - isinstance(self.scale, list) and len(self.scale) == ntypes - ), "Scale should be a list of length ntypes." + if isinstance(self.scale, list): + assert ( + len(self.scale) == ntypes + ), "Scale should be a list of length ntypes." + elif isinstance(self.scale, float): + self.scale = [self.scale for _ in range(ntypes)] + else: + raise ValueError( + "Scale must be a list of float of length ntypes or a float." + ) self.scale = np.array(self.scale, dtype=GLOBAL_NP_FLOAT_PRECISION).reshape( ntypes, 1 ) diff --git a/deepmd/pt/model/task/polarizability.py b/deepmd/pt/model/task/polarizability.py index c240567903..9b2d2635cb 100644 --- a/deepmd/pt/model/task/polarizability.py +++ b/deepmd/pt/model/task/polarizability.py @@ -3,6 +3,7 @@ from typing import ( List, Optional, + Union, ) import torch @@ -85,7 +86,7 @@ def __init__( seed: Optional[int] = None, exclude_types: List[int] = [], fit_diag: bool = True, - scale: Optional[List[float]] = None, + scale: Optional[Union[List[float], float]] = None, shift_diag: bool = True, **kwargs, ): @@ -95,9 +96,16 @@ def __init__( if self.scale is None: self.scale = [1.0 for _ in range(ntypes)] else: - assert ( - isinstance(self.scale, list) and len(self.scale) == ntypes - ), "Scale should be a list of length ntypes." + if isinstance(self.scale, list): + assert ( + len(self.scale) == ntypes + ), "Scale should be a list of length ntypes." + elif isinstance(self.scale, float): + self.scale = [self.scale for _ in range(ntypes)] + else: + raise ValueError( + "Scale must be a list of float of length ntypes or a float." + ) self.scale = torch.tensor( self.scale, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE ).view(ntypes, 1) diff --git a/deepmd/tf/fit/polar.py b/deepmd/tf/fit/polar.py index ae02c02064..2f4dbfc280 100644 --- a/deepmd/tf/fit/polar.py +++ b/deepmd/tf/fit/polar.py @@ -42,8 +42,12 @@ class PolarFittingSeA(Fitting): Parameters ---------- - descrpt : tf.Tensor - The descrptor + ntypes + The ntypes of the descrptor :math:`\mathcal{D}` + dim_descrpt + The dimension of the descrptor :math:`\mathcal{D}` + embedding_width + The rotation matrix dimension of the descrptor :math:`\mathcal{D}` neuron : List[int] Number of neurons in each hidden layer of the fitting net resnet_dt : bool @@ -69,7 +73,9 @@ class PolarFittingSeA(Fitting): def __init__( self, - descrpt: tf.Tensor, + ntypes: int, + dim_descrpt: int, + embedding_width: int, neuron: List[int] = [120, 120, 120], resnet_dt: bool = True, sel_type: Optional[List[int]] = None, @@ -84,8 +90,8 @@ def __init__( **kwargs, ) -> None: """Constructor.""" - self.ntypes = descrpt.get_ntypes() - self.dim_descrpt = descrpt.get_dim_out() + self.ntypes = ntypes + self.dim_descrpt = dim_descrpt self.n_neuron = neuron self.resnet_dt = resnet_dt self.sel_type = sel_type @@ -96,6 +102,7 @@ def __init__( # self.diag_shift = diag_shift self.shift_diag = shift_diag self.scale = scale + self.activation_function_name = activation_function self.fitting_activation_fn = get_activation_func(activation_function) self.fitting_precision = get_precision(precision) if self.sel_type is None: @@ -104,7 +111,19 @@ def __init__( [ii in self.sel_type for ii in range(self.ntypes)], dtype=bool ) if self.scale is None: - self.scale = [1.0 for ii in range(self.ntypes)] + self.scale = np.array([1.0 for ii in range(self.ntypes)]) + else: + if isinstance(self.scale, list): + assert ( + len(self.scale) == ntypes + ), "Scale should be a list of length ntypes." + elif isinstance(self.scale, float): + self.scale = [self.scale for _ in range(ntypes)] + else: + raise ValueError( + "Scale must be a list of float of length ntypes or a float." + ) + self.scale = np.array(self.scale) # if self.diag_shift is None: # self.diag_shift = [0.0 for ii in range(self.ntypes)] if not isinstance(self.sel_type, list): @@ -115,10 +134,7 @@ def __init__( ) # self.ntypes x 1, store the average diagonal value # if type(self.diag_shift) is not list: # self.diag_shift = [self.diag_shift] - if not isinstance(self.scale, list): - self.scale = [self.scale for ii in range(self.ntypes)] - self.scale = np.array(self.scale) - self.dim_rot_mat_1 = descrpt.get_dim_rot_mat_1() + self.dim_rot_mat_1 = embedding_width self.dim_rot_mat = self.dim_rot_mat_1 * 3 self.useBN = False self.fitting_net_variables = None @@ -509,6 +525,67 @@ def get_loss(self, loss: dict, lr) -> Loss: label_name="polarizability", ) + def serialize(self, suffix: str) -> dict: + """Serialize the model. + + Returns + ------- + dict + The serialized data + """ + data = { + "var_name": "polar", + "ntypes": self.ntypes, + "dim_descrpt": self.dim_descrpt, + "embedding_width": self.dim_rot_mat_1, + # very bad design: type embedding is not passed to the class + # TODO: refactor the class + "mixed_types": False, + "dim_out": 3, + "neuron": self.n_neuron, + "resnet_dt": self.resnet_dt, + "activation_function": self.activation_function_name, + "precision": self.fitting_precision.name, + "exclude_types": [], + "fit_diag": self.fit_diag, + "scale": list(self.scale), + "shift_diag": self.shift_diag, + "nets": self.serialize_network( + ntypes=self.ntypes, + # TODO: consider type embeddings + ndim=1, + in_dim=self.dim_descrpt, + out_dim=self.dim_rot_mat_1, + neuron=self.n_neuron, + activation_function=self.activation_function_name, + resnet_dt=self.resnet_dt, + variables=self.fitting_net_variables, + suffix=suffix, + ), + } + return data + + @classmethod + def deserialize(cls, data: dict, suffix: str): + """Deserialize the model. + + Parameters + ---------- + data : dict + The serialized data + + Returns + ------- + Model + The deserialized model + """ + fitting = cls(**data) + fitting.fitting_net_variables = cls.deserialize_network( + data["nets"], + suffix=suffix, + ) + return fitting + class GlobalPolarFittingSeA: r"""Fit the system polarizability with descriptor se_a. diff --git a/source/tests/consistent/fitting/test_polar.py b/source/tests/consistent/fitting/test_polar.py new file mode 100644 index 0000000000..7bc11961eb --- /dev/null +++ b/source/tests/consistent/fitting/test_polar.py @@ -0,0 +1,192 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest +from typing import ( + Any, + Tuple, +) + +import numpy as np + +from deepmd.dpmodel.fitting.polarizability_fitting import PolarFitting as PolarFittingDP +from deepmd.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) + +from ..common import ( + INSTALLED_PT, + INSTALLED_TF, + CommonTest, + parameterized, +) +from .common import ( + DipoleFittingTest, +) + +if INSTALLED_PT: + import torch + + from deepmd.pt.model.task.polarizability import PolarFittingNet as PolarFittingPT + from deepmd.pt.utils.env import DEVICE as PT_DEVICE +else: + PolarFittingPT = object +if INSTALLED_TF: + from deepmd.tf.fit.polar import PolarFittingSeA as PolarFittingTF +else: + PolarFittingTF = object +from deepmd.utils.argcheck import ( + fitting_polar, +) + + +@parameterized( + (True, False), # resnet_dt + ("float64", "float32"), # precision + (True, False), # mixed_types +) +class TestPolar(CommonTest, DipoleFittingTest, unittest.TestCase): + @property + def data(self) -> dict: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + return { + "neuron": [5, 5, 5], + "resnet_dt": resnet_dt, + "precision": precision, + "seed": 20240217, + } + + @property + def skip_tf(self) -> bool: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + # TODO: mixed_types + return mixed_types or CommonTest.skip_pt + + @property + def skip_pt(self) -> bool: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + return CommonTest.skip_pt + + tf_class = PolarFittingTF + dp_class = PolarFittingDP + pt_class = PolarFittingPT + args = fitting_polar() + + def setUp(self): + CommonTest.setUp(self) + + self.ntypes = 2 + self.natoms = np.array([6, 6, 2, 4], dtype=np.int32) + self.inputs = np.ones((1, 6, 20), dtype=GLOBAL_NP_FLOAT_PRECISION) + self.gr = np.ones((1, 6, 30, 3), dtype=GLOBAL_NP_FLOAT_PRECISION) + self.atype = np.array([0, 1, 1, 0, 1, 1], dtype=np.int32) + # inconsistent if not sorted + self.atype.sort() + + @property + def addtional_data(self) -> dict: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + return { + "ntypes": self.ntypes, + "dim_descrpt": self.inputs.shape[-1], + "mixed_types": mixed_types, + "var_name": "polar", + "embedding_width": 30, + } + + def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + return self.build_tf_fitting( + obj, + self.inputs.ravel(), + self.gr, + self.natoms, + self.atype, + None, + suffix, + ) + + def eval_pt(self, pt_obj: Any) -> Any: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + return ( + pt_obj( + torch.from_numpy(self.inputs).to(device=PT_DEVICE), + torch.from_numpy(self.atype.reshape(1, -1)).to(device=PT_DEVICE), + torch.from_numpy(self.gr).to(device=PT_DEVICE), + None, + )["polar"] + .detach() + .cpu() + .numpy() + ) + + def eval_dp(self, dp_obj: Any) -> Any: + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + return dp_obj( + self.inputs, + self.atype.reshape(1, -1), + self.gr, + None, + )["polar"] + + def extract_ret(self, ret: Any, backend) -> Tuple[np.ndarray, ...]: + if backend == self.RefBackend.TF: + # shape is not same + ret = ret[0].reshape(-1, self.natoms[0], 1) + return (ret,) + + @property + def rtol(self) -> float: + """Relative tolerance for comparing the return value.""" + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + if precision == "float64": + return 1e-10 + elif precision == "float32": + return 1e-3 + else: + raise ValueError(f"Unknown precision: {precision}") + + @property + def atol(self) -> float: + """Absolute tolerance for comparing the return value.""" + ( + resnet_dt, + precision, + mixed_types, + ) = self.param + if precision == "float64": + return 1e-10 + elif precision == "float32": + return 1e-3 + else: + raise ValueError(f"Unknown precision: {precision}") diff --git a/source/tests/tf/test_polar_se_a.py b/source/tests/tf/test_polar_se_a.py index 04487dec7b..031d9330bc 100644 --- a/source/tests/tf/test_polar_se_a.py +++ b/source/tests/tf/test_polar_se_a.py @@ -55,7 +55,9 @@ def test_model(self): jdata["model"]["descriptor"].pop("type", None) jdata["model"]["fitting_net"].pop("type", None) descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["embedding_width"] = descrpt.get_dim_rot_mat_1() fitting = PolarFittingSeA(**jdata["model"]["fitting_net"], uniform_seed=True) model = PolarModel(descrpt, fitting) diff --git a/source/tests/tf/test_polar_se_a_tebd.py b/source/tests/tf/test_polar_se_a_tebd.py index 38c3ae20ef..c7aa94d5e8 100644 --- a/source/tests/tf/test_polar_se_a_tebd.py +++ b/source/tests/tf/test_polar_se_a_tebd.py @@ -65,7 +65,9 @@ def test_model(self): jdata["model"]["descriptor"].pop("type", None) jdata["model"]["fitting_net"].pop("type", None) descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() + jdata["model"]["fitting_net"]["embedding_width"] = descrpt.get_dim_rot_mat_1() fitting = PolarFittingSeA(**jdata["model"]["fitting_net"], uniform_seed=True) typeebd_param = jdata["model"]["type_embedding"] typeebd = TypeEmbedNet( From 649fdcacec434e0275009434fc2573efec53cedb Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 23 Feb 2024 04:20:45 -0500 Subject: [PATCH 120/686] store type in descriptor serialization data (#3325) Signed-off-by: Jinzhe Zeng --- deepmd/common.py | 21 ++++++ .../dpmodel/atomic_model/dp_atomic_model.py | 9 +-- deepmd/dpmodel/descriptor/base_descriptor.py | 1 + .../descriptor/make_base_descriptor.py | 66 +++++++++++++++++-- deepmd/dpmodel/descriptor/se_e2_a.py | 5 ++ .../pt/model/atomic_model/dp_atomic_model.py | 9 +-- deepmd/pt/model/descriptor/descriptor.py | 41 +++++++++--- deepmd/pt/model/descriptor/se_a.py | 4 ++ deepmd/tf/descriptor/descriptor.py | 17 ++--- deepmd/tf/descriptor/se_a.py | 4 ++ deepmd/tf/fit/fitting.py | 21 +++--- deepmd/tf/model/model.py | 28 ++++---- 12 files changed, 170 insertions(+), 56 deletions(-) diff --git a/deepmd/common.py b/deepmd/common.py index 691cc262df..d7e485788b 100644 --- a/deepmd/common.py +++ b/deepmd/common.py @@ -313,3 +313,24 @@ def get_hash(obj) -> str: object to hash """ return sha1(json.dumps(obj).encode("utf-8")).hexdigest() + + +def j_get_type(data: dict, class_name: str = "object") -> str: + """Get the type from the data. + + Parameters + ---------- + data : dict + the data + class_name : str, optional + the name of the class for error message, by default "object" + + Returns + ------- + str + the type + """ + try: + return data["type"] + except KeyError as e: + raise KeyError(f"the type of the {class_name} should be set by `type`") from e diff --git a/deepmd/dpmodel/atomic_model/dp_atomic_model.py b/deepmd/dpmodel/atomic_model/dp_atomic_model.py index 220c072765..99fee6e050 100644 --- a/deepmd/dpmodel/atomic_model/dp_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/dp_atomic_model.py @@ -9,8 +9,8 @@ import numpy as np -from deepmd.dpmodel.descriptor import ( # noqa # TODO: should import all descriptors! - DescrptSeA, +from deepmd.dpmodel.descriptor.base_descriptor import ( + BaseDescriptor, ) from deepmd.dpmodel.fitting import ( # noqa # TODO: should import all fittings! EnergyFittingNet, @@ -135,16 +135,13 @@ def serialize(self) -> dict: "type_map": self.type_map, "descriptor": self.descriptor.serialize(), "fitting": self.fitting.serialize(), - "descriptor_name": self.descriptor.__class__.__name__, "fitting_name": self.fitting.__class__.__name__, } @classmethod def deserialize(cls, data) -> "DPAtomicModel": data = copy.deepcopy(data) - descriptor_obj = getattr( - sys.modules[__name__], data["descriptor_name"] - ).deserialize(data["descriptor"]) + descriptor_obj = BaseDescriptor.deserialize(data["descriptor"]) fitting_obj = getattr(sys.modules[__name__], data["fitting_name"]).deserialize( data["fitting"] ) diff --git a/deepmd/dpmodel/descriptor/base_descriptor.py b/deepmd/dpmodel/descriptor/base_descriptor.py index ca403d7f8e..7429d3f213 100644 --- a/deepmd/dpmodel/descriptor/base_descriptor.py +++ b/deepmd/dpmodel/descriptor/base_descriptor.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later + import numpy as np from .make_base_descriptor import ( diff --git a/deepmd/dpmodel/descriptor/make_base_descriptor.py b/deepmd/dpmodel/descriptor/make_base_descriptor.py index 7bd553db9e..2cdb5abd52 100644 --- a/deepmd/dpmodel/descriptor/make_base_descriptor.py +++ b/deepmd/dpmodel/descriptor/make_base_descriptor.py @@ -1,17 +1,24 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from abc import ( ABC, - abstractclassmethod, abstractmethod, ) from typing import ( + Callable, List, Optional, + Type, ) +from deepmd.common import ( + j_get_type, +) from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.plugin import ( + Plugin, +) def make_base_descriptor( @@ -33,6 +40,42 @@ def make_base_descriptor( class BD(ABC): """Base descriptor provides the interfaces of descriptor.""" + __plugins = Plugin() + + @staticmethod + def register(key: str) -> Callable: + """Register a descriptor plugin. + + Parameters + ---------- + key : str + the key of a descriptor + + Returns + ------- + Descriptor + the registered descriptor + + Examples + -------- + >>> @Descriptor.register("some_descrpt") + class SomeDescript(Descriptor): + pass + """ + return BD.__plugins.register(key) + + def __new__(cls, *args, **kwargs): + if cls is BD: + cls = cls.get_class_by_type(j_get_type(kwargs, cls.__name__)) + return super().__new__(cls) + + @classmethod + def get_class_by_type(cls, descrpt_type: str) -> Type["BD"]: + if descrpt_type in BD.__plugins.plugins: + return BD.__plugins.plugins[descrpt_type] + else: + raise RuntimeError("Unknown descriptor type: " + descrpt_type) + @abstractmethod def get_rcut(self) -> float: """Returns the cut-off radius.""" @@ -95,10 +138,23 @@ def serialize(self) -> dict: """Serialize the obj to dict.""" pass - @abstractclassmethod - def deserialize(cls): - """Deserialize from a dict.""" - pass + @classmethod + def deserialize(cls, data: dict) -> "BD": + """Deserialize the model. + + Parameters + ---------- + data : dict + The serialized data + + Returns + ------- + BD + The deserialized descriptor + """ + if cls is BD: + return BD.get_class_by_type(data["type"]).deserialize(data) + raise NotImplementedError("Not implemented in class %s" % cls.__name__) setattr(BD, fwd_method_name, BD.fwd) delattr(BD, "fwd") diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index 1802b5bab6..be2ed12394 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -34,6 +34,7 @@ ) +@BaseDescriptor.register("se_e2_a") class DescrptSeA(NativeOP, BaseDescriptor): r"""DeepPot-SE constructed from all information (both angular and radial) of atomic configurations. The embedding takes the distance between atoms as input. @@ -313,6 +314,8 @@ def call( def serialize(self) -> dict: """Serialize the descriptor to dict.""" return { + "@class": "Descriptor", + "type": "se_e2_a", "rcut": self.rcut, "rcut_smth": self.rcut_smth, "sel": self.sel, @@ -339,6 +342,8 @@ def serialize(self) -> dict: def deserialize(cls, data: dict) -> "DescrptSeA": """Deserialize from dict.""" data = copy.deepcopy(data) + data.pop("@class", None) + data.pop("type", None) variables = data.pop("@variables") embeddings = data.pop("embeddings") env_mat = data.pop("env_mat") diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index dafc9e109e..98bf6c0fde 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -13,8 +13,8 @@ from deepmd.dpmodel import ( FittingOutputDef, ) -from deepmd.pt.model.descriptor.se_a import ( # noqa # TODO: should import all descriptors!!! - DescrptSeA, +from deepmd.pt.model.descriptor.descriptor import ( + Descriptor, ) from deepmd.pt.model.task.ener import ( # noqa # TODO: should import all fittings! EnergyFittingNet, @@ -98,16 +98,13 @@ def serialize(self) -> dict: "type_map": self.type_map, "descriptor": self.descriptor.serialize(), "fitting": self.fitting_net.serialize(), - "descriptor_name": self.descriptor.__class__.__name__, "fitting_name": self.fitting_net.__class__.__name__, } @classmethod def deserialize(cls, data) -> "DPAtomicModel": data = copy.deepcopy(data) - descriptor_obj = getattr( - sys.modules[__name__], data["descriptor_name"] - ).deserialize(data["descriptor"]) + descriptor_obj = Descriptor.deserialize(data["descriptor"]) fitting_obj = getattr(sys.modules[__name__], data["fitting_name"]).deserialize( data["fitting"] ) diff --git a/deepmd/pt/model/descriptor/descriptor.py b/deepmd/pt/model/descriptor/descriptor.py index 16659e444d..091f2b1e20 100644 --- a/deepmd/pt/model/descriptor/descriptor.py +++ b/deepmd/pt/model/descriptor/descriptor.py @@ -9,10 +9,14 @@ Dict, List, Optional, + Type, ) import torch +from deepmd.common import ( + j_get_type, +) from deepmd.pt.model.network.network import ( TypeEmbedNet, ) @@ -92,16 +96,37 @@ def data_stat_key(self): def __new__(cls, *args, **kwargs): if cls is Descriptor: - try: - descrpt_type = kwargs["type"] - except KeyError: - raise KeyError("the type of descriptor should be set by `type`") - if descrpt_type in Descriptor.__plugins.plugins: - cls = Descriptor.__plugins.plugins[descrpt_type] - else: - raise RuntimeError("Unknown descriptor type: " + descrpt_type) + cls = cls.get_class_by_type(j_get_type(kwargs, cls.__name__)) return super().__new__(cls) + @classmethod + def get_class_by_type(cls, descrpt_type: str) -> Type["Descriptor"]: + if descrpt_type in Descriptor.__plugins.plugins: + return Descriptor.__plugins.plugins[descrpt_type] + else: + raise RuntimeError("Unknown descriptor type: " + descrpt_type) + + @classmethod + def deserialize(cls, data: dict) -> "Descriptor": + """Deserialize the model. + + There is no suffix in a native DP model, but it is important + for the TF backend. + + Parameters + ---------- + data : dict + The serialized data + + Returns + ------- + Descriptor + The deserialized descriptor + """ + if cls is Descriptor: + return Descriptor.get_class_by_type(data["type"]).deserialize(data) + raise NotImplementedError("Not implemented in class %s" % cls.__name__) + class DescriptorBlock(torch.nn.Module, ABC): """The building block of descriptor. diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 4134c963da..0550488ecf 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -193,6 +193,8 @@ def set_stat_mean_and_stddev( def serialize(self) -> dict: obj = self.sea return { + "@class": "Descriptor", + "type": "se_e2_a", "rcut": obj.rcut, "rcut_smth": obj.rcut_smth, "sel": obj.sel, @@ -219,6 +221,8 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "DescrptSeA": data = data.copy() + data.pop("@class", None) + data.pop("type", None) variables = data.pop("@variables") embeddings = data.pop("embeddings") env_mat = data.pop("env_mat") diff --git a/deepmd/tf/descriptor/descriptor.py b/deepmd/tf/descriptor/descriptor.py index 768e233245..48329ceb48 100644 --- a/deepmd/tf/descriptor/descriptor.py +++ b/deepmd/tf/descriptor/descriptor.py @@ -13,6 +13,9 @@ import numpy as np +from deepmd.common import ( + j_get_type, +) from deepmd.tf.env import ( GLOBAL_TF_FLOAT_PRECISION, tf, @@ -67,11 +70,7 @@ class SomeDescript(Descriptor): return Descriptor.__plugins.register(key) @classmethod - def get_class_by_input(cls, input: dict): - try: - descrpt_type = input["type"] - except KeyError: - raise KeyError("the type of descriptor should be set by `type`") + def get_class_by_type(cls, descrpt_type: str): if descrpt_type in Descriptor.__plugins.plugins: return Descriptor.__plugins.plugins[descrpt_type] else: @@ -79,7 +78,7 @@ def get_class_by_input(cls, input: dict): def __new__(cls, *args, **kwargs): if cls is Descriptor: - cls = cls.get_class_by_input(kwargs) + cls = cls.get_class_by_type(j_get_type(kwargs, cls.__name__)) return super().__new__(cls) @abstractmethod @@ -507,7 +506,7 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): The local data refer to the current class """ # call subprocess - cls = cls.get_class_by_input(local_jdata) + cls = cls.get_class_by_type(j_get_type(local_jdata, cls.__name__)) return cls.update_sel(global_jdata, local_jdata) @classmethod @@ -530,7 +529,9 @@ def deserialize(cls, data: dict, suffix: str = "") -> "Descriptor": The deserialized descriptor """ if cls is Descriptor: - return Descriptor.get_class_by_input(data).deserialize(data, suffix=suffix) + return Descriptor.get_class_by_type( + j_get_type(data, cls.__name__) + ).deserialize(data, suffix=suffix) raise NotImplementedError("Not implemented in class %s" % cls.__name__) def serialize(self, suffix: str = "") -> dict: diff --git a/deepmd/tf/descriptor/se_a.py b/deepmd/tf/descriptor/se_a.py index 0e0cb664a4..e1b7258c63 100644 --- a/deepmd/tf/descriptor/se_a.py +++ b/deepmd/tf/descriptor/se_a.py @@ -1368,6 +1368,8 @@ def deserialize(cls, data: dict, suffix: str = ""): if cls is not DescrptSeA: raise NotImplementedError("Not implemented in class %s" % cls.__name__) data = data.copy() + data.pop("@class", None) + data.pop("type", None) embedding_net_variables = cls.deserialize_network( data.pop("embeddings"), suffix=suffix ) @@ -1418,6 +1420,8 @@ def serialize(self, suffix: str = "") -> dict: # but instead a part of the input data. Maybe the interface should be refactored... return { + "@class": "Descriptor", + "type": "se_e2_a", "rcut": self.rcut_r, "rcut_smth": self.rcut_r_smth, "sel": self.sel_a, diff --git a/deepmd/tf/fit/fitting.py b/deepmd/tf/fit/fitting.py index 598d020fe9..a24efcfdcd 100644 --- a/deepmd/tf/fit/fitting.py +++ b/deepmd/tf/fit/fitting.py @@ -10,6 +10,9 @@ Type, ) +from deepmd.common import ( + j_get_type, +) from deepmd.dpmodel.utils.network import ( FittingNet, NetworkCollection, @@ -53,23 +56,19 @@ class SomeFitting(Fitting): return Fitting.__plugins.register(key) @classmethod - def get_class_by_input(cls, data: dict) -> Type["Fitting"]: - """Get the fitting class by the input data. + def get_class_by_type(cls, fitting_type: str) -> Type["Fitting"]: + """Get the fitting class by the input type. Parameters ---------- - data : dict - The input data + fitting_type : str + The input type Returns ------- Fitting The fitting class """ - try: - fitting_type = data["type"] - except KeyError: - raise KeyError("the type of fitting should be set by `type`") if fitting_type in Fitting.__plugins.plugins: cls = Fitting.__plugins.plugins[fitting_type] else: @@ -78,7 +77,7 @@ def get_class_by_input(cls, data: dict) -> Type["Fitting"]: def __new__(cls, *args, **kwargs): if cls is Fitting: - cls = cls.get_class_by_input(kwargs) + cls = cls.get_class_by_type(j_get_type(kwargs, cls.__name__)) return super().__new__(cls) @property @@ -149,7 +148,9 @@ def deserialize(cls, data: dict, suffix: str = "") -> "Fitting": The deserialized fitting """ if cls is Fitting: - return Fitting.get_class_by_input(data).deserialize(data, suffix=suffix) + return Fitting.get_class_by_type( + j_get_type(data, cls.__name__) + ).deserialize(data, suffix=suffix) raise NotImplementedError("Not implemented in class %s" % cls.__name__) def serialize(self, suffix: str = "") -> dict: diff --git a/deepmd/tf/model/model.py b/deepmd/tf/model/model.py index 09b55f4a04..666ac5f1f4 100644 --- a/deepmd/tf/model/model.py +++ b/deepmd/tf/model/model.py @@ -14,6 +14,9 @@ Union, ) +from deepmd.common import ( + j_get_type, +) from deepmd.tf.descriptor.descriptor import ( Descriptor, ) @@ -92,13 +95,13 @@ class Model(ABC): """ @classmethod - def get_class_by_input(cls, input: dict): - """Get the class by input data. + def get_class_by_type(cls, model_type: str): + """Get the class by input type. Parameters ---------- - input : dict - The input data + model_type : str + The input type """ # infer model type by fitting_type from deepmd.tf.model.frozen import ( @@ -117,7 +120,6 @@ def get_class_by_input(cls, input: dict): PairwiseDPRc, ) - model_type = input.get("type", "standard") if model_type == "standard": return StandardModel elif model_type == "multi": @@ -136,7 +138,7 @@ def get_class_by_input(cls, input: dict): def __new__(cls, *args, **kwargs): if cls is Model: # init model - cls = cls.get_class_by_input(kwargs) + cls = cls.get_class_by_type(kwargs.get("type", "standard")) return cls.__new__(cls, *args, **kwargs) return super().__new__(cls) @@ -575,7 +577,7 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict) -> dict: dict The updated local data """ - cls = cls.get_class_by_input(local_jdata) + cls = cls.get_class_by_type(local_jdata.get("type", "standard")) return cls.update_sel(global_jdata, local_jdata) @classmethod @@ -598,7 +600,9 @@ def deserialize(cls, data: dict, suffix: str = "") -> "Model": The deserialized Model """ if cls is Model: - return Model.get_class_by_input(data).deserialize(data) + return Model.get_class_by_type(data.get("type", "standard")).deserialize( + data + ) raise NotImplementedError("Not implemented in class %s" % cls.__name__) def serialize(self, suffix: str = "") -> dict: @@ -646,7 +650,9 @@ def __new__(cls, *args, **kwargs): if cls is StandardModel: if isinstance(kwargs["fitting_net"], dict): - fitting_type = Fitting.get_class_by_input(kwargs["fitting_net"]) + fitting_type = Fitting.get_class_by_type( + j_get_type(kwargs["fitting_net"], cls.__name__) + ) elif isinstance(kwargs["fitting_net"], Fitting): fitting_type = type(kwargs["fitting_net"]) else: @@ -810,9 +816,6 @@ def deserialize(cls, data: dict, suffix: str = "") -> "Descriptor": """ data = copy.deepcopy(data) - data["descriptor"]["type"] = { - "DescrptSeA": "se_e2_a", - }[data.pop("descriptor_name")] data["fitting"]["type"] = { "EnergyFittingNet": "ener", }[data.pop("fitting_name")] @@ -845,7 +848,6 @@ def serialize(self, suffix: str = "") -> dict: "type_map": self.type_map, "descriptor": self.descrpt.serialize(suffix=suffix), "fitting": self.fitting.serialize(suffix=suffix), - "descriptor_name": self.descrpt.__class__.__name__, "fitting_name": {"EnerFitting": "EnergyFittingNet"}[ self.fitting.__class__.__name__ ], From 03ca9abe033398b88592784c5589a900fbb239a5 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 24 Feb 2024 10:46:45 -0500 Subject: [PATCH 121/686] docs: install pytorch in RTD (#3333) Fix No module named 'torch' --------- Signed-off-by: Jinzhe Zeng --- doc/environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/environment.yml b/doc/environment.yml index 97060c3004..635f24fe1e 100644 --- a/doc/environment.yml +++ b/doc/environment.yml @@ -7,7 +7,7 @@ dependencies: - python=3.9 - pip>=20.1 - pip: - - ..[docs,cpu] + - ..[docs,cpu,torch] - "exhale @ https://github.com/svenevs/exhale/archive/2759a394268307b88f5440487ae0920ee4ebf81e.zip" # https://github.com/mcmtroffaes/sphinxcontrib-bibtex/issues/309 - docutils!=0.18.*,!=0.19.* From 15df69be9e214493dbeb137bb2857ece6863ac74 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 24 Feb 2024 10:48:13 -0500 Subject: [PATCH 122/686] feat: add NumPy DeepPot (#3332) While a DPModel cannot be directly trained, it can be converted from another model: ```sh dp convert-backend frozen_model.pth frozen_model.dp dp test -m frozen_model.dp -s ../data/ ``` The energy result is consistent with TF and PT. Force and virial are NaN, as expected. Signed-off-by: Jinzhe Zeng --- deepmd/backend/dpmodel.py | 8 +- deepmd/dpmodel/infer/__init__.py | 1 + deepmd/dpmodel/infer/deep_eval.py | 372 ++++++++++++++++++++++++++ deepmd/dpmodel/utils/batch_size.py | 27 ++ source/tests/consistent/io/test_io.py | 5 +- 5 files changed, 410 insertions(+), 3 deletions(-) create mode 100644 deepmd/dpmodel/infer/__init__.py create mode 100644 deepmd/dpmodel/infer/deep_eval.py create mode 100644 deepmd/dpmodel/utils/batch_size.py diff --git a/deepmd/backend/dpmodel.py b/deepmd/backend/dpmodel.py index e5c09349cf..64df95586d 100644 --- a/deepmd/backend/dpmodel.py +++ b/deepmd/backend/dpmodel.py @@ -34,7 +34,7 @@ class DPModelBackend(Backend): name = "DPModel" """The formal name of the backend.""" features: ClassVar[Backend.Feature] = ( - Backend.Feature.NEIGHBOR_STAT | Backend.Feature.IO + Backend.Feature.DEEP_EVAL | Backend.Feature.NEIGHBOR_STAT | Backend.Feature.IO ) """The features of the backend.""" suffixes: ClassVar[List[str]] = [".dp"] @@ -70,7 +70,11 @@ def deep_eval(self) -> Type["DeepEvalBackend"]: type[DeepEvalBackend] The Deep Eval backend of the backend. """ - raise NotImplementedError(f"Unsupported backend: {self.name}") + from deepmd.dpmodel.infer.deep_eval import ( + DeepEval, + ) + + return DeepEval @property def neighbor_stat(self) -> Type["NeighborStat"]: diff --git a/deepmd/dpmodel/infer/__init__.py b/deepmd/dpmodel/infer/__init__.py new file mode 100644 index 0000000000..6ceb116d85 --- /dev/null +++ b/deepmd/dpmodel/infer/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/deepmd/dpmodel/infer/deep_eval.py b/deepmd/dpmodel/infer/deep_eval.py new file mode 100644 index 0000000000..4e2349c0e8 --- /dev/null +++ b/deepmd/dpmodel/infer/deep_eval.py @@ -0,0 +1,372 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Optional, + Tuple, + Type, + Union, +) + +import numpy as np + +from deepmd.dpmodel.model.dp_model import ( + DPModel, +) +from deepmd.dpmodel.output_def import ( + ModelOutputDef, + OutputVariableCategory, + OutputVariableDef, +) +from deepmd.dpmodel.utils.batch_size import ( + AutoBatchSize, +) +from deepmd.dpmodel.utils.network import ( + load_dp_model, +) +from deepmd.infer.deep_dipole import ( + DeepDipole, +) +from deepmd.infer.deep_dos import ( + DeepDOS, +) +from deepmd.infer.deep_eval import DeepEval as DeepEvalWrapper +from deepmd.infer.deep_eval import ( + DeepEvalBackend, +) +from deepmd.infer.deep_polar import ( + DeepPolar, +) +from deepmd.infer.deep_pot import ( + DeepPot, +) +from deepmd.infer.deep_wfc import ( + DeepWFC, +) + +if TYPE_CHECKING: + import ase.neighborlist + + +class DeepEval(DeepEvalBackend): + """NumPy backend implementaion of DeepEval. + + Parameters + ---------- + model_file : Path + The name of the frozen model file. + output_def : ModelOutputDef + The output definition of the model. + *args : list + Positional arguments. + auto_batch_size : bool or int or AutomaticBatchSize, default: False + If True, automatic batch size will be used. If int, it will be used + as the initial batch size. + neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional + The ASE neighbor list class to produce the neighbor list. If None, the + neighbor list will be built natively in the model. + **kwargs : dict + Keyword arguments. + """ + + def __init__( + self, + model_file: str, + output_def: ModelOutputDef, + *args: List[Any], + auto_batch_size: Union[bool, int, AutoBatchSize] = True, + neighbor_list: Optional["ase.neighborlist.NewPrimitiveNeighborList"] = None, + **kwargs: Dict[str, Any], + ): + self.output_def = output_def + self.model_path = model_file + + model_data = load_dp_model(model_file) + self.dp = DPModel.deserialize(model_data["model"]) + self.rcut = self.dp.get_rcut() + self.type_map = self.dp.get_type_map() + if isinstance(auto_batch_size, bool): + if auto_batch_size: + self.auto_batch_size = AutoBatchSize() + else: + self.auto_batch_size = None + elif isinstance(auto_batch_size, int): + self.auto_batch_size = AutoBatchSize(auto_batch_size) + elif isinstance(auto_batch_size, AutoBatchSize): + self.auto_batch_size = auto_batch_size + else: + raise TypeError("auto_batch_size should be bool, int, or AutoBatchSize") + + def get_rcut(self) -> float: + """Get the cutoff radius of this model.""" + return self.rcut + + def get_ntypes(self) -> int: + """Get the number of atom types of this model.""" + return len(self.type_map) + + def get_type_map(self) -> List[str]: + """Get the type map (element name of the atom types) of this model.""" + return self.type_map + + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this DP.""" + return self.dp.get_dim_fparam() + + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this DP.""" + return self.dp.get_dim_aparam() + + @property + def model_type(self) -> Type["DeepEvalWrapper"]: + """The the evaluator of the model type.""" + model_type = self.dp.model_output_type() + if model_type == "energy": + return DeepPot + elif model_type == "dos": + return DeepDOS + elif model_type == "dipole": + return DeepDipole + elif model_type == "polar": + return DeepPolar + elif model_type == "wfc": + return DeepWFC + else: + raise RuntimeError("Unknown model type") + + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return self.dp.get_sel_type() + + def get_numb_dos(self) -> int: + """Get the number of DOS.""" + return 0 + + def get_has_efield(self): + """Check if the model has efield.""" + return False + + def get_ntypes_spin(self): + """Get the number of spin atom types of this model.""" + return 0 + + def eval( + self, + coords: np.ndarray, + cells: np.ndarray, + atom_types: np.ndarray, + atomic: bool = False, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + **kwargs: Dict[str, Any], + ) -> Dict[str, np.ndarray]: + """Evaluate the energy, force and virial by using this DP. + + Parameters + ---------- + coords + The coordinates of atoms. + The array should be of size nframes x natoms x 3 + cells + The cell of the region. + If None then non-PBC is assumed, otherwise using PBC. + The array should be of size nframes x 9 + atom_types + The atom types + The list should contain natoms ints + atomic + Calculate the atomic energy and virial + fparam + The frame parameter. + The array can be of size : + - nframes x dim_fparam. + - dim_fparam. Then all frames are assumed to be provided with the same fparam. + aparam + The atomic parameter + The array can be of size : + - nframes x natoms x dim_aparam. + - natoms x dim_aparam. Then all frames are assumed to be provided with the same aparam. + - dim_aparam. Then all frames and atoms are provided with the same aparam. + **kwargs + Other parameters + + Returns + ------- + output_dict : dict + The output of the evaluation. The keys are the names of the output + variables, and the values are the corresponding output arrays. + """ + if fparam is not None or aparam is not None: + raise NotImplementedError + # convert all of the input to numpy array + atom_types = np.array(atom_types, dtype=np.int32) + coords = np.array(coords) + if cells is not None: + cells = np.array(cells) + natoms, numb_test = self._get_natoms_and_nframes( + coords, atom_types, len(atom_types.shape) > 1 + ) + request_defs = self._get_request_defs(atomic) + out = self._eval_func(self._eval_model, numb_test, natoms)( + coords, cells, atom_types, request_defs + ) + return dict( + zip( + [x.name for x in request_defs], + out, + ) + ) + + def _get_request_defs(self, atomic: bool) -> List[OutputVariableDef]: + """Get the requested output definitions. + + When atomic is True, all output_def are requested. + When atomic is False, only energy (tensor), force, and virial + are requested. + + Parameters + ---------- + atomic : bool + Whether to request the atomic output. + + Returns + ------- + list[OutputVariableDef] + The requested output definitions. + """ + if atomic: + return list(self.output_def.var_defs.values()) + else: + return [ + x + for x in self.output_def.var_defs.values() + if x.category + in ( + OutputVariableCategory.REDU, + OutputVariableCategory.DERV_R, + OutputVariableCategory.DERV_C_REDU, + ) + ] + + def _eval_func(self, inner_func: Callable, numb_test: int, natoms: int) -> Callable: + """Wrapper method with auto batch size. + + Parameters + ---------- + inner_func : Callable + the method to be wrapped + numb_test : int + number of tests + natoms : int + number of atoms + + Returns + ------- + Callable + the wrapper + """ + if self.auto_batch_size is not None: + + def eval_func(*args, **kwargs): + return self.auto_batch_size.execute_all( + inner_func, numb_test, natoms, *args, **kwargs + ) + + else: + eval_func = inner_func + return eval_func + + def _get_natoms_and_nframes( + self, + coords: np.ndarray, + atom_types: np.ndarray, + mixed_type: bool = False, + ) -> Tuple[int, int]: + if mixed_type: + natoms = len(atom_types[0]) + else: + natoms = len(atom_types) + if natoms == 0: + assert coords.size == 0 + else: + coords = np.reshape(np.array(coords), [-1, natoms * 3]) + nframes = coords.shape[0] + return natoms, nframes + + def _eval_model( + self, + coords: np.ndarray, + cells: Optional[np.ndarray], + atom_types: np.ndarray, + request_defs: List[OutputVariableDef], + ): + model = self.dp + + nframes = coords.shape[0] + if len(atom_types.shape) == 1: + natoms = len(atom_types) + atom_types = np.tile(atom_types, nframes).reshape(nframes, -1) + else: + natoms = len(atom_types[0]) + + coord_input = coords.reshape([-1, natoms, 3]) + type_input = atom_types + if cells is not None: + box_input = cells.reshape([-1, 3, 3]) + else: + box_input = None + + do_atomic_virial = any( + x.category == OutputVariableCategory.DERV_C_REDU for x in request_defs + ) + batch_output = model( + coord_input, type_input, box=box_input, do_atomic_virial=do_atomic_virial + ) + if isinstance(batch_output, tuple): + batch_output = batch_output[0] + + results = [] + for odef in request_defs: + # it seems not doing conversion + # dp_name = self._OUTDEF_DP2BACKEND[odef.name] + dp_name = odef.name + if dp_name in batch_output: + shape = self._get_output_shape(odef, nframes, natoms) + if batch_output[dp_name] is not None: + out = batch_output[dp_name].reshape(shape) + else: + out = np.full(shape, np.nan) + results.append(out) + else: + shape = self._get_output_shape(odef, nframes, natoms) + results.append(np.full(np.abs(shape), np.nan)) # this is kinda hacky + return tuple(results) + + def _get_output_shape(self, odef, nframes, natoms): + if odef.category == OutputVariableCategory.DERV_C_REDU: + # virial + return [nframes, *odef.shape[:-1], 9] + elif odef.category == OutputVariableCategory.REDU: + # energy + return [nframes, *odef.shape, 1] + elif odef.category == OutputVariableCategory.DERV_C: + # atom_virial + return [nframes, *odef.shape[:-1], natoms, 9] + elif odef.category == OutputVariableCategory.DERV_R: + # force + return [nframes, *odef.shape[:-1], natoms, 3] + elif odef.category == OutputVariableCategory.OUT: + # atom_energy, atom_tensor + # Something wrong here? + # return [nframes, *shape, natoms, 1] + return [nframes, natoms, *odef.shape, 1] + else: + raise RuntimeError("unknown category") diff --git a/deepmd/dpmodel/utils/batch_size.py b/deepmd/dpmodel/utils/batch_size.py new file mode 100644 index 0000000000..ec9503f3b1 --- /dev/null +++ b/deepmd/dpmodel/utils/batch_size.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from deepmd.utils.batch_size import AutoBatchSize as AutoBatchSizeBase + + +class AutoBatchSize(AutoBatchSizeBase): + """Automatic batch size for NumPy.""" + + def is_gpu_available(self) -> bool: + """Check if GPU is available. + + Returns + ------- + bool + True if GPU is available + """ + return False + + def is_oom_error(self, e: Exception) -> bool: + """Check if the exception is an OOM error. + + Parameters + ---------- + e : Exception + Exception + """ + # NumPy never export numpy.core._exceptions.MemoryError + return False diff --git a/source/tests/consistent/io/test_io.py b/source/tests/consistent/io/test_io.py index b8fae40cda..7b6d374168 100644 --- a/source/tests/consistent/io/test_io.py +++ b/source/tests/consistent/io/test_io.py @@ -115,7 +115,7 @@ def test_deep_eval(self): ).reshape(1, 9) prefix = "test_consistent_io_" + self.__class__.__name__.lower() rets = [] - for backend_name in ("tensorflow", "pytorch"): + for backend_name in ("tensorflow", "pytorch", "dpmodel"): backend = Backend.get_backend(backend_name)() if not backend.is_available: continue @@ -130,6 +130,9 @@ def test_deep_eval(self): rets.append(ret) for ret in rets[1:]: for vv1, vv2 in zip(rets[0], ret): + if np.isnan(vv2).all(): + # expect all nan if not supported + continue np.testing.assert_allclose(vv1, vv2, rtol=1e-12, atol=1e-12) From 91049df4d4cfbdf5074a4915e4409c01cae2333c Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 24 Feb 2024 10:50:57 -0500 Subject: [PATCH 123/686] store type in fitting serialization data (#3331) Signed-off-by: Jinzhe Zeng --- .../dpmodel/atomic_model/dp_atomic_model.py | 11 +--- deepmd/dpmodel/fitting/dipole_fitting.py | 5 ++ deepmd/dpmodel/fitting/ener_fitting.py | 8 +++ deepmd/dpmodel/fitting/general_fitting.py | 3 + deepmd/dpmodel/fitting/invar_fitting.py | 2 + deepmd/dpmodel/fitting/make_base_fitting.py | 66 +++++++++++++++++-- .../dpmodel/fitting/polarizability_fitting.py | 5 ++ .../pt/model/atomic_model/dp_atomic_model.py | 11 +--- deepmd/pt/model/task/__init__.py | 4 ++ deepmd/pt/model/task/dipole.py | 2 + deepmd/pt/model/task/ener.py | 9 +++ deepmd/pt/model/task/fitting.py | 38 +---------- deepmd/pt/model/task/polarizability.py | 2 + deepmd/tf/fit/dipole.py | 2 + deepmd/tf/fit/ener.py | 2 + deepmd/tf/fit/polar.py | 2 + deepmd/tf/model/model.py | 6 -- 17 files changed, 116 insertions(+), 62 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/dp_atomic_model.py b/deepmd/dpmodel/atomic_model/dp_atomic_model.py index 99fee6e050..1a823e369e 100644 --- a/deepmd/dpmodel/atomic_model/dp_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/dp_atomic_model.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import copy -import sys from typing import ( Dict, List, @@ -12,9 +11,8 @@ from deepmd.dpmodel.descriptor.base_descriptor import ( BaseDescriptor, ) -from deepmd.dpmodel.fitting import ( # noqa # TODO: should import all fittings! - EnergyFittingNet, - InvarFitting, +from deepmd.dpmodel.fitting.base_fitting import ( + BaseFitting, ) from deepmd.dpmodel.output_def import ( FittingOutputDef, @@ -135,16 +133,13 @@ def serialize(self) -> dict: "type_map": self.type_map, "descriptor": self.descriptor.serialize(), "fitting": self.fitting.serialize(), - "fitting_name": self.fitting.__class__.__name__, } @classmethod def deserialize(cls, data) -> "DPAtomicModel": data = copy.deepcopy(data) descriptor_obj = BaseDescriptor.deserialize(data["descriptor"]) - fitting_obj = getattr(sys.modules[__name__], data["fitting_name"]).deserialize( - data["fitting"] - ) + fitting_obj = BaseFitting.deserialize(data["fitting"]) obj = cls(descriptor_obj, fitting_obj, type_map=data["type_map"]) return obj diff --git a/deepmd/dpmodel/fitting/dipole_fitting.py b/deepmd/dpmodel/fitting/dipole_fitting.py index 55b237e0fe..e00f031549 100644 --- a/deepmd/dpmodel/fitting/dipole_fitting.py +++ b/deepmd/dpmodel/fitting/dipole_fitting.py @@ -11,6 +11,9 @@ from deepmd.dpmodel import ( DEFAULT_PRECISION, ) +from deepmd.dpmodel.fitting.base_fitting import ( + BaseFitting, +) from deepmd.dpmodel.output_def import ( FittingOutputDef, OutputVariableDef, @@ -22,6 +25,7 @@ ) +@BaseFitting.register("dipole") @fitting_check_output class DipoleFitting(GeneralFitting): r"""Fitting rotationally equivariant diploe of the system. @@ -142,6 +146,7 @@ def _net_out_dim(self): def serialize(self) -> dict: data = super().serialize() + data["type"] = "dipole" data["embedding_width"] = self.embedding_width data["old_impl"] = self.old_impl data["r_differentiable"] = self.r_differentiable diff --git a/deepmd/dpmodel/fitting/ener_fitting.py b/deepmd/dpmodel/fitting/ener_fitting.py index 4196d07926..de41bebf6d 100644 --- a/deepmd/dpmodel/fitting/ener_fitting.py +++ b/deepmd/dpmodel/fitting/ener_fitting.py @@ -20,6 +20,7 @@ ) +@InvarFitting.register("ener") class EnergyFittingNet(InvarFitting): def __init__( self, @@ -70,3 +71,10 @@ def deserialize(cls, data: dict) -> "GeneralFitting": data.pop("var_name") data.pop("dim_out") return super().deserialize(data) + + def serialize(self) -> dict: + """Serialize the fitting to dict.""" + return { + **super().serialize(), + "type": "ener", + } diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index a64fa283ec..152836e928 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -209,6 +209,7 @@ def __getitem__(self, key): def serialize(self) -> dict: """Serialize the fitting to dict.""" return { + "@class": "Fitting", "var_name": self.var_name, "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, @@ -240,6 +241,8 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "GeneralFitting": data = copy.deepcopy(data) + data.pop("@class") + data.pop("type") variables = data.pop("@variables") nets = data.pop("nets") obj = cls(**data) diff --git a/deepmd/dpmodel/fitting/invar_fitting.py b/deepmd/dpmodel/fitting/invar_fitting.py index 3d958301ff..769dc45042 100644 --- a/deepmd/dpmodel/fitting/invar_fitting.py +++ b/deepmd/dpmodel/fitting/invar_fitting.py @@ -22,6 +22,7 @@ ) +@GeneralFitting.register("invar") @fitting_check_output class InvarFitting(GeneralFitting): r"""Fitting the energy (or a rotationally invariant porperty of `dim_out`) of the system. The force and the virial can also be trained. @@ -162,6 +163,7 @@ def __init__( def serialize(self) -> dict: data = super().serialize() + data["type"] = "invar" data["dim_out"] = self.dim_out data["atom_ener"] = self.atom_ener return data diff --git a/deepmd/dpmodel/fitting/make_base_fitting.py b/deepmd/dpmodel/fitting/make_base_fitting.py index 620ff316f1..d206f8e39e 100644 --- a/deepmd/dpmodel/fitting/make_base_fitting.py +++ b/deepmd/dpmodel/fitting/make_base_fitting.py @@ -1,17 +1,24 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from abc import ( ABC, - abstractclassmethod, abstractmethod, ) from typing import ( + Callable, Dict, Optional, + Type, ) +from deepmd.common import ( + j_get_type, +) from deepmd.dpmodel.output_def import ( FittingOutputDef, ) +from deepmd.utils.plugin import ( + Plugin, +) def make_base_fitting( @@ -33,6 +40,42 @@ def make_base_fitting( class BF(ABC): """Base fitting provides the interfaces of fitting net.""" + __plugins = Plugin() + + @staticmethod + def register(key: str) -> Callable[[object], object]: + """Register a descriptor plugin. + + Parameters + ---------- + key : str + the key of a descriptor + + Returns + ------- + callable[[object], object] + the registered descriptor + + Examples + -------- + >>> @Fitting.register("some_fitting") + class SomeFitting(Fitting): + pass + """ + return BF.__plugins.register(key) + + def __new__(cls, *args, **kwargs): + if cls is BF: + cls = cls.get_class_by_type(j_get_type(kwargs, cls.__name__)) + return super().__new__(cls) + + @classmethod + def get_class_by_type(cls, fitting_type: str) -> Type["BF"]: + if fitting_type in BF.__plugins.plugins: + return BF.__plugins.plugins[fitting_type] + else: + raise RuntimeError("Unknown fitting type: " + fitting_type) + @abstractmethod def output_def(self) -> FittingOutputDef: """Returns the output def of the fitting net.""" @@ -65,10 +108,23 @@ def serialize(self) -> dict: """Serialize the obj to dict.""" pass - @abstractclassmethod - def deserialize(cls): - """Deserialize from a dict.""" - pass + @classmethod + def deserialize(cls, data: dict) -> "BF": + """Deserialize the fitting. + + Parameters + ---------- + data : dict + The serialized data + + Returns + ------- + BF + The deserialized fitting + """ + if cls is BF: + return BF.get_class_by_type(data["type"]).deserialize(data) + raise NotImplementedError("Not implemented in class %s" % cls.__name__) setattr(BF, fwd_method_name, BF.fwd) delattr(BF, "fwd") diff --git a/deepmd/dpmodel/fitting/polarizability_fitting.py b/deepmd/dpmodel/fitting/polarizability_fitting.py index 0b22fa03f8..4f7c33b9a8 100644 --- a/deepmd/dpmodel/fitting/polarizability_fitting.py +++ b/deepmd/dpmodel/fitting/polarizability_fitting.py @@ -14,6 +14,9 @@ from deepmd.dpmodel import ( DEFAULT_PRECISION, ) +from deepmd.dpmodel.fitting.base_fitting import ( + BaseFitting, +) from deepmd.dpmodel.output_def import ( FittingOutputDef, OutputVariableDef, @@ -25,6 +28,7 @@ ) +@BaseFitting.register("polar") @fitting_check_output class PolarFitting(GeneralFitting): r"""Fitting rotationally equivariant polarizability of the system. @@ -166,6 +170,7 @@ def _net_out_dim(self): def serialize(self) -> dict: data = super().serialize() + data["type"] = "polar" data["embedding_width"] = self.embedding_width data["old_impl"] = self.old_impl data["fit_diag"] = self.fit_diag diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index 98bf6c0fde..08fd5898a0 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import copy import logging -import sys from typing import ( Dict, List, @@ -16,9 +15,8 @@ from deepmd.pt.model.descriptor.descriptor import ( Descriptor, ) -from deepmd.pt.model.task.ener import ( # noqa # TODO: should import all fittings! - EnergyFittingNet, - InvarFitting, +from deepmd.pt.model.task.base_fitting import ( + BaseFitting, ) from deepmd.pt.utils.utils import ( dict_to_device, @@ -98,16 +96,13 @@ def serialize(self) -> dict: "type_map": self.type_map, "descriptor": self.descriptor.serialize(), "fitting": self.fitting_net.serialize(), - "fitting_name": self.fitting_net.__class__.__name__, } @classmethod def deserialize(cls, data) -> "DPAtomicModel": data = copy.deepcopy(data) descriptor_obj = Descriptor.deserialize(data["descriptor"]) - fitting_obj = getattr(sys.modules[__name__], data["fitting_name"]).deserialize( - data["fitting"] - ) + fitting_obj = BaseFitting.deserialize(data["fitting"]) obj = cls(descriptor_obj, fitting_obj, type_map=data["type_map"]) return obj diff --git a/deepmd/pt/model/task/__init__.py b/deepmd/pt/model/task/__init__.py index 180e5a5211..9430ede766 100644 --- a/deepmd/pt/model/task/__init__.py +++ b/deepmd/pt/model/task/__init__.py @@ -18,6 +18,9 @@ from .fitting import ( Fitting, ) +from .polarizability import ( + PolarFittingNet, +) from .type_predict import ( TypePredictNet, ) @@ -31,4 +34,5 @@ "Fitting", "BaseFitting", "TypePredictNet", + "PolarFittingNet", ] diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py index 88391b1922..bff3dd93bc 100644 --- a/deepmd/pt/model/task/dipole.py +++ b/deepmd/pt/model/task/dipole.py @@ -24,6 +24,7 @@ log = logging.getLogger(__name__) +@GeneralFitting.register("dipole") class DipoleFittingNet(GeneralFitting): """Construct a dipole fitting net. @@ -111,6 +112,7 @@ def _net_out_dim(self): def serialize(self) -> dict: data = super().serialize() + data["type"] = "dipole" data["embedding_width"] = self.embedding_width data["old_impl"] = self.old_impl data["r_differentiable"] = self.r_differentiable diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index d0acc6fe2b..8479111819 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -41,6 +41,7 @@ log = logging.getLogger(__name__) +@GeneralFitting.register("invar") @fitting_check_output class InvarFitting(GeneralFitting): """Construct a fitting net for energy. @@ -129,6 +130,7 @@ def _net_out_dim(self): def serialize(self) -> dict: data = super().serialize() + data["type"] = "invar" data["dim_out"] = self.dim_out data["atom_ener"] = self.atom_ener return data @@ -238,6 +240,13 @@ def deserialize(cls, data: dict) -> "GeneralFitting": data.pop("dim_out") return super().deserialize(data) + def serialize(self) -> dict: + """Serialize the fitting to dict.""" + return { + **super().serialize(), + "type": "ener", + } + @Fitting.register("direct_force") @Fitting.register("direct_force_ener") diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 080bfb5172..6cdf9d9bb7 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -5,7 +5,6 @@ abstractmethod, ) from typing import ( - Callable, List, Optional, ) @@ -37,9 +36,6 @@ from deepmd.pt.utils.exclude_mask import ( AtomExcludeMask, ) -from deepmd.pt.utils.plugin import ( - Plugin, -) from deepmd.pt.utils.stat import ( make_stat_input, ) @@ -55,40 +51,11 @@ class Fitting(torch.nn.Module, BaseFitting): - __plugins = Plugin() - - @staticmethod - def register(key: str) -> Callable: - """Register a Fitting plugin. - - Parameters - ---------- - key : str - the key of a Fitting - - Returns - ------- - Fitting - the registered Fitting - - Examples - -------- - >>> @Fitting.register("some_fitting") - class SomeFitting(Fitting): - pass - """ - return Fitting.__plugins.register(key) + # plugin moved to BaseFitting def __new__(cls, *args, **kwargs): if cls is Fitting: - try: - fitting_type = kwargs["type"] - except KeyError: - raise KeyError("the type of fitting should be set by `type`") - if fitting_type in Fitting.__plugins.plugins: - cls = Fitting.__plugins.plugins[fitting_type] - else: - raise RuntimeError("Unknown fitting type: " + fitting_type) + return BaseFitting.__new__(BaseFitting, *args, **kwargs) return super().__new__(cls) def share_params(self, base_class, shared_level, resume=False): @@ -399,6 +366,7 @@ def __init__( def serialize(self) -> dict: """Serialize the fitting to dict.""" return { + "@class": "Fitting", "var_name": self.var_name, "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, diff --git a/deepmd/pt/model/task/polarizability.py b/deepmd/pt/model/task/polarizability.py index 9b2d2635cb..13b0d56e31 100644 --- a/deepmd/pt/model/task/polarizability.py +++ b/deepmd/pt/model/task/polarizability.py @@ -28,6 +28,7 @@ log = logging.getLogger(__name__) +@GeneralFitting.register("polar") class PolarFittingNet(GeneralFitting): """Construct a polar fitting net. @@ -138,6 +139,7 @@ def _net_out_dim(self): def serialize(self) -> dict: data = super().serialize() + data["type"] = "polar" data["embedding_width"] = self.embedding_width data["old_impl"] = self.old_impl data["fit_diag"] = self.fit_diag diff --git a/deepmd/tf/fit/dipole.py b/deepmd/tf/fit/dipole.py index efb94a85c0..3557d00aa0 100644 --- a/deepmd/tf/fit/dipole.py +++ b/deepmd/tf/fit/dipole.py @@ -344,6 +344,8 @@ def serialize(self, suffix: str) -> dict: The serialized data """ data = { + "@class": "Fitting", + "type": "dipole", "var_name": "dipole", "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index 59a1bfe0bd..0cdd1a1676 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -982,6 +982,8 @@ def serialize(self, suffix: str = "") -> dict: The serialized data """ data = { + "@class": "Fitting", + "type": "ener", "var_name": "energy", "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, diff --git a/deepmd/tf/fit/polar.py b/deepmd/tf/fit/polar.py index 2f4dbfc280..f5cebf9a39 100644 --- a/deepmd/tf/fit/polar.py +++ b/deepmd/tf/fit/polar.py @@ -534,6 +534,8 @@ def serialize(self, suffix: str) -> dict: The serialized data """ data = { + "@class": "Fitting", + "type": "polar", "var_name": "polar", "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, diff --git a/deepmd/tf/model/model.py b/deepmd/tf/model/model.py index 666ac5f1f4..8af4771ff6 100644 --- a/deepmd/tf/model/model.py +++ b/deepmd/tf/model/model.py @@ -816,9 +816,6 @@ def deserialize(cls, data: dict, suffix: str = "") -> "Descriptor": """ data = copy.deepcopy(data) - data["fitting"]["type"] = { - "EnergyFittingNet": "ener", - }[data.pop("fitting_name")] descriptor = Descriptor.deserialize(data.pop("descriptor"), suffix=suffix) fitting = Fitting.deserialize(data.pop("fitting"), suffix=suffix) return cls( @@ -848,7 +845,4 @@ def serialize(self, suffix: str = "") -> dict: "type_map": self.type_map, "descriptor": self.descrpt.serialize(suffix=suffix), "fitting": self.fitting.serialize(suffix=suffix), - "fitting_name": {"EnerFitting": "EnergyFittingNet"}[ - self.fitting.__class__.__name__ - ], } From 261c802d91e2bbcb1ba995c07a3061297ce05aa8 Mon Sep 17 00:00:00 2001 From: Lysithea <52808607+CaRoLZhangxy@users.noreply.github.com> Date: Sun, 25 Feb 2024 11:38:56 +0800 Subject: [PATCH 124/686] pt: add necessary jit.export (#3337) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/pt/model/model/dipole_model.py | 1 + deepmd/pt/model/model/dp_zbl_model.py | 1 + deepmd/pt/model/model/ener_model.py | 1 + deepmd/pt/model/model/make_model.py | 1 + deepmd/pt/model/model/polar_model.py | 1 + 5 files changed, 5 insertions(+) diff --git a/deepmd/pt/model/model/dipole_model.py b/deepmd/pt/model/model/dipole_model.py index 220fdbb273..6629541459 100644 --- a/deepmd/pt/model/model/dipole_model.py +++ b/deepmd/pt/model/model/dipole_model.py @@ -55,6 +55,7 @@ def forward( model_predict["updated_coord"] += coord return model_predict + @torch.jit.export def forward_lower( self, extended_coord, diff --git a/deepmd/pt/model/model/dp_zbl_model.py b/deepmd/pt/model/model/dp_zbl_model.py index 4683f62466..0fd8008f21 100644 --- a/deepmd/pt/model/model/dp_zbl_model.py +++ b/deepmd/pt/model/model/dp_zbl_model.py @@ -58,6 +58,7 @@ def forward( model_predict["force"] = model_ret["dforce"] return model_predict + @torch.jit.export def forward_lower( self, extended_coord, diff --git a/deepmd/pt/model/model/ener_model.py b/deepmd/pt/model/model/ener_model.py index 946cfd20f8..1a5706dbbf 100644 --- a/deepmd/pt/model/model/ener_model.py +++ b/deepmd/pt/model/model/ener_model.py @@ -57,6 +57,7 @@ def forward( model_predict["updated_coord"] += coord return model_predict + @torch.jit.export def forward_lower( self, extended_coord, diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 5c2cb3d298..79634186e4 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -202,6 +202,7 @@ def forward_common_lower( ) return model_predict + @torch.jit.export def format_nlist( self, extended_coord: torch.Tensor, diff --git a/deepmd/pt/model/model/polar_model.py b/deepmd/pt/model/model/polar_model.py index 85aeebc0f5..d956a0344c 100644 --- a/deepmd/pt/model/model/polar_model.py +++ b/deepmd/pt/model/model/polar_model.py @@ -47,6 +47,7 @@ def forward( model_predict["updated_coord"] += coord return model_predict + @torch.jit.export def forward_lower( self, extended_coord, From a3f4a67167ae5fbe4b5f3caf4708aca12960c01e Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 26 Feb 2024 09:33:52 -0500 Subject: [PATCH 125/686] pt: remove env.DEVICE in all `forward` functions (#3330) Ensure the saved JIT model can run on both CPUs and GPUs. --------- Signed-off-by: Jinzhe Zeng Co-authored-by: Chun Cai --- .../model/atomic_model/linear_atomic_model.py | 14 ++++------ .../atomic_model/pairtab_atomic_model.py | 9 +++--- deepmd/pt/model/descriptor/repformer_layer.py | 4 +-- deepmd/pt/model/descriptor/se_a.py | 4 ++- deepmd/pt/model/task/fitting.py | 2 +- deepmd/pt/utils/nlist.py | 21 +++++++------- source/tests/pt/model/test_deeppot.py | 8 ++++++ source/tests/pt/model/test_dipole_fitting.py | 28 +++++++++++++------ 8 files changed, 54 insertions(+), 36 deletions(-) diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 70afbcb0bc..f90fa5f237 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -92,16 +92,14 @@ def get_model_sels(self) -> List[List[int]]: """Get the sels for each individual models.""" return [model.get_sel() for model in self.models] - def _sort_rcuts_sels(self) -> Tuple[List[float], List[int]]: + def _sort_rcuts_sels(self, device: torch.device) -> Tuple[List[float], List[int]]: # sort the pair of rcut and sels in ascending order, first based on sel, then on rcut. - rcuts = torch.tensor( - self.get_model_rcuts(), dtype=torch.float64, device=env.DEVICE - ) - nsels = torch.tensor(self.get_model_nsels(), device=env.DEVICE) + rcuts = torch.tensor(self.get_model_rcuts(), dtype=torch.float64, device=device) + nsels = torch.tensor(self.get_model_nsels(), device=device) zipped = torch.stack( [ - torch.tensor(rcuts, device=env.DEVICE), - torch.tensor(nsels, device=env.DEVICE), + torch.tensor(rcuts, device=device), + torch.tensor(nsels, device=device), ], dim=0, ).T @@ -148,7 +146,7 @@ def forward_atomic( if self.do_grad_r() or self.do_grad_c(): extended_coord.requires_grad_(True) extended_coord = extended_coord.view(nframes, -1, 3) - sorted_rcuts, sorted_sels = self._sort_rcuts_sels() + sorted_rcuts, sorted_sels = self._sort_rcuts_sels(device=extended_coord.device) nlists = build_multiple_neighbor_list( extended_coord, nlist, diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index cf5a70eb88..eff445e799 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -12,9 +12,6 @@ FittingOutputDef, OutputVariableDef, ) -from deepmd.pt.utils import ( - env, -) from deepmd.utils.pair_tab import ( PairTab, ) @@ -160,7 +157,7 @@ def forward_atomic( pairwise_rr = self._get_pairwise_dist( extended_coord, masked_nlist ) # (nframes, nloc, nnei) - self.tab_data = self.tab_data.to(device=env.DEVICE).view( + self.tab_data = self.tab_data.to(device=extended_coord.device).view( int(self.tab_info[-1]), int(self.tab_info[-1]), int(self.tab_info[2]), 4 ) @@ -168,7 +165,9 @@ def forward_atomic( # i_type : (nframes, nloc), this is atype. # j_type : (nframes, nloc, nnei) j_type = extended_atype[ - torch.arange(extended_atype.size(0), device=env.DEVICE)[:, None, None], + torch.arange(extended_atype.size(0), device=extended_coord.device)[ + :, None, None + ], masked_nlist, ] diff --git a/deepmd/pt/model/descriptor/repformer_layer.py b/deepmd/pt/model/descriptor/repformer_layer.py index 66ce38c0f7..55a2cba708 100644 --- a/deepmd/pt/model/descriptor/repformer_layer.py +++ b/deepmd/pt/model/descriptor/repformer_layer.py @@ -446,7 +446,7 @@ def _update_g1_conv( else: gg1 = _apply_switch(gg1, sw) invnnei = (1.0 / float(nnei)) * torch.ones( - (nb, nloc, 1), dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + (nb, nloc, 1), dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=gg1.device ) # nb x nloc x ng2 g1_11 = torch.sum(g2 * gg1, dim=2) * invnnei @@ -474,7 +474,7 @@ def _cal_h2g2( else: g2 = _apply_switch(g2, sw) invnnei = (1.0 / float(nnei)) * torch.ones( - (nb, nloc, 1, 1), dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + (nb, nloc, 1, 1), dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=g2.device ) # nb x nloc x 3 x ng2 h2g2 = torch.matmul(torch.transpose(h2, -1, -2), g2) * invnnei diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 0550488ecf..7fd8b1dc7d 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -467,7 +467,9 @@ def forward( nfnl = dmatrix.shape[0] # pre-allocate a shape to pass jit xyz_scatter = torch.zeros( - [nfnl, 4, self.filter_neuron[-1]], dtype=self.prec, device=env.DEVICE + [nfnl, 4, self.filter_neuron[-1]], + dtype=self.prec, + device=extended_coord.device, ) # nfnl x nnei exclude_mask = self.emask(nlist, extended_atype).view(nfnl, -1) diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 6cdf9d9bb7..6c395d3800 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -535,7 +535,7 @@ def _forward_common( outs = torch.zeros( (nf, nloc, net_dim_out), dtype=env.GLOBAL_PT_FLOAT_PRECISION, - device=env.DEVICE, + device=descriptor.device, ) # jit assertion if self.old_impl: assert self.filter_layers_old is not None diff --git a/deepmd/pt/utils/nlist.py b/deepmd/pt/utils/nlist.py index 0e2d9785f8..cfc75d9438 100644 --- a/deepmd/pt/utils/nlist.py +++ b/deepmd/pt/utils/nlist.py @@ -288,8 +288,9 @@ def extend_coord_with_ghosts( maping extended index to the local index """ + device = coord.device nf, nloc = atype.shape - aidx = torch.tile(torch.arange(nloc, device=env.DEVICE).unsqueeze(0), [nf, 1]) + aidx = torch.tile(torch.arange(nloc, device=device).unsqueeze(0), [nf, 1]) if cell is None: nall = nloc extend_coord = coord.clone() @@ -306,17 +307,17 @@ def extend_coord_with_ghosts( nbuff = torch.ceil(rcut / to_face).to(torch.long) # 3 nbuff = torch.max(nbuff, dim=0, keepdim=False).values - xi = torch.arange(-nbuff[0], nbuff[0] + 1, 1, device=env.DEVICE) - yi = torch.arange(-nbuff[1], nbuff[1] + 1, 1, device=env.DEVICE) - zi = torch.arange(-nbuff[2], nbuff[2] + 1, 1, device=env.DEVICE) + xi = torch.arange(-nbuff[0], nbuff[0] + 1, 1, device=device) + yi = torch.arange(-nbuff[1], nbuff[1] + 1, 1, device=device) + zi = torch.arange(-nbuff[2], nbuff[2] + 1, 1, device=device) xyz = xi.view(-1, 1, 1, 1) * torch.tensor( - [1, 0, 0], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + [1, 0, 0], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=device ) xyz = xyz + yi.view(1, -1, 1, 1) * torch.tensor( - [0, 1, 0], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + [0, 1, 0], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=device ) xyz = xyz + zi.view(1, 1, -1, 1) * torch.tensor( - [0, 0, 1], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + [0, 0, 1], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=device ) xyz = xyz.view(-1, 3) # ns x 3 @@ -333,7 +334,7 @@ def extend_coord_with_ghosts( extend_aidx = torch.tile(aidx.unsqueeze(-2), [1, ns, 1]) return ( - extend_coord.reshape([nf, nall * 3]).to(env.DEVICE), - extend_atype.view([nf, nall]).to(env.DEVICE), - extend_aidx.view([nf, nall]).to(env.DEVICE), + extend_coord.reshape([nf, nall * 3]).to(device), + extend_atype.view([nf, nall]).to(device), + extend_aidx.view([nf, nall]).to(device), ) diff --git a/source/tests/pt/model/test_deeppot.py b/source/tests/pt/model/test_deeppot.py index ee04942ae7..334206a2b0 100644 --- a/source/tests/pt/model/test_deeppot.py +++ b/source/tests/pt/model/test_deeppot.py @@ -115,3 +115,11 @@ def setUp(self): ) freeze(ns) self.model = frozen_model + + # Note: this can not actually disable cuda device to be used + # only can be used to test whether devices are mismatched + @unittest.skipIf(not torch.cuda.is_available(), "CUDA not available") + @unittest.mock.patch("deepmd.pt.utils.env.DEVICE", torch.device("cpu")) + @unittest.mock.patch("deepmd.pt.infer.deep_eval.DEVICE", torch.device("cpu")) + def test_dp_test_cpu(self): + self.test_dp_test() diff --git a/source/tests/pt/model/test_dipole_fitting.py b/source/tests/pt/model/test_dipole_fitting.py index fb04e49484..fcdd408726 100644 --- a/source/tests/pt/model/test_dipole_fitting.py +++ b/source/tests/pt/model/test_dipole_fitting.py @@ -30,6 +30,7 @@ ) from deepmd.pt.utils.utils import ( to_numpy_array, + to_torch_tensor, ) from .test_env_mat import ( @@ -298,10 +299,10 @@ def setUp(self): self.rcut_smth = 0.5 self.sel = [46, 92, 4] self.nf = 1 - self.coord = 2 * torch.rand([self.natoms, 3], dtype=dtype, device="cpu") - cell = torch.rand([3, 3], dtype=dtype, device="cpu") - self.cell = (cell + cell.T) + 5.0 * torch.eye(3, device="cpu") - self.atype = torch.IntTensor([0, 0, 0, 1, 1], device="cpu") + self.coord = 2 * torch.rand([self.natoms, 3], dtype=dtype, device=env.DEVICE) + cell = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) + self.cell = (cell + cell.T) + 5.0 * torch.eye(3, device=env.DEVICE) + self.atype = torch.IntTensor([0, 0, 0, 1, 1], device="cpu").to(env.DEVICE) self.dd0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) self.ft0 = DipoleFittingNet( "dipole", @@ -322,17 +323,26 @@ def test_auto_diff(self): atype = self.atype.view(self.nf, self.natoms) def ff(coord, atype): - return self.model(coord, atype)["global_dipole"].detach().cpu().numpy() + return ( + self.model(to_torch_tensor(coord), to_torch_tensor(atype))[ + "global_dipole" + ] + .detach() + .cpu() + .numpy() + ) - fdf = -finite_difference(ff, self.coord, atype, delta=delta) + fdf = -finite_difference( + ff, to_numpy_array(self.coord), to_numpy_array(atype), delta=delta + ) rff = self.model(self.coord, atype)["force"].detach().cpu().numpy() np.testing.assert_almost_equal(fdf, rff.transpose(0, 2, 1, 3), decimal=places) def test_deepdipole_infer(self): - atype = self.atype.view(self.nf, self.natoms) - coord = self.coord.reshape(1, 5, 3) - cell = self.cell.reshape(1, 9) + atype = to_numpy_array(self.atype.view(self.nf, self.natoms)) + coord = to_numpy_array(self.coord.reshape(1, 5, 3)) + cell = to_numpy_array(self.cell.reshape(1, 9)) jit_md = torch.jit.script(self.model) torch.jit.save(jit_md, self.file_path) load_md = DeepDipole(self.file_path) From 4f70073385fa10832e7f9f81d6133c45272322c9 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 26 Feb 2024 19:56:20 -0500 Subject: [PATCH 126/686] feat(pt/dpmodel): support type_one_side in se_e2_a (#3339) Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/descriptor/se_e2_a.py | 58 +++++++++++++------ deepmd/dpmodel/utils/exclude_mask.py | 3 + deepmd/dpmodel/utils/network.py | 9 +++ deepmd/pt/model/descriptor/se_a.py | 36 +++++++++--- deepmd/tf/descriptor/se.py | 11 +--- .../consistent/descriptor/test_se_e2_a.py | 15 ++++- 6 files changed, 93 insertions(+), 39 deletions(-) diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index be2ed12394..97ab719c62 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -1,4 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import itertools + import numpy as np from deepmd.utils.path import ( @@ -144,8 +146,6 @@ def __init__( seed: Optional[int] = None, ) -> None: ## seed, uniform_seed, multi_task, not included. - if not type_one_side: - raise NotImplementedError("type_one_side == False not implemented") if spin is not None: raise NotImplementedError("spin is not implemented") @@ -171,10 +171,10 @@ def __init__( ndim=(1 if self.type_one_side else 2), network_type="embedding_network", ) - if not self.type_one_side: - raise NotImplementedError("type_one_side == False not implemented") - for ii in range(self.ntypes): - self.embeddings[(ii,)] = EmbeddingNet( + for embedding_idx in itertools.product( + range(self.ntypes), repeat=self.embeddings.ndim + ): + self.embeddings[embedding_idx] = EmbeddingNet( in_dim, self.neuron, self.activation_function, @@ -241,12 +241,12 @@ def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None) def cal_g( self, ss, - ll, + embedding_idx, ): - nf, nloc, nnei = ss.shape[0:3] - ss = ss.reshape(nf, nloc, nnei, 1) - # nf x nloc x nnei x ng - gg = self.embeddings[(ll,)].call(ss) + nf_times_nloc, nnei = ss.shape[0:2] + ss = ss.reshape(nf_times_nloc, nnei, 1) + # (nf x nloc) x nnei x ng + gg = self.embeddings[embedding_idx].call(ss) return gg def call( @@ -292,16 +292,30 @@ def call( sec = np.append([0], np.cumsum(self.sel)) ng = self.neuron[-1] - gr = np.zeros([nf, nloc, ng, 4]) + gr = np.zeros([nf * nloc, ng, 4]) exclude_mask = self.emask.build_type_exclude_mask(nlist, atype_ext) - for tt in range(self.ntypes): - mm = exclude_mask[:, :, sec[tt] : sec[tt + 1]] - tr = rr[:, :, sec[tt] : sec[tt + 1], :] - tr = tr * mm[:, :, :, None] + # merge nf and nloc axis, so for type_one_side == False, + # we don't require atype is the same in all frames + exclude_mask = exclude_mask.reshape(nf * nloc, nnei) + rr = rr.reshape(nf * nloc, nnei, 4) + + for embedding_idx in itertools.product( + range(self.ntypes), repeat=self.embeddings.ndim + ): + if self.type_one_side: + (tt,) = embedding_idx + ti_mask = np.s_[:] + else: + ti, tt = embedding_idx + ti_mask = atype_ext[:, :nloc].ravel() == ti + mm = exclude_mask[ti_mask, sec[tt] : sec[tt + 1]] + tr = rr[ti_mask, sec[tt] : sec[tt + 1], :] + tr = tr * mm[:, :, None] ss = tr[..., 0:1] - gg = self.cal_g(ss, tt) - # nf x nloc x ng x 4 - gr += np.einsum("flni,flnj->flij", gg, tr) + gg = self.cal_g(ss, embedding_idx) + gr_tmp = np.einsum("lni,lnj->lij", gg, tr) + gr[ti_mask] += gr_tmp + gr = gr.reshape(nf, nloc, ng, 4) # nf x nloc x ng x 4 gr /= self.nnei gr1 = gr[:, :, : self.axis_neuron, :] @@ -313,6 +327,12 @@ def call( def serialize(self) -> dict: """Serialize the descriptor to dict.""" + if not self.type_one_side and self.exclude_types: + for embedding_idx in itertools.product(range(self.ntypes), repeat=2): + # not actually used; to match serilization data from TF to pass the test + if embedding_idx in self.emask: + self.embeddings[embedding_idx].clear() + return { "@class": "Descriptor", "type": "se_e2_a", diff --git a/deepmd/dpmodel/utils/exclude_mask.py b/deepmd/dpmodel/utils/exclude_mask.py index 83e3c7a363..360f190e13 100644 --- a/deepmd/dpmodel/utils/exclude_mask.py +++ b/deepmd/dpmodel/utils/exclude_mask.py @@ -115,3 +115,6 @@ def build_type_exclude_mask( type_ij = type_ij.reshape(nf, nloc * nnei) mask = self.type_mask[type_ij].reshape(nf, nloc, nnei) return mask + + def __contains__(self, item): + return item in self.exclude_types diff --git a/deepmd/dpmodel/utils/network.py b/deepmd/dpmodel/utils/network.py index c0a62c9a3e..2133bc4889 100644 --- a/deepmd/dpmodel/utils/network.py +++ b/deepmd/dpmodel/utils/network.py @@ -396,6 +396,15 @@ def call(self, x): x = layer(x) return x + def clear(self): + """Clear the network parameters to zero.""" + for layer in self.layers: + layer.w.fill(0.0) + if layer.b is not None: + layer.b.fill(0.0) + if layer.idt is not None: + layer.idt.fill(0.0) + return NN diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 7fd8b1dc7d..44a7fef126 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import itertools from typing import ( ClassVar, Dict, @@ -67,6 +68,7 @@ def __init__( resnet_dt: bool = False, exclude_types: List[Tuple[int, int]] = [], old_impl: bool = False, + type_one_side: bool = True, **kwargs, ): super().__init__() @@ -82,6 +84,7 @@ def __init__( resnet_dt=resnet_dt, exclude_types=exclude_types, old_impl=old_impl, + type_one_side=type_one_side, **kwargs, ) @@ -214,7 +217,7 @@ def serialize(self) -> dict: }, ## to be updated when the options are supported. "trainable": True, - "type_one_side": True, + "type_one_side": obj.type_one_side, "spin": None, } @@ -255,6 +258,7 @@ def __init__( resnet_dt: bool = False, exclude_types: List[Tuple[int, int]] = [], old_impl: bool = False, + type_one_side: bool = True, **kwargs, ): """Construct an embedding net of type `se_a`. @@ -281,6 +285,7 @@ def __init__( self.exclude_types = exclude_types self.ntypes = len(sel) self.emask = PairExcludeMask(len(sel), exclude_types=exclude_types) + self.type_one_side = type_one_side self.sel = sel self.sec = torch.tensor( @@ -299,6 +304,10 @@ def __init__( self.filter_layers = None if self.old_impl: + if not self.type_one_side: + raise ValueError( + "The old implementation does not support type_one_side=False." + ) filter_layers = [] # TODO: remove start_index = 0 @@ -308,12 +317,12 @@ def __init__( start_index += sel[type_i] self.filter_layers_old = torch.nn.ModuleList(filter_layers) else: + ndim = 1 if self.type_one_side else 2 filter_layers = NetworkCollection( - ndim=1, ntypes=len(sel), network_type="embedding_network" + ndim=ndim, ntypes=len(sel), network_type="embedding_network" ) - # TODO: ndim=2 if type_one_side=False - for ii in range(self.ntypes): - filter_layers[(ii,)] = EmbeddingNet( + for embedding_idx in itertools.product(range(self.ntypes), repeat=ndim): + filter_layers[embedding_idx] = EmbeddingNet( 1, self.filter_neuron, activation_function=self.activation_function, @@ -473,18 +482,27 @@ def forward( ) # nfnl x nnei exclude_mask = self.emask(nlist, extended_atype).view(nfnl, -1) - for ii, ll in enumerate(self.filter_layers.networks): + for embedding_idx, ll in enumerate(self.filter_layers.networks): + if self.type_one_side: + ii = embedding_idx + # torch.jit is not happy with slice(None) + ti_mask = torch.ones(nfnl, dtype=torch.bool, device=dmatrix.device) + else: + # ti: center atom type, ii: neighbor type... + ii = embedding_idx // self.ntypes + ti = embedding_idx % self.ntypes + ti_mask = atype.ravel().eq(ti) # nfnl x nt - mm = exclude_mask[:, self.sec[ii] : self.sec[ii + 1]] + mm = exclude_mask[ti_mask, self.sec[ii] : self.sec[ii + 1]] # nfnl x nt x 4 - rr = dmatrix[:, self.sec[ii] : self.sec[ii + 1], :] + rr = dmatrix[ti_mask, self.sec[ii] : self.sec[ii + 1], :] rr = rr * mm[:, :, None] ss = rr[:, :, :1] # nfnl x nt x ng gg = ll.forward(ss) # nfnl x 4 x ng gr = torch.matmul(rr.permute(0, 2, 1), gg) - xyz_scatter += gr + xyz_scatter[ti_mask] += gr xyz_scatter /= self.nnei xyz_scatter_1 = xyz_scatter.permute(0, 2, 1) diff --git a/deepmd/tf/descriptor/se.py b/deepmd/tf/descriptor/se.py index 98d98cd467..857c8b28df 100644 --- a/deepmd/tf/descriptor/se.py +++ b/deepmd/tf/descriptor/se.py @@ -231,15 +231,8 @@ def serialize_network( resnet_dt=resnet_dt, precision=self.precision.name, ) - for layer in range(len(neuron)): - embeddings[(type_i, type_j)][layer]["w"][:] = 0.0 - embeddings[(type_i, type_j)][layer]["b"][:] = 0.0 - if embeddings[(type_i, type_j)][layer]["idt"] is not None: - embeddings[(type_i, type_j)][layer]["idt"][:] = 0.0 - embeddings[(type_j, type_i)][layer]["w"][:] = 0.0 - embeddings[(type_j, type_i)][layer]["b"][:] = 0.0 - if embeddings[(type_j, type_i)][layer]["idt"] is not None: - embeddings[(type_j, type_i)][layer]["idt"][:] = 0.0 + embeddings[(type_i, type_j)].clear() + embeddings[(type_j, type_i)].clear() if suffix != "": embedding_net_pattern = ( diff --git a/source/tests/consistent/descriptor/test_se_e2_a.py b/source/tests/consistent/descriptor/test_se_e2_a.py index fe20278e6f..0243a77044 100644 --- a/source/tests/consistent/descriptor/test_se_e2_a.py +++ b/source/tests/consistent/descriptor/test_se_e2_a.py @@ -71,7 +71,7 @@ def skip_pt(self) -> bool: excluded_types, precision, ) = self.param - return not type_one_side or CommonTest.skip_pt + return CommonTest.skip_pt @property def skip_dp(self) -> bool: @@ -81,7 +81,7 @@ def skip_dp(self) -> bool: excluded_types, precision, ) = self.param - return not type_one_side or CommonTest.skip_dp + return CommonTest.skip_dp tf_class = DescrptSeATF dp_class = DescrptSeADP @@ -121,6 +121,17 @@ def setUp(self): dtype=GLOBAL_NP_FLOAT_PRECISION, ) self.natoms = np.array([6, 6, 2, 4], dtype=np.int32) + # TF se_e2_a type_one_side=False requires atype sorted + ( + resnet_dt, + type_one_side, + excluded_types, + precision, + ) = self.param + if not type_one_side: + idx = np.argsort(self.atype) + self.atype = self.atype[idx] + self.coords = self.coords.reshape(-1, 3)[idx].ravel() def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: return self.build_tf_descriptor( From 3e6b5077d2e27ade55cab4919b7c4d895320901d Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Tue, 27 Feb 2024 11:05:11 +0800 Subject: [PATCH 127/686] refact: pt: mv all plugin support to base descriptor. (#3340) thus pt reusing the dp code. --------- Co-authored-by: Han Wang --- .../pt/model/atomic_model/dp_atomic_model.py | 6 +- deepmd/pt/model/descriptor/__init__.py | 2 - deepmd/pt/model/descriptor/descriptor.py | 93 ------------------- deepmd/pt/model/descriptor/dpa1.py | 31 ++----- deepmd/pt/model/descriptor/dpa2.py | 32 +------ deepmd/pt/model/descriptor/gaussian_lcc.py | 6 +- deepmd/pt/model/descriptor/se_a.py | 28 ++---- deepmd/pt/model/model/__init__.py | 8 +- .../pt/model/test_polarizability_fitting.py | 1 - 9 files changed, 27 insertions(+), 180 deletions(-) diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index 08fd5898a0..31b1c08a14 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -12,8 +12,8 @@ from deepmd.dpmodel import ( FittingOutputDef, ) -from deepmd.pt.model.descriptor.descriptor import ( - Descriptor, +from deepmd.pt.model.descriptor.base_descriptor import ( + BaseDescriptor, ) from deepmd.pt.model.task.base_fitting import ( BaseFitting, @@ -101,7 +101,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "DPAtomicModel": data = copy.deepcopy(data) - descriptor_obj = Descriptor.deserialize(data["descriptor"]) + descriptor_obj = BaseDescriptor.deserialize(data["descriptor"]) fitting_obj = BaseFitting.deserialize(data["fitting"]) obj = cls(descriptor_obj, fitting_obj, type_map=data["type_map"]) return obj diff --git a/deepmd/pt/model/descriptor/__init__.py b/deepmd/pt/model/descriptor/__init__.py index 1c2e943369..e3bd37ccea 100644 --- a/deepmd/pt/model/descriptor/__init__.py +++ b/deepmd/pt/model/descriptor/__init__.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from .descriptor import ( - Descriptor, DescriptorBlock, make_default_type_embedding, ) @@ -29,7 +28,6 @@ ) __all__ = [ - "Descriptor", "DescriptorBlock", "make_default_type_embedding", "DescrptBlockSeA", diff --git a/deepmd/pt/model/descriptor/descriptor.py b/deepmd/pt/model/descriptor/descriptor.py index 091f2b1e20..29c574bb1f 100644 --- a/deepmd/pt/model/descriptor/descriptor.py +++ b/deepmd/pt/model/descriptor/descriptor.py @@ -9,14 +9,10 @@ Dict, List, Optional, - Type, ) import torch -from deepmd.common import ( - j_get_type, -) from deepmd.pt.model.network.network import ( TypeEmbedNet, ) @@ -36,98 +32,9 @@ DPPath, ) -from .base_descriptor import ( - BaseDescriptor, -) - log = logging.getLogger(__name__) -class Descriptor(torch.nn.Module, BaseDescriptor): - """The descriptor. - Given the atomic coordinates, atomic types and neighbor list, - calculate the descriptor. - """ - - __plugins = Plugin() - local_cluster = False - - @staticmethod - def register(key: str) -> Callable: - """Register a descriptor plugin. - - Parameters - ---------- - key : str - the key of a descriptor - - Returns - ------- - Descriptor - the registered descriptor - - Examples - -------- - >>> @Descriptor.register("some_descrpt") - class SomeDescript(Descriptor): - pass - """ - return Descriptor.__plugins.register(key) - - @classmethod - def get_data_process_key(cls, config): - """ - Get the keys for the data preprocess. - Usually need the information of rcut and sel. - TODO Need to be deprecated when the dataloader has been cleaned up. - """ - if cls is not Descriptor: - raise NotImplementedError("get_data_process_key is not implemented!") - descrpt_type = config["type"] - return Descriptor.__plugins.plugins[descrpt_type].get_data_process_key(config) - - @property - def data_stat_key(self): - """ - Get the keys for the data statistic of the descriptor. - Return a list of statistic names needed, such as "sumr", "suma" or "sumn". - """ - raise NotImplementedError("data_stat_key is not implemented!") - - def __new__(cls, *args, **kwargs): - if cls is Descriptor: - cls = cls.get_class_by_type(j_get_type(kwargs, cls.__name__)) - return super().__new__(cls) - - @classmethod - def get_class_by_type(cls, descrpt_type: str) -> Type["Descriptor"]: - if descrpt_type in Descriptor.__plugins.plugins: - return Descriptor.__plugins.plugins[descrpt_type] - else: - raise RuntimeError("Unknown descriptor type: " + descrpt_type) - - @classmethod - def deserialize(cls, data: dict) -> "Descriptor": - """Deserialize the model. - - There is no suffix in a native DP model, but it is important - for the TF backend. - - Parameters - ---------- - data : dict - The serialized data - - Returns - ------- - Descriptor - The deserialized descriptor - """ - if cls is Descriptor: - return Descriptor.get_class_by_type(data["type"]).deserialize(data) - raise NotImplementedError("Not implemented in class %s" % cls.__name__) - - class DescriptorBlock(torch.nn.Module, ABC): """The building block of descriptor. Given the input descriptor, provide with the atomic coordinates, diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index d948c7abf7..b616d20cd8 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -6,9 +6,6 @@ import torch -from deepmd.pt.model.descriptor import ( - Descriptor, -) from deepmd.pt.model.network.network import ( TypeEmbedNet, ) @@ -16,14 +13,17 @@ DPPath, ) +from .base_descriptor import ( + BaseDescriptor, +) from .se_atten import ( DescrptBlockSeAtten, ) -@Descriptor.register("dpa1") -@Descriptor.register("se_atten") -class DescrptDPA1(Descriptor): +@BaseDescriptor.register("dpa1") +@BaseDescriptor.register("se_atten") +class DescrptDPA1(BaseDescriptor, torch.nn.Module): def __init__( self, rcut, @@ -131,25 +131,6 @@ def dim_emb(self): def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): return self.se_atten.compute_input_stats(merged, path) - @classmethod - def get_data_process_key(cls, config): - """ - Get the keys for the data preprocess. - Usually need the information of rcut and sel. - TODO Need to be deprecated when the dataloader has been cleaned up. - """ - descrpt_type = config["type"] - assert descrpt_type in ["dpa1", "se_atten"] - return {"sel": config["sel"], "rcut": config["rcut"]} - - @property - def data_stat_key(self): - """ - Get the keys for the data statistic of the descriptor. - Return a list of statistic names needed, such as "sumr", "suma" or "sumn". - """ - return ["sumr", "suma", "sumn", "sumr2", "suma2"] - def serialize(self) -> dict: """Serialize the obj to dict.""" raise NotImplementedError diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index 90ec56e0bf..e693116cf4 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -6,9 +6,6 @@ import torch -from deepmd.pt.model.descriptor import ( - Descriptor, -) from deepmd.pt.model.network.network import ( Identity, Linear, @@ -22,6 +19,9 @@ DPPath, ) +from .base_descriptor import ( + BaseDescriptor, +) from .repformers import ( DescrptBlockRepformers, ) @@ -30,8 +30,8 @@ ) -@Descriptor.register("dpa2") -class DescrptDPA2(Descriptor): +@BaseDescriptor.register("dpa2") +class DescrptDPA2(torch.nn.Module, BaseDescriptor): def __init__( self, ntypes: int, @@ -306,28 +306,6 @@ def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None) ] descrpt.compute_input_stats(merged_tmp) - @classmethod - def get_data_process_key(cls, config): - """ - Get the keys for the data preprocess. - Usually need the information of rcut and sel. - TODO Need to be deprecated when the dataloader has been cleaned up. - """ - descrpt_type = config["type"] - assert descrpt_type in ["dpa2"] - return { - "sel": [config["repinit_nsel"], config["repformer_nsel"]], - "rcut": [config["repinit_rcut"], config["repformer_rcut"]], - } - - @property - def data_stat_key(self): - """ - Get the keys for the data statistic of the descriptor. - Return a list of statistic names needed, such as "sumr", "suma" or "sumn". - """ - return ["sumr", "suma", "sumn", "sumr2", "suma2"] - def serialize(self) -> dict: """Serialize the obj to dict.""" raise NotImplementedError diff --git a/deepmd/pt/model/descriptor/gaussian_lcc.py b/deepmd/pt/model/descriptor/gaussian_lcc.py index 72c9f27b2a..e0708dd9e0 100644 --- a/deepmd/pt/model/descriptor/gaussian_lcc.py +++ b/deepmd/pt/model/descriptor/gaussian_lcc.py @@ -7,8 +7,8 @@ import torch import torch.nn as nn -from deepmd.pt.model.descriptor import ( - Descriptor, +from deepmd.pt.model.descriptor.base_descriptor import ( + BaseDescriptor, ) from deepmd.pt.model.network.network import ( Evoformer3bEncoder, @@ -23,7 +23,7 @@ ) -class DescrptGaussianLcc(Descriptor): +class DescrptGaussianLcc(torch.nn.Module, BaseDescriptor): def __init__( self, rcut, diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 44a7fef126..ea216eddfc 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -12,7 +12,6 @@ import torch from deepmd.pt.model.descriptor import ( - Descriptor, DescriptorBlock, prod_env_mat_se_a, ) @@ -52,9 +51,13 @@ PairExcludeMask, ) +from .base_descriptor import ( + BaseDescriptor, +) + -@Descriptor.register("se_e2_a") -class DescrptSeA(Descriptor): +@BaseDescriptor.register("se_e2_a") +class DescrptSeA(BaseDescriptor, torch.nn.Module): def __init__( self, rcut, @@ -127,25 +130,6 @@ def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None) """Update mean and stddev for descriptor elements.""" return self.sea.compute_input_stats(merged, path) - @classmethod - def get_data_process_key(cls, config): - """ - Get the keys for the data preprocess. - Usually need the information of rcut and sel. - TODO Need to be deprecated when the dataloader has been cleaned up. - """ - descrpt_type = config["type"] - assert descrpt_type in ["se_e2_a"] - return {"sel": config["sel"], "rcut": config["rcut"]} - - @property - def data_stat_key(self): - """ - Get the keys for the data statistic of the descriptor. - Return a list of statistic names needed, such as "sumr", "suma" or "sumn". - """ - return ["sumr", "suma", "sumn", "sumr2", "suma2"] - def forward( self, coord_ext: torch.Tensor, diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index 974c42ee41..0dc9ae20af 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -16,8 +16,8 @@ DPAtomicModel, PairTabAtomicModel, ) -from deepmd.pt.model.descriptor.descriptor import ( - Descriptor, +from deepmd.pt.model.descriptor.base_descriptor import ( + BaseDescriptor, ) from deepmd.pt.model.task import ( Fitting, @@ -48,7 +48,7 @@ def get_zbl_model(model_params): ntypes = len(model_params["type_map"]) # descriptor model_params["descriptor"]["ntypes"] = ntypes - descriptor = Descriptor(**model_params["descriptor"]) + descriptor = BaseDescriptor(**model_params["descriptor"]) # fitting fitting_net = model_params.get("fitting_net", None) fitting_net["type"] = fitting_net.get("type", "ener") @@ -84,7 +84,7 @@ def get_model(model_params): ntypes = len(model_params["type_map"]) # descriptor model_params["descriptor"]["ntypes"] = ntypes - descriptor = Descriptor(**model_params["descriptor"]) + descriptor = BaseDescriptor(**model_params["descriptor"]) # fitting fitting_net = model_params.get("fitting_net", None) fitting_net["type"] = fitting_net.get("type", "ener") diff --git a/source/tests/pt/model/test_polarizability_fitting.py b/source/tests/pt/model/test_polarizability_fitting.py index 3f154383b2..f76a9e28ac 100644 --- a/source/tests/pt/model/test_polarizability_fitting.py +++ b/source/tests/pt/model/test_polarizability_fitting.py @@ -221,7 +221,6 @@ def test_rot(self): ret0 = ft0(rd0, extended_atype, gr0, fparam=ifp, aparam=iap) res.append(ret0["foo"]) - print(res[1].shape) np.testing.assert_allclose( to_numpy_array(res[1]), to_numpy_array( From 473cc0a31d27bfafa45ed3040e0ce0c1ea80244c Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 27 Feb 2024 01:21:46 -0500 Subject: [PATCH 128/686] bump python to 3.12 in the test environment (#3343) Fix a bug caused by the breaking change in Keras 3 (shipped by TF 2.16). --------- Signed-off-by: Jinzhe Zeng --- .github/workflows/test_python.yml | 2 +- backend/find_tensorflow.py | 5 +++++ deepmd/tf/descriptor/se_atten.py | 1 + deepmd/tf/env.py | 4 +++- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index 60b5ecf0e0..514b552aec 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -18,7 +18,7 @@ jobs: - python: 3.8 tf: torch: - - python: "3.11" + - python: "3.12" tf: torch: diff --git a/backend/find_tensorflow.py b/backend/find_tensorflow.py index c4d58ea0cd..fb9e719600 100644 --- a/backend/find_tensorflow.py +++ b/backend/find_tensorflow.py @@ -136,6 +136,11 @@ def get_tf_requirement(tf_version: str = "") -> dict: extra_select = {} if not (tf_version == "" or tf_version in SpecifierSet(">=2.12", prereleases=True)): extra_requires.append("protobuf<3.20") + # keras 3 is not compatible with tf.compat.v1 + if tf_version == "" or tf_version in SpecifierSet(">=2.15.0rc0", prereleases=True): + extra_requires.append("tf-keras; python_version>='3.9'") + # only TF>=2.16 is compatible with Python 3.12 + extra_requires.append("tf-keras>=2.16.0rc0; python_version>='3.12'") if tf_version == "" or tf_version in SpecifierSet(">=1.15", prereleases=True): extra_select["mpi"] = [ "horovod", diff --git a/deepmd/tf/descriptor/se_atten.py b/deepmd/tf/descriptor/se_atten.py index 327c3c1d3d..1c3c48e484 100644 --- a/deepmd/tf/descriptor/se_atten.py +++ b/deepmd/tf/descriptor/se_atten.py @@ -990,6 +990,7 @@ def _attention_layers( input_xyz = tf.keras.layers.LayerNormalization( beta_initializer=tf.constant_initializer(self.beta[i]), gamma_initializer=tf.constant_initializer(self.gamma[i]), + dtype=self.filter_precision, )(input_xyz) # input_xyz = self._feedforward(input_xyz, outputs_size[-1], self.att_n) return input_xyz diff --git a/deepmd/tf/env.py b/deepmd/tf/env.py index 2afe5cc862..3127e01e97 100644 --- a/deepmd/tf/env.py +++ b/deepmd/tf/env.py @@ -75,7 +75,9 @@ def dlopen_library(module: str, filename: str): dlopen_library("nvidia.cusparse.lib", "libcusparse.so*") dlopen_library("nvidia.cudnn.lib", "libcudnn.so*") - +# keras 3 is incompatible with tf.compat.v1 +# https://keras.io/getting_started/#tensorflow--keras-2-backwards-compatibility +os.environ["TF_USE_LEGACY_KERAS"] = "1" # import tensorflow v1 compatability try: import tensorflow.compat.v1 as tf From 254afc83e17310d375a3a8a3d4483a3b16503eb3 Mon Sep 17 00:00:00 2001 From: shiruosong <95087033+shiruosong@users.noreply.github.com> Date: Tue, 27 Feb 2024 21:34:07 +0800 Subject: [PATCH 129/686] fix_dplr.cpp delete redundant setup (#3344) Redundant setup was removed. The setup has already been executed in the initial lines of post_force, along with subsequent calculations. Reinitialization will lead to an error. --- source/lmp/fix_dplr.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/source/lmp/fix_dplr.cpp b/source/lmp/fix_dplr.cpp index ea60023e26..14d966a044 100644 --- a/source/lmp/fix_dplr.cpp +++ b/source/lmp/fix_dplr.cpp @@ -313,11 +313,6 @@ void FixDPLR::setup(int vflag) { // else { // error->all(FLERR, "respa is not supported by this fix"); // } - if (vflag) { - v_setup(vflag); - } else { - evflag = 0; - } } /* ---------------------------------------------------------------------- */ From c538d04254aad18fe46bf169dad908ec91288a4f Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Tue, 27 Feb 2024 21:38:15 +0800 Subject: [PATCH 130/686] Feat: add se_r descriptor (#3338) This PR is to support `se_r` descriptor in pytorch and numpy. - [x] Refactor Pytorch env_mat: possibly combine `r` and `a`. - [x] Add numpy implementation. - [x] Add consistency test with `tf`. - [x] Refactor device as parameter --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/dpmodel/descriptor/__init__.py | 4 + deepmd/dpmodel/descriptor/se_e2_a.py | 17 +- deepmd/dpmodel/descriptor/se_r.py | 321 ++++++++++++++++++ deepmd/dpmodel/utils/env_mat.py | 32 +- deepmd/pt/model/descriptor/__init__.py | 8 +- deepmd/pt/model/descriptor/descriptor.py | 4 +- deepmd/pt/model/descriptor/env_mat.py | 44 ++- deepmd/pt/model/descriptor/repformers.py | 9 +- deepmd/pt/model/descriptor/se_a.py | 11 +- deepmd/pt/model/descriptor/se_atten.py | 8 +- deepmd/pt/model/descriptor/se_r.py | 316 +++++++++++++++++ deepmd/pt/utils/env_mat_stat.py | 53 ++- deepmd/tf/descriptor/se_r.py | 96 +++++- .../tests/consistent/descriptor/test_se_r.py | 185 ++++++++++ source/tests/pt/model/test_descriptor.py | 4 +- source/tests/pt/model/test_descriptor_se_r.py | 134 ++++++++ source/tests/pt/model/test_env_mat.py | 4 +- 17 files changed, 1175 insertions(+), 75 deletions(-) create mode 100644 deepmd/dpmodel/descriptor/se_r.py create mode 100644 deepmd/pt/model/descriptor/se_r.py create mode 100644 source/tests/consistent/descriptor/test_se_r.py create mode 100644 source/tests/pt/model/test_descriptor_se_r.py diff --git a/deepmd/dpmodel/descriptor/__init__.py b/deepmd/dpmodel/descriptor/__init__.py index 5eca26acc5..08f8eb5052 100644 --- a/deepmd/dpmodel/descriptor/__init__.py +++ b/deepmd/dpmodel/descriptor/__init__.py @@ -5,8 +5,12 @@ from .se_e2_a import ( DescrptSeA, ) +from .se_r import ( + DescrptSeR, +) __all__ = [ "DescrptSeA", + "DescrptSeR", "make_base_descriptor", ] diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index 97ab719c62..a28215c35a 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -3,6 +3,9 @@ import numpy as np +from deepmd.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) from deepmd.utils.path import ( DPPath, ) @@ -183,8 +186,12 @@ def __init__( ) self.env_mat = EnvMat(self.rcut, self.rcut_smth) self.nnei = np.sum(self.sel) - self.davg = np.zeros([self.ntypes, self.nnei, 4]) - self.dstd = np.ones([self.ntypes, self.nnei, 4]) + self.davg = np.zeros( + [self.ntypes, self.nnei, 4], dtype=PRECISION_DICT[self.precision] + ) + self.dstd = np.ones( + [self.ntypes, self.nnei, 4], dtype=PRECISION_DICT[self.precision] + ) self.orig_sel = self.sel def __setitem__(self, key, value): @@ -292,7 +299,7 @@ def call( sec = np.append([0], np.cumsum(self.sel)) ng = self.neuron[-1] - gr = np.zeros([nf * nloc, ng, 4]) + gr = np.zeros([nf * nloc, ng, 4], dtype=PRECISION_DICT[self.precision]) exclude_mask = self.emask.build_type_exclude_mask(nlist, atype_ext) # merge nf and nloc axis, so for type_one_side == False, # we don't require atype is the same in all frames @@ -322,7 +329,9 @@ def call( # nf x nloc x ng x ng1 grrg = np.einsum("flid,fljd->flij", gr, gr1) # nf x nloc x (ng x ng1) - grrg = grrg.reshape(nf, nloc, ng * self.axis_neuron) + grrg = grrg.reshape(nf, nloc, ng * self.axis_neuron).astype( + GLOBAL_NP_FLOAT_PRECISION + ) return grrg, gr[..., 1:], None, None, ww def serialize(self) -> dict: diff --git a/deepmd/dpmodel/descriptor/se_r.py b/deepmd/dpmodel/descriptor/se_r.py new file mode 100644 index 0000000000..77e43f7d85 --- /dev/null +++ b/deepmd/dpmodel/descriptor/se_r.py @@ -0,0 +1,321 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import numpy as np + +from deepmd.utils.path import ( + DPPath, +) + +try: + from deepmd._version import version as __version__ +except ImportError: + __version__ = "unknown" + +import copy +from typing import ( + Any, + List, + Optional, +) + +from deepmd.dpmodel import ( + DEFAULT_PRECISION, + PRECISION_DICT, + NativeOP, +) +from deepmd.dpmodel.utils import ( + EmbeddingNet, + EnvMat, + NetworkCollection, + PairExcludeMask, +) +from deepmd.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) + +from .base_descriptor import ( + BaseDescriptor, +) + + +@BaseDescriptor.register("se_e2_r") +@BaseDescriptor.register("se_r") +class DescrptSeR(NativeOP, BaseDescriptor): + r"""DeepPot-SE_R constructed from only the radial imformation of atomic configurations. + + + Parameters + ---------- + rcut + The cut-off radius :math:`r_c` + rcut_smth + From where the environment matrix should be smoothed :math:`r_s` + sel : list[str] + sel[i] specifies the maxmum number of type i atoms in the cut-off radius + neuron : list[int] + Number of neurons in each hidden layers of the embedding net :math:`\mathcal{N}` + resnet_dt + Time-step `dt` in the resnet construction: + y = x + dt * \phi (Wx + b) + trainable + If the weights of embedding net are trainable. + type_one_side + Try to build N_types embedding nets. Otherwise, building N_types^2 embedding nets + exclude_types : List[List[int]] + The excluded pairs of types which have no interaction with each other. + For example, `[[0, 1]]` means no interaction between type 0 and type 1. + set_davg_zero + Set the shift of embedding net input to zero. + activation_function + The activation function in the embedding net. Supported options are |ACTIVATION_FN| + precision + The precision of the embedding net parameters. Supported options are |PRECISION| + multi_task + If the model has multi fitting nets to train. + spin + The deepspin object. + + Limitations + ----------- + The currently implementation does not support the following features + + 1. type_one_side == False + 2. exclude_types != [] + 3. spin is not None + + References + ---------- + .. [1] Linfeng Zhang, Jiequn Han, Han Wang, Wissam A. Saidi, Roberto Car, and E. Weinan. 2018. + End-to-end symmetry preserving inter-atomic potential energy model for finite and extended + systems. In Proceedings of the 32nd International Conference on Neural Information Processing + Systems (NIPS'18). Curran Associates Inc., Red Hook, NY, USA, 4441-4451. + """ + + def __init__( + self, + rcut: float, + rcut_smth: float, + sel: List[str], + neuron: List[int] = [24, 48, 96], + resnet_dt: bool = False, + trainable: bool = True, + type_one_side: bool = True, + exclude_types: List[List[int]] = [], + set_davg_zero: bool = False, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + spin: Optional[Any] = None, + # consistent with argcheck, not used though + seed: Optional[int] = None, + ) -> None: + ## seed, uniform_seed, multi_task, not included. + if not type_one_side: + raise NotImplementedError("type_one_side == False not implemented") + if spin is not None: + raise NotImplementedError("spin is not implemented") + + self.rcut = rcut + self.rcut_smth = rcut_smth + self.sel = sel + self.ntypes = len(self.sel) + self.neuron = neuron + self.resnet_dt = resnet_dt + self.trainable = trainable + self.type_one_side = type_one_side + self.exclude_types = exclude_types + self.set_davg_zero = set_davg_zero + self.activation_function = activation_function + self.precision = precision + self.spin = spin + self.emask = PairExcludeMask(self.ntypes, self.exclude_types) + + in_dim = 1 # not considiering type embedding + self.embeddings = NetworkCollection( + ntypes=self.ntypes, + ndim=(1 if self.type_one_side else 2), + network_type="embedding_network", + ) + if not self.type_one_side: + raise NotImplementedError("type_one_side == False not implemented") + for ii in range(self.ntypes): + self.embeddings[(ii,)] = EmbeddingNet( + in_dim, + self.neuron, + self.activation_function, + self.resnet_dt, + self.precision, + ) + self.env_mat = EnvMat(self.rcut, self.rcut_smth) + self.nnei = np.sum(self.sel) + self.davg = np.zeros( + [self.ntypes, self.nnei, 1], dtype=PRECISION_DICT[self.precision] + ) + self.dstd = np.ones( + [self.ntypes, self.nnei, 1], dtype=PRECISION_DICT[self.precision] + ) + self.orig_sel = self.sel + + def __setitem__(self, key, value): + if key in ("avg", "data_avg", "davg"): + self.davg = value + elif key in ("std", "data_std", "dstd"): + self.dstd = value + else: + raise KeyError(key) + + def __getitem__(self, key): + if key in ("avg", "data_avg", "davg"): + return self.davg + elif key in ("std", "data_std", "dstd"): + return self.dstd + else: + raise KeyError(key) + + @property + def dim_out(self): + """Returns the output dimension of this descriptor.""" + return self.get_dim_out() + + def get_dim_out(self): + """Returns the output dimension of this descriptor.""" + return self.neuron[-1] + + def get_dim_emb(self): + """Returns the embedding (g2) dimension of this descriptor.""" + raise NotImplementedError + + def get_rcut(self): + """Returns cutoff radius.""" + return self.rcut + + def get_sel(self): + """Returns cutoff radius.""" + return self.sel + + def mixed_types(self): + """Returns if the descriptor requires a neighbor list that distinguish different + atomic types or not. + """ + return False + + def get_ntypes(self) -> int: + """Returns the number of element types.""" + return self.ntypes + + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): + """Update mean and stddev for descriptor elements.""" + raise NotImplementedError + + def cal_g( + self, + ss, + ll, + ): + nf, nloc, nnei = ss.shape[0:3] + ss = ss.reshape(nf, nloc, nnei, 1) + # nf x nloc x nnei x ng + gg = self.embeddings[(ll,)].call(ss) + return gg + + def call( + self, + coord_ext, + atype_ext, + nlist, + mapping: Optional[np.ndarray] = None, + ): + """Compute the descriptor. + + Parameters + ---------- + coord_ext + The extended coordinates of atoms. shape: nf x (nallx3) + atype_ext + The extended aotm types. shape: nf x nall + nlist + The neighbor list. shape: nf x nloc x nnei + mapping + The index mapping from extended to lcoal region. not used by this descriptor. + + Returns + ------- + descriptor + The descriptor. shape: nf x nloc x (ng x axis_neuron) + gr + The rotationally equivariant and permutationally invariant single particle + representation. shape: nf x nloc x ng x 3 + g2 + The rotationally invariant pair-partical representation. + this descriptor returns None + h2 + The rotationally equivariant pair-partical representation. + this descriptor returns None + sw + The smooth switch function. + """ + del mapping + # nf x nloc x nnei x 1 + rr, ww = self.env_mat.call( + coord_ext, atype_ext, nlist, self.davg, self.dstd, True + ) + nf, nloc, nnei, _ = rr.shape + sec = np.append([0], np.cumsum(self.sel)) + + ng = self.neuron[-1] + xyz_scatter = np.zeros([nf, nloc, ng], dtype=PRECISION_DICT[self.precision]) + exclude_mask = self.emask.build_type_exclude_mask(nlist, atype_ext) + for tt in range(self.ntypes): + mm = exclude_mask[:, :, sec[tt] : sec[tt + 1]] + tr = rr[:, :, sec[tt] : sec[tt + 1], :] + tr = tr * mm[:, :, :, None] + gg = self.cal_g(tr, tt) + gg = np.mean(gg, axis=2) + # nf x nloc x ng x 1 + xyz_scatter += gg + + res_rescale = 1.0 / 10.0 + res = xyz_scatter * res_rescale + res = res.reshape(nf, nloc, -1).astype(GLOBAL_NP_FLOAT_PRECISION) + return res, None, None, None, ww + + def serialize(self) -> dict: + """Serialize the descriptor to dict.""" + return { + "@class": "Descriptor", + "type": "se_r", + "rcut": self.rcut, + "rcut_smth": self.rcut_smth, + "sel": self.sel, + "neuron": self.neuron, + "resnet_dt": self.resnet_dt, + "trainable": self.trainable, + "type_one_side": self.type_one_side, + "exclude_types": self.exclude_types, + "set_davg_zero": self.set_davg_zero, + "activation_function": self.activation_function, + # make deterministic + "precision": np.dtype(PRECISION_DICT[self.precision]).name, + "spin": self.spin, + "env_mat": self.env_mat.serialize(), + "embeddings": self.embeddings.serialize(), + "@variables": { + "davg": self.davg, + "dstd": self.dstd, + }, + } + + @classmethod + def deserialize(cls, data: dict) -> "DescrptSeR": + """Deserialize from dict.""" + data = copy.deepcopy(data) + data.pop("@class", None) + data.pop("type", None) + variables = data.pop("@variables") + embeddings = data.pop("embeddings") + env_mat = data.pop("env_mat") + obj = cls(**data) + + obj["davg"] = variables["davg"] + obj["dstd"] = variables["dstd"] + obj.embeddings = NetworkCollection.deserialize(embeddings) + obj.env_mat = EnvMat.deserialize(env_mat) + return obj diff --git a/deepmd/dpmodel/utils/env_mat.py b/deepmd/dpmodel/utils/env_mat.py index 070b0e1549..0e861d9f38 100644 --- a/deepmd/dpmodel/utils/env_mat.py +++ b/deepmd/dpmodel/utils/env_mat.py @@ -30,6 +30,7 @@ def _make_env_mat( coord, rcut: float, ruct_smth: float, + radial_only: bool = False, ): """Make smooth environment matrix.""" nf, nloc, nnei = nlist.shape @@ -54,8 +55,11 @@ def _make_env_mat( t1 = diff / length**2 weight = compute_smooth_weight(length, ruct_smth, rcut) weight = weight * np.expand_dims(mask, -1) - env_mat_se_a = np.concatenate([t0, t1], axis=-1) * weight - return env_mat_se_a, diff * np.expand_dims(mask, -1), weight + if radial_only: + env_mat = t0 * weight + else: + env_mat = np.concatenate([t0, t1], axis=-1) * weight + return env_mat, diff * np.expand_dims(mask, -1), weight class EnvMat(NativeOP): @@ -74,6 +78,7 @@ def call( nlist: np.ndarray, davg: Optional[np.ndarray] = None, dstd: Optional[np.ndarray] = None, + radial_only: bool = False, ) -> Union[np.ndarray, np.ndarray]: """Compute the environment matrix. @@ -86,18 +91,23 @@ def call( atype_ext The extended aotm types. shape: nf x nall davg - The data avg. shape: nt x nnei x 4 + The data avg. shape: nt x nnei x (4 or 1) dstd - The inverse of data std. shape: nt x nnei x 4 + The inverse of data std. shape: nt x nnei x (4 or 1) + radial_only + Whether to only compute radial part of the environment matrix. + If True, the output will be of shape nf x nloc x nnei x 1. + Otherwise, the output will be of shape nf x nloc x nnei x 4. + Default: False. Returns ------- env_mat - The environment matrix. shape: nf x nloc x nnei x 4 + The environment matrix. shape: nf x nloc x nnei x (4 or 1) switch The value of switch function. shape: nf x nloc x nnei """ - em, sw = self._call(nlist, coord_ext) + em, sw = self._call(nlist, coord_ext, radial_only) nf, nloc, nnei = nlist.shape atype = atype_ext[:, :nloc] if davg is not None: @@ -106,12 +116,10 @@ def call( em /= dstd[atype] return em, sw - def _call( - self, - nlist, - coord_ext, - ): - em, diff, ww = _make_env_mat(nlist, coord_ext, self.rcut, self.rcut_smth) + def _call(self, nlist, coord_ext, radial_only): + em, diff, ww = _make_env_mat( + nlist, coord_ext, self.rcut, self.rcut_smth, radial_only + ) return em, ww def serialize( diff --git a/deepmd/pt/model/descriptor/__init__.py b/deepmd/pt/model/descriptor/__init__.py index e3bd37ccea..5fd644f149 100644 --- a/deepmd/pt/model/descriptor/__init__.py +++ b/deepmd/pt/model/descriptor/__init__.py @@ -11,7 +11,7 @@ DescrptDPA2, ) from .env_mat import ( - prod_env_mat_se_a, + prod_env_mat, ) from .gaussian_lcc import ( DescrptGaussianLcc, @@ -26,6 +26,9 @@ DescrptBlockSeA, DescrptSeA, ) +from .se_r import ( + DescrptSeR, +) __all__ = [ "DescriptorBlock", @@ -33,9 +36,10 @@ "DescrptBlockSeA", "DescrptBlockSeAtten", "DescrptSeA", + "DescrptSeR", "DescrptDPA1", "DescrptDPA2", - "prod_env_mat_se_a", + "prod_env_mat", "DescrptGaussianLcc", "DescrptBlockHybrid", "DescrptBlockRepformers", diff --git a/deepmd/pt/model/descriptor/descriptor.py b/deepmd/pt/model/descriptor/descriptor.py index 29c574bb1f..91e0a2527a 100644 --- a/deepmd/pt/model/descriptor/descriptor.py +++ b/deepmd/pt/model/descriptor/descriptor.py @@ -20,7 +20,7 @@ env, ) from deepmd.pt.utils.env_mat_stat import ( - EnvMatStatSeA, + EnvMatStatSe, ) from deepmd.pt.utils.plugin import ( Plugin, @@ -129,7 +129,7 @@ def share_params(self, base_class, shared_level, resume=False): # link buffers if hasattr(self, "mean") and not resume: # in case of change params during resume - base_env = EnvMatStatSeA(base_class) + base_env = EnvMatStatSe(base_class) base_env.stats = base_class.stats for kk in base_class.get_stats(): base_env.stats[kk] += self.get_stats()[kk] diff --git a/deepmd/pt/model/descriptor/env_mat.py b/deepmd/pt/model/descriptor/env_mat.py index b3235de175..4e6ffb7785 100644 --- a/deepmd/pt/model/descriptor/env_mat.py +++ b/deepmd/pt/model/descriptor/env_mat.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later + import torch from deepmd.pt.utils.preprocess import ( @@ -6,7 +7,9 @@ ) -def _make_env_mat_se_a(nlist, coord, rcut: float, ruct_smth: float): +def _make_env_mat( + nlist, coord, rcut: float, ruct_smth: float, radial_only: bool = False +): """Make smooth environment matrix.""" bsz, natoms, nnei = nlist.shape coord = coord.view(bsz, -1, 3) @@ -26,35 +29,42 @@ def _make_env_mat_se_a(nlist, coord, rcut: float, ruct_smth: float): t1 = diff / length**2 weight = compute_smooth_weight(length, ruct_smth, rcut) weight = weight * mask.unsqueeze(-1) - env_mat_se_a = torch.cat([t0, t1], dim=-1) * weight - return env_mat_se_a, diff * mask.unsqueeze(-1), weight + if radial_only: + env_mat = t0 * weight + else: + env_mat = torch.cat([t0, t1], dim=-1) * weight + return env_mat, diff * mask.unsqueeze(-1), weight -def prod_env_mat_se_a( - extended_coord, nlist, atype, mean, stddev, rcut: float, rcut_smth: float +def prod_env_mat( + extended_coord, + nlist, + atype, + mean, + stddev, + rcut: float, + rcut_smth: float, + radial_only: bool = False, ): """Generate smooth environment matrix from atom coordinates and other context. Args: - extended_coord: Copied atom coordinates with shape [nframes, nall*3]. - atype: Atom types with shape [nframes, nloc]. - - natoms: Batched atom statisics with shape [len(sec)+2]. - - box: Batched simulation box with shape [nframes, 9]. - - mean: Average value of descriptor per element type with shape [len(sec), nnei, 4]. - - stddev: Standard deviation of descriptor per element type with shape [len(sec), nnei, 4]. - - deriv_stddev: StdDev of descriptor derivative per element type with shape [len(sec), nnei, 4, 3]. + - mean: Average value of descriptor per element type with shape [len(sec), nnei, 4 or 1]. + - stddev: Standard deviation of descriptor per element type with shape [len(sec), nnei, 4 or 1]. - rcut: Cut-off radius. - rcut_smth: Smooth hyper-parameter for pair force & energy. + - radial_only: Whether to return a full description or a radial-only descriptor. Returns ------- - - env_mat_se_a: Shape is [nframes, natoms[1]*nnei*4]. + - env_mat: Shape is [nframes, natoms[1]*nnei*4]. """ - nframes = extended_coord.shape[0] - _env_mat_se_a, diff, switch = _make_env_mat_se_a( - nlist, extended_coord, rcut, rcut_smth - ) # shape [n_atom, dim, 4] - t_avg = mean[atype] # [n_atom, dim, 4] - t_std = stddev[atype] # [n_atom, dim, 4] + _env_mat_se_a, diff, switch = _make_env_mat( + nlist, extended_coord, rcut, rcut_smth, radial_only + ) # shape [n_atom, dim, 4 or 1] + t_avg = mean[atype] # [n_atom, dim, 4 or 1] + t_std = stddev[atype] # [n_atom, dim, 4 or 1] env_mat_se_a = (_env_mat_se_a - t_avg) / t_std return env_mat_se_a, diff, switch diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py index 8aa1114fdc..ad523bcc2d 100644 --- a/deepmd/pt/model/descriptor/repformers.py +++ b/deepmd/pt/model/descriptor/repformers.py @@ -11,7 +11,7 @@ DescriptorBlock, ) from deepmd.pt.model.descriptor.env_mat import ( - prod_env_mat_se_a, + prod_env_mat, ) from deepmd.pt.model.network.network import ( SimpleLinear, @@ -20,7 +20,7 @@ env, ) from deepmd.pt.utils.env_mat_stat import ( - EnvMatStatSeA, + EnvMatStatSe, ) from deepmd.pt.utils.utils import ( get_activation_fn, @@ -100,6 +100,7 @@ def __init__( self.nlayers = nlayers sel = [sel] if isinstance(sel, int) else sel self.nnei = sum(sel) + self.ndescrpt = self.nnei * 4 # use full descriptor. assert len(sel) == 1 self.sel = sel self.sec = self.sel @@ -222,7 +223,7 @@ def forward( nall = extended_coord.view(nframes, -1).shape[1] // 3 atype = extended_atype[:, :nloc] # nb x nloc x nnei x 4, nb x nloc x nnei x 3, nb x nloc x nnei x 1 - dmatrix, diff, sw = prod_env_mat_se_a( + dmatrix, diff, sw = prod_env_mat( extended_coord, nlist, atype, @@ -279,7 +280,7 @@ def forward( def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): """Update mean and stddev for descriptor elements.""" - env_mat_stat = EnvMatStatSeA(self) + env_mat_stat = EnvMatStatSe(self) if path is not None: path = path / env_mat_stat.get_hash() env_mat_stat.load_or_compute_stats(merged, path) diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index ea216eddfc..033d640ad8 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -13,7 +13,7 @@ from deepmd.pt.model.descriptor import ( DescriptorBlock, - prod_env_mat_se_a, + prod_env_mat, ) from deepmd.pt.utils import ( env, @@ -23,7 +23,7 @@ RESERVED_PRECISON_DICT, ) from deepmd.pt.utils.env_mat_stat import ( - EnvMatStatSeA, + EnvMatStatSe, ) from deepmd.utils.env_mat_stat import ( StatItem, @@ -384,7 +384,7 @@ def __getitem__(self, key): def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): """Update mean and stddev for descriptor elements.""" - env_mat_stat = EnvMatStatSeA(self) + env_mat_stat = EnvMatStatSe(self) if path is not None: path = path / env_mat_stat.get_hash() env_mat_stat.load_or_compute_stats(merged, path) @@ -393,9 +393,6 @@ def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None) if not self.set_davg_zero: self.mean.copy_(torch.tensor(mean, device=env.DEVICE)) self.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) - if not self.set_davg_zero: - self.mean.copy_(torch.tensor(mean, device=env.DEVICE)) - self.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) def get_stats(self) -> Dict[str, StatItem]: """Get the statistics of the descriptor.""" @@ -428,7 +425,7 @@ def forward( del extended_atype_embd, mapping nloc = nlist.shape[1] atype = extended_atype[:, :nloc] - dmatrix, diff, sw = prod_env_mat_se_a( + dmatrix, diff, sw = prod_env_mat( extended_coord, nlist, atype, diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index 4a7469a804..0b32bd9341 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -12,7 +12,7 @@ DescriptorBlock, ) from deepmd.pt.model.descriptor.env_mat import ( - prod_env_mat_se_a, + prod_env_mat, ) from deepmd.pt.model.network.network import ( NeighborWiseAttention, @@ -22,7 +22,7 @@ env, ) from deepmd.pt.utils.env_mat_stat import ( - EnvMatStatSeA, + EnvMatStatSe, ) from deepmd.utils.env_mat_stat import ( StatItem, @@ -202,7 +202,7 @@ def dim_emb(self): def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): """Update mean and stddev for descriptor elements.""" - env_mat_stat = EnvMatStatSeA(self) + env_mat_stat = EnvMatStatSe(self) if path is not None: path = path / env_mat_stat.get_hash() env_mat_stat.load_or_compute_stats(merged, path) @@ -247,7 +247,7 @@ def forward( atype = extended_atype[:, :nloc] nb = nframes nall = extended_coord.view(nb, -1, 3).shape[1] - dmatrix, diff, sw = prod_env_mat_se_a( + dmatrix, diff, sw = prod_env_mat( extended_coord, nlist, atype, diff --git a/deepmd/pt/model/descriptor/se_r.py b/deepmd/pt/model/descriptor/se_r.py new file mode 100644 index 0000000000..c685640426 --- /dev/null +++ b/deepmd/pt/model/descriptor/se_r.py @@ -0,0 +1,316 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + List, + Optional, + Tuple, +) + +import numpy as np +import torch + +from deepmd.dpmodel.utils import EnvMat as DPEnvMat +from deepmd.pt.model.descriptor import ( + prod_env_mat, +) +from deepmd.pt.model.network.mlp import ( + EmbeddingNet, + NetworkCollection, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.env import ( + PRECISION_DICT, + RESERVED_PRECISON_DICT, +) +from deepmd.pt.utils.env_mat_stat import ( + EnvMatStatSe, +) +from deepmd.pt.utils.exclude_mask import ( + PairExcludeMask, +) +from deepmd.utils.env_mat_stat import ( + StatItem, +) +from deepmd.utils.path import ( + DPPath, +) + +from .base_descriptor import ( + BaseDescriptor, +) + + +@BaseDescriptor.register("se_e2_r") +@BaseDescriptor.register("se_r") +class DescrptSeR(BaseDescriptor, torch.nn.Module): + def __init__( + self, + rcut, + rcut_smth, + sel, + neuron=[25, 50, 100], + set_davg_zero: bool = False, + activation_function: str = "tanh", + precision: str = "float64", + resnet_dt: bool = False, + exclude_types: List[Tuple[int, int]] = [], + old_impl: bool = False, + **kwargs, + ): + super().__init__() + self.rcut = rcut + self.rcut_smth = rcut_smth + self.neuron = neuron + self.filter_neuron = self.neuron + self.set_davg_zero = set_davg_zero + self.activation_function = activation_function + self.precision = precision + self.prec = PRECISION_DICT[self.precision] + self.resnet_dt = resnet_dt + self.old_impl = False # this does not support old implementation. + self.exclude_types = exclude_types + self.ntypes = len(sel) + self.emask = PairExcludeMask(len(sel), exclude_types=exclude_types) + + self.sel = sel + self.sec = torch.tensor( + np.append([0], np.cumsum(self.sel)), dtype=int, device=env.DEVICE + ) + self.split_sel = self.sel + self.nnei = sum(sel) + self.ndescrpt = self.nnei * 1 + + wanted_shape = (self.ntypes, self.nnei, 1) + mean = torch.zeros(wanted_shape, dtype=self.prec, device=env.DEVICE) + stddev = torch.ones(wanted_shape, dtype=self.prec, device=env.DEVICE) + self.register_buffer("mean", mean) + self.register_buffer("stddev", stddev) + self.filter_layers_old = None + self.filter_layers = None + + filter_layers = NetworkCollection( + ndim=1, ntypes=len(sel), network_type="embedding_network" + ) + # TODO: ndim=2 if type_one_side=False + for ii in range(self.ntypes): + filter_layers[(ii,)] = EmbeddingNet( + 1, + self.filter_neuron, + activation_function=self.activation_function, + precision=self.precision, + resnet_dt=self.resnet_dt, + ) + self.filter_layers = filter_layers + self.stats = None + + def get_rcut(self) -> float: + """Returns the cut-off radius.""" + return self.rcut + + def get_nsel(self) -> int: + """Returns the number of selected atoms in the cut-off radius.""" + return sum(self.sel) + + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + return self.sel + + def get_ntypes(self) -> int: + """Returns the number of element types.""" + return self.ntypes + + def get_dim_out(self) -> int: + """Returns the output dimension.""" + return self.neuron[-1] + + def get_dim_emb(self) -> int: + """Returns the output dimension.""" + raise NotImplementedError + + def get_dim_in(self) -> int: + """Returns the input dimension.""" + return 0 + + def mixed_types(self) -> bool: + """If true, the discriptor + 1. assumes total number of atoms aligned across frames; + 2. requires a neighbor list that does not distinguish different atomic types. + + If false, the discriptor + 1. assumes total number of atoms of each atom type aligned across frames; + 2. requires a neighbor list that distinguishes different atomic types. + + """ + return False + + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): + """Update mean and stddev for descriptor elements.""" + env_mat_stat = EnvMatStatSe(self) + if path is not None: + path = path / env_mat_stat.get_hash() + env_mat_stat.load_or_compute_stats(merged, path) + self.stats = env_mat_stat.stats + mean, stddev = env_mat_stat() + if not self.set_davg_zero: + self.mean.copy_(torch.tensor(mean, device=env.DEVICE)) + self.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) + + def get_stats(self) -> Dict[str, StatItem]: + """Get the statistics of the descriptor.""" + if self.stats is None: + raise RuntimeError( + "The statistics of the descriptor has not been computed." + ) + return self.stats + + def __setitem__(self, key, value): + if key in ("avg", "data_avg", "davg"): + self.mean = value + elif key in ("std", "data_std", "dstd"): + self.stddev = value + else: + raise KeyError(key) + + def __getitem__(self, key): + if key in ("avg", "data_avg", "davg"): + return self.mean + elif key in ("std", "data_std", "dstd"): + return self.stddev + else: + raise KeyError(key) + + def forward( + self, + coord_ext: torch.Tensor, + atype_ext: torch.Tensor, + nlist: torch.Tensor, + mapping: Optional[torch.Tensor] = None, + ): + """Compute the descriptor. + + Parameters + ---------- + coord_ext + The extended coordinates of atoms. shape: nf x (nallx3) + atype_ext + The extended aotm types. shape: nf x nall + nlist + The neighbor list. shape: nf x nloc x nnei + mapping + The index mapping, not required by this descriptor. + + Returns + ------- + descriptor + The descriptor. shape: nf x nloc x (ng x axis_neuron) + gr + The rotationally equivariant and permutationally invariant single particle + representation. shape: nf x nloc x ng x 3 + g2 + The rotationally invariant pair-partical representation. + this descriptor returns None + h2 + The rotationally equivariant pair-partical representation. + this descriptor returns None + sw + The smooth switch function. + + """ + del mapping + nloc = nlist.shape[1] + atype = atype_ext[:, :nloc] + dmatrix, diff, sw = prod_env_mat( + coord_ext, + nlist, + atype, + self.mean, + self.stddev, + self.rcut, + self.rcut_smth, + True, + ) + + assert self.filter_layers is not None + dmatrix = dmatrix.view(-1, self.nnei, 1) + dmatrix = dmatrix.to(dtype=self.prec) + nfnl = dmatrix.shape[0] + # pre-allocate a shape to pass jit + xyz_scatter = torch.zeros( + [nfnl, 1, self.filter_neuron[-1]], dtype=self.prec, device=coord_ext.device + ) + + # nfnl x nnei + exclude_mask = self.emask(nlist, atype_ext).view(nfnl, -1) + for ii, ll in enumerate(self.filter_layers.networks): + # nfnl x nt + mm = exclude_mask[:, self.sec[ii] : self.sec[ii + 1]] + # nfnl x nt x 1 + ss = dmatrix[:, self.sec[ii] : self.sec[ii + 1], :] + ss = ss * mm[:, :, None] + # nfnl x nt x ng + gg = ll.forward(ss) + gg = torch.mean(gg, dim=1).unsqueeze(1) + xyz_scatter += gg + + res_rescale = 1.0 / 10.0 + result = xyz_scatter * res_rescale + result = result.view(-1, nloc, self.filter_neuron[-1]) + return ( + result.to(dtype=env.GLOBAL_PT_FLOAT_PRECISION), + None, + None, + None, + sw, + ) + + def set_stat_mean_and_stddev( + self, + mean: torch.Tensor, + stddev: torch.Tensor, + ) -> None: + self.mean = mean + self.stddev = stddev + + def serialize(self) -> dict: + return { + "@class": "Descriptor", + "type": "se_r", + "rcut": self.rcut, + "rcut_smth": self.rcut_smth, + "sel": self.sel, + "neuron": self.neuron, + "resnet_dt": self.resnet_dt, + "set_davg_zero": self.set_davg_zero, + "activation_function": self.activation_function, + # make deterministic + "precision": RESERVED_PRECISON_DICT[self.prec], + "embeddings": self.filter_layers.serialize(), + "env_mat": DPEnvMat(self.rcut, self.rcut_smth).serialize(), + "exclude_types": self.exclude_types, + "@variables": { + "davg": self["davg"].detach().cpu().numpy(), + "dstd": self["dstd"].detach().cpu().numpy(), + }, + ## to be updated when the options are supported. + "trainable": True, + "type_one_side": True, + "spin": None, + } + + @classmethod + def deserialize(cls, data: dict) -> "DescrptSeR": + data = data.copy() + variables = data.pop("@variables") + embeddings = data.pop("embeddings") + env_mat = data.pop("env_mat") + obj = cls(**data) + + def t_cvt(xx): + return torch.tensor(xx, dtype=obj.prec, device=env.DEVICE) + + obj["davg"] = t_cvt(variables["davg"]) + obj["dstd"] = t_cvt(variables["dstd"]) + obj.filter_layers = NetworkCollection.deserialize(embeddings) + return obj diff --git a/deepmd/pt/utils/env_mat_stat.py b/deepmd/pt/utils/env_mat_stat.py index 2f3c728c99..3af03bda97 100644 --- a/deepmd/pt/utils/env_mat_stat.py +++ b/deepmd/pt/utils/env_mat_stat.py @@ -13,7 +13,7 @@ get_hash, ) from deepmd.pt.model.descriptor.env_mat import ( - prod_env_mat_se_a, + prod_env_mat, ) from deepmd.pt.utils import ( env, @@ -56,8 +56,8 @@ def compute_stat(self, env_mat: Dict[str, torch.Tensor]) -> Dict[str, StatItem]: return stats -class EnvMatStatSeA(EnvMatStat): - """Environmental matrix statistics for the se_a environemntal matrix. +class EnvMatStatSe(EnvMatStat): + """Environmental matrix statistics for the se_a/se_r environemntal matrix. Parameters ---------- @@ -68,6 +68,9 @@ class EnvMatStatSeA(EnvMatStat): def __init__(self, descriptor: "DescriptorBlock"): super().__init__() self.descriptor = descriptor + self.last_dim = ( + self.descriptor.ndescrpt // self.descriptor.nnei + ) # se_r=1, se_a=4 def iter( self, data: List[Dict[str, torch.Tensor]] @@ -87,14 +90,14 @@ def iter( zero_mean = torch.zeros( self.descriptor.get_ntypes(), self.descriptor.get_nsel(), - 4, + self.last_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE, ) one_stddev = torch.ones( self.descriptor.get_ntypes(), self.descriptor.get_nsel(), - 4, + self.last_dim, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE, ) @@ -118,7 +121,7 @@ def iter( mixed_types=self.descriptor.mixed_types(), box=box, ) - env_mat, _, _ = prod_env_mat_se_a( + env_mat, _, _ = prod_env_mat( extended_coord, nlist, atype, @@ -131,7 +134,9 @@ def iter( # reshape to nframes * nloc at the atom level, # so nframes/mixed_type do not matter env_mat = env_mat.view( - coord.shape[0] * coord.shape[1], self.descriptor.get_nsel(), 4 + coord.shape[0] * coord.shape[1], + self.descriptor.get_nsel(), + self.last_dim, ) atype = atype.view(coord.shape[0] * coord.shape[1]) # (1, nloc) eq (ntypes, 1), so broadcast is possible @@ -144,10 +149,11 @@ def iter( ) for type_i in range(self.descriptor.get_ntypes()): dd = env_mat[type_idx[type_i]] - dd = dd.reshape([-1, 4]) # typen_atoms * nnei, 4 + dd = dd.reshape([-1, self.last_dim]) # typen_atoms * nnei, 4 env_mats = {} env_mats[f"r_{type_i}"] = dd[:, :1] - env_mats[f"a_{type_i}"] = dd[:, 1:] + if self.last_dim == 4: + env_mats[f"a_{type_i}"] = dd[:, 1:] yield self.compute_stat(env_mats) def get_hash(self) -> str: @@ -158,9 +164,10 @@ def get_hash(self) -> str: str The hash of the environment matrix. """ + dscpt_type = "se_a" if self.last_dim == 4 else "se_r" return get_hash( { - "type": "se_a", + "type": dscpt_type, "ntypes": self.descriptor.get_ntypes(), "rcut": round(self.descriptor.get_rcut(), 2), "rcut_smth": round(self.descriptor.rcut_smth, 2), @@ -176,20 +183,30 @@ def __call__(self): all_davg = [] all_dstd = [] + for type_i in range(self.descriptor.get_ntypes()): - davgunit = [[avgs[f"r_{type_i}"], 0, 0, 0]] - dstdunit = [ - [ - stds[f"r_{type_i}"], - stds[f"a_{type_i}"], - stds[f"a_{type_i}"], - stds[f"a_{type_i}"], + if self.last_dim == 4: + davgunit = [[avgs[f"r_{type_i}"], 0, 0, 0]] + dstdunit = [ + [ + stds[f"r_{type_i}"], + stds[f"a_{type_i}"], + stds[f"a_{type_i}"], + stds[f"a_{type_i}"], + ] + ] + elif self.last_dim == 1: + davgunit = [[avgs[f"r_{type_i}"]]] + dstdunit = [ + [ + stds[f"r_{type_i}"], + ] ] - ] davg = np.tile(davgunit, [self.descriptor.get_nsel(), 1]) dstd = np.tile(dstdunit, [self.descriptor.get_nsel(), 1]) all_davg.append(davg) all_dstd.append(dstd) + mean = np.stack(all_davg) stddev = np.stack(all_dstd) return mean, stddev diff --git a/deepmd/tf/descriptor/se_r.py b/deepmd/tf/descriptor/se_r.py index f790d0a8fb..1a12befdf0 100644 --- a/deepmd/tf/descriptor/se_r.py +++ b/deepmd/tf/descriptor/se_r.py @@ -7,6 +7,9 @@ import numpy as np +from deepmd.dpmodel.utils.env_mat import ( + EnvMat, +) from deepmd.tf.common import ( cast_precision, get_activation_func, @@ -115,8 +118,9 @@ def __init__( self.seed_shift = embedding_net_rand_seed_shift(self.filter_neuron) self.trainable = trainable self.filter_activation_fn = get_activation_func(activation_function) + self.activation_function_name = activation_function self.filter_precision = get_precision(precision) - exclude_types = exclude_types + self.orig_exclude_types = exclude_types self.exclude_types = set() for tt in exclude_types: assert len(tt) == 2 @@ -698,3 +702,93 @@ def _filter_r( result = tf.reduce_mean(xyz_scatter, axis=1) * res_rescale return result + + @classmethod + def deserialize(cls, data: dict, suffix: str = ""): + """Deserialize the model. + + Parameters + ---------- + data : dict + The serialized data + + Returns + ------- + Model + The deserialized model + """ + if cls is not DescrptSeR: + raise NotImplementedError("Not implemented in class %s" % cls.__name__) + data = data.copy() + embedding_net_variables = cls.deserialize_network( + data.pop("embeddings"), suffix=suffix + ) + data.pop("env_mat") + variables = data.pop("@variables") + descriptor = cls(**data) + descriptor.embedding_net_variables = embedding_net_variables + descriptor.davg = variables["davg"].reshape( + descriptor.ntypes, descriptor.ndescrpt + ) + descriptor.dstd = variables["dstd"].reshape( + descriptor.ntypes, descriptor.ndescrpt + ) + return descriptor + + def serialize(self, suffix: str = "") -> dict: + """Serialize the model. + + Parameters + ---------- + suffix : str, optional + The suffix of the scope + + Returns + ------- + dict + The serialized data + """ + if type(self) is not DescrptSeR: + raise NotImplementedError( + "Not implemented in class %s" % self.__class__.__name__ + ) + if self.embedding_net_variables is None: + raise RuntimeError("init_variables must be called before serialize") + if self.spin is not None: + raise NotImplementedError("spin is unsupported") + assert self.davg is not None + assert self.dstd is not None + # TODO: not sure how to handle type embedding - type embedding is not a model parameter, + # but instead a part of the input data. Maybe the interface should be refactored... + return { + "@class": "Descriptor", + "type": "se_r", + "rcut": self.rcut, + "rcut_smth": self.rcut_smth, + "sel": self.sel_r, + "neuron": self.filter_neuron, + "resnet_dt": self.filter_resnet_dt, + "trainable": self.trainable, + "type_one_side": self.type_one_side, + "exclude_types": list(self.orig_exclude_types), + "set_davg_zero": self.set_davg_zero, + "activation_function": self.activation_function_name, + "precision": self.filter_precision.name, + "embeddings": self.serialize_network( + ntypes=self.ntypes, + ndim=(1 if self.type_one_side else 2), + in_dim=1, + neuron=self.filter_neuron, + activation_function=self.activation_function_name, + resnet_dt=self.filter_resnet_dt, + variables=self.embedding_net_variables, + excluded_types=self.exclude_types, + suffix=suffix, + ), + "env_mat": EnvMat(self.rcut, self.rcut_smth).serialize(), + "@variables": { + "davg": self.davg.reshape(self.ntypes, self.nnei_r, 1), + "dstd": self.dstd.reshape(self.ntypes, self.nnei_r, 1), + }, + "spin": self.spin, + } diff --git a/source/tests/consistent/descriptor/test_se_r.py b/source/tests/consistent/descriptor/test_se_r.py new file mode 100644 index 0000000000..354ae1cc99 --- /dev/null +++ b/source/tests/consistent/descriptor/test_se_r.py @@ -0,0 +1,185 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest +from typing import ( + Any, + Tuple, +) + +import numpy as np + +from deepmd.dpmodel.descriptor.se_r import DescrptSeR as DescrptSeRDP +from deepmd.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) + +from ..common import ( + INSTALLED_PT, + INSTALLED_TF, + CommonTest, + parameterized, +) +from .common import ( + DescriptorTest, +) + +if INSTALLED_PT: + from deepmd.pt.model.descriptor.se_r import DescrptSeR as DescrptSeRPT +else: + DescrptSeAPT = None +if INSTALLED_TF: + from deepmd.tf.descriptor.se_r import DescrptSeR as DescrptSeRTF +else: + DescrptSeATF = None +from deepmd.utils.argcheck import ( + descrpt_se_r_args, +) + + +@parameterized( + (True, False), # resnet_dt + (True, False), # type_one_side + ([], [[0, 1]]), # excluded_types + ("float32", "float64"), # precision +) +class TestSeA(CommonTest, DescriptorTest, unittest.TestCase): + @property + def data(self) -> dict: + ( + resnet_dt, + type_one_side, + excluded_types, + precision, + ) = self.param + return { + "sel": [10, 10], + "rcut_smth": 5.80, + "rcut": 6.00, + "neuron": [6, 12, 24], + "resnet_dt": resnet_dt, + "type_one_side": type_one_side, + "exclude_types": excluded_types, + "precision": precision, + "seed": 1145141919810, + } + + @property + def skip_pt(self) -> bool: + ( + resnet_dt, + type_one_side, + excluded_types, + precision, + ) = self.param + return not type_one_side or CommonTest.skip_pt + + @property + def skip_dp(self) -> bool: + ( + resnet_dt, + type_one_side, + excluded_types, + precision, + ) = self.param + return not type_one_side or CommonTest.skip_dp + + tf_class = DescrptSeRTF + dp_class = DescrptSeRDP + pt_class = DescrptSeRPT + args = descrpt_se_r_args() + + def setUp(self): + CommonTest.setUp(self) + + self.ntypes = 2 + self.coords = np.array( + [ + 12.83, + 2.56, + 2.18, + 12.09, + 2.87, + 2.74, + 00.25, + 3.32, + 1.68, + 3.36, + 3.00, + 1.81, + 3.51, + 2.51, + 2.60, + 4.27, + 3.22, + 1.56, + ], + dtype=GLOBAL_NP_FLOAT_PRECISION, + ) + self.atype = np.array([0, 1, 1, 0, 1, 1], dtype=np.int32) + self.box = np.array( + [13.0, 0.0, 0.0, 0.0, 13.0, 0.0, 0.0, 0.0, 13.0], + dtype=GLOBAL_NP_FLOAT_PRECISION, + ) + self.natoms = np.array([6, 6, 2, 4], dtype=np.int32) + + def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: + return self.build_tf_descriptor( + obj, + self.natoms, + self.coords, + self.atype, + self.box, + suffix, + ) + + def eval_dp(self, dp_obj: Any) -> Any: + return self.eval_dp_descriptor( + dp_obj, + self.natoms, + self.coords, + self.atype, + self.box, + ) + + def eval_pt(self, pt_obj: Any) -> Any: + return self.eval_pt_descriptor( + pt_obj, + self.natoms, + self.coords, + self.atype, + self.box, + ) + + def extract_ret(self, ret: Any, backend) -> Tuple[np.ndarray, ...]: + return (ret[0],) + + @property + def rtol(self) -> float: + """Relative tolerance for comparing the return value.""" + ( + resnet_dt, + type_one_side, + excluded_types, + precision, + ) = self.param + if precision == "float64": + return 1e-10 + elif precision == "float32": + return 1e-4 + else: + raise ValueError(f"Unknown precision: {precision}") + + @property + def atol(self) -> float: + """Absolute tolerance for comparing the return value.""" + ( + resnet_dt, + type_one_side, + excluded_types, + precision, + ) = self.param + if precision == "float64": + return 1e-10 + elif precision == "float32": + return 1e-4 + else: + raise ValueError(f"Unknown precision: {precision}") diff --git a/source/tests/pt/model/test_descriptor.py b/source/tests/pt/model/test_descriptor.py index 529a83ac6d..ffad27201a 100644 --- a/source/tests/pt/model/test_descriptor.py +++ b/source/tests/pt/model/test_descriptor.py @@ -14,7 +14,7 @@ ) from deepmd.pt.model.descriptor import ( - prod_env_mat_se_a, + prod_env_mat, ) from deepmd.pt.utils import ( dp_random, @@ -155,7 +155,7 @@ def test_consistency(self): mixed_types=False, box=self.pt_batch["box"].to(env.DEVICE), ) - my_d, _, _ = prod_env_mat_se_a( + my_d, _, _ = prod_env_mat( extended_coord, nlist, atype, diff --git a/source/tests/pt/model/test_descriptor_se_r.py b/source/tests/pt/model/test_descriptor_se_r.py new file mode 100644 index 0000000000..c999f06863 --- /dev/null +++ b/source/tests/pt/model/test_descriptor_se_r.py @@ -0,0 +1,134 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import itertools +import unittest + +import numpy as np +import torch + +from deepmd.dpmodel.descriptor import DescrptSeR as DPDescrptSeR +from deepmd.pt.model.descriptor.se_r import ( + DescrptSeR, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.env import ( + PRECISION_DICT, +) + +from .test_env_mat import ( + TestCaseSingleFrameWithNlist, +) +from .test_mlp import ( + get_tols, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION + + +# to be merged with the tf test case +class TestDescrptSeR(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_consistency( + self, + ): + rng = np.random.default_rng() + _, _, nnei = self.nlist.shape + davg = rng.normal(size=(self.nt, nnei, 1)) + dstd = rng.normal(size=(self.nt, nnei, 1)) + dstd = 0.1 + np.abs(dstd) + + for idt, prec, em in itertools.product( + [False, True], + ["float64", "float32"], + [[], [[0, 1]], [[1, 1]]], + ): + dtype = PRECISION_DICT[prec] + rtol, atol = get_tols(prec) + err_msg = f"idt={idt} prec={prec}" + # sea new impl + dd0 = DescrptSeR( + self.rcut, + self.rcut_smth, + self.sel, + precision=prec, + resnet_dt=idt, + old_impl=False, + exclude_mask=em, + ).to(env.DEVICE) + dd0.mean = torch.tensor(davg, dtype=dtype, device=env.DEVICE) + dd0.dstd = torch.tensor(dstd, dtype=dtype, device=env.DEVICE) + + rd0, _, _, _, _ = dd0( + torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), + torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), + torch.tensor(self.nlist, dtype=int, device=env.DEVICE), + ) + # serialization + dd1 = DescrptSeR.deserialize(dd0.serialize()) + rd1, _, _, _, sw1 = dd1( + torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), + torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), + torch.tensor(self.nlist, dtype=int, device=env.DEVICE), + ) + np.testing.assert_allclose( + rd0.detach().cpu().numpy(), + rd1.detach().cpu().numpy(), + rtol=rtol, + atol=atol, + err_msg=err_msg, + ) + np.testing.assert_allclose( + rd0.detach().cpu().numpy()[0][self.perm[: self.nloc]], + rd0.detach().cpu().numpy()[1], + rtol=rtol, + atol=atol, + err_msg=err_msg, + ) + # dp impl + dd2 = DPDescrptSeR.deserialize(dd0.serialize()) + rd2, _, _, _, sw2 = dd2.call( + self.coord_ext, + self.atype_ext, + self.nlist, + ) + for aa, bb in zip([rd1, sw1], [rd2, sw2]): + np.testing.assert_allclose( + aa.detach().cpu().numpy(), + bb, + rtol=rtol, + atol=atol, + err_msg=err_msg, + ) + + def test_jit( + self, + ): + rng = np.random.default_rng() + _, _, nnei = self.nlist.shape + davg = rng.normal(size=(self.nt, nnei, 4)) + dstd = rng.normal(size=(self.nt, nnei, 4)) + dstd = 0.1 + np.abs(dstd) + + for idt, prec in itertools.product( + [False, True], + ["float64", "float32"], + ): + dtype = PRECISION_DICT[prec] + + # sea new impl + dd0 = DescrptSeR( + self.rcut, + self.rcut_smth, + self.sel, + precision=prec, + resnet_dt=idt, + old_impl=False, + ) + dd0.mean = torch.tensor(davg, dtype=dtype, device=env.DEVICE) + dd0.dstd = torch.tensor(dstd, dtype=dtype, device=env.DEVICE) + dd1 = DescrptSeR.deserialize(dd0.serialize()) + torch.jit.script(dd0) + torch.jit.script(dd1) diff --git a/source/tests/pt/model/test_env_mat.py b/source/tests/pt/model/test_env_mat.py index ee262e7ee5..fee3fd6fea 100644 --- a/source/tests/pt/model/test_env_mat.py +++ b/source/tests/pt/model/test_env_mat.py @@ -8,7 +8,7 @@ EnvMat, ) from deepmd.pt.model.descriptor.env_mat import ( - prod_env_mat_se_a, + prod_env_mat, ) from deepmd.pt.utils import ( env, @@ -99,7 +99,7 @@ def test_consistency( dstd = 0.1 + np.abs(dstd) em0 = EnvMat(self.rcut, self.rcut_smth) mm0, ww0 = em0.call(self.coord_ext, self.atype_ext, self.nlist, davg, dstd) - mm1, _, ww1 = prod_env_mat_se_a( + mm1, _, ww1 = prod_env_mat( torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), torch.tensor(self.nlist, dtype=int, device=env.DEVICE), torch.tensor(self.atype_ext[:, :nloc], dtype=int, device=env.DEVICE), From 854d998b06c73a9276735057dbe45219f1838b48 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 27 Feb 2024 09:03:37 -0500 Subject: [PATCH 131/686] add BaseModel; store type in serialization (#3335) Signed-off-by: Jinzhe Zeng Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- .../dpmodel/atomic_model/dp_atomic_model.py | 4 + .../atomic_model/linear_atomic_model.py | 11 ++ .../atomic_model/make_base_atomic_model.py | 4 + .../atomic_model/pairtab_atomic_model.py | 12 +- deepmd/dpmodel/infer/deep_eval.py | 6 +- deepmd/dpmodel/model/base_model.py | 158 ++++++++++++++++++ deepmd/dpmodel/model/dp_model.py | 6 +- .../pt/model/atomic_model/dp_atomic_model.py | 2 + .../model/atomic_model/linear_atomic_model.py | 4 + .../atomic_model/pairtab_atomic_model.py | 8 +- deepmd/pt/model/model/dp_model.py | 38 ++++- deepmd/pt/model/model/dp_zbl_model.py | 6 +- deepmd/pt/model/model/model.py | 54 +++++- deepmd/pt/utils/serialization.py | 7 +- deepmd/tf/model/model.py | 2 + deepmd/utils/plugin.py | 60 +++++++ source/tests/consistent/io/test_io.py | 6 +- 17 files changed, 370 insertions(+), 18 deletions(-) create mode 100644 deepmd/dpmodel/model/base_model.py diff --git a/deepmd/dpmodel/atomic_model/dp_atomic_model.py b/deepmd/dpmodel/atomic_model/dp_atomic_model.py index 1a823e369e..178b286e79 100644 --- a/deepmd/dpmodel/atomic_model/dp_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/dp_atomic_model.py @@ -130,6 +130,8 @@ def forward_atomic( def serialize(self) -> dict: return { + "@class": "Model", + "type": "standard", "type_map": self.type_map, "descriptor": self.descriptor.serialize(), "fitting": self.fitting.serialize(), @@ -138,6 +140,8 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "DPAtomicModel": data = copy.deepcopy(data) + data.pop("@class") + data.pop("type") descriptor_obj = BaseDescriptor.deserialize(data["descriptor"]) fitting_obj = BaseFitting.deserialize(data["fitting"]) obj = cls(descriptor_obj, fitting_obj, type_map=data["type_map"]) diff --git a/deepmd/dpmodel/atomic_model/linear_atomic_model.py b/deepmd/dpmodel/atomic_model/linear_atomic_model.py index 520ad9185e..e1130eaf45 100644 --- a/deepmd/dpmodel/atomic_model/linear_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/linear_atomic_model.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy import sys from abc import ( abstractmethod, @@ -182,12 +183,17 @@ def fitting_output_def(self) -> FittingOutputDef: @staticmethod def serialize(models) -> dict: return { + "@class": "Model", + "type": "linear", "models": [model.serialize() for model in models], "model_name": [model.__class__.__name__ for model in models], } @staticmethod def deserialize(data) -> List[BaseAtomicModel]: + data = copy.deepcopy(data) + data.pop("@class") + data.pop("type") model_names = data["model_name"] models = [ getattr(sys.modules[__name__], name).deserialize(model) @@ -263,6 +269,8 @@ def __init__( def serialize(self) -> dict: return { + "@class": "Model", + "type": "zbl", "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), "sw_rmin": self.sw_rmin, "sw_rmax": self.sw_rmax, @@ -271,6 +279,9 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "DPZBLLinearAtomicModel": + data = copy.deepcopy(data) + data.pop("@class") + data.pop("type") sw_rmin = data["sw_rmin"] sw_rmax = data["sw_rmax"] smin_alpha = data["smin_alpha"] diff --git a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py index b6c6b8460f..d4186c990d 100644 --- a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py @@ -160,6 +160,10 @@ def do_grad_(self, var_name: str, base: str) -> bool: return self.fitting_output_def()[var_name].c_differentiable return self.fitting_output_def()[var_name].r_differentiable + def get_model_def_script(self) -> str: + # TODO: implement this method; saved to model + raise NotImplementedError + setattr(BAM, fwd_method_name, BAM.fwd) delattr(BAM, "fwd") diff --git a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py index 34f6514986..dc3dfaf2ed 100644 --- a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy from typing import ( Dict, List, @@ -105,10 +106,19 @@ def mixed_types(self) -> bool: return True def serialize(self) -> dict: - return {"tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel} + return { + "@class": "Model", + "type": "pairtab", + "tab": self.tab.serialize(), + "rcut": self.rcut, + "sel": self.sel, + } @classmethod def deserialize(cls, data) -> "PairTabAtomicModel": + data = copy.deepcopy(data) + data.pop("@class") + data.pop("type") rcut = data["rcut"] sel = data["sel"] tab = PairTab.deserialize(data["tab"]) diff --git a/deepmd/dpmodel/infer/deep_eval.py b/deepmd/dpmodel/infer/deep_eval.py index 4e2349c0e8..1fd36bd7e8 100644 --- a/deepmd/dpmodel/infer/deep_eval.py +++ b/deepmd/dpmodel/infer/deep_eval.py @@ -13,8 +13,8 @@ import numpy as np -from deepmd.dpmodel.model.dp_model import ( - DPModel, +from deepmd.dpmodel.model.base_model import ( + BaseModel, ) from deepmd.dpmodel.output_def import ( ModelOutputDef, @@ -85,7 +85,7 @@ def __init__( self.model_path = model_file model_data = load_dp_model(model_file) - self.dp = DPModel.deserialize(model_data["model"]) + self.dp = BaseModel.deserialize(model_data["model"]) self.rcut = self.dp.get_rcut() self.type_map = self.dp.get_type_map() if isinstance(auto_batch_size, bool): diff --git a/deepmd/dpmodel/model/base_model.py b/deepmd/dpmodel/model/base_model.py new file mode 100644 index 0000000000..df9c926d6c --- /dev/null +++ b/deepmd/dpmodel/model/base_model.py @@ -0,0 +1,158 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import inspect +from abc import ( + ABC, + abstractmethod, +) +from typing import ( + Any, + List, + Type, +) + +from deepmd.utils.plugin import ( + make_plugin_registry, +) + + +def make_base_model() -> Type[object]: + class BaseBaseModel(ABC, make_plugin_registry("model")): + """Base class for final exported model that will be directly used for inference. + + The class defines some abstractmethods that will be directly called by the + inference interface. If the final model class inherits some of those methods + from other classes, `BaseModel` should be inherited as the last class to ensure + the correct method resolution order. + + This class is backend-indepedent. + + See Also + -------- + deepmd.dpmodel.model.base_model.BaseModel + BaseModel class for DPModel backend. + """ + + def __new__(cls, *args, **kwargs): + if inspect.isabstract(cls): + cls = cls.get_class_by_type(kwargs.get("type", "standard")) + return super().__new__(cls) + + @abstractmethod + def __call__(self, *args: Any, **kwds: Any) -> Any: + """Inference method. + + Parameters + ---------- + *args : Any + The input data for inference. + **kwds : Any + The input data for inference. + + Returns + ------- + Any + The output of the inference. + """ + pass + + @abstractmethod + def get_type_map(self) -> List[str]: + """Get the type map.""" + + @abstractmethod + def get_rcut(self): + """Get the cut-off radius.""" + + @abstractmethod + def get_dim_fparam(self): + """Get the number (dimension) of frame parameters of this atomic model.""" + + @abstractmethod + def get_dim_aparam(self): + """Get the number (dimension) of atomic parameters of this atomic model.""" + + @abstractmethod + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + + @abstractmethod + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + + If False, the shape is (nframes, nloc, ndim). + """ + + @abstractmethod + def model_output_type(self) -> str: + """Get the output type for the model.""" + + @abstractmethod + def serialize(self) -> dict: + """Serialize the model. + + Returns + ------- + dict + The serialized data + """ + pass + + @classmethod + def deserialize(cls, data: dict) -> "BaseBaseModel": + """Deserialize the model. + + Parameters + ---------- + data : dict + The serialized data + + Returns + ------- + BaseModel + The deserialized model + """ + if inspect.isabstract(cls): + return cls.get_class_by_type(data["type"]).deserialize(data) + raise NotImplementedError("Not implemented in class %s" % cls.__name__) + + model_def_script: str + + @abstractmethod + def get_model_def_script(self) -> str: + """Get the model definition script.""" + pass + + @abstractmethod + def get_nnei(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + # for C++ interface + pass + + @abstractmethod + def get_nsel(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + pass + + return BaseBaseModel + + +class BaseModel(make_base_model()): + """Base class for final exported model that will be directly used for inference. + + The class defines some abstractmethods that will be directly called by the + inference interface. If the final model class inherbits some of those methods + from other classes, `BaseModel` should be inherited as the last class to ensure + the correct method resolution order. + + This class is for the DPModel backend. + + See Also + -------- + deepmd.dpmodel.model.base_model.BaseBaseModel + Backend-independent BaseModel class. + """ diff --git a/deepmd/dpmodel/model/dp_model.py b/deepmd/dpmodel/model/dp_model.py index c2c40b40ba..804ce51dfd 100644 --- a/deepmd/dpmodel/model/dp_model.py +++ b/deepmd/dpmodel/model/dp_model.py @@ -2,6 +2,9 @@ from deepmd.dpmodel.atomic_model import ( DPAtomicModel, ) +from deepmd.dpmodel.model.base_model import ( + BaseModel, +) from .make_model import ( make_model, @@ -9,5 +12,6 @@ # use "class" to resolve "Variable not allowed in type expression" -class DPModel(make_model(DPAtomicModel)): +@BaseModel.register("standard") +class DPModel(make_model(DPAtomicModel), BaseModel): pass diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index 31b1c08a14..881ea4c97d 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -93,6 +93,8 @@ def mixed_types(self) -> bool: def serialize(self) -> dict: return { + "@class": "Model", + "type": "standard", "type_map": self.type_map, "descriptor": self.descriptor.serialize(), "fitting": self.fitting_net.serialize(), diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index f90fa5f237..68ff303d64 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -205,6 +205,8 @@ def fitting_output_def(self) -> FittingOutputDef: @staticmethod def serialize(models) -> dict: return { + "@class": "Model", + "type": "linear", "models": [model.serialize() for model in models], "model_name": [model.__class__.__name__ for model in models], } @@ -299,6 +301,8 @@ def __init__( def serialize(self) -> dict: return { + "@class": "Model", + "type": "zbl", "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), "sw_rmin": self.sw_rmin, "sw_rmax": self.sw_rmax, diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index eff445e799..86bfe98c36 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -121,7 +121,13 @@ def mixed_types(self) -> bool: return True def serialize(self) -> dict: - return {"tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel} + return { + "@class": "Model", + "type": "pairtab", + "tab": self.tab.serialize(), + "rcut": self.rcut, + "sel": self.sel, + } @classmethod def deserialize(cls, data) -> "PairTabAtomicModel": diff --git a/deepmd/pt/model/model/dp_model.py b/deepmd/pt/model/model/dp_model.py index 75d3820e45..5410f518d1 100644 --- a/deepmd/pt/model/model/dp_model.py +++ b/deepmd/pt/model/model/dp_model.py @@ -2,9 +2,45 @@ from deepmd.pt.model.atomic_model import ( DPAtomicModel, ) +from deepmd.pt.model.model.model import ( + BaseModel, +) +from deepmd.pt.model.task.dipole import ( + DipoleFittingNet, +) +from deepmd.pt.model.task.ener import ( + EnergyFittingNet, +) +from deepmd.pt.model.task.polarizability import ( + PolarFittingNet, +) from .make_model import ( make_model, ) -DPModel = make_model(DPAtomicModel) + +@BaseModel.register("standard") +class DPModel(make_model(DPAtomicModel), BaseModel): + def __new__(cls, descriptor, fitting, *args, **kwargs): + from deepmd.pt.model.model.dipole_model import ( + DipoleModel, + ) + from deepmd.pt.model.model.ener_model import ( + EnergyModel, + ) + from deepmd.pt.model.model.polar_model import ( + PolarModel, + ) + + # according to the fitting network to decide the type of the model + if cls is DPModel: + # map fitting to model + if isinstance(fitting, EnergyFittingNet): + cls = EnergyModel + elif isinstance(fitting, DipoleFittingNet): + cls = DipoleModel + elif isinstance(fitting, PolarFittingNet): + cls = PolarModel + # else: unknown fitting type, fall back to DPModel + return super().__new__(cls) diff --git a/deepmd/pt/model/model/dp_zbl_model.py b/deepmd/pt/model/model/dp_zbl_model.py index 0fd8008f21..c8264f2007 100644 --- a/deepmd/pt/model/model/dp_zbl_model.py +++ b/deepmd/pt/model/model/dp_zbl_model.py @@ -9,6 +9,9 @@ from deepmd.pt.model.atomic_model import ( DPZBLLinearAtomicModel, ) +from deepmd.pt.model.model.model import ( + BaseModel, +) from .make_model import ( make_model, @@ -17,7 +20,8 @@ DPZBLModel_ = make_model(DPZBLLinearAtomicModel) -class DPZBLModel(DPZBLModel_): +@BaseModel.register("zbl") +class DPZBLModel(DPZBLModel_, BaseModel): model_type = "ener" def __init__( diff --git a/deepmd/pt/model/model/model.py b/deepmd/pt/model/model/model.py index d98d25d539..0f5e27aea9 100644 --- a/deepmd/pt/model/model/model.py +++ b/deepmd/pt/model/model/model.py @@ -3,14 +3,62 @@ Optional, ) -import torch - +from deepmd.dpmodel.model.base_model import ( + make_base_model, +) from deepmd.utils.path import ( DPPath, ) -class BaseModel(torch.nn.Module): +# trick: torch.nn.Module should not be inherbited here, otherwise, +# the abstract method will override the method from the atomic model +# as Python resolves method lookups using the C3 linearisation. +# See https://stackoverflow.com/a/47117600/9567349 +# Take an example, this is the situation for only inheriting make_model(): +# torch.nn.Module BaseAtomicModel make_model() +# | | | +# ------------------------- | +# | | +# DPAtomicModel BaseModel +# | | +# make_model(DPAtomicModel) | +# | | +# ---------------------------------- +# | +# DPModel +# +# The order is: DPModel -> make_model(DPAtomicModel) -> DPAtomicModel -> +# torch.nn.Module -> BaseAtomicModel -> BaseModel -> make_model() +# +# However, if BaseModel also inherbits from torch.nn.Module: +# torch.nn.Module make_model() +# | | +# |--------------------------- | +# | | | +# | BaseAtomicModel | | +# | | | | +# |------------- ---------- +# | | +# DPAtomicModel BaseModel +# | | +# | | +# make_model(DPAtomicModel) | +# | | +# | | +# -------------------------------- +# | +# | +# DPModel +# +# The order is DPModel -> make_model(DPAtomicModel) -> DPAtomicModel -> +# BaseModel -> torch.nn.Module -> BaseAtomicModel -> make_model() +# BaseModel has higher proirity than BaseAtomicModel, which is not what +# we want. +# Alternatively, we can also make BaseAtomicModel in front of torch.nn.Module +# in DPAtomicModel (and other classes), but this requires the developer aware +# of it when developing it... +class BaseModel(make_base_model()): def __init__(self): """Construct a basic model for different tasks.""" super().__init__() diff --git a/deepmd/pt/utils/serialization.py b/deepmd/pt/utils/serialization.py index 91d1a3c76f..c99ddbb3c6 100644 --- a/deepmd/pt/utils/serialization.py +++ b/deepmd/pt/utils/serialization.py @@ -6,8 +6,8 @@ from deepmd.pt.model.model import ( get_model, ) -from deepmd.pt.model.model.ener_model import ( - EnergyModel, +from deepmd.pt.model.model.model import ( + BaseModel, ) from deepmd.pt.train.wrapper import ( ModelWrapper, @@ -68,8 +68,7 @@ def deserialize_to_file(model_file: str, data: dict) -> None: """ if not model_file.endswith(".pth"): raise ValueError("PyTorch backend only supports converting .pth file") - # TODO: read class type from data; see #3319 - model = EnergyModel.deserialize(data["model"]) + model = BaseModel.deserialize(data["model"]) # JIT will happy in this way... model.model_def_script = json.dumps(data["model_def_script"]) model = torch.jit.script(model) diff --git a/deepmd/tf/model/model.py b/deepmd/tf/model/model.py index 8af4771ff6..76310834a7 100644 --- a/deepmd/tf/model/model.py +++ b/deepmd/tf/model/model.py @@ -842,6 +842,8 @@ def serialize(self, suffix: str = "") -> dict: if self.spin is not None: raise NotImplementedError("spin is not supported") return { + "@class": "Model", + "type": "standard", "type_map": self.type_map, "descriptor": self.descrpt.serialize(suffix=suffix), "fitting": self.fitting.serialize(suffix=suffix), diff --git a/deepmd/utils/plugin.py b/deepmd/utils/plugin.py index a564ed61af..e6433ee681 100644 --- a/deepmd/utils/plugin.py +++ b/deepmd/utils/plugin.py @@ -2,11 +2,14 @@ """Base of plugin systems.""" # copied from https://github.com/deepmodeling/dpdata/blob/a3e76d75de53f6076254de82d18605a010dc3b00/dpdata/plugin.py +import difflib from abc import ( ABCMeta, ) from typing import ( Callable, + Optional, + Type, ) @@ -93,3 +96,60 @@ class PluginVariant(metaclass=VariantABCMeta): """A class to remove `type` from input arguments.""" pass + + +def make_plugin_registry(name: Optional[str] = None) -> Type[object]: + """Make a plugin registry. + + Parameters + ---------- + name : Optional[str] + the name of the registry for the error message, e.g. descriptor, backend, etc. + + Examples + -------- + >>> class BaseClass(make_plugin_registry()): + pass + """ + if name is None: + name = "class" + + class PR: + __plugins = Plugin() + + @staticmethod + def register(key: str) -> Callable[[object], object]: + """Register a descriptor plugin. + + Parameters + ---------- + key : str + the key of a descriptor + + Returns + ------- + callable[[object], object] + the registered descriptor + + Examples + -------- + >>> @BaseClass.register("some_class") + class SomeClass(BaseClass): + pass + """ + return PR.__plugins.register(key) + + @classmethod + def get_class_by_type(cls, class_type: str) -> Type[object]: + """Get the class by the plugin type.""" + if class_type in PR.__plugins.plugins: + return PR.__plugins.plugins[class_type] + else: + # did you mean + matches = difflib.get_close_matches( + class_type, PR.__plugins.plugins.keys() + ) + dym_message = f"Did you mean: {matches[0]}?" if matches else "" + raise RuntimeError(f"Unknown {name} type: {class_type}. {dym_message}") + + return PR diff --git a/source/tests/consistent/io/test_io.py b/source/tests/consistent/io/test_io.py index 7b6d374168..be599b0805 100644 --- a/source/tests/consistent/io/test_io.py +++ b/source/tests/consistent/io/test_io.py @@ -16,8 +16,8 @@ from deepmd.env import ( GLOBAL_NP_FLOAT_PRECISION, ) -from deepmd.infer.deep_pot import ( - DeepPot, +from deepmd.infer.deep_eval import ( + DeepEval, ) infer_path = Path(__file__).parent.parent.parent / "infer" @@ -121,7 +121,7 @@ def test_deep_eval(self): continue reference_data = copy.deepcopy(self.data) self.save_data_to_model(prefix + backend.suffixes[0], reference_data) - deep_eval = DeepPot(prefix + backend.suffixes[0]) + deep_eval = DeepEval(prefix + backend.suffixes[0]) ret = deep_eval.eval( self.coords, self.box, From b1de9e61a2ba9a309b130ac20fe2169bb0ce543a Mon Sep 17 00:00:00 2001 From: shiruosong <95087033+shiruosong@users.noreply.github.com> Date: Wed, 28 Feb 2024 05:33:43 +0800 Subject: [PATCH 132/686] fix_dplr.cpp set atom->image when pre_force (#3345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The atom->image of the wannier centroid should be set to the same as its real counterpart when assigning the position. --------- Co-authored-by: Yifan Li李一帆 --- source/lmp/fix_dplr.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/source/lmp/fix_dplr.cpp b/source/lmp/fix_dplr.cpp index 14d966a044..57bbd5765a 100644 --- a/source/lmp/fix_dplr.cpp +++ b/source/lmp/fix_dplr.cpp @@ -522,6 +522,7 @@ void FixDPLR::pre_force(int vflag) { // int res_idx = sort_fwd_map[sel_fwd[idx0]]; int res_idx = sel_fwd[idx0]; // int ret_idx = dpl_bwd[res_idx]; + atom->image[idx1] = atom->image[idx0]; for (int dd = 0; dd < 3; ++dd) { x[idx1][dd] = x[idx0][dd] + tensor[res_idx * 3 + dd] * dist_unit_cvt_factor; From 004ebd6ea540a31536b0b2893768ed9caef622c5 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 27 Feb 2024 19:39:45 -0500 Subject: [PATCH 133/686] apply PluginVariant and make_plugin_registry to classes (#3346) Signed-off-by: Jinzhe Zeng --- deepmd/backend/backend.py | 33 ++---------- .../descriptor/make_base_descriptor.py | 38 ++------------ deepmd/dpmodel/fitting/make_base_fitting.py | 38 ++------------ deepmd/dpmodel/model/base_model.py | 3 +- deepmd/pt/model/descriptor/descriptor.py | 37 ++----------- deepmd/tf/descriptor/descriptor.py | 38 ++------------ deepmd/tf/fit/fitting.py | 52 ++----------------- deepmd/tf/model/__init__.py | 17 ++++++ deepmd/tf/model/frozen.py | 1 + deepmd/tf/model/linear.py | 1 + deepmd/tf/model/model.py | 47 ++--------------- deepmd/tf/model/multi.py | 1 + deepmd/tf/model/pairtab.py | 1 + deepmd/tf/model/pairwise_dprc.py | 1 + deepmd/utils/plugin.py | 6 +++ 15 files changed, 59 insertions(+), 255 deletions(-) diff --git a/deepmd/backend/backend.py b/deepmd/backend/backend.py index f1ef4cb52a..8f7bca319e 100644 --- a/deepmd/backend/backend.py +++ b/deepmd/backend/backend.py @@ -16,8 +16,8 @@ ) from deepmd.utils.plugin import ( - Plugin, PluginVariant, + make_plugin_registry, ) if TYPE_CHECKING: @@ -33,7 +33,7 @@ ) -class Backend(PluginVariant): +class Backend(PluginVariant, make_plugin_registry("backend")): r"""General backend class. Examples @@ -44,24 +44,6 @@ class Backend(PluginVariant): ... pass """ - __plugins = Plugin() - - @staticmethod - def register(key: str) -> Callable[[object], object]: - """Register a backend plugin. - - Parameters - ---------- - key : str - the key of a backend - - Returns - ------- - Callable[[object], object] - the decorator to register backend - """ - return Backend.__plugins.register(key.lower()) - @staticmethod def get_backend(key: str) -> Type["Backend"]: """Get the backend by key. @@ -76,12 +58,7 @@ def get_backend(key: str) -> Type["Backend"]: Backend the backend """ - try: - backend = Backend.__plugins.get_plugin(key.lower()) - except KeyError: - raise KeyError(f"Backend {key} is not registered.") - assert isinstance(backend, type) - return backend + return Backend.get_class_by_type(key) @staticmethod def get_backends() -> Dict[str, Type["Backend"]]: @@ -92,7 +69,7 @@ def get_backends() -> Dict[str, Type["Backend"]]: list all the registered backends """ - return Backend.__plugins.plugins + return Backend.get_plugins() @staticmethod def get_backends_by_feature( @@ -112,7 +89,7 @@ def get_backends_by_feature( """ return { key: backend - for key, backend in Backend.__plugins.plugins.items() + for key, backend in Backend.get_backends().items() if backend.features & feature } diff --git a/deepmd/dpmodel/descriptor/make_base_descriptor.py b/deepmd/dpmodel/descriptor/make_base_descriptor.py index 2cdb5abd52..18416ff16b 100644 --- a/deepmd/dpmodel/descriptor/make_base_descriptor.py +++ b/deepmd/dpmodel/descriptor/make_base_descriptor.py @@ -4,10 +4,8 @@ abstractmethod, ) from typing import ( - Callable, List, Optional, - Type, ) from deepmd.common import ( @@ -17,7 +15,8 @@ DPPath, ) from deepmd.utils.plugin import ( - Plugin, + PluginVariant, + make_plugin_registry, ) @@ -37,45 +36,14 @@ def make_base_descriptor( """ - class BD(ABC): + class BD(ABC, PluginVariant, make_plugin_registry("descriptor")): """Base descriptor provides the interfaces of descriptor.""" - __plugins = Plugin() - - @staticmethod - def register(key: str) -> Callable: - """Register a descriptor plugin. - - Parameters - ---------- - key : str - the key of a descriptor - - Returns - ------- - Descriptor - the registered descriptor - - Examples - -------- - >>> @Descriptor.register("some_descrpt") - class SomeDescript(Descriptor): - pass - """ - return BD.__plugins.register(key) - def __new__(cls, *args, **kwargs): if cls is BD: cls = cls.get_class_by_type(j_get_type(kwargs, cls.__name__)) return super().__new__(cls) - @classmethod - def get_class_by_type(cls, descrpt_type: str) -> Type["BD"]: - if descrpt_type in BD.__plugins.plugins: - return BD.__plugins.plugins[descrpt_type] - else: - raise RuntimeError("Unknown descriptor type: " + descrpt_type) - @abstractmethod def get_rcut(self) -> float: """Returns the cut-off radius.""" diff --git a/deepmd/dpmodel/fitting/make_base_fitting.py b/deepmd/dpmodel/fitting/make_base_fitting.py index d206f8e39e..041076ba89 100644 --- a/deepmd/dpmodel/fitting/make_base_fitting.py +++ b/deepmd/dpmodel/fitting/make_base_fitting.py @@ -4,10 +4,8 @@ abstractmethod, ) from typing import ( - Callable, Dict, Optional, - Type, ) from deepmd.common import ( @@ -17,7 +15,8 @@ FittingOutputDef, ) from deepmd.utils.plugin import ( - Plugin, + PluginVariant, + make_plugin_registry, ) @@ -37,45 +36,14 @@ def make_base_fitting( """ - class BF(ABC): + class BF(ABC, PluginVariant, make_plugin_registry("fitting")): """Base fitting provides the interfaces of fitting net.""" - __plugins = Plugin() - - @staticmethod - def register(key: str) -> Callable[[object], object]: - """Register a descriptor plugin. - - Parameters - ---------- - key : str - the key of a descriptor - - Returns - ------- - callable[[object], object] - the registered descriptor - - Examples - -------- - >>> @Fitting.register("some_fitting") - class SomeFitting(Fitting): - pass - """ - return BF.__plugins.register(key) - def __new__(cls, *args, **kwargs): if cls is BF: cls = cls.get_class_by_type(j_get_type(kwargs, cls.__name__)) return super().__new__(cls) - @classmethod - def get_class_by_type(cls, fitting_type: str) -> Type["BF"]: - if fitting_type in BF.__plugins.plugins: - return BF.__plugins.plugins[fitting_type] - else: - raise RuntimeError("Unknown fitting type: " + fitting_type) - @abstractmethod def output_def(self) -> FittingOutputDef: """Returns the output def of the fitting net.""" diff --git a/deepmd/dpmodel/model/base_model.py b/deepmd/dpmodel/model/base_model.py index df9c926d6c..faf3e7cfff 100644 --- a/deepmd/dpmodel/model/base_model.py +++ b/deepmd/dpmodel/model/base_model.py @@ -11,12 +11,13 @@ ) from deepmd.utils.plugin import ( + PluginVariant, make_plugin_registry, ) def make_base_model() -> Type[object]: - class BaseBaseModel(ABC, make_plugin_registry("model")): + class BaseBaseModel(ABC, PluginVariant, make_plugin_registry("model")): """Base class for final exported model that will be directly used for inference. The class defines some abstractmethods that will be directly called by the diff --git a/deepmd/pt/model/descriptor/descriptor.py b/deepmd/pt/model/descriptor/descriptor.py index 91e0a2527a..964cdb01eb 100644 --- a/deepmd/pt/model/descriptor/descriptor.py +++ b/deepmd/pt/model/descriptor/descriptor.py @@ -5,7 +5,6 @@ abstractmethod, ) from typing import ( - Callable, Dict, List, Optional, @@ -22,60 +21,34 @@ from deepmd.pt.utils.env_mat_stat import ( EnvMatStatSe, ) -from deepmd.pt.utils.plugin import ( - Plugin, -) from deepmd.utils.env_mat_stat import ( StatItem, ) from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.plugin import ( + make_plugin_registry, +) log = logging.getLogger(__name__) -class DescriptorBlock(torch.nn.Module, ABC): +class DescriptorBlock(torch.nn.Module, ABC, make_plugin_registry("DescriptorBlock")): """The building block of descriptor. Given the input descriptor, provide with the atomic coordinates, atomic types and neighbor list, calculate the new descriptor. """ - __plugins = Plugin() local_cluster = False - @staticmethod - def register(key: str) -> Callable: - """Register a DescriptorBlock plugin. - - Parameters - ---------- - key : str - the key of a DescriptorBlock - - Returns - ------- - DescriptorBlock - the registered DescriptorBlock - - Examples - -------- - >>> @DescriptorBlock.register("some_descrpt") - class SomeDescript(DescriptorBlock): - pass - """ - return DescriptorBlock.__plugins.register(key) - def __new__(cls, *args, **kwargs): if cls is DescriptorBlock: try: descrpt_type = kwargs["type"] except KeyError: raise KeyError("the type of DescriptorBlock should be set by `type`") - if descrpt_type in DescriptorBlock.__plugins.plugins: - cls = DescriptorBlock.__plugins.plugins[descrpt_type] - else: - raise RuntimeError("Unknown DescriptorBlock type: " + descrpt_type) + cls = cls.get_class_by_type(descrpt_type) return super().__new__(cls) @abstractmethod diff --git a/deepmd/tf/descriptor/descriptor.py b/deepmd/tf/descriptor/descriptor.py index 48329ceb48..dbf260bfe8 100644 --- a/deepmd/tf/descriptor/descriptor.py +++ b/deepmd/tf/descriptor/descriptor.py @@ -4,7 +4,6 @@ ) from typing import ( Any, - Callable, Dict, List, Optional, @@ -21,12 +20,14 @@ tf, ) from deepmd.tf.utils import ( - Plugin, PluginVariant, ) +from deepmd.utils.plugin import ( + make_plugin_registry, +) -class Descriptor(PluginVariant): +class Descriptor(PluginVariant, make_plugin_registry("descriptor")): r"""The abstract class for descriptors. All specific descriptors should be based on this class. @@ -45,37 +46,6 @@ class Descriptor(PluginVariant): that can be called by other classes. """ - __plugins = Plugin() - - @staticmethod - def register(key: str) -> Callable: - """Register a descriptor plugin. - - Parameters - ---------- - key : str - the key of a descriptor - - Returns - ------- - Descriptor - the registered descriptor - - Examples - -------- - >>> @Descriptor.register("some_descrpt") - class SomeDescript(Descriptor): - pass - """ - return Descriptor.__plugins.register(key) - - @classmethod - def get_class_by_type(cls, descrpt_type: str): - if descrpt_type in Descriptor.__plugins.plugins: - return Descriptor.__plugins.plugins[descrpt_type] - else: - raise RuntimeError("Unknown descriptor type: " + descrpt_type) - def __new__(cls, *args, **kwargs): if cls is Descriptor: cls = cls.get_class_by_type(j_get_type(kwargs, cls.__name__)) diff --git a/deepmd/tf/fit/fitting.py b/deepmd/tf/fit/fitting.py index a24efcfdcd..6a7398daac 100644 --- a/deepmd/tf/fit/fitting.py +++ b/deepmd/tf/fit/fitting.py @@ -4,10 +4,8 @@ abstractmethod, ) from typing import ( - Callable, List, Optional, - Type, ) from deepmd.common import ( @@ -25,56 +23,14 @@ Loss, ) from deepmd.tf.utils import ( - Plugin, PluginVariant, ) +from deepmd.utils.plugin import ( + make_plugin_registry, +) -class Fitting(PluginVariant): - __plugins = Plugin() - - @staticmethod - def register(key: str) -> Callable: - """Register a Fitting plugin. - - Parameters - ---------- - key : str - the key of a Fitting - - Returns - ------- - Fitting - the registered Fitting - - Examples - -------- - >>> @Fitting.register("some_fitting") - class SomeFitting(Fitting): - pass - """ - return Fitting.__plugins.register(key) - - @classmethod - def get_class_by_type(cls, fitting_type: str) -> Type["Fitting"]: - """Get the fitting class by the input type. - - Parameters - ---------- - fitting_type : str - The input type - - Returns - ------- - Fitting - The fitting class - """ - if fitting_type in Fitting.__plugins.plugins: - cls = Fitting.__plugins.plugins[fitting_type] - else: - raise RuntimeError("Unknown descriptor type: " + fitting_type) - return cls - +class Fitting(PluginVariant, make_plugin_registry("fitting")): def __new__(cls, *args, **kwargs): if cls is Fitting: cls = cls.get_class_by_type(j_get_type(kwargs, cls.__name__)) diff --git a/deepmd/tf/model/__init__.py b/deepmd/tf/model/__init__.py index d366ca1441..1d100f2b09 100644 --- a/deepmd/tf/model/__init__.py +++ b/deepmd/tf/model/__init__.py @@ -1,4 +1,17 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from deepmd.tf.model.frozen import ( + FrozenModel, +) +from deepmd.tf.model.linear import ( + LinearEnergyModel, +) +from deepmd.tf.model.pairtab import ( + PairTabModel, +) +from deepmd.tf.model.pairwise_dprc import ( + PairwiseDPRc, +) + from .dos import ( DOSModel, ) @@ -23,4 +36,8 @@ "GlobalPolarModel", "PolarModel", "WFCModel", + "FrozenModel", + "LinearEnergyModel", + "PairTabModel", + "PairwiseDPRc", ] diff --git a/deepmd/tf/model/frozen.py b/deepmd/tf/model/frozen.py index f06ae954d1..1933690ca7 100644 --- a/deepmd/tf/model/frozen.py +++ b/deepmd/tf/model/frozen.py @@ -30,6 +30,7 @@ ) +@Model.register("frozen") class FrozenModel(Model): """Load model from a frozen model, which cannot be trained. diff --git a/deepmd/tf/model/linear.py b/deepmd/tf/model/linear.py index 7563e36b3f..da866ccc5f 100644 --- a/deepmd/tf/model/linear.py +++ b/deepmd/tf/model/linear.py @@ -147,6 +147,7 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): return local_jdata_cpy +@Model.register("linear_ener") class LinearEnergyModel(LinearModel): """Linear energy model make linear combinations of several existing energy models.""" diff --git a/deepmd/tf/model/model.py b/deepmd/tf/model/model.py index 76310834a7..2ae2879226 100644 --- a/deepmd/tf/model/model.py +++ b/deepmd/tf/model/model.py @@ -60,9 +60,12 @@ from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) +from deepmd.utils.plugin import ( + make_plugin_registry, +) -class Model(ABC): +class Model(ABC, make_plugin_registry("model")): """Abstract base model. Parameters @@ -94,47 +97,6 @@ class Model(ABC): Compression information for internal use """ - @classmethod - def get_class_by_type(cls, model_type: str): - """Get the class by input type. - - Parameters - ---------- - model_type : str - The input type - """ - # infer model type by fitting_type - from deepmd.tf.model.frozen import ( - FrozenModel, - ) - from deepmd.tf.model.linear import ( - LinearEnergyModel, - ) - from deepmd.tf.model.multi import ( - MultiModel, - ) - from deepmd.tf.model.pairtab import ( - PairTabModel, - ) - from deepmd.tf.model.pairwise_dprc import ( - PairwiseDPRc, - ) - - if model_type == "standard": - return StandardModel - elif model_type == "multi": - return MultiModel - elif model_type == "pairwise_dprc": - return PairwiseDPRc - elif model_type == "frozen": - return FrozenModel - elif model_type == "linear_ener": - return LinearEnergyModel - elif model_type == "pairtab": - return PairTabModel - else: - raise ValueError(f"unknown model type: {model_type}") - def __new__(cls, *args, **kwargs): if cls is Model: # init model @@ -621,6 +583,7 @@ def serialize(self, suffix: str = "") -> dict: raise NotImplementedError("Not implemented in class %s" % self.__name__) +@Model.register("standard") class StandardModel(Model): """Standard model, which must contain a descriptor and a fitting. diff --git a/deepmd/tf/model/multi.py b/deepmd/tf/model/multi.py index 2acf00fd52..6280fcd2f6 100644 --- a/deepmd/tf/model/multi.py +++ b/deepmd/tf/model/multi.py @@ -55,6 +55,7 @@ ) +@Model.register("multi") class MultiModel(Model): """Multi-task model. diff --git a/deepmd/tf/model/pairtab.py b/deepmd/tf/model/pairtab.py index fe94c43f64..2cb0dc6e52 100644 --- a/deepmd/tf/model/pairtab.py +++ b/deepmd/tf/model/pairtab.py @@ -31,6 +31,7 @@ ) +@Model.register("pairtab") class PairTabModel(Model): """Pairwise tabulation energy model. diff --git a/deepmd/tf/model/pairwise_dprc.py b/deepmd/tf/model/pairwise_dprc.py index 51296a0df9..5a377cdfa4 100644 --- a/deepmd/tf/model/pairwise_dprc.py +++ b/deepmd/tf/model/pairwise_dprc.py @@ -33,6 +33,7 @@ ) +@Model.register("pairwise_dprc") class PairwiseDPRc(Model): """Pairwise Deep Potential - Range Correction.""" diff --git a/deepmd/utils/plugin.py b/deepmd/utils/plugin.py index e6433ee681..22f315f63d 100644 --- a/deepmd/utils/plugin.py +++ b/deepmd/utils/plugin.py @@ -8,6 +8,7 @@ ) from typing import ( Callable, + Dict, Optional, Type, ) @@ -152,4 +153,9 @@ def get_class_by_type(cls, class_type: str) -> Type[object]: dym_message = f"Did you mean: {matches[0]}?" if matches else "" raise RuntimeError(f"Unknown {name} type: {class_type}. {dym_message}") + @classmethod + def get_plugins(cls) -> Dict[str, Type[object]]: + """Get all the registered plugins.""" + return PR.__plugins.plugins + return PR From f8ad655173b10d9bdc7aaa77f07214b2bde2f8d8 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 28 Feb 2024 01:33:38 -0500 Subject: [PATCH 134/686] add @version to serialization data (#3349) At this time, all current versions and the minimal and maximum versions are set to 1. If any changes, whether breaking or not, are made to the serialization data, the version should be bumped. --------- Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../dpmodel/atomic_model/dp_atomic_model.py | 5 ++++ .../atomic_model/linear_atomic_model.py | 7 +++++ .../atomic_model/pairtab_atomic_model.py | 5 ++++ deepmd/dpmodel/descriptor/se_e2_a.py | 5 ++++ deepmd/dpmodel/descriptor/se_r.py | 5 ++++ deepmd/dpmodel/fitting/general_fitting.py | 5 ++++ deepmd/dpmodel/utils/network.py | 30 ++++++++++++++++++- .../pt/model/atomic_model/dp_atomic_model.py | 5 ++++ .../model/atomic_model/linear_atomic_model.py | 10 +++++++ .../atomic_model/pairtab_atomic_model.py | 7 +++++ deepmd/pt/model/descriptor/se_a.py | 5 ++++ deepmd/pt/model/descriptor/se_r.py | 5 ++++ deepmd/pt/model/task/fitting.py | 5 ++++ deepmd/tf/descriptor/se_a.py | 5 ++++ deepmd/tf/descriptor/se_r.py | 5 ++++ deepmd/tf/fit/dipole.py | 6 ++++ deepmd/tf/fit/ener.py | 6 ++++ deepmd/tf/fit/polar.py | 6 ++++ deepmd/tf/model/model.py | 6 +++- deepmd/utils/pair_tab.py | 9 ++++++ deepmd/utils/version.py | 27 +++++++++++++++++ 21 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 deepmd/utils/version.py diff --git a/deepmd/dpmodel/atomic_model/dp_atomic_model.py b/deepmd/dpmodel/atomic_model/dp_atomic_model.py index 178b286e79..cd349749fa 100644 --- a/deepmd/dpmodel/atomic_model/dp_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/dp_atomic_model.py @@ -17,6 +17,9 @@ from deepmd.dpmodel.output_def import ( FittingOutputDef, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_atomic_model import ( BaseAtomicModel, @@ -132,6 +135,7 @@ def serialize(self) -> dict: return { "@class": "Model", "type": "standard", + "@version": 1, "type_map": self.type_map, "descriptor": self.descriptor.serialize(), "fitting": self.fitting.serialize(), @@ -140,6 +144,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "DPAtomicModel": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") descriptor_obj = BaseDescriptor.deserialize(data["descriptor"]) diff --git a/deepmd/dpmodel/atomic_model/linear_atomic_model.py b/deepmd/dpmodel/atomic_model/linear_atomic_model.py index e1130eaf45..6d8aea499e 100644 --- a/deepmd/dpmodel/atomic_model/linear_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/linear_atomic_model.py @@ -19,6 +19,9 @@ get_multiple_nlist_key, nlist_distinguish_types, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from ..output_def import ( FittingOutputDef, @@ -185,6 +188,7 @@ def serialize(models) -> dict: return { "@class": "Model", "type": "linear", + "@version": 1, "models": [model.serialize() for model in models], "model_name": [model.__class__.__name__ for model in models], } @@ -192,6 +196,7 @@ def serialize(models) -> dict: @staticmethod def deserialize(data) -> List[BaseAtomicModel]: data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") model_names = data["model_name"] @@ -271,6 +276,7 @@ def serialize(self) -> dict: return { "@class": "Model", "type": "zbl", + "@version": 1, "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), "sw_rmin": self.sw_rmin, "sw_rmax": self.sw_rmax, @@ -280,6 +286,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "DPZBLLinearAtomicModel": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") sw_rmin = data["sw_rmin"] diff --git a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py index dc3dfaf2ed..ddece80f2d 100644 --- a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py @@ -16,6 +16,9 @@ from deepmd.utils.pair_tab import ( PairTab, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_atomic_model import ( BaseAtomicModel, @@ -109,6 +112,7 @@ def serialize(self) -> dict: return { "@class": "Model", "type": "pairtab", + "@version": 1, "tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel, @@ -117,6 +121,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "PairTabAtomicModel": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") rcut = data["rcut"] diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index a28215c35a..b102933ac9 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -9,6 +9,9 @@ from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) try: from deepmd._version import version as __version__ @@ -345,6 +348,7 @@ def serialize(self) -> dict: return { "@class": "Descriptor", "type": "se_e2_a", + "@version": 1, "rcut": self.rcut, "rcut_smth": self.rcut_smth, "sel": self.sel, @@ -371,6 +375,7 @@ def serialize(self) -> dict: def deserialize(cls, data: dict) -> "DescrptSeA": """Deserialize from dict.""" data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class", None) data.pop("type", None) variables = data.pop("@variables") diff --git a/deepmd/dpmodel/descriptor/se_r.py b/deepmd/dpmodel/descriptor/se_r.py index 77e43f7d85..5973c55353 100644 --- a/deepmd/dpmodel/descriptor/se_r.py +++ b/deepmd/dpmodel/descriptor/se_r.py @@ -4,6 +4,9 @@ from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) try: from deepmd._version import version as __version__ @@ -282,6 +285,7 @@ def serialize(self) -> dict: return { "@class": "Descriptor", "type": "se_r", + "@version": 1, "rcut": self.rcut, "rcut_smth": self.rcut_smth, "sel": self.sel, @@ -307,6 +311,7 @@ def serialize(self) -> dict: def deserialize(cls, data: dict) -> "DescrptSeR": """Deserialize from dict.""" data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class", None) data.pop("type", None) variables = data.pop("@variables") diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index 152836e928..752a550849 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -21,6 +21,9 @@ FittingNet, NetworkCollection, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_fitting import ( BaseFitting, @@ -210,6 +213,7 @@ def serialize(self) -> dict: """Serialize the fitting to dict.""" return { "@class": "Fitting", + "@version": 1, "var_name": self.var_name, "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, @@ -241,6 +245,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "GeneralFitting": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") variables = data.pop("@variables") diff --git a/deepmd/dpmodel/utils/network.py b/deepmd/dpmodel/utils/network.py index 2133bc4889..feb3355e77 100644 --- a/deepmd/dpmodel/utils/network.py +++ b/deepmd/dpmodel/utils/network.py @@ -20,6 +20,10 @@ import h5py import numpy as np +from deepmd.utils.version import ( + check_version_compatibility, +) + try: from deepmd._version import version as __version__ except ImportError: @@ -189,6 +193,8 @@ def serialize(self) -> dict: "idt": self.idt, } return { + "@class": "Layer", + "@version": 1, "bias": self.b is not None, "use_timestep": self.idt is not None, "activation_function": self.activation_function, @@ -208,6 +214,8 @@ def deserialize(cls, data: dict) -> "NativeLayer": The dict to deserialize from. """ data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) + data.pop("@class", None) variables = data.pop("@variables") assert variables["w"] is not None and len(variables["w"].shape) == 2 num_in, num_out = variables["w"].shape @@ -349,7 +357,11 @@ def serialize(self) -> dict: dict The serialized network. """ - return {"layers": [layer.serialize() for layer in self.layers]} + return { + "@class": "NN", + "@version": 1, + "layers": [layer.serialize() for layer in self.layers], + } @classmethod def deserialize(cls, data: dict) -> "NN": @@ -360,6 +372,9 @@ def deserialize(cls, data: dict) -> "NN": data : dict The dict to deserialize from. """ + data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) + data.pop("@class", None) return cls(data["layers"]) def __getitem__(self, key): @@ -471,6 +486,8 @@ def serialize(self) -> dict: The serialized network. """ return { + "@class": "EmbeddingNetwork", + "@version": 1, "in_dim": self.in_dim, "neuron": self.neuron.copy(), "activation_function": self.activation_function, @@ -490,6 +507,8 @@ def deserialize(cls, data: dict) -> "EmbeddingNet": The dict to deserialize from. """ data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) + data.pop("@class", None) layers = data.pop("layers") obj = cls(**data) super(EN, obj).__init__(layers) @@ -566,6 +585,8 @@ def serialize(self) -> dict: The serialized network. """ return { + "@class": "FittingNetwork", + "@version": 1, "in_dim": self.in_dim, "out_dim": self.out_dim, "neuron": self.neuron.copy(), @@ -586,6 +607,8 @@ def deserialize(cls, data: dict) -> "FittingNet": The dict to deserialize from. """ data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) + data.pop("@class", None) layers = data.pop("layers") obj = cls(**data) T_Network.__init__(obj, layers) @@ -688,6 +711,8 @@ def serialize(self) -> dict: network_type_map_inv = {v: k for k, v in self.NETWORK_TYPE_MAP.items()} network_type_name = network_type_map_inv[self.network_type] return { + "@class": "NetworkCollection", + "@version": 1, "ndim": self.ndim, "ntypes": self.ntypes, "network_type": network_type_name, @@ -703,4 +728,7 @@ def deserialize(cls, data: dict) -> "NetworkCollection": data : dict The dict to deserialize from. """ + data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) + data.pop("@class", None) return cls(**data) diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index 881ea4c97d..d2c1743d30 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -24,6 +24,9 @@ from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_atomic_model import ( BaseAtomicModel, @@ -95,6 +98,7 @@ def serialize(self) -> dict: return { "@class": "Model", "type": "standard", + "@version": 1, "type_map": self.type_map, "descriptor": self.descriptor.serialize(), "fitting": self.fitting_net.serialize(), @@ -103,6 +107,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "DPAtomicModel": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) descriptor_obj = BaseDescriptor.deserialize(data["descriptor"]) fitting_obj = BaseFitting.deserialize(data["fitting"]) obj = cls(descriptor_obj, fitting_obj, type_map=data["type_map"]) diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 68ff303d64..52f5f1d13c 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy import sys from abc import ( abstractmethod, @@ -24,6 +25,9 @@ get_multiple_nlist_key, nlist_distinguish_types, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_atomic_model import ( BaseAtomicModel, @@ -206,6 +210,7 @@ def fitting_output_def(self) -> FittingOutputDef: def serialize(models) -> dict: return { "@class": "Model", + "@version": 1, "type": "linear", "models": [model.serialize() for model in models], "model_name": [model.__class__.__name__ for model in models], @@ -213,6 +218,8 @@ def serialize(models) -> dict: @staticmethod def deserialize(data) -> List[BaseAtomicModel]: + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) model_names = data["model_name"] models = [ getattr(sys.modules[__name__], name).deserialize(model) @@ -303,6 +310,7 @@ def serialize(self) -> dict: return { "@class": "Model", "type": "zbl", + "@version": 1, "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), "sw_rmin": self.sw_rmin, "sw_rmax": self.sw_rmax, @@ -311,6 +319,8 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "DPZBLLinearAtomicModel": + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) sw_rmin = data["sw_rmin"] sw_rmax = data["sw_rmax"] smin_alpha = data["smin_alpha"] diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index 86bfe98c36..c0b7c65d7a 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy from typing import ( Dict, List, @@ -15,6 +16,9 @@ from deepmd.utils.pair_tab import ( PairTab, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_atomic_model import ( BaseAtomicModel, @@ -124,6 +128,7 @@ def serialize(self) -> dict: return { "@class": "Model", "type": "pairtab", + "@version": 1, "tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel, @@ -131,6 +136,8 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "PairTabAtomicModel": + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) rcut = data["rcut"] sel = data["sel"] tab = PairTab.deserialize(data["tab"]) diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 033d640ad8..6c29636d6d 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -31,6 +31,9 @@ from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) try: from typing import ( @@ -182,6 +185,7 @@ def serialize(self) -> dict: return { "@class": "Descriptor", "type": "se_e2_a", + "@version": 1, "rcut": obj.rcut, "rcut_smth": obj.rcut_smth, "sel": obj.sel, @@ -208,6 +212,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "DescrptSeA": data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class", None) data.pop("type", None) variables = data.pop("@variables") diff --git a/deepmd/pt/model/descriptor/se_r.py b/deepmd/pt/model/descriptor/se_r.py index c685640426..bdb7dafe73 100644 --- a/deepmd/pt/model/descriptor/se_r.py +++ b/deepmd/pt/model/descriptor/se_r.py @@ -36,6 +36,9 @@ from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_descriptor import ( BaseDescriptor, @@ -277,6 +280,7 @@ def serialize(self) -> dict: return { "@class": "Descriptor", "type": "se_r", + "@version": 1, "rcut": self.rcut, "rcut_smth": self.rcut_smth, "sel": self.sel, @@ -302,6 +306,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "DescrptSeR": data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) variables = data.pop("@variables") embeddings = data.pop("embeddings") env_mat = data.pop("env_mat") diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 6c395d3800..0c64983f60 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -43,6 +43,9 @@ to_numpy_array, to_torch_tensor, ) +from deepmd.utils.version import ( + check_version_compatibility, +) dtype = env.GLOBAL_PT_FLOAT_PRECISION device = env.DEVICE @@ -367,6 +370,7 @@ def serialize(self) -> dict: """Serialize the fitting to dict.""" return { "@class": "Fitting", + "@version": 1, "var_name": self.var_name, "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, @@ -404,6 +408,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "GeneralFitting": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) variables = data.pop("@variables") nets = data.pop("nets") obj = cls(**data) diff --git a/deepmd/tf/descriptor/se_a.py b/deepmd/tf/descriptor/se_a.py index e1b7258c63..0e15ba13a8 100644 --- a/deepmd/tf/descriptor/se_a.py +++ b/deepmd/tf/descriptor/se_a.py @@ -65,6 +65,9 @@ from deepmd.tf.utils.type_embed import ( embed_atom_type, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .descriptor import ( Descriptor, @@ -1368,6 +1371,7 @@ def deserialize(cls, data: dict, suffix: str = ""): if cls is not DescrptSeA: raise NotImplementedError("Not implemented in class %s" % cls.__name__) data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class", None) data.pop("type", None) embedding_net_variables = cls.deserialize_network( @@ -1422,6 +1426,7 @@ def serialize(self, suffix: str = "") -> dict: return { "@class": "Descriptor", "type": "se_e2_a", + "@version": 1, "rcut": self.rcut_r, "rcut_smth": self.rcut_r_smth, "sel": self.sel_a, diff --git a/deepmd/tf/descriptor/se_r.py b/deepmd/tf/descriptor/se_r.py index 1a12befdf0..ba1a261390 100644 --- a/deepmd/tf/descriptor/se_r.py +++ b/deepmd/tf/descriptor/se_r.py @@ -38,6 +38,9 @@ from deepmd.tf.utils.tabulate import ( DPTabulate, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .descriptor import ( Descriptor, @@ -720,6 +723,7 @@ def deserialize(cls, data: dict, suffix: str = ""): if cls is not DescrptSeR: raise NotImplementedError("Not implemented in class %s" % cls.__name__) data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) embedding_net_variables = cls.deserialize_network( data.pop("embeddings"), suffix=suffix ) @@ -763,6 +767,7 @@ def serialize(self, suffix: str = "") -> dict: return { "@class": "Descriptor", "type": "se_r", + "@version": 1, "rcut": self.rcut, "rcut_smth": self.rcut_smth, "sel": self.sel_r, diff --git a/deepmd/tf/fit/dipole.py b/deepmd/tf/fit/dipole.py index 3557d00aa0..f503789308 100644 --- a/deepmd/tf/fit/dipole.py +++ b/deepmd/tf/fit/dipole.py @@ -30,6 +30,9 @@ one_layer, one_layer_rand_seed_shift, ) +from deepmd.utils.version import ( + check_version_compatibility, +) @Fitting.register("dipole") @@ -346,6 +349,7 @@ def serialize(self, suffix: str) -> dict: data = { "@class": "Fitting", "type": "dipole", + "@version": 1, "var_name": "dipole", "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, @@ -388,6 +392,8 @@ def deserialize(cls, data: dict, suffix: str): Model The deserialized model """ + data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) fitting = cls(**data) fitting.fitting_net_variables = cls.deserialize_network( data["nets"], diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index 0cdd1a1676..106e10839d 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -53,6 +53,9 @@ from deepmd.tf.utils.spin import ( Spin, ) +from deepmd.utils.version import ( + check_version_compatibility, +) if TYPE_CHECKING: pass @@ -959,6 +962,8 @@ def deserialize(cls, data: dict, suffix: str = ""): Model The deserialized model """ + data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) fitting = cls(**data) fitting.fitting_net_variables = cls.deserialize_network( data["nets"], @@ -984,6 +989,7 @@ def serialize(self, suffix: str = "") -> dict: data = { "@class": "Fitting", "type": "ener", + "@version": 1, "var_name": "energy", "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, diff --git a/deepmd/tf/fit/polar.py b/deepmd/tf/fit/polar.py index f5cebf9a39..002082ad2e 100644 --- a/deepmd/tf/fit/polar.py +++ b/deepmd/tf/fit/polar.py @@ -34,6 +34,9 @@ one_layer, one_layer_rand_seed_shift, ) +from deepmd.utils.version import ( + check_version_compatibility, +) @Fitting.register("polar") @@ -536,6 +539,7 @@ def serialize(self, suffix: str) -> dict: data = { "@class": "Fitting", "type": "polar", + "@version": 1, "var_name": "polar", "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, @@ -581,6 +585,8 @@ def deserialize(cls, data: dict, suffix: str): Model The deserialized model """ + data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) fitting = cls(**data) fitting.fitting_net_variables = cls.deserialize_network( data["nets"], diff --git a/deepmd/tf/model/model.py b/deepmd/tf/model/model.py index 2ae2879226..889f7ccc4d 100644 --- a/deepmd/tf/model/model.py +++ b/deepmd/tf/model/model.py @@ -63,6 +63,9 @@ from deepmd.utils.plugin import ( make_plugin_registry, ) +from deepmd.utils.version import ( + check_version_compatibility, +) class Model(ABC, make_plugin_registry("model")): @@ -778,7 +781,7 @@ def deserialize(cls, data: dict, suffix: str = "") -> "Descriptor": The deserialized descriptor """ data = copy.deepcopy(data) - + check_version_compatibility(data.pop("@version", 1), 1, 1) descriptor = Descriptor.deserialize(data.pop("descriptor"), suffix=suffix) fitting = Fitting.deserialize(data.pop("fitting"), suffix=suffix) return cls( @@ -807,6 +810,7 @@ def serialize(self, suffix: str = "") -> dict: return { "@class": "Model", "type": "standard", + "@version": 1, "type_map": self.type_map, "descriptor": self.descrpt.serialize(suffix=suffix), "fitting": self.fitting.serialize(suffix=suffix), diff --git a/deepmd/utils/pair_tab.py b/deepmd/utils/pair_tab.py index b807354171..1b397a3cfa 100644 --- a/deepmd/utils/pair_tab.py +++ b/deepmd/utils/pair_tab.py @@ -12,6 +12,10 @@ CubicSpline, ) +from deepmd.utils.version import ( + check_version_compatibility, +) + log = logging.getLogger(__name__) @@ -72,6 +76,8 @@ def reinit(self, filename: str, rcut: Optional[float] = None) -> None: def serialize(self) -> dict: return { + "@class": "PairTab", + "@version": 1, "rmin": self.rmin, "rmax": self.rmax, "hh": self.hh, @@ -87,6 +93,9 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "PairTab": + data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) + data.pop("@class") variables = data.pop("@variables") tab = PairTab(None, None) tab.vdata = variables["vdata"] diff --git a/deepmd/utils/version.py b/deepmd/utils/version.py new file mode 100644 index 0000000000..a0b479778d --- /dev/null +++ b/deepmd/utils/version.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +def check_version_compatibility( + current_version: int, + maximum_supported_version: int, + minimal_supported_version: int = 1, +): + """Check if the current version is compatible with the supported versions. + + Parameters + ---------- + current_version : int + The current version. + maximum_supported_version : int + The maximum supported version. + minimal_supported_version : int, optional + The minimal supported version. Default is 1. + + Raises + ------ + ValueError + If the current version is not compatible with the supported versions. + """ + if not minimal_supported_version <= current_version <= maximum_supported_version: + raise ValueError( + f"Current version {current_version} is not compatible with supported versions " + f"[{minimal_supported_version}, {maximum_supported_version}]." + ) From fd17e2efcf347790100f170fffbaaafb494ee7c2 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:08:43 +0800 Subject: [PATCH 135/686] Fix: se_r prod_env_mat (#3351) This should fix the bug. image --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/pt/utils/env_mat_stat.py | 9 +++ source/tests/pt/model/test_descriptor_se_r.py | 55 ++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/deepmd/pt/utils/env_mat_stat.py b/deepmd/pt/utils/env_mat_stat.py index 3af03bda97..70b7228440 100644 --- a/deepmd/pt/utils/env_mat_stat.py +++ b/deepmd/pt/utils/env_mat_stat.py @@ -101,6 +101,14 @@ def iter( dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE, ) + if self.last_dim == 4: + radial_only = False + elif self.last_dim == 1: + radial_only = True + else: + raise ValueError( + "last_dim should be 1 for raial-only or 4 for full descriptor." + ) for system in data: coord, atype, box, natoms = ( system["coord"], @@ -130,6 +138,7 @@ def iter( self.descriptor.get_rcut(), # TODO: export rcut_smth from DescriptorBlock self.descriptor.rcut_smth, + radial_only, ) # reshape to nframes * nloc at the atom level, # so nframes/mixed_type do not matter diff --git a/source/tests/pt/model/test_descriptor_se_r.py b/source/tests/pt/model/test_descriptor_se_r.py index c999f06863..5b8b6c9251 100644 --- a/source/tests/pt/model/test_descriptor_se_r.py +++ b/source/tests/pt/model/test_descriptor_se_r.py @@ -15,6 +15,9 @@ from deepmd.pt.utils.env import ( PRECISION_DICT, ) +from deepmd.pt.utils.env_mat_stat import ( + EnvMatStatSe, +) from .test_env_mat import ( TestCaseSingleFrameWithNlist, @@ -103,13 +106,61 @@ def test_consistency( err_msg=err_msg, ) + def test_load_stat(self): + rng = np.random.default_rng() + _, _, nnei = self.nlist.shape + davg = rng.normal(size=(self.nt, nnei, 1)) + dstd = rng.normal(size=(self.nt, nnei, 1)) + dstd = 0.1 + np.abs(dstd) + + for idt, prec in itertools.product( + [False, True], + ["float64", "float32"], + ): + dtype = PRECISION_DICT[prec] + + # sea new impl + dd0 = DescrptSeR( + self.rcut, + self.rcut_smth, + self.sel, + precision=prec, + resnet_dt=idt, + old_impl=False, + ) + dd0.mean = torch.tensor(davg, dtype=dtype, device=env.DEVICE) + dd0.dstd = torch.tensor(dstd, dtype=dtype, device=env.DEVICE) + dd1 = DescrptSeR.deserialize(dd0.serialize()) + dd1.compute_input_stats( + [ + { + "r0": None, + "coord": torch.from_numpy(self.coord_ext) + .reshape(-1, self.nall, 3) + .to(env.DEVICE), + "atype": torch.from_numpy(self.atype_ext).to(env.DEVICE), + "box": None, + "natoms": self.nall, + } + ] + ) + + with self.assertRaises(ValueError) as cm: + ev = EnvMatStatSe(dd1) + ev.last_dim = 3 + ev.load_or_compute_stats([]) + self.assertEqual( + "last_dim should be 1 for raial-only or 4 for full descriptor.", + str(cm.exception), + ) + def test_jit( self, ): rng = np.random.default_rng() _, _, nnei = self.nlist.shape - davg = rng.normal(size=(self.nt, nnei, 4)) - dstd = rng.normal(size=(self.nt, nnei, 4)) + davg = rng.normal(size=(self.nt, nnei, 1)) + dstd = rng.normal(size=(self.nt, nnei, 1)) dstd = 0.1 + np.abs(dstd) for idt, prec in itertools.product( From d377ccb19bc3d2fd5fc3161ba76668772362a519 Mon Sep 17 00:00:00 2001 From: Lysithea <52808607+CaRoLZhangxy@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:13:52 +0800 Subject: [PATCH 136/686] cc: add torch backend support for Multiple backend (#3162) need to test in union environment (tf and pt) see https://github.com/deepmodeling/deepmd-kit/issues/3119 --------- Signed-off-by: Lysithea <52808607+CaRoLZhangxy@users.noreply.github.com> Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jinzhe Zeng Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- .github/workflows/test_cc.yml | 4 +- .github/workflows/test_cuda.yml | 12 +- deepmd/pt/model/model/make_model.py | 7 + source/api_cc/include/DeepPotPT.h | 332 ++++++++++++ source/api_cc/include/commonPT.h | 24 + source/api_cc/src/DeepPot.cc | 19 +- source/api_cc/src/DeepPotPT.cc | 356 ++++++++++++- source/api_cc/src/commonPT.cc | 23 + source/api_cc/tests/test_deeppot_pt.cc | 625 ++++++++++++++++++++++ source/install/test_cc_local.sh | 10 +- source/ipi/tests/test_driver.py | 105 ++++ source/lmp/tests/test_lammps_pt.py | 695 +++++++++++++++++++++++++ source/tests/infer/deeppot_sea.pth | Bin 0 -> 123025 bytes 13 files changed, 2201 insertions(+), 11 deletions(-) create mode 100644 source/api_cc/include/DeepPotPT.h create mode 100644 source/api_cc/include/commonPT.h create mode 100644 source/api_cc/src/commonPT.cc create mode 100644 source/api_cc/tests/test_deeppot_pt.cc create mode 100644 source/lmp/tests/test_lammps_pt.py create mode 100644 source/tests/infer/deeppot_sea.pth diff --git a/.github/workflows/test_cc.yml b/.github/workflows/test_cc.yml index 2082e7e4cc..d98f8ca58d 100644 --- a/.github/workflows/test_cc.yml +++ b/.github/workflows/test_cc.yml @@ -56,7 +56,7 @@ jobs: TF_INTRA_OP_PARALLELISM_THREADS: 1 TF_INTER_OP_PARALLELISM_THREADS: 1 LAMMPS_PLUGIN_PATH: ${{ github.workspace }}/dp_test/lib/deepmd_lmp - LD_LIBRARY_PATH: ${{ github.workspace }}/dp_test/lib + LD_LIBRARY_PATH: ${{ github.workspace }}/dp_test/lib:${{ github.workspace }}/libtorch/lib if: ${{ !matrix.check_memleak }} # test ipi - run: pytest --cov=deepmd source/ipi/tests @@ -65,7 +65,7 @@ jobs: TF_INTRA_OP_PARALLELISM_THREADS: 1 TF_INTER_OP_PARALLELISM_THREADS: 1 PATH: ${{ github.workspace }}/dp_test/bin:$PATH - LD_LIBRARY_PATH: ${{ github.workspace }}/dp_test/lib + LD_LIBRARY_PATH: ${{ github.workspace }}/dp_test/lib:${{ github.workspace }}/libtorch/lib if: ${{ !matrix.check_memleak }} - uses: codecov/codecov-action@v4 env: diff --git a/.github/workflows/test_cuda.yml b/.github/workflows/test_cuda.yml index 0d934e6d77..915d983663 100644 --- a/.github/workflows/test_cuda.yml +++ b/.github/workflows/test_cuda.yml @@ -38,6 +38,8 @@ jobs: with: useLocalCache: true useCloudCache: false + - name: Install wget and unzip + run: apt-get update && apt-get install -y wget unzip - run: | wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.0-1_all.deb \ && sudo dpkg -i cuda-keyring_1.0-1_all.deb \ @@ -53,7 +55,13 @@ jobs: DP_ENABLE_NATIVE_OPTIMIZATION: 1 - run: dp --version - run: python -m pytest source/tests --durations=0 - - run: source/install/test_cc_local.sh + - name: Download libtorch + run: | + wget https://download.pytorch.org/libtorch/cu121/libtorch-cxx11-abi-shared-with-deps-2.2.1%2Bcu121.zip -O libtorch.zip + unzip libtorch.zip + - run: | + export CMAKE_PREFIX_PATH=$GITHUB_WORKSPACE/libtorch + source/install/test_cc_local.sh env: OMP_NUM_THREADS: 1 TF_INTRA_OP_PARALLELISM_THREADS: 1 @@ -63,7 +71,7 @@ jobs: DP_VARIANT: cuda DP_USE_MPICH2: 1 - run: | - export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/dp_test/lib:$CUDA_PATH/lib64:$LD_LIBRARY_PATH + export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/dp_test/lib:$GITHUB_WORKSPACE/libtorch/lib:$CUDA_PATH/lib64:$LD_LIBRARY_PATH export PATH=$GITHUB_WORKSPACE/dp_test/bin:$PATH python -m pytest source/lmp/tests python -m pytest source/ipi/tests diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 79634186e4..b6478d297f 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -17,6 +17,9 @@ communicate_extended_output, fit_output_to_model_output, ) +from deepmd.pt.utils import ( + env, +) from deepmd.pt.utils.nlist import ( extend_input_and_build_neighbor_list, nlist_distinguish_types, @@ -115,6 +118,9 @@ def forward_common( The keys are defined by the `ModelOutputDef`. """ + coord = coord.to(env.GLOBAL_PT_FLOAT_PRECISION) + if box is not None: + box = box.to(env.GLOBAL_PT_FLOAT_PRECISION) ( extended_coord, extended_atype, @@ -183,6 +189,7 @@ def forward_common_lower( the result dict, defined by the `FittingOutputDef`. """ + extended_coord = extended_coord.to(env.GLOBAL_PT_FLOAT_PRECISION) nframes, nall = extended_atype.shape[:2] extended_coord = extended_coord.view(nframes, -1, 3) nlist = self.format_nlist(extended_coord, extended_atype, nlist) diff --git a/source/api_cc/include/DeepPotPT.h b/source/api_cc/include/DeepPotPT.h new file mode 100644 index 0000000000..1b757069c3 --- /dev/null +++ b/source/api_cc/include/DeepPotPT.h @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#pragma once + +#include + +#include "DeepPot.h" +#include "commonPT.h" + +namespace deepmd { +/** + * @brief PyTorch implementation for Deep Potential. + **/ +class DeepPotPT : public DeepPotBase { + public: + /** + * @brief DP constructor without initialization. + **/ + DeepPotPT(); + ~DeepPotPT(); + /** + * @brief DP constructor with initialization. + * @param[in] model The name of the frozen model file. + * @param[in] gpu_rank The GPU rank. Default is 0. + * @param[in] file_content The content of the model file. If it is not empty, + *DP will read from the string instead of the file. + **/ + DeepPotPT(const std::string& model, + const int& gpu_rank = 0, + const std::string& file_content = ""); + /** + * @brief Initialize the DP. + * @param[in] model The name of the frozen model file. + * @param[in] gpu_rank The GPU rank. Default is 0. + * @param[in] file_content The content of the model file. If it is not empty, + *DP will read from the string instead of the file. + **/ + void init(const std::string& model, + const int& gpu_rank = 0, + const std::string& file_content = ""); + + private: + /** + * @brief Evaluate the energy, force, virial, atomic energy, and atomic virial + *by using this DP. + * @param[out] ener The system energy. + * @param[out] force The force on each atom. + * @param[out] virial The virial. + * @param[out] atom_energy The atomic energy. + * @param[out] atom_virial The atomic virial. + * @param[in] coord The coordinates of atoms. The array should be of size + *nframes x natoms x 3. + * @param[in] atype The atom types. The list should contain natoms ints. + * @param[in] box The cell of the region. The array should be of size nframes + *x 9. + * @param[in] fparam The frame parameter. The array can be of size : + * nframes x dim_fparam. + * dim_fparam. Then all frames are assumed to be provided with the same + *fparam. + * @param[in] aparam The atomic parameter The array can be of size : + * nframes x natoms x dim_aparam. + * natoms x dim_aparam. Then all frames are assumed to be provided with the + *same aparam. + **/ + template + void compute(ENERGYVTYPE& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box); + // const std::vector& fparam = std::vector(), + // const std::vector& aparam = std::vector()); + /** + * @brief Evaluate the energy, force, virial, atomic energy, and atomic virial + *by using this DP. + * @param[out] ener The system energy. + * @param[out] force The force on each atom. + * @param[out] virial The virial. + * @param[out] atom_energy The atomic energy. + * @param[out] atom_virial The atomic virial. + * @param[in] coord The coordinates of atoms. The array should be of size + *nframes x natoms x 3. + * @param[in] atype The atom types. The list should contain natoms ints. + * @param[in] box The cell of the region. The array should be of size nframes + *x 9. + * @param[in] nghost The number of ghost atoms. + * @param[in] lmp_list The input neighbour list. + * @param[in] ago Update the internal neighbour list if ago is 0. + * @param[in] fparam The frame parameter. The array can be of size : + * nframes x dim_fparam. + * dim_fparam. Then all frames are assumed to be provided with the same + *fparam. + * @param[in] aparam The atomic parameter The array can be of size : + * nframes x natoms x dim_aparam. + * natoms x dim_aparam. Then all frames are assumed to be provided with the + *same aparam. + **/ + template + void compute(ENERGYVTYPE& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + // const int nghost, + const InputNlist& lmp_list, + const int& ago); + // const std::vector& fparam = std::vector(), + // const std::vector& aparam = std::vector()); + /** + * @brief Evaluate the energy, force, and virial with the mixed type + *by using this DP. + * @param[out] ener The system energy. + * @param[out] force The force on each atom. + * @param[out] virial The virial. + * @param[in] nframes The number of frames. + * @param[in] coord The coordinates of atoms. The array should be of size + *nframes x natoms x 3. + * @param[in] atype The atom types. The array should be of size nframes x + *natoms. + * @param[in] box The cell of the region. The array should be of size nframes + *x 9. + * @param[in] fparam The frame parameter. The array can be of size : + * nframes x dim_fparam. + * dim_fparam. Then all frames are assumed to be provided with the same + *fparam. + * @param[in] aparam The atomic parameter The array can be of size : + * nframes x natoms x dim_aparam. + * natoms x dim_aparam. Then all frames are assumed to be provided with the + *same aparam. + **/ + template + void compute_mixed_type( + ENERGYVTYPE& ener, + std::vector& force, + std::vector& virial, + const int& nframes, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + /** + * @brief Evaluate the energy, force, and virial with the mixed type + *by using this DP. + * @param[out] ener The system energy. + * @param[out] force The force on each atom. + * @param[out] virial The virial. + * @param[out] atom_energy The atomic energy. + * @param[out] atom_virial The atomic virial. + * @param[in] nframes The number of frames. + * @param[in] coord The coordinates of atoms. The array should be of size + *nframes x natoms x 3. + * @param[in] atype The atom types. The array should be of size nframes x + *natoms. + * @param[in] box The cell of the region. The array should be of size nframes + *x 9. + * @param[in] fparam The frame parameter. The array can be of size : + * nframes x dim_fparam. + * dim_fparam. Then all frames are assumed to be provided with the same + *fparam. + * @param[in] aparam The atomic parameter The array can be of size : + * nframes x natoms x dim_aparam. + * natoms x dim_aparam. Then all frames are assumed to be provided with the + *same aparam. + **/ + template + void compute_mixed_type( + ENERGYVTYPE& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const int& nframes, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + + public: + /** + * @brief Get the cutoff radius. + * @return The cutoff radius. + **/ + double cutoff() const { + assert(inited); + return rcut; + }; + /** + * @brief Get the number of types. + * @return The number of types. + **/ + int numb_types() const { + assert(inited); + return ntypes; + }; + /** + * @brief Get the number of types with spin. + * @return The number of types with spin. + **/ + int numb_types_spin() const { + assert(inited); + return ntypes_spin; + }; + /** + * @brief Get the dimension of the frame parameter. + * @return The dimension of the frame parameter. + **/ + int dim_fparam() const { + assert(inited); + return dfparam; + }; + /** + * @brief Get the dimension of the atomic parameter. + * @return The dimension of the atomic parameter. + **/ + int dim_aparam() const { + assert(inited); + return daparam; + }; + /** + * @brief Get the type map (element name of the atom types) of this model. + * @param[out] type_map The type map of this model. + **/ + void get_type_map(std::string& type_map); + + /** + * @brief Get whether the atom dimension of aparam is nall instead of fparam. + * @param[out] aparam_nall whether the atom dimension of aparam is nall + *instead of fparam. + **/ + bool is_aparam_nall() const { + assert(inited); + return aparam_nall; + }; + + // forward to template class + void computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + void computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + void computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const int nghost, + const InputNlist& inlist, + const int& ago, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + void computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const int nghost, + const InputNlist& inlist, + const int& ago, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + void computew_mixed_type( + std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const int& nframes, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + void computew_mixed_type( + std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const int& nframes, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + + private: + int num_intra_nthreads, num_inter_nthreads; + bool inited; + int ntypes; + int ntypes_spin; + int dfparam; + int daparam; + bool aparam_nall; + // copy neighbor list info from host + torch::jit::script::Module module; + double rcut; + NeighborListDataPT nlist_data; + int max_num_neighbors; + int gpu_id; + bool gpu_enabled; + at::Tensor firstneigh_tensor; +}; + +} // namespace deepmd diff --git a/source/api_cc/include/commonPT.h b/source/api_cc/include/commonPT.h new file mode 100644 index 0000000000..57ffd5b295 --- /dev/null +++ b/source/api_cc/include/commonPT.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#include + +#include +#include +#include +#include + +#include "neighbor_list.h" +namespace deepmd { +struct NeighborListDataPT { + /// Array stores the core region atom's index + std::vector ilist; + /// Array stores the core region atom's neighbor index + std::vector jlist; + /// Array stores the number of neighbors of core region atoms + std::vector numneigh; + /// Array stores the the location of the first neighbor of core region atoms + std::vector firstneigh; + + public: + void copy_from_nlist(const InputNlist& inlist, int& max_num_neighbors); +}; +} // namespace deepmd diff --git a/source/api_cc/src/DeepPot.cc b/source/api_cc/src/DeepPot.cc index c598549844..442e2d90cc 100644 --- a/source/api_cc/src/DeepPot.cc +++ b/source/api_cc/src/DeepPot.cc @@ -10,6 +10,9 @@ #ifdef BUILD_TENSORFLOW #include "DeepPotTF.h" #endif +#ifdef BUILD_PYTORCH +#include "DeepPotPT.h" +#endif #include "device.h" using namespace deepmd; @@ -34,8 +37,14 @@ void DeepPot::init(const std::string& model, << std::endl; return; } - // TODO: To implement detect_backend - DPBackend backend = deepmd::DPBackend::TensorFlow; + DPBackend backend; + if (model.length() >= 4 && model.substr(model.length() - 4) == ".pth") { + backend = deepmd::DPBackend::PyTorch; + } else if (model.length() >= 3 && model.substr(model.length() - 3) == ".pb") { + backend = deepmd::DPBackend::TensorFlow; + } else { + throw deepmd::deepmd_exception("Unsupported model file format"); + } if (deepmd::DPBackend::TensorFlow == backend) { #ifdef BUILD_TENSORFLOW dp = std::make_shared(model, gpu_rank, file_content); @@ -43,7 +52,11 @@ void DeepPot::init(const std::string& model, throw deepmd::deepmd_exception("TensorFlow backend is not built"); #endif } else if (deepmd::DPBackend::PyTorch == backend) { - throw deepmd::deepmd_exception("PyTorch backend is not supported yet"); +#ifdef BUILD_PYTORCH + dp = std::make_shared(model, gpu_rank, file_content); +#else + throw deepmd::deepmd_exception("PyTorch backend is not built"); +#endif } else if (deepmd::DPBackend::Paddle == backend) { throw deepmd::deepmd_exception("PaddlePaddle backend is not supported yet"); } else { diff --git a/source/api_cc/src/DeepPotPT.cc b/source/api_cc/src/DeepPotPT.cc index c94fb4247b..f05e27b9b2 100644 --- a/source/api_cc/src/DeepPotPT.cc +++ b/source/api_cc/src/DeepPotPT.cc @@ -1,8 +1,358 @@ // SPDX-License-Identifier: LGPL-3.0-or-later #ifdef BUILD_PYTORCH -#include +#include "DeepPotPT.h" -void test_function_please_remove_after_torch_is_actually_used() { - torch::Tensor tensor = torch::rand({2, 3}); +#include "common.h" +using namespace deepmd; +DeepPotPT::DeepPotPT() : inited(false) {} +DeepPotPT::DeepPotPT(const std::string& model, + const int& gpu_rank, + const std::string& file_content) + : inited(false) { + try { + init(model, gpu_rank, file_content); + } catch (...) { + // Clean up and rethrow, as the destructor will not be called + throw; + } +} +void DeepPotPT::init(const std::string& model, + const int& gpu_rank, + const std::string& file_content) { + if (inited) { + std::cerr << "WARNING: deepmd-kit should not be initialized twice, do " + "nothing at the second call of initializer" + << std::endl; + return; + } + gpu_id = gpu_rank; + torch::Device device(torch::kCUDA, gpu_rank); + gpu_enabled = torch::cuda::is_available(); + if (!gpu_enabled) { + device = torch::Device(torch::kCPU); + std::cout << "load model from: " << model << " to cpu " << gpu_rank + << std::endl; + } else { + std::cout << "load model from: " << model << " to gpu " << gpu_rank + << std::endl; + } + module = torch::jit::load(model, device); + + torch::jit::FusionStrategy strategy; + strategy = {{torch::jit::FusionBehavior::DYNAMIC, 10}}; + torch::jit::setFusionStrategy(strategy); + + get_env_nthreads(num_intra_nthreads, + num_inter_nthreads); // need to be fixed as + // DP_INTRA_OP_PARALLELISM_THREADS + if (num_inter_nthreads) { + try { + at::set_num_interop_threads(num_inter_nthreads); + } catch (...) { + } + } + if (num_intra_nthreads) { + try { + at::set_num_threads(num_intra_nthreads); + } catch (...) { + } + } + + auto rcut_ = module.run_method("get_rcut").toDouble(); + rcut = static_cast(rcut_); + ntypes = 0; + ntypes_spin = 0; + dfparam = 0; + daparam = 0; + aparam_nall = false; + inited = true; +} +DeepPotPT::~DeepPotPT() {} + +template +void DeepPotPT::compute(ENERGYVTYPE& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const InputNlist& lmp_list, + const int& ago) { + torch::Device device(torch::kCUDA, gpu_id); + if (!gpu_enabled) { + device = torch::Device(torch::kCPU); + } + std::vector coord_wrapped = coord; + int natoms = atype.size(); + auto options = torch::TensorOptions().dtype(torch::kFloat64); + torch::ScalarType floatType = torch::kFloat64; + if (std::is_same_v) { + options = torch::TensorOptions().dtype(torch::kFloat32); + floatType = torch::kFloat32; + } + auto int_options = torch::TensorOptions().dtype(torch::kInt64); + auto int32_options = torch::TensorOptions().dtype(torch::kInt32); + at::Tensor coord_wrapped_Tensor = + torch::from_blob(coord_wrapped.data(), {1, natoms, 3}, options) + .to(device); + std::vector atype_64(atype.begin(), atype.end()); + at::Tensor atype_Tensor = + torch::from_blob(atype_64.data(), {1, natoms}, int_options).to(device); + if (ago == 0) { + nlist_data.copy_from_nlist(lmp_list, max_num_neighbors); + } + at::Tensor firstneigh = + torch::from_blob(nlist_data.jlist.data(), + {1, lmp_list.inum, max_num_neighbors}, int32_options); + firstneigh_tensor = firstneigh.to(torch::kInt64).to(device); + bool do_atom_virial_tensor = true; + c10::optional optional_tensor; + c10::Dict outputs = + module + .run_method("forward_lower", coord_wrapped_Tensor, atype_Tensor, + firstneigh_tensor, optional_tensor, optional_tensor, + optional_tensor, do_atom_virial_tensor) + .toGenericDict(); + c10::IValue energy_ = outputs.at("energy"); + c10::IValue force_ = outputs.at("extended_force"); + c10::IValue virial_ = outputs.at("virial"); + c10::IValue atom_virial_ = outputs.at("extended_virial"); + c10::IValue atom_energy_ = outputs.at("atom_energy"); + torch::Tensor flat_energy_ = energy_.toTensor().view({-1}); + torch::Tensor cpu_energy_ = flat_energy_.to(torch::kCPU); + ener.assign(cpu_energy_.data_ptr(), + cpu_energy_.data_ptr() + cpu_energy_.numel()); + torch::Tensor flat_atom_energy_ = + atom_energy_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_atom_energy_ = flat_atom_energy_.to(torch::kCPU); + atom_energy.resize(natoms, 0.0); // resize to nall to be consistenet with TF. + atom_energy.assign( + cpu_atom_energy_.data_ptr(), + cpu_atom_energy_.data_ptr() + cpu_atom_energy_.numel()); + torch::Tensor flat_force_ = force_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_force_ = flat_force_.to(torch::kCPU); + force.assign(cpu_force_.data_ptr(), + cpu_force_.data_ptr() + cpu_force_.numel()); + torch::Tensor flat_virial_ = virial_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_virial_ = flat_virial_.to(torch::kCPU); + virial.assign(cpu_virial_.data_ptr(), + cpu_virial_.data_ptr() + cpu_virial_.numel()); + torch::Tensor flat_atom_virial_ = + atom_virial_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_atom_virial_ = flat_atom_virial_.to(torch::kCPU); + atom_virial.assign( + cpu_atom_virial_.data_ptr(), + cpu_atom_virial_.data_ptr() + cpu_atom_virial_.numel()); +} +template void DeepPotPT::compute>( + std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const InputNlist& lmp_list, + const int& ago); +template void DeepPotPT::compute>( + std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const InputNlist& lmp_list, + const int& ago); +template +void DeepPotPT::compute(ENERGYVTYPE& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box) { + torch::Device device(torch::kCUDA, gpu_id); + if (!gpu_enabled) { + device = torch::Device(torch::kCPU); + } + std::vector coord_wrapped = coord; + int natoms = atype.size(); + auto options = torch::TensorOptions().dtype(torch::kFloat64); + torch::ScalarType floatType = torch::kFloat64; + if (std::is_same_v) { + options = torch::TensorOptions().dtype(torch::kFloat32); + floatType = torch::kFloat32; + } + auto int_options = torch::TensorOptions().dtype(torch::kInt64); + std::vector inputs; + at::Tensor coord_wrapped_Tensor = + torch::from_blob(coord_wrapped.data(), {1, natoms, 3}, options) + .to(device); + inputs.push_back(coord_wrapped_Tensor); + std::vector atype_64(atype.begin(), atype.end()); + at::Tensor atype_Tensor = + torch::from_blob(atype_64.data(), {1, natoms}, int_options).to(device); + inputs.push_back(atype_Tensor); + c10::optional box_Tensor; + if (!box.empty()) { + box_Tensor = + torch::from_blob(const_cast(box.data()), {1, 9}, options) + .to(device); + } + inputs.push_back(box_Tensor); + c10::optional fparam_tensor; + inputs.push_back(fparam_tensor); + c10::optional aparam_tensor; + inputs.push_back(aparam_tensor); + bool do_atom_virial_tensor = true; + inputs.push_back(do_atom_virial_tensor); + c10::Dict outputs = + module.forward(inputs).toGenericDict(); + c10::IValue energy_ = outputs.at("energy"); + c10::IValue force_ = outputs.at("force"); + c10::IValue virial_ = outputs.at("virial"); + c10::IValue atom_virial_ = outputs.at("atom_virial"); + c10::IValue atom_energy_ = outputs.at("atom_energy"); + torch::Tensor flat_energy_ = energy_.toTensor().view({-1}); + torch::Tensor cpu_energy_ = flat_energy_.to(torch::kCPU); + ener.assign(cpu_energy_.data_ptr(), + cpu_energy_.data_ptr() + cpu_energy_.numel()); + torch::Tensor flat_atom_energy_ = + atom_energy_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_atom_energy_ = flat_atom_energy_.to(torch::kCPU); + atom_energy.assign( + cpu_atom_energy_.data_ptr(), + cpu_atom_energy_.data_ptr() + cpu_atom_energy_.numel()); + torch::Tensor flat_force_ = force_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_force_ = flat_force_.to(torch::kCPU); + force.assign(cpu_force_.data_ptr(), + cpu_force_.data_ptr() + cpu_force_.numel()); + torch::Tensor flat_virial_ = virial_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_virial_ = flat_virial_.to(torch::kCPU); + virial.assign(cpu_virial_.data_ptr(), + cpu_virial_.data_ptr() + cpu_virial_.numel()); + torch::Tensor flat_atom_virial_ = + atom_virial_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_atom_virial_ = flat_atom_virial_.to(torch::kCPU); + atom_virial.assign( + cpu_atom_virial_.data_ptr(), + cpu_atom_virial_.data_ptr() + cpu_atom_virial_.numel()); +} + +template void DeepPotPT::compute>( + std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box); +template void DeepPotPT::compute>( + std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box); +void DeepPotPT::get_type_map(std::string& type_map) { + auto ret = module.run_method("get_type_map").toList(); + for (const torch::IValue& element : ret) { + type_map += torch::str(element); // Convert each element to a string + type_map += " "; // Add a space between elements + } +} + +// forward to template method +void DeepPotPT::computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam, + const std::vector& aparam) { + compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box); +} +void DeepPotPT::computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam, + const std::vector& aparam) { + compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box); +} +void DeepPotPT::computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const int nghost, + const InputNlist& inlist, + const int& ago, + const std::vector& fparam, + const std::vector& aparam) { + // TODO: atomic compute unsupported + compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box, + inlist, ago); +} +void DeepPotPT::computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const int nghost, + const InputNlist& inlist, + const int& ago, + const std::vector& fparam, + const std::vector& aparam) { + compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box, + inlist, ago); +} +void DeepPotPT::computew_mixed_type(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const int& nframes, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam, + const std::vector& aparam) { + throw deepmd::deepmd_exception("computew_mixed_type is not implemented"); +} +void DeepPotPT::computew_mixed_type(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const int& nframes, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam, + const std::vector& aparam) { + throw deepmd::deepmd_exception("computew_mixed_type is not implemented"); } #endif diff --git a/source/api_cc/src/commonPT.cc b/source/api_cc/src/commonPT.cc new file mode 100644 index 0000000000..4ed3b21fe8 --- /dev/null +++ b/source/api_cc/src/commonPT.cc @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#ifdef BUILD_PYTORCH +#include "commonPT.h" +using namespace deepmd; +void NeighborListDataPT::copy_from_nlist(const InputNlist& inlist, + int& max_num_neighbors) { + int inum = inlist.inum; + ilist.resize(inum); + numneigh.resize(inum); + memcpy(&ilist[0], inlist.ilist, inum * sizeof(int)); + int* max_element = std::max_element(inlist.numneigh, inlist.numneigh + inum); + max_num_neighbors = *max_element; + unsigned long nlist_size = (unsigned long)inum * max_num_neighbors; + jlist.resize(nlist_size); + jlist.assign(nlist_size, -1); + for (int ii = 0; ii < inum; ++ii) { + int jnum = inlist.numneigh[ii]; + numneigh[ii] = inlist.numneigh[ii]; + memcpy(&jlist[(unsigned long)ii * max_num_neighbors], inlist.firstneigh[ii], + jnum * sizeof(int)); + } +} +#endif diff --git a/source/api_cc/tests/test_deeppot_pt.cc b/source/api_cc/tests/test_deeppot_pt.cc new file mode 100644 index 0000000000..e0e90ac75c --- /dev/null +++ b/source/api_cc/tests/test_deeppot_pt.cc @@ -0,0 +1,625 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "DeepPot.h" +#include "neighbor_list.h" +#include "test_utils.h" + +template +class TestInferDeepPotAPt : public ::testing::Test { + protected: + std::vector coord = {12.83, 2.56, 2.18, 12.09, 2.87, 2.74, + 00.25, 3.32, 1.68, 3.36, 3.00, 1.81, + 3.51, 2.51, 2.60, 4.27, 3.22, 1.56}; + std::vector atype = {0, 1, 1, 0, 1, 1}; + std::vector box = {13., 0., 0., 0., 13., 0., 0., 0., 13.}; + std::vector expected_e = { + + -93.016873944029, -185.923296645958, -185.927096544970, + -93.019371018039, -185.926179995548, -185.924351901852}; + std::vector expected_f = { + + 0.006277522211, -0.001117962774, 0.000618580445, 0.009928999655, + 0.003026035654, -0.006941982227, 0.000667853212, -0.002449963843, + 0.006506463508, -0.007284129115, 0.000530662205, -0.000028806821, + 0.000068097781, 0.006121331983, -0.009019754602, -0.009658343745, + -0.006110103225, 0.008865499697}; + std::vector expected_v = { + -0.000155238009, 0.000116605516, -0.007869862476, 0.000465578340, + 0.008182547185, -0.002398713212, -0.008112887338, -0.002423738425, + 0.007210716605, -0.019203504012, 0.001724938709, 0.009909211091, + 0.001153857542, -0.001600015103, -0.000560024090, 0.010727836276, + -0.001034836404, -0.007973454377, -0.021517399106, -0.004064359664, + 0.004866398692, -0.003360038617, -0.007241406162, 0.005920941051, + 0.004899151657, 0.006290788591, -0.006478820311, 0.001921504710, + 0.001313470921, -0.000304091236, 0.001684345981, 0.004124109256, + -0.006396084465, -0.000701095618, -0.006356507032, 0.009818550859, + -0.015230664587, -0.000110244376, 0.000690319396, 0.000045953023, + -0.005726548770, 0.008769818495, -0.000572380210, 0.008860603423, + -0.013819348050, -0.021227082558, -0.004977781343, 0.006646239696, + -0.005987066507, -0.002767831232, 0.003746502525, 0.007697590397, + 0.003746130152, -0.005172634748}; + int natoms; + double expected_tot_e; + std::vector expected_tot_v; + + deepmd::DeepPot dp; + + void SetUp() override { + std::string file_name = "../../tests/infer/deeppot_sea.pth"; + + dp.init(file_name); + + natoms = expected_e.size(); + EXPECT_EQ(natoms * 3, expected_f.size()); + EXPECT_EQ(natoms * 9, expected_v.size()); + expected_tot_e = 0.; + expected_tot_v.resize(9); + std::fill(expected_tot_v.begin(), expected_tot_v.end(), 0.); + for (int ii = 0; ii < natoms; ++ii) { + expected_tot_e += expected_e[ii]; + } + for (int ii = 0; ii < natoms; ++ii) { + for (int dd = 0; dd < 9; ++dd) { + expected_tot_v[dd] += expected_v[ii * 9 + dd]; + } + } + }; + + void TearDown() override { remove("deeppot.pb"); }; +}; + +TYPED_TEST_SUITE(TestInferDeepPotAPt, ValueTypes); + +TYPED_TEST(TestInferDeepPotAPt, cpu_build_nlist) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + double ener; + std::vector force, virial; + dp.compute(ener, force, virial, coord, atype, box); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_build_nlist_numfv) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + class MyModel : public EnergyModelTest { + deepmd::DeepPot& mydp; + const std::vector atype; + + public: + MyModel(deepmd::DeepPot& dp_, const std::vector& atype_) + : mydp(dp_), atype(atype_){}; + virtual void compute(double& ener, + std::vector& force, + std::vector& virial, + const std::vector& coord, + const std::vector& box) { + mydp.compute(ener, force, virial, coord, atype, box); + } + }; + MyModel model(dp, atype); + model.test_f(coord, box); + model.test_v(coord, box); + std::vector box_(box); + box_[1] -= 0.4; + model.test_f(coord, box_); + model.test_v(coord, box_); + box_[2] += 0.5; + model.test_f(coord, box_); + model.test_v(coord, box_); + box_[4] += 0.2; + model.test_f(coord, box_); + model.test_v(coord, box_); + box_[3] -= 0.3; + model.test_f(coord, box_); + model.test_v(coord, box_); + box_[6] -= 0.7; + model.test_f(coord, box_); + model.test_v(coord, box_); + box_[7] += 0.6; + model.test_f(coord, box_); + model.test_v(coord, box_); +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_build_nlist_atomic) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + double ener; + std::vector force, virial, atom_ener, atom_vir; + dp.compute(ener, force, virial, atom_ener, atom_vir, coord, atype, box); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + EXPECT_EQ(atom_ener.size(), natoms); + EXPECT_EQ(atom_vir.size(), natoms * 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + for (int ii = 0; ii < natoms; ++ii) { + EXPECT_LT(fabs(atom_ener[ii] - expected_e[ii]), EPSILON); + } + for (int ii = 0; ii < natoms * 9; ++ii) { + EXPECT_LT(fabs(atom_vir[ii] - expected_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + float rc = dp.cutoff(); + int nloc = coord.size() / 3; + std::vector coord_cpy; + std::vector atype_cpy, mapping; + std::vector > nlist_data; + _build_nlist(nlist_data, coord_cpy, atype_cpy, mapping, coord, + atype, box, rc); + int nall = coord_cpy.size() / 3; + std::vector ilist(nloc), numneigh(nloc); + std::vector firstneigh(nloc); + deepmd::InputNlist inlist(nloc, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + + double ener; + std::vector force_, virial; + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 0); + std::vector force; + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + + ener = 0.; + std::fill(force_.begin(), force_.end(), 0.0); + std::fill(virial.begin(), virial.end(), 0.0); + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 1); + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist_atomic) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + float rc = dp.cutoff(); + int nloc = coord.size() / 3; + std::vector coord_cpy; + std::vector atype_cpy, mapping; + std::vector > nlist_data; + _build_nlist(nlist_data, coord_cpy, atype_cpy, mapping, coord, + atype, box, rc); + int nall = coord_cpy.size() / 3; + std::vector ilist(nloc), numneigh(nloc); + std::vector firstneigh(nloc); + deepmd::InputNlist inlist(nloc, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + double ener; + std::vector force_, atom_ener_, atom_vir_, virial; + std::vector force, atom_ener, atom_vir; + dp.compute(ener, force_, virial, atom_ener_, atom_vir_, coord_cpy, atype_cpy, + box, nall - nloc, inlist, 0); + _fold_back(force, force_, mapping, nloc, nall, 3); + _fold_back(atom_ener, atom_ener_, mapping, nloc, nall, 1); + _fold_back(atom_vir, atom_vir_, mapping, nloc, nall, 9); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + EXPECT_EQ(atom_ener.size(), natoms); + EXPECT_EQ(atom_vir.size(), natoms * 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + for (int ii = 0; ii < natoms; ++ii) { + EXPECT_LT(fabs(atom_ener[ii] - expected_e[ii]), EPSILON); + } + for (int ii = 0; ii < natoms * 9; ++ii) { + EXPECT_LT(fabs(atom_vir[ii] - expected_v[ii]), EPSILON); + } + + ener = 0.; + std::fill(force_.begin(), force_.end(), 0.0); + std::fill(virial.begin(), virial.end(), 0.0); + std::fill(atom_ener_.begin(), atom_ener_.end(), 0.0); + std::fill(atom_vir_.begin(), atom_vir_.end(), 0.0); + dp.compute(ener, force_, virial, atom_ener_, atom_vir_, coord_cpy, atype_cpy, + box, nall - nloc, inlist, 1); + _fold_back(force, force_, mapping, nloc, nall, 3); + _fold_back(atom_ener, atom_ener_, mapping, nloc, nall, 1); + _fold_back(atom_vir, atom_vir_, mapping, nloc, nall, 9); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + EXPECT_EQ(atom_ener.size(), natoms); + EXPECT_EQ(atom_vir.size(), natoms * 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + for (int ii = 0; ii < natoms; ++ii) { + EXPECT_LT(fabs(atom_ener[ii] - expected_e[ii]), EPSILON); + } + for (int ii = 0; ii < natoms * 9; ++ii) { + EXPECT_LT(fabs(atom_vir[ii] - expected_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist_2rc) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + float rc = dp.cutoff(); + int nloc = coord.size() / 3; + std::vector coord_cpy; + std::vector atype_cpy, mapping; + std::vector > nlist_data; + _build_nlist(nlist_data, coord_cpy, atype_cpy, mapping, coord, + atype, box, rc * 2); + int nall = coord_cpy.size() / 3; + std::vector ilist(nloc), numneigh(nloc); + std::vector firstneigh(nloc); + deepmd::InputNlist inlist(nloc, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + + double ener; + std::vector force_(nall * 3, 0.0), virial(9, 0.0); + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 0); + std::vector force; + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + + ener = 0.; + std::fill(force_.begin(), force_.end(), 0.0); + std::fill(virial.begin(), virial.end(), 0.0); + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 1); + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist_type_sel) { + GTEST_SKIP() << "Skipping this test for unsupported"; + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + float rc = dp.cutoff(); + + // add vir atoms + int nvir = 2; + std::vector coord_vir(nvir * 3); + std::vector atype_vir(nvir, 2); + for (int ii = 0; ii < nvir; ++ii) { + coord_vir[ii] = coord[ii]; + } + coord.insert(coord.begin(), coord_vir.begin(), coord_vir.end()); + atype.insert(atype.begin(), atype_vir.begin(), atype_vir.end()); + natoms += nvir; + std::vector expected_f_vir(nvir * 3, 0.0); + expected_f.insert(expected_f.begin(), expected_f_vir.begin(), + expected_f_vir.end()); + + // build nlist + int nloc = coord.size() / 3; + std::vector coord_cpy; + std::vector atype_cpy, mapping; + std::vector > nlist_data; + _build_nlist(nlist_data, coord_cpy, atype_cpy, mapping, coord, + atype, box, rc); + int nall = coord_cpy.size() / 3; + std::vector ilist(nloc), numneigh(nloc); + std::vector firstneigh(nloc); + deepmd::InputNlist inlist(nloc, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + + // dp compute + double ener; + std::vector force_(nall * 3, 0.0), virial(9, 0.0); + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 0); + // fold back + std::vector force; + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist_type_sel_atomic) { + GTEST_SKIP() << "Skipping this test for unsupported"; + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + float rc = dp.cutoff(); + + // add vir atoms + int nvir = 2; + std::vector coord_vir(nvir * 3); + std::vector atype_vir(nvir, 2); + for (int ii = 0; ii < nvir; ++ii) { + coord_vir[ii] = coord[ii]; + } + coord.insert(coord.begin(), coord_vir.begin(), coord_vir.end()); + atype.insert(atype.begin(), atype_vir.begin(), atype_vir.end()); + natoms += nvir; + std::vector expected_f_vir(nvir * 3, 0.0); + expected_f.insert(expected_f.begin(), expected_f_vir.begin(), + expected_f_vir.end()); + + // build nlist + int nloc = coord.size() / 3; + std::vector coord_cpy; + std::vector atype_cpy, mapping; + std::vector > nlist_data; + _build_nlist(nlist_data, coord_cpy, atype_cpy, mapping, coord, + atype, box, rc); + int nall = coord_cpy.size() / 3; + std::vector ilist(nloc), numneigh(nloc); + std::vector firstneigh(nloc); + deepmd::InputNlist inlist(nloc, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + + // dp compute + double ener; + std::vector force_(nall * 3, 0.0), virial(9, 0.0), atomic_energy, + atomic_virial; + dp.compute(ener, force_, virial, atomic_energy, atomic_virial, coord_cpy, + atype_cpy, box, nall - nloc, inlist, 0); + // fold back + std::vector force; + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, print_summary) { + deepmd::DeepPot& dp = this->dp; + dp.print_summary(""); +} + +template +class TestInferDeepPotAPtNoPbc : public ::testing::Test { + protected: + std::vector coord = {12.83, 2.56, 2.18, 12.09, 2.87, 2.74, + 00.25, 3.32, 1.68, 3.36, 3.00, 1.81, + 3.51, 2.51, 2.60, 4.27, 3.22, 1.56}; + std::vector atype = {0, 1, 1, 0, 1, 1}; + std::vector box = {}; + std::vector expected_e = {-93.003304908874, -185.915806542480, + -185.928116717624, -93.017934934346, + -185.924393412278, -185.923906740801}; + std::vector expected_f = { + 0.000868182637, -0.000363698132, -0.000657003077, -0.000868182637, + 0.000363698132, 0.000657003077, 0.007932614680, -0.001003609844, + 0.000737731722, -0.003883788858, 0.000686896282, -0.000578400682, + 0.004064895086, 0.006115547962, -0.008747097814, -0.008113720908, + -0.005798834400, 0.008587766774}; + std::vector expected_v = { + 0.007762485364, -0.003251851977, -0.005874313248, -0.003251851977, + 0.001362262315, 0.002460860955, -0.005874313248, 0.002460860955, + 0.004445426242, -0.007120030212, 0.002982715359, 0.005388130971, + 0.002982715359, -0.001249515894, -0.002257190002, 0.005388130971, + -0.002257190002, -0.004077504519, -0.015805863589, 0.001952684835, + -0.001522876482, 0.001796574704, -0.000358803950, 0.000369710813, + -0.001108943040, 0.000332585300, -0.000395481309, 0.008873525623, + 0.001919112114, -0.001486235522, 0.002002929532, 0.004222469272, + -0.006517211126, -0.001656192522, -0.006501210045, 0.010118622295, + -0.006548889778, -0.000465126991, 0.001002876603, 0.000240398734, + -0.005794489784, 0.008940685179, -0.000121727685, 0.008931999051, + -0.013852797563, -0.017962955675, -0.004645050453, 0.006214692837, + -0.005278283465, -0.002662692758, 0.003618275905, 0.007095320684, + 0.003648086464, -0.005023397513}; + int natoms; + double expected_tot_e; + std::vector expected_tot_v; + + deepmd::DeepPot dp; + + void SetUp() override { + std::string file_name = "../../tests/infer/deeppot_sea.pth"; + dp.init(file_name); + + natoms = expected_e.size(); + EXPECT_EQ(natoms * 3, expected_f.size()); + EXPECT_EQ(natoms * 9, expected_v.size()); + expected_tot_e = 0.; + expected_tot_v.resize(9); + std::fill(expected_tot_v.begin(), expected_tot_v.end(), 0.); + for (int ii = 0; ii < natoms; ++ii) { + expected_tot_e += expected_e[ii]; + } + for (int ii = 0; ii < natoms; ++ii) { + for (int dd = 0; dd < 9; ++dd) { + expected_tot_v[dd] += expected_v[ii * 9 + dd]; + } + } + }; + + void TearDown() override { remove("deeppot.pb"); }; +}; + +TYPED_TEST_SUITE(TestInferDeepPotAPtNoPbc, ValueTypes); + +TYPED_TEST(TestInferDeepPotAPtNoPbc, cpu_build_nlist) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + double ener; + std::vector force, virial; + dp.compute(ener, force, virial, coord, atype, box); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} diff --git a/source/install/test_cc_local.sh b/source/install/test_cc_local.sh index 22d22a27f6..73aa74ed90 100755 --- a/source/install/test_cc_local.sh +++ b/source/install/test_cc_local.sh @@ -18,7 +18,15 @@ INSTALL_PREFIX=${SCRIPT_PATH}/../../dp_test BUILD_TMP_DIR=${SCRIPT_PATH}/../build_tests mkdir -p ${BUILD_TMP_DIR} cd ${BUILD_TMP_DIR} -cmake -DINSTALL_TENSORFLOW=FALSE -DUSE_TF_PYTHON_LIBS=TRUE -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} -DBUILD_TESTING:BOOL=TRUE -DLAMMPS_VERSION=stable_2Aug2023_update2 ${CUDA_ARGS} .. +cmake \ + -D ENABLE_TENSORFLOW=TRUE \ + -D ENABLE_PYTORCH=TRUE \ + -D INSTALL_TENSORFLOW=FALSE \ + -D USE_TF_PYTHON_LIBS=TRUE \ + -D CMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} \ + -D BUILD_TESTING:BOOL=TRUE \ + -D LAMMPS_VERSION=stable_2Aug2023_update2 \ + ${CUDA_ARGS} .. cmake --build . -j${NPROC} cmake --install . ctest --output-on-failure diff --git a/source/ipi/tests/test_driver.py b/source/ipi/tests/test_driver.py index 1b2e1dd951..b0fbf53b01 100644 --- a/source/ipi/tests/test_driver.py +++ b/source/ipi/tests/test_driver.py @@ -251,3 +251,108 @@ def test_normalize_coords(self): ) expected_se = np.sum(self.expected_e.reshape([nframes, -1]), axis=1) np.testing.assert_almost_equal(ee.ravel(), expected_se.ravel(), default_places) + + +class TestDPIPIPt(TestDPIPI): + @classmethod + def setUpClass(cls): + cls.model_file = str(tests_path / "infer" / "deeppot_sea.pth") + + def setUp(self): + super().setUp() + + self.box = np.array([13.0, 0.0, 0.0, 0.0, 13.0, 0.0, 0.0, 0.0, 13.0]) + self.expected_e = np.array( + [ + -93.016873944029, + -185.923296645958, + -185.927096544970, + -93.019371018039, + -185.926179995548, + -185.924351901852, + ] + ) + self.expected_f = np.array( + [ + 0.006277522211, + -0.001117962774, + 0.000618580445, + 0.009928999655, + 0.003026035654, + -0.006941982227, + 0.000667853212, + -0.002449963843, + 0.006506463508, + -0.007284129115, + 0.000530662205, + -0.000028806821, + 0.000068097781, + 0.006121331983, + -0.009019754602, + -0.009658343745, + -0.006110103225, + 0.008865499697, + ] + ) + self.expected_v = np.array( + [ + -0.000155238009, + 0.000116605516, + -0.007869862476, + 0.000465578340, + 0.008182547185, + -0.002398713212, + -0.008112887338, + -0.002423738425, + 0.007210716605, + -0.019203504012, + 0.001724938709, + 0.009909211091, + 0.001153857542, + -0.001600015103, + -0.000560024090, + 0.010727836276, + -0.001034836404, + -0.007973454377, + -0.021517399106, + -0.004064359664, + 0.004866398692, + -0.003360038617, + -0.007241406162, + 0.005920941051, + 0.004899151657, + 0.006290788591, + -0.006478820311, + 0.001921504710, + 0.001313470921, + -0.000304091236, + 0.001684345981, + 0.004124109256, + -0.006396084465, + -0.000701095618, + -0.006356507032, + 0.009818550859, + -0.015230664587, + -0.000110244376, + 0.000690319396, + 0.000045953023, + -0.005726548770, + 0.008769818495, + -0.000572380210, + 0.008860603423, + -0.013819348050, + -0.021227082558, + -0.004977781343, + 0.006646239696, + -0.005987066507, + -0.002767831232, + 0.003746502525, + 0.007697590397, + 0.003746130152, + -0.005172634748, + ] + ) + + @classmethod + def tearDownClass(cls): + cls.dp = None diff --git a/source/lmp/tests/test_lammps_pt.py b/source/lmp/tests/test_lammps_pt.py new file mode 100644 index 0000000000..bf1ef97e2b --- /dev/null +++ b/source/lmp/tests/test_lammps_pt.py @@ -0,0 +1,695 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import os +import subprocess as sp +import sys +from pathlib import ( + Path, +) + +import constants +import numpy as np +import pytest +from lammps import ( + PyLammps, +) +from write_lmp_data import ( + write_lmp_data, +) + +pbtxt_file2 = ( + Path(__file__).parent.parent.parent / "tests" / "infer" / "deeppot-1.pbtxt" +) +pb_file = Path(__file__).parent.parent.parent / "tests" / "infer" / "deeppot_sea.pth" +pb_file2 = Path(__file__).parent / "graph2.pb" +system_file = Path(__file__).parent.parent.parent / "tests" +data_file = Path(__file__).parent / "data.lmp" +data_file_si = Path(__file__).parent / "data.si" +data_type_map_file = Path(__file__).parent / "data_type_map.lmp" +md_file = Path(__file__).parent / "md.out" + +# this is as the same as python and c++ tests, test_deeppot_a.py +expected_ae = np.array( + [ + -93.016873944029, + -185.923296645958, + -185.927096544970, + -93.019371018039, + -185.926179995548, + -185.924351901852, + ] +) +expected_e = np.sum(expected_ae) +expected_f = np.array( + [ + 0.006277522211, + -0.001117962774, + 0.000618580445, + 0.009928999655, + 0.003026035654, + -0.006941982227, + 0.000667853212, + -0.002449963843, + 0.006506463508, + -0.007284129115, + 0.000530662205, + -0.000028806821, + 0.000068097781, + 0.006121331983, + -0.009019754602, + -0.009658343745, + -0.006110103225, + 0.008865499697, + ] +).reshape(6, 3) + +expected_f2 = np.array( + [ + [-0.6454949, 1.72457783, 0.18897958], + [1.68936514, -0.36995299, -1.36044464], + [-1.09902692, -1.35487928, 1.17416702], + [1.68426111, -0.50835585, 0.98340415], + [0.05771758, 1.12515818, -1.77561531], + [-1.686822, -0.61654789, 0.78950921], + ] +) + +expected_v = -np.array( + [ + -0.000155238009, + 0.000116605516, + -0.007869862476, + 0.000465578340, + 0.008182547185, + -0.002398713212, + -0.008112887338, + -0.002423738425, + 0.007210716605, + -0.019203504012, + 0.001724938709, + 0.009909211091, + 0.001153857542, + -0.001600015103, + -0.000560024090, + 0.010727836276, + -0.001034836404, + -0.007973454377, + -0.021517399106, + -0.004064359664, + 0.004866398692, + -0.003360038617, + -0.007241406162, + 0.005920941051, + 0.004899151657, + 0.006290788591, + -0.006478820311, + 0.001921504710, + 0.001313470921, + -0.000304091236, + 0.001684345981, + 0.004124109256, + -0.006396084465, + -0.000701095618, + -0.006356507032, + 0.009818550859, + -0.015230664587, + -0.000110244376, + 0.000690319396, + 0.000045953023, + -0.005726548770, + 0.008769818495, + -0.000572380210, + 0.008860603423, + -0.013819348050, + -0.021227082558, + -0.004977781343, + 0.006646239696, + -0.005987066507, + -0.002767831232, + 0.003746502525, + 0.007697590397, + 0.003746130152, + -0.005172634748, + ] +).reshape(6, 9) +expected_v2 = -np.array( + [ + [ + -0.70008436, + -0.06399891, + 0.63678391, + -0.07642171, + -0.70580035, + 0.20506145, + 0.64098364, + 0.20305781, + -0.57906794, + ], + [ + -0.6372635, + 0.14315552, + 0.51952246, + 0.04604049, + -0.06003681, + -0.02688702, + 0.54489318, + -0.10951559, + -0.43730539, + ], + [ + -0.25090748, + -0.37466262, + 0.34085833, + -0.26690852, + -0.37676917, + 0.29080825, + 0.31600481, + 0.37558276, + -0.33251064, + ], + [ + -0.80195614, + -0.10273138, + 0.06935364, + -0.10429256, + -0.29693811, + 0.45643496, + 0.07247872, + 0.45604679, + -0.71048816, + ], + [ + -0.03840668, + -0.07680205, + 0.10940472, + -0.02374189, + -0.27610266, + 0.4336071, + 0.02465248, + 0.4290638, + -0.67496763, + ], + [ + -0.61475065, + -0.21163135, + 0.26652929, + -0.26134659, + -0.11560267, + 0.15415902, + 0.34343952, + 0.1589482, + -0.21370642, + ], + ] +).reshape(6, 9) + +box = np.array([0, 13, 0, 13, 0, 13, 0, 0, 0]) +coord = np.array( + [ + [12.83, 2.56, 2.18], + [12.09, 2.87, 2.74], + [0.25, 3.32, 1.68], + [3.36, 3.00, 1.81], + [3.51, 2.51, 2.60], + [4.27, 3.22, 1.56], + ] +) +type_OH = np.array([1, 2, 2, 1, 2, 2]) +type_HO = np.array([2, 1, 1, 2, 1, 1]) + + +sp.check_output( + "{} -m deepmd convert-from pbtxt -i {} -o {}".format( + sys.executable, + pbtxt_file2.resolve(), + pb_file2.resolve(), + ).split() +) + + +def setup_module(): + write_lmp_data(box, coord, type_OH, data_file) + write_lmp_data(box, coord, type_HO, data_type_map_file) + write_lmp_data( + box * constants.dist_metal2si, + coord * constants.dist_metal2si, + type_OH, + data_file_si, + ) + + +def teardown_module(): + os.remove(data_file) + os.remove(data_type_map_file) + + +def _lammps(data_file, units="metal") -> PyLammps: + lammps = PyLammps() + lammps.units(units) + lammps.boundary("p p p") + lammps.atom_style("atomic") + if units == "metal" or units == "real": + lammps.neighbor("2.0 bin") + elif units == "si": + lammps.neighbor("2.0e-10 bin") + else: + raise ValueError("units should be metal, real, or si") + lammps.neigh_modify("every 10 delay 0 check no") + lammps.read_data(data_file.resolve()) + if units == "metal" or units == "real": + lammps.mass("1 16") + lammps.mass("2 2") + elif units == "si": + lammps.mass("1 %.10e" % (16 * constants.mass_metal2si)) + lammps.mass("2 %.10e" % (2 * constants.mass_metal2si)) + else: + raise ValueError("units should be metal, real, or si") + if units == "metal": + lammps.timestep(0.0005) + elif units == "real": + lammps.timestep(0.5) + elif units == "si": + lammps.timestep(5e-16) + else: + raise ValueError("units should be metal, real, or si") + lammps.fix("1 all nve") + return lammps + + +@pytest.fixture +def lammps(): + lmp = _lammps(data_file=data_file) + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_type_map(): + lmp = _lammps(data_file=data_type_map_file) + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_real(): + lmp = _lammps(data_file=data_file, units="real") + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_si(): + lmp = _lammps(data_file=data_file_si, units="si") + yield lmp + lmp.close() + + +def test_pair_deepmd(lammps): + lammps.pair_style(f"deepmd {pb_file.resolve()}") + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + lammps.run(1) + + +def test_pair_deepmd_virial(lammps): + lammps.pair_style(f"deepmd {pb_file.resolve()}") + lammps.pair_coeff("* *") + lammps.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps.variable(f"virial{jj} atom c_virial[{ii+1}]") + lammps.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + idx_map = lammps.lmp.numpy.extract_atom("id") - 1 + for ii in range(9): + assert np.array( + lammps.variables[f"virial{ii}"].value + ) / constants.nktv2p == pytest.approx(expected_v[idx_map, ii]) + + +def test_pair_deepmd_model_devi(lammps): + lammps.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic".format( + pb_file.resolve(), pb_file2.resolve(), md_file.resolve() + ) + ) + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f) + assert md[4] == pytest.approx(np.max(expected_md_f)) + assert md[5] == pytest.approx(np.min(expected_md_f)) + assert md[6] == pytest.approx(np.mean(expected_md_f)) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v)) + assert md[2] == pytest.approx(np.min(expected_md_v)) + assert md[3] == pytest.approx(np.sqrt(np.mean(np.square(expected_md_v)))) + + +def test_pair_deepmd_model_devi_virial(lammps): + lammps.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic".format( + pb_file.resolve(), pb_file2.resolve(), md_file.resolve() + ) + ) + lammps.pair_coeff("* *") + lammps.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps.variable(f"virial{jj} atom c_virial[{ii+1}]") + lammps.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + idx_map = lammps.lmp.numpy.extract_atom("id") - 1 + for ii in range(9): + assert np.array( + lammps.variables[f"virial{ii}"].value + ) / constants.nktv2p == pytest.approx(expected_v[idx_map, ii]) + # load model devi + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f) + assert md[4] == pytest.approx(np.max(expected_md_f)) + assert md[5] == pytest.approx(np.min(expected_md_f)) + assert md[6] == pytest.approx(np.mean(expected_md_f)) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v)) + assert md[2] == pytest.approx(np.min(expected_md_v)) + assert md[3] == pytest.approx(np.sqrt(np.mean(np.square(expected_md_v)))) + + +def test_pair_deepmd_model_devi_atomic_relative(lammps): + relative = 1.0 + lammps.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic relative {}".format( + pb_file.resolve(), pb_file2.resolve(), md_file.resolve(), relative + ) + ) + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + norm = np.linalg.norm(np.mean([expected_f, expected_f2], axis=0), axis=1) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + expected_md_f /= norm + relative + assert md[7:] == pytest.approx(expected_md_f) + assert md[4] == pytest.approx(np.max(expected_md_f)) + assert md[5] == pytest.approx(np.min(expected_md_f)) + assert md[6] == pytest.approx(np.mean(expected_md_f)) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v)) + assert md[2] == pytest.approx(np.min(expected_md_v)) + assert md[3] == pytest.approx(np.sqrt(np.mean(np.square(expected_md_v)))) + + +def test_pair_deepmd_model_devi_atomic_relative_v(lammps): + relative = 1.0 + lammps.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic relative_v {}".format( + pb_file.resolve(), pb_file2.resolve(), md_file.resolve(), relative + ) + ) + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f) + assert md[4] == pytest.approx(np.max(expected_md_f)) + assert md[5] == pytest.approx(np.min(expected_md_f)) + assert md[6] == pytest.approx(np.mean(expected_md_f)) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + norm = ( + np.abs( + np.mean([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) + ) + / 6 + ) + expected_md_v /= norm + relative + assert md[1] == pytest.approx(np.max(expected_md_v)) + assert md[2] == pytest.approx(np.min(expected_md_v)) + assert md[3] == pytest.approx(np.sqrt(np.mean(np.square(expected_md_v)))) + + +def test_pair_deepmd_type_map(lammps_type_map): + lammps_type_map.pair_style(f"deepmd {pb_file.resolve()}") + lammps_type_map.pair_coeff("* * H O") + lammps_type_map.run(0) + assert lammps_type_map.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps_type_map.atoms[ii].force == pytest.approx( + expected_f[lammps_type_map.atoms[ii].id - 1] + ) + lammps_type_map.run(1) + + +def test_pair_deepmd_real(lammps_real): + lammps_real.pair_style(f"deepmd {pb_file.resolve()}") + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + lammps_real.run(1) + + +def test_pair_deepmd_virial_real(lammps_real): + lammps_real.pair_style(f"deepmd {pb_file.resolve()}") + lammps_real.pair_coeff("* *") + lammps_real.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps_real.variable(f"virial{jj} atom c_virial[{ii+1}]") + lammps_real.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + idx_map = lammps_real.lmp.numpy.extract_atom("id") - 1 + for ii in range(9): + assert np.array( + lammps_real.variables[f"virial{ii}"].value + ) / constants.nktv2p_real == pytest.approx( + expected_v[idx_map, ii] * constants.ener_metal2real + ) + + +def test_pair_deepmd_model_devi_real(lammps_real): + lammps_real.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic".format( + pb_file.resolve(), pb_file2.resolve(), md_file.resolve() + ) + ) + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f * constants.force_metal2real) + assert md[4] == pytest.approx(np.max(expected_md_f) * constants.force_metal2real) + assert md[5] == pytest.approx(np.min(expected_md_f) * constants.force_metal2real) + assert md[6] == pytest.approx(np.mean(expected_md_f) * constants.force_metal2real) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v) * constants.ener_metal2real) + assert md[2] == pytest.approx(np.min(expected_md_v) * constants.ener_metal2real) + assert md[3] == pytest.approx( + np.sqrt(np.mean(np.square(expected_md_v))) * constants.ener_metal2real + ) + + +def test_pair_deepmd_model_devi_virial_real(lammps_real): + lammps_real.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic".format( + pb_file.resolve(), pb_file2.resolve(), md_file.resolve() + ) + ) + lammps_real.pair_coeff("* *") + lammps_real.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps_real.variable(f"virial{jj} atom c_virial[{ii+1}]") + lammps_real.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + idx_map = lammps_real.lmp.numpy.extract_atom("id") - 1 + for ii in range(9): + assert np.array( + lammps_real.variables[f"virial{ii}"].value + ) / constants.nktv2p_real == pytest.approx( + expected_v[idx_map, ii] * constants.ener_metal2real + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f * constants.force_metal2real) + assert md[4] == pytest.approx(np.max(expected_md_f) * constants.force_metal2real) + assert md[5] == pytest.approx(np.min(expected_md_f) * constants.force_metal2real) + assert md[6] == pytest.approx(np.mean(expected_md_f) * constants.force_metal2real) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v) * constants.ener_metal2real) + assert md[2] == pytest.approx(np.min(expected_md_v) * constants.ener_metal2real) + assert md[3] == pytest.approx( + np.sqrt(np.mean(np.square(expected_md_v))) * constants.ener_metal2real + ) + + +def test_pair_deepmd_model_devi_atomic_relative_real(lammps_real): + relative = 1.0 + lammps_real.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic relative {}".format( + pb_file.resolve(), + pb_file2.resolve(), + md_file.resolve(), + relative * constants.force_metal2real, + ) + ) + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + norm = np.linalg.norm(np.mean([expected_f, expected_f2], axis=0), axis=1) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + expected_md_f /= norm + relative + assert md[7:] == pytest.approx(expected_md_f * constants.force_metal2real) + assert md[4] == pytest.approx(np.max(expected_md_f) * constants.force_metal2real) + assert md[5] == pytest.approx(np.min(expected_md_f) * constants.force_metal2real) + assert md[6] == pytest.approx(np.mean(expected_md_f) * constants.force_metal2real) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v) * constants.ener_metal2real) + assert md[2] == pytest.approx(np.min(expected_md_v) * constants.ener_metal2real) + assert md[3] == pytest.approx( + np.sqrt(np.mean(np.square(expected_md_v))) * constants.ener_metal2real + ) + + +def test_pair_deepmd_model_devi_atomic_relative_v_real(lammps_real): + relative = 1.0 + lammps_real.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic relative_v {}".format( + pb_file.resolve(), + pb_file2.resolve(), + md_file.resolve(), + relative * constants.ener_metal2real, + ) + ) + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f * constants.force_metal2real) + assert md[4] == pytest.approx(np.max(expected_md_f) * constants.force_metal2real) + assert md[5] == pytest.approx(np.min(expected_md_f) * constants.force_metal2real) + assert md[6] == pytest.approx(np.mean(expected_md_f) * constants.force_metal2real) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + norm = ( + np.abs( + np.mean([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) + ) + / 6 + ) + expected_md_v /= norm + relative + assert md[1] == pytest.approx(np.max(expected_md_v) * constants.ener_metal2real) + assert md[2] == pytest.approx(np.min(expected_md_v) * constants.ener_metal2real) + assert md[3] == pytest.approx( + np.sqrt(np.mean(np.square(expected_md_v))) * constants.ener_metal2real + ) + + +def test_pair_deepmd_si(lammps_si): + lammps_si.pair_style(f"deepmd {pb_file.resolve()}") + lammps_si.pair_coeff("* *") + lammps_si.run(0) + assert lammps_si.eval("pe") == pytest.approx(expected_e * constants.ener_metal2si) + for ii in range(6): + assert lammps_si.atoms[ii].force == pytest.approx( + expected_f[lammps_si.atoms[ii].id - 1] * constants.force_metal2si + ) + lammps_si.run(1) diff --git a/source/tests/infer/deeppot_sea.pth b/source/tests/infer/deeppot_sea.pth new file mode 100644 index 0000000000000000000000000000000000000000..98aaa8a2ad0ccb1f462ff3475f28fea7d28e24fb GIT binary patch literal 123025 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�Q>X0Uo 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�=x9(71$;AXlw+(Q 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 F{vWF2=WGA~ literal 0 HcmV?d00001 From 897fcc5b9b5f6758f25ac912f5acb3ba16121a78 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 28 Feb 2024 03:14:16 -0500 Subject: [PATCH 137/686] pt: support `--init-frz-model` (#3350) Signed-off-by: Jinzhe Zeng --- deepmd/main.py | 2 +- deepmd/pt/entrypoints/main.py | 3 + deepmd/pt/train/training.py | 22 +++++- source/tests/pt/test_init_frz_model.py | 101 +++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 source/tests/pt/test_init_frz_model.py diff --git a/deepmd/main.py b/deepmd/main.py index 4d2d62ed14..df5c99bb2d 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -226,7 +226,7 @@ def main_parser() -> argparse.ArgumentParser: "--init-frz-model", type=str, default=None, - help="(Supported backend: TensorFlow) Initialize the training from the frozen model.", + help="Initialize the training from the frozen model.", ) parser_train_subgroup.add_argument( "-t", diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 212a6824e7..a317cea6a9 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -65,6 +65,7 @@ def get_trainer( finetune_model=None, model_branch="", force_load=False, + init_frz_model=None, ): # Initialize DDP local_rank = os.environ.get("LOCAL_RANK") @@ -200,6 +201,7 @@ def prepare_trainer_input_single( finetune_model=finetune_model, force_load=force_load, shared_links=shared_links, + init_frz_model=init_frz_model, ) return trainer @@ -243,6 +245,7 @@ def train(FLAGS): FLAGS.finetune, FLAGS.model_branch, FLAGS.force_load, + FLAGS.init_frz_model, ) trainer.run() diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 5a783e412b..152c69a444 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -75,6 +75,7 @@ def __init__( finetune_model=None, force_load=False, shared_links=None, + init_frz_model=None, ): """Construct a DeePMD trainer. @@ -271,7 +272,7 @@ def get_loss(loss_params, start_lr, _ntypes): self.warmup_steps = training_params.get("warmup_steps", 0) self.gradient_max_norm = training_params.get("gradient_max_norm", 0.0) assert ( - self.num_steps - self.warmup_steps > 0 + self.num_steps - self.warmup_steps > 0 or self.warmup_steps == 0 ), "Warm up steps must be less than total training steps!" if self.multi_task and config.get("learning_rate_dict", None) is not None: self.lr_exp = {} @@ -394,6 +395,9 @@ def get_loss(loss_params, start_lr, _ntypes): ntest=ntest, bias_shift=model_params.get("bias_shift", "delta"), ) + if init_frz_model is not None: + frz_model = torch.jit.load(init_frz_model, map_location=DEVICE) + self.model.load_state_dict(frz_model.state_dict()) # Set trainable params self.wrapper.set_trainable_params() @@ -724,6 +728,15 @@ def log_loss_valid(_task_key="Default"): if ( self.rank == 0 or dist.get_rank() == 0 ): # Handle the case if rank 0 aborted and re-assigned + if self.num_steps == 0: + # when num_steps is 0, the checkpoint is never not saved + self.latest_model = Path(self.save_ckpt + "-0.pt") + self.save_model(self.latest_model, lr=0, step=0) + log.info(f"Saved model to {self.latest_model}") + symlink_prefix_files(self.latest_model.stem, self.save_ckpt) + with open("checkpoint", "w") as f: + f.write(str(self.latest_model)) + if JIT: pth_model_path = ( "frozen_model.pth" # We use .pth to denote the frozen model @@ -759,9 +772,10 @@ def get_data(self, is_train=True, task_key="Default"): batch_data = next(iter(self.training_data)) except StopIteration: # Refresh the status of the dataloader to start from a new epoch - self.training_data = BufferedIterator( - iter(self.training_dataloader) - ) + with torch.device("cpu"): + self.training_data = BufferedIterator( + iter(self.training_dataloader) + ) batch_data = next(iter(self.training_data)) else: try: diff --git a/source/tests/pt/test_init_frz_model.py b/source/tests/pt/test_init_frz_model.py new file mode 100644 index 0000000000..d156eddc41 --- /dev/null +++ b/source/tests/pt/test_init_frz_model.py @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import unittest +from argparse import ( + Namespace, +) +from copy import ( + deepcopy, +) +from pathlib import ( + Path, +) + +import numpy as np + +from deepmd.pt.entrypoints.main import ( + freeze, + get_trainer, +) +from deepmd.pt.infer.deep_eval import ( + DeepPot, +) + + +class TestInitFrzModel(unittest.TestCase): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + config = json.load(f) + config["training"]["numb_steps"] = 1 + config["training"]["save_freq"] = 1 + config["learning_rate"]["start_lr"] = 1.0 + config["training"]["training_data"]["systems"] = [ + str(Path(__file__).parent / "water/data/single") + ] + config["training"]["validation_data"]["systems"] = [ + str(Path(__file__).parent / "water/data/single") + ] + + self.models = [] + for imodel in range(2): + if imodel == 1: + config["training"]["numb_steps"] = 0 + trainer = get_trainer(deepcopy(config), init_frz_model=self.models[-1]) + else: + trainer = get_trainer(deepcopy(config)) + trainer.run() + + frozen_model = f"frozen_model{imodel}.pth" + ns = Namespace( + model="model.pt", + output=frozen_model, + head=None, + ) + freeze(ns) + self.models.append(frozen_model) + + def test_dp_test(self): + dp1 = DeepPot(str(self.models[0])) + dp2 = DeepPot(str(self.models[1])) + cell = np.array( + [ + 5.122106549439247480e00, + 4.016537340154059388e-01, + 6.951654033828678081e-01, + 4.016537340154059388e-01, + 6.112136112297989143e00, + 8.178091365465004481e-01, + 6.951654033828678081e-01, + 8.178091365465004481e-01, + 6.159552512682983760e00, + ] + ).reshape(1, 3, 3) + coord = np.array( + [ + 2.978060152121375648e00, + 3.588469695887098077e00, + 2.792459820604495491e00, + 3.895592322591093115e00, + 2.712091020667753760e00, + 1.366836847133650501e00, + 9.955616170888935690e-01, + 4.121324820711413039e00, + 1.817239061889086571e00, + 3.553661462345699906e00, + 5.313046969500791583e00, + 6.635182659098815883e00, + 6.088601018589653080e00, + 6.575011420004332585e00, + 6.825240650611076099e00, + ] + ).reshape(1, -1, 3) + atype = np.array([0, 0, 0, 1, 1]).reshape(1, -1) + + e1, f1, v1, ae1, av1 = dp1.eval(coord, cell, atype, atomic=True) + e2, f2, v2, ae2, av2 = dp2.eval(coord, cell, atype, atomic=True) + np.testing.assert_allclose(e1, e2, rtol=1e-10, atol=1e-10) + np.testing.assert_allclose(f1, f2, rtol=1e-10, atol=1e-10) + np.testing.assert_allclose(v1, v2, rtol=1e-10, atol=1e-10) + np.testing.assert_allclose(ae1, ae2, rtol=1e-10, atol=1e-10) + np.testing.assert_allclose(av1, av2, rtol=1e-10, atol=1e-10) From 16cd26ca8b4687fc54e32bf226ee2075bad94d64 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Wed, 28 Feb 2024 18:29:12 +0800 Subject: [PATCH 138/686] Fix single-task training&data stat (#3355) --- deepmd/pt/model/descriptor/dpa2.py | 2 +- deepmd/pt/model/model/__init__.py | 9 ++++----- deepmd/pt/model/model/dp_model.py | 5 ++++- deepmd/pt/model/model/model.py | 4 ++-- deepmd/utils/path.py | 1 + examples/water/dpa2/input_torch.json | 8 ++------ examples/water/se_atten/input_torch.json | 2 ++ examples/water/se_e2_a/input_torch.json | 1 + 8 files changed, 17 insertions(+), 15 deletions(-) diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index e693116cf4..b1df56a004 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -304,7 +304,7 @@ def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None) } for item in merged ] - descrpt.compute_input_stats(merged_tmp) + descrpt.compute_input_stats(merged_tmp, path) def serialize(self) -> dict: """Serialize the obj to dict.""" diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index 0dc9ae20af..b823a051f5 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -20,7 +20,7 @@ BaseDescriptor, ) from deepmd.pt.model.task import ( - Fitting, + BaseFitting, ) from .dp_model import ( @@ -61,7 +61,7 @@ def get_zbl_model(model_params): fitting_net["out_dim"] = descriptor.get_dim_emb() if "ener" in fitting_net["type"]: fitting_net["return_energy"] = True - fitting = Fitting(**fitting_net) + fitting = BaseFitting(**fitting_net) dp_model = DPAtomicModel(descriptor, fitting, type_map=model_params["type_map"]) # pairtab filepath = model_params["use_srtab"] @@ -97,9 +97,8 @@ def get_model(model_params): fitting_net["out_dim"] = descriptor.get_dim_emb() if "ener" in fitting_net["type"]: fitting_net["return_energy"] = True - fitting = Fitting(**fitting_net) - - model = EnergyModel(descriptor, fitting, type_map=model_params["type_map"]) + fitting = BaseFitting(**fitting_net) + model = DPModel(descriptor, fitting, type_map=model_params["type_map"]) model.model_def_script = json.dumps(model_params) return model diff --git a/deepmd/pt/model/model/dp_model.py b/deepmd/pt/model/model/dp_model.py index 5410f518d1..79c129334a 100644 --- a/deepmd/pt/model/model/dp_model.py +++ b/deepmd/pt/model/model/dp_model.py @@ -10,6 +10,7 @@ ) from deepmd.pt.model.task.ener import ( EnergyFittingNet, + EnergyFittingNetDirect, ) from deepmd.pt.model.task.polarizability import ( PolarFittingNet, @@ -36,7 +37,9 @@ def __new__(cls, descriptor, fitting, *args, **kwargs): # according to the fitting network to decide the type of the model if cls is DPModel: # map fitting to model - if isinstance(fitting, EnergyFittingNet): + if isinstance(fitting, EnergyFittingNet) or isinstance( + fitting, EnergyFittingNetDirect + ): cls = EnergyModel elif isinstance(fitting, DipoleFittingNet): cls = DipoleModel diff --git a/deepmd/pt/model/model/model.py b/deepmd/pt/model/model/model.py index 0f5e27aea9..e32d2f307d 100644 --- a/deepmd/pt/model/model/model.py +++ b/deepmd/pt/model/model/model.py @@ -59,9 +59,9 @@ # in DPAtomicModel (and other classes), but this requires the developer aware # of it when developing it... class BaseModel(make_base_model()): - def __init__(self): + def __init__(self, *args, **kwargs): """Construct a basic model for different tasks.""" - super().__init__() + super().__init__(*args, **kwargs) def compute_or_load_stat( self, diff --git a/deepmd/utils/path.py b/deepmd/utils/path.py index c9a7cd8554..79361b6c23 100644 --- a/deepmd/utils/path.py +++ b/deepmd/utils/path.py @@ -355,6 +355,7 @@ def save_numpy(self, arr: np.ndarray) -> None: if self._name in self._keys: del self.root[self._name] self.root.create_dataset(self._name, data=arr) + self.root.flush() def glob(self, pattern: str) -> List["DPPath"]: """Search path using the glob pattern. diff --git a/examples/water/dpa2/input_torch.json b/examples/water/dpa2/input_torch.json index 9d783b35d5..108e75df62 100644 --- a/examples/water/dpa2/input_torch.json +++ b/examples/water/dpa2/input_torch.json @@ -1,18 +1,13 @@ { "_comment": "that's all", "model": { - "type_embedding": { - "neuron": [ - 8 - ], - "tebd_input_mode": "concat" - }, "type_map": [ "O", "H" ], "descriptor": { "type": "dpa2", + "tebd_dim": 8, "repinit_rcut": 9.0, "repinit_rcut_smth": 8.0, "repinit_nsel": 120, @@ -74,6 +69,7 @@ "_comment": " that's all" }, "training": { + "stat_file": "./dpa2", "training_data": { "systems": [ "../data/data_0", diff --git a/examples/water/se_atten/input_torch.json b/examples/water/se_atten/input_torch.json index 7da3d64164..bc948cc2a0 100644 --- a/examples/water/se_atten/input_torch.json +++ b/examples/water/se_atten/input_torch.json @@ -15,6 +15,7 @@ 50, 100 ], + "tebd_dim": 8, "axis_neuron": 16, "attn": 128, "attn_layer": 2, @@ -59,6 +60,7 @@ "_comment": " that's all" }, "training": { + "stat_file": "./dpa1", "training_data": { "systems": [ "../data/data_0", diff --git a/examples/water/se_e2_a/input_torch.json b/examples/water/se_e2_a/input_torch.json index 053a721a44..c686b49d45 100644 --- a/examples/water/se_e2_a/input_torch.json +++ b/examples/water/se_e2_a/input_torch.json @@ -51,6 +51,7 @@ "_comment": " that's all" }, "training": { + "stat_file": "./se_e2_a", "training_data": { "systems": [ "../data/data_0", From 3ad57daf39eb5d91d62ce8866e6e42643a49d335 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 28 Feb 2024 05:34:34 -0500 Subject: [PATCH 139/686] merge compute_output_stat (#3310) Signed-off-by: Jinzhe Zeng --- deepmd/pt/model/task/dipole.py | 8 -- deepmd/pt/model/task/ener.py | 34 ++++--- deepmd/pt/model/task/fitting.py | 8 -- deepmd/pt/model/task/polarizability.py | 8 -- deepmd/pt/utils/env_mat_stat.py | 2 +- deepmd/pt/utils/stat.py | 21 ----- deepmd/tf/fit/dos.py | 9 +- deepmd/tf/fit/ener.py | 25 +++-- deepmd/tf/fit/polar.py | 6 +- deepmd/utils/data_system.py | 11 ++- deepmd/utils/out_stat.py | 117 +++++++++++++++++++++++ source/tests/common/test_out_stat.py | 124 +++++++++++++++++++++++++ source/tests/pt/test_stat.py | 17 ++-- source/tests/tf/common.py | 11 ++- 14 files changed, 312 insertions(+), 89 deletions(-) create mode 100644 deepmd/utils/out_stat.py create mode 100644 source/tests/common/test_out_stat.py diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py index bff3dd93bc..9df3a5fb32 100644 --- a/deepmd/pt/model/task/dipole.py +++ b/deepmd/pt/model/task/dipole.py @@ -132,14 +132,6 @@ def output_def(self) -> FittingOutputDef: ] ) - @property - def data_stat_key(self): - """ - Get the keys for the data statistic of the fitting. - Return a list of statistic names needed, such as "bias_atom_e". - """ - return [] - def forward( self, descriptor: torch.Tensor, diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 8479111819..ff7ae6f8ec 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -28,8 +28,11 @@ from deepmd.pt.utils.env import ( DEFAULT_PRECISION, ) -from deepmd.pt.utils.stat import ( - compute_output_bias, +from deepmd.pt.utils.utils import ( + to_numpy_array, +) +from deepmd.utils.out_stat import ( + compute_stats_from_redu, ) from deepmd.utils.path import ( DPPath, @@ -135,16 +138,8 @@ def serialize(self) -> dict: data["atom_ener"] = self.atom_ener return data - @property - def data_stat_key(self): - """ - Get the keys for the data statistic of the fitting. - Return a list of statistic names needed, such as "bias_atom_e". - """ - return ["bias_atom_e"] - def compute_output_stats(self, merged, stat_file_path: Optional[DPPath] = None): - energy = [item["energy"] for item in merged] + energy = [item[self.var_name] for item in merged] data_mixed_type = "real_natoms_vec" in merged[0] if data_mixed_type: input_natoms = [item["real_natoms_vec"] for item in merged] @@ -155,7 +150,22 @@ def compute_output_stats(self, merged, stat_file_path: Optional[DPPath] = None): if stat_file_path is not None and stat_file_path.is_file(): bias_atom_e = stat_file_path.load_numpy() else: - bias_atom_e = compute_output_bias(energy, input_natoms, rcond=self.rcond) + # shape: (nframes, ndim) + merged_energy = to_numpy_array(torch.cat(energy)) + # shape: (nframes, ntypes) + merged_natoms = to_numpy_array(torch.cat(input_natoms)[:, 2:]) + if self.atom_ener is not None and len(self.atom_ener) > 0: + assigned_atom_ener = np.array( + [ee if ee is not None else np.nan for ee in self.atom_ener] + ) + else: + assigned_atom_ener = None + bias_atom_e, _ = compute_stats_from_redu( + merged_energy, + merged_natoms, + assigned_bias=assigned_atom_ener, + rcond=self.rcond, + ) if stat_file_path is not None: stat_file_path.save_numpy(bias_atom_e) assert all(x is not None for x in [bias_atom_e]) diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 0c64983f60..20876d9be7 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -92,14 +92,6 @@ def share_params(self, base_class, shared_level, resume=False): else: raise NotImplementedError - @property - def data_stat_key(self): - """ - Get the keys for the data statistic of the fitting. - Return a list of statistic names needed, such as "bias_atom_e". - """ - raise NotImplementedError("data_stat_key is not implemented!") - def change_energy_bias( self, config, model, old_type_map, new_type_map, bias_shift="delta", ntest=10 ): diff --git a/deepmd/pt/model/task/polarizability.py b/deepmd/pt/model/task/polarizability.py index 13b0d56e31..1bc4798c48 100644 --- a/deepmd/pt/model/task/polarizability.py +++ b/deepmd/pt/model/task/polarizability.py @@ -160,14 +160,6 @@ def output_def(self) -> FittingOutputDef: ] ) - @property - def data_stat_key(self): - """ - Get the keys for the data statistic of the fitting. - Return a list of statistic names needed, such as "bias_atom_e". - """ - return [] - def forward( self, descriptor: torch.Tensor, diff --git a/deepmd/pt/utils/env_mat_stat.py b/deepmd/pt/utils/env_mat_stat.py index 70b7228440..cd2943e6a8 100644 --- a/deepmd/pt/utils/env_mat_stat.py +++ b/deepmd/pt/utils/env_mat_stat.py @@ -80,7 +80,7 @@ def iter( Parameters ---------- data : List[Dict[str, torch.Tensor]] - The environment matrix. + The data. Yields ------ diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index 38f71d6994..4c769f019e 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging -import numpy as np import torch log = logging.getLogger(__name__) @@ -57,23 +56,3 @@ def make_stat_input(datasets, dataloaders, nbatches): sys_stat[key] = sys_stat_list lst.append(sys_stat) return lst - - -def compute_output_bias(energy, natoms, rcond=None): - """Update output bias for fitting net. - - Args: - - energy: Batched energy with shape [nframes, 1]. - - natoms: Batched atom statisics with shape [self.ntypes+2]. - - Returns - ------- - - energy_coef: Average enery per atom for each element. - """ - for i in range(len(energy)): - energy[i] = energy[i].mean(dim=0, keepdim=True) - natoms[i] = natoms[i].double().mean(dim=0, keepdim=True) - sys_ener = torch.cat(energy).cpu() - sys_tynatom = torch.cat(natoms)[:, 2:].cpu() - energy_coef, _, _, _ = np.linalg.lstsq(sys_tynatom, sys_ener, rcond) - return energy_coef diff --git a/deepmd/tf/fit/dos.py b/deepmd/tf/fit/dos.py index e8681f47ea..0cc5a7df62 100644 --- a/deepmd/tf/fit/dos.py +++ b/deepmd/tf/fit/dos.py @@ -43,6 +43,9 @@ from deepmd.tf.utils.network import ( one_layer_rand_seed_shift, ) +from deepmd.utils.out_stat import ( + compute_stats_from_redu, +) log = logging.getLogger(__name__) @@ -225,8 +228,10 @@ def _compute_output_stats(self, all_stat, rcond=1e-3, mixed_type=False): sys_tynatom = np.reshape(sys_tynatom, [nsys, -1]) sys_tynatom = sys_tynatom[:, 2:] - dos_shift, resd, rank, s_value = np.linalg.lstsq( - sys_tynatom, sys_dos, rcond=rcond + dos_shift, _ = compute_stats_from_redu( + sys_dos, + sys_tynatom, + rcond=rcond, ) return dos_shift diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index 106e10839d..a842df50bd 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -53,6 +53,9 @@ from deepmd.tf.utils.spin import ( Spin, ) +from deepmd.utils.out_stat import ( + compute_stats_from_redu, +) from deepmd.utils.version import ( check_version_compatibility, ) @@ -295,21 +298,17 @@ def _compute_output_stats(self, all_stat, rcond=1e-3, mixed_type=False): # In this situation, we directly use these assigned energies instead of computing stats. # This will make the loss decrease quickly assigned_atom_ener = np.array( - [ee for ee in self.atom_ener_v if ee is not None] + [ee if ee is not None else np.nan for ee in self.atom_ener_v] ) - assigned_ener_idx = [ - ii for ii, ee in enumerate(self.atom_ener_v) if ee is not None - ] - # np.dot out size: nframe - sys_ener -= np.dot(sys_tynatom[:, assigned_ener_idx], assigned_atom_ener) - sys_tynatom[:, assigned_ener_idx] = 0.0 - energy_shift, resd, rank, s_value = np.linalg.lstsq( - sys_tynatom, sys_ener, rcond=rcond + else: + assigned_atom_ener = None + energy_shift, _ = compute_stats_from_redu( + sys_ener.reshape(-1, 1), + sys_tynatom, + assigned_bias=assigned_atom_ener, + rcond=rcond, ) - if len(self.atom_ener) > 0: - for ii in assigned_ener_idx: - energy_shift[ii] = self.atom_ener_v[ii] - return energy_shift + return energy_shift.ravel() def compute_input_stats(self, all_stat: dict, protection: float = 1e-2) -> None: """Compute the input statistics. diff --git a/deepmd/tf/fit/polar.py b/deepmd/tf/fit/polar.py index 002082ad2e..7ac31809f3 100644 --- a/deepmd/tf/fit/polar.py +++ b/deepmd/tf/fit/polar.py @@ -151,16 +151,14 @@ def get_out_size(self) -> int: """Get the output size. Should be 9.""" return 9 - def compute_input_stats(self, all_stat, protection=1e-2): - """Compute the input statistics. + def compute_output_stats(self, all_stat): + """Compute the output statistics. Parameters ---------- all_stat Dictionary of inputs. can be prepared by model.make_stat_input - protection - Divided-by-zero protection """ if "polarizability" not in all_stat.keys(): self.avgeig = np.zeros([9]) diff --git a/deepmd/utils/data_system.py b/deepmd/utils/data_system.py index 20111558cf..592b1f9748 100644 --- a/deepmd/utils/data_system.py +++ b/deepmd/utils/data_system.py @@ -22,6 +22,9 @@ from deepmd.utils.data import ( DeepmdData, ) +from deepmd.utils.out_stat import ( + compute_stats_from_redu, +) log = logging.getLogger(__name__) @@ -248,10 +251,12 @@ def compute_energy_shift(self, rcond=None, key="energy"): sys_tynatom = np.array(self.natoms_vec, dtype=GLOBAL_NP_FLOAT_PRECISION) sys_tynatom = np.reshape(sys_tynatom, [self.nsystems, -1]) sys_tynatom = sys_tynatom[:, 2:] - energy_shift, resd, rank, s_value = np.linalg.lstsq( - sys_tynatom, sys_ener, rcond=rcond + energy_shift, _ = compute_stats_from_redu( + sys_ener.reshape(-1, 1), + sys_tynatom, + rcond=rcond, ) - return energy_shift + return energy_shift.ravel() def add_dict(self, adict: dict) -> None: """Add items to the data system by a `dict`. diff --git a/deepmd/utils/out_stat.py b/deepmd/utils/out_stat.py new file mode 100644 index 0000000000..8f68e32417 --- /dev/null +++ b/deepmd/utils/out_stat.py @@ -0,0 +1,117 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Output statistics.""" +from typing import ( + Optional, + Tuple, +) + +import numpy as np + + +def compute_stats_from_redu( + output_redu: np.ndarray, + natoms: np.ndarray, + assigned_bias: Optional[np.ndarray] = None, + rcond: Optional[float] = None, +) -> Tuple[np.ndarray, np.ndarray]: + """Compute the output statistics. + + Given the reduced output value and the number of atoms for each atom, + compute the least-squares solution as the atomic output bais and std. + + Parameters + ---------- + output_redu + The reduced output value, shape is [nframes, ndim]. + natoms + The number of atoms for each atom, shape is [nframes, ntypes]. + assigned_bias + The assigned output bias, shape is [ntypes, ndim]. Set to nan + if not assigned. + rcond + Cut-off ratio for small singular values of a. + + Returns + ------- + np.ndarray + The computed output bias, shape is [ntypes, ndim]. + np.ndarray + The computed output std, shape is [ntypes, ndim]. + """ + output_redu = np.array(output_redu) + natoms = np.array(natoms) + # check shape + assert output_redu.ndim == 2 + assert natoms.ndim == 2 + assert output_redu.shape[0] == natoms.shape[0] # nframes + if assigned_bias is not None: + assigned_bias = np.array(assigned_bias).reshape( + natoms.shape[1], output_redu.shape[1] + ) + # compute output bias + if assigned_bias is not None: + # Atomic energies stats are incorrect if atomic energies are assigned. + # In this situation, we directly use these assigned energies instead of computing stats. + # This will make the loss decrease quickly + assigned_bias_atom_mask = ~np.isnan(assigned_bias).any(axis=1) + # assigned_bias_masked: nmask, ndim + assigned_bias_masked = assigned_bias[assigned_bias_atom_mask] + # assigned_bias_natoms: nframes, nmask + assigned_bias_natoms = natoms[:, assigned_bias_atom_mask] + # output_redu: nframes, ndim + output_redu -= np.einsum( + "ij,jk->ik", assigned_bias_natoms, assigned_bias_masked + ) + # remove assigned atom + natoms[:, assigned_bias_atom_mask] = 0 + + # computed_output_bias: ntypes, ndim + computed_output_bias, _, _, _ = np.linalg.lstsq(natoms, output_redu, rcond=rcond) + if assigned_bias is not None: + # add back assigned atom; this might not be required + computed_output_bias[assigned_bias_atom_mask] = assigned_bias_masked + # rest_redu: nframes, ndim + rest_redu = output_redu - np.einsum("ij,jk->ik", natoms, computed_output_bias) + output_std = rest_redu.std(axis=0) + return computed_output_bias, output_std + + +def compute_stats_from_atomic( + output: np.ndarray, + atype: np.ndarray, +) -> Tuple[np.ndarray, np.ndarray]: + """Compute the output statistics. + + Given the output value and the type of atoms, + compute the atomic output bais and std. + + Parameters + ---------- + output + The output value, shape is [nframes, nloc, ndim]. + atype + The type of atoms, shape is [nframes, nloc]. + + Returns + ------- + np.ndarray + The computed output bias, shape is [ntypes, ndim]. + np.ndarray + The computed output std, shape is [ntypes, ndim]. + """ + output = np.array(output) + atype = np.array(atype) + # check shape + assert output.ndim == 3 + assert atype.ndim == 2 + assert output.shape[:2] == atype.shape + # compute output bias + nframes, nloc, ndim = output.shape + ntypes = atype.max() + 1 + output_bias = np.zeros((ntypes, ndim)) + output_std = np.zeros((ntypes, ndim)) + for type_i in range(ntypes): + mask = atype == type_i + output_bias[type_i] = output[mask].mean(axis=0) + output_std[type_i] = output[mask].std(axis=0) + return output_bias, output_std diff --git a/source/tests/common/test_out_stat.py b/source/tests/common/test_out_stat.py new file mode 100644 index 0000000000..c0cfc25071 --- /dev/null +++ b/source/tests/common/test_out_stat.py @@ -0,0 +1,124 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np + +from deepmd.utils.out_stat import ( + compute_stats_from_atomic, + compute_stats_from_redu, +) + + +class TestOutStat(unittest.TestCase): + def setUp(self) -> None: + rng = np.random.default_rng(20240227) + ndim = 5 + nframes = 1000 + ntypes = 3 + nloc = 1000 + self.atype = rng.integers(0, ntypes, size=(nframes, nloc)) + # compute the number of atoms for each type in each frame + self.natoms = np.zeros((nframes, ntypes), dtype=np.int64) + for i in range(ntypes): + self.natoms[:, i] = (self.atype == i).sum(axis=1) + self.mean = rng.random((ntypes, ndim)) * 1e4 + self.std = rng.random((ntypes, ndim)) * 1e-3 + + # generate random output + self.output = rng.normal( + loc=self.mean[self.atype, :], + scale=self.std[self.atype, :], + size=(nframes, nloc, ndim), + ) + self.output_redu = self.output.sum(axis=1) + + return super().setUp() + + def test_compute_stats_from_redu(self): + bias, std = compute_stats_from_redu(self.output_redu, self.natoms) + np.testing.assert_allclose(bias, self.mean, rtol=1e-7) + reference_std = np.array( + [ + 0.01700638138272794, + 0.01954897296228177, + 0.020281857747683162, + 0.010741237959989648, + 0.020258211828681347, + ] + ) + np.testing.assert_allclose( + std, + reference_std, + rtol=1e-7, + ) + # ensure the sum is close + np.testing.assert_allclose( + self.output_redu, + self.natoms @ bias, + rtol=1e-7, + ) + + def test_compute_stats_from_redu_with_assigned_bias(self): + assigned_bias = np.full_like(self.mean, np.nan) + assigned_bias[0] = self.mean[0] + bias, std = compute_stats_from_redu( + self.output_redu, + self.natoms, + assigned_bias=assigned_bias, + ) + np.testing.assert_allclose(bias, self.mean, rtol=1e-7) + np.testing.assert_allclose(bias[0], self.mean[0], rtol=1e-14) + reference_std = np.array( + [ + 0.017015794087883902, + 0.019549011723239484, + 0.020285565914828625, + 0.01074124012073672, + 0.020283557003416414, + ] + ) + np.testing.assert_allclose( + std, + reference_std, + rtol=1e-7, + ) + # ensure the sum is close + np.testing.assert_allclose( + self.output_redu, + self.natoms @ bias, + rtol=1e-7, + ) + + def test_compute_stats_from_atomic(self): + bias, std = compute_stats_from_atomic(self.output, self.atype) + np.testing.assert_allclose(bias, self.mean) + reference_std = np.array( + [ + [ + 0.0005452949516910239, + 0.000686732800598535, + 0.00089423457667224, + 7.818017989121455e-05, + 0.0004758637035637342, + ], + [ + 2.0610161678825724e-05, + 0.0007728218734771541, + 0.0004754659308165858, + 0.0001809007655290948, + 0.0008187364708029638, + ], + [ + 0.0007935836092665254, + 0.00031176505013516624, + 0.0005469653430009186, + 0.0005652240916389281, + 0.0006087722080071852, + ], + ] + ) + np.testing.assert_allclose( + std, + reference_std, + rtol=1e-7, + ) diff --git a/source/tests/pt/test_stat.py b/source/tests/pt/test_stat.py index 1e3c707d6f..98d4e59d95 100644 --- a/source/tests/pt/test_stat.py +++ b/source/tests/pt/test_stat.py @@ -20,15 +20,15 @@ from deepmd.pt.model.descriptor.dpa1 import ( DescrptDPA1, ) +from deepmd.pt.model.task.ener import ( + EnergyFittingNet, +) from deepmd.pt.utils import ( env, ) from deepmd.pt.utils.dataloader import ( DpLoaderSet, ) -from deepmd.pt.utils.stat import ( - compute_output_bias, -) from deepmd.pt.utils.stat import make_stat_input as my_make from deepmd.tf.common import ( expand_sys_str, @@ -145,9 +145,14 @@ def my_merge(energy, natoms): dp_fn = EnerFitting( self.dp_d.get_ntypes(), self.dp_d.get_dim_out(), self.n_neuron ) - dp_fn.compute_output_stats(self.dp_sampled) - bias_atom_e = compute_output_bias(energy, natoms) - self.assertTrue(np.allclose(dp_fn.bias_atom_e, bias_atom_e[:, 0])) + dp_fn.compute_output_stats(self.dp_sampled, mixed_type=self.mixed_type) + pt_fn = EnergyFittingNet( + self.dp_d.get_ntypes(), self.dp_d.get_dim_out(), self.n_neuron + ) + pt_fn.compute_output_stats(self.my_sampled) + np.testing.assert_allclose( + dp_fn.bias_atom_e, pt_fn.bias_atom_e.detach().cpu().numpy().ravel() + ) # temporarily delete this function for performance of seeds in tf and pytorch may be different """ diff --git a/source/tests/tf/common.py b/source/tests/tf/common.py index a83397c11c..0bcb29b4b5 100644 --- a/source/tests/tf/common.py +++ b/source/tests/tf/common.py @@ -17,6 +17,9 @@ tf, ) from deepmd.tf.utils import random as dp_random +from deepmd.utils.out_stat import ( + compute_stats_from_redu, +) if GLOBAL_NP_FLOAT_PRECISION == np.float32: global_default_fv_hh = 1e-2 @@ -1041,10 +1044,12 @@ def compute_energy_shift(self): sys_tynatom = np.array(self.natoms_vec, dtype=GLOBAL_NP_FLOAT_PRECISION) sys_tynatom = np.reshape(sys_tynatom, [self.nsystems, -1]) sys_tynatom = sys_tynatom[:, 2:] - energy_shift, resd, rank, s_value = np.linalg.lstsq( - sys_tynatom, sys_ener, rcond=None + energy_shift, _ = compute_stats_from_redu( + sys_ener.reshape(-1, 1), + sys_tynatom, + rcond=None, ) - return energy_shift + return energy_shift.ravel() def process_sys_weights(self, sys_weights): sys_weights = np.array(sys_weights) From 2a1508df452b6f538892ebda4a618d45a9c14848 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 28 Feb 2024 09:19:43 -0500 Subject: [PATCH 140/686] feat(pt): support fparam/aparam in DeepEval (#3356) Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/infer/deep_eval.py | 27 +++++++++ deepmd/pt/infer/deep_eval.py | 32 ++++++++-- deepmd/pt/train/wrapper.py | 9 ++- source/tests/infer/fparam_aparam.pbtxt | 2 +- source/tests/infer/fparam_aparam.pth | Bin 0 -> 105871 bytes source/tests/pt/model/test_deeppot.py | 22 +++++++ source/tests/tf/test_deeppot_a.py | 77 ++++++++++++------------- 7 files changed, 121 insertions(+), 48 deletions(-) create mode 100644 source/tests/infer/fparam_aparam.pth diff --git a/deepmd/infer/deep_eval.py b/deepmd/infer/deep_eval.py index 35d170cdab..de964b88b9 100644 --- a/deepmd/infer/deep_eval.py +++ b/deepmd/infer/deep_eval.py @@ -472,6 +472,33 @@ def _standard_input(self, coords, cells, atom_types, fparam, aparam, mixed_type) aparam = np.array(aparam) natoms, nframes = self._get_natoms_and_nframes(coords, atom_types, mixed_type) atom_types = self._expande_atype(atom_types, nframes, mixed_type) + coords = coords.reshape(nframes, natoms, 3) + if cells is not None: + cells = cells.reshape(nframes, 3, 3) + if fparam is not None: + fdim = self.get_dim_fparam() + if fparam.size == nframes * fdim: + fparam = np.reshape(fparam, [nframes, fdim]) + elif fparam.size == fdim: + fparam = np.tile(fparam.reshape([-1]), [nframes, 1]) + else: + raise RuntimeError( + "got wrong size of frame param, should be either %d x %d or %d" + % (nframes, fdim, fdim) + ) + if aparam is not None: + fdim = self.get_dim_aparam() + if aparam.size == nframes * natoms * fdim: + aparam = np.reshape(aparam, [nframes, natoms * fdim]) + elif aparam.size == natoms * fdim: + aparam = np.tile(aparam.reshape([-1]), [nframes, 1]) + elif aparam.size == fdim: + aparam = np.tile(aparam.reshape([-1]), [nframes, natoms]) + else: + raise RuntimeError( + "got wrong size of frame param, should be either %d x %d x %d or %d x %d or %d" + % (nframes, natoms, fdim, natoms, fdim, fdim) + ) return coords, cells, atom_types, fparam, aparam, nframes, natoms def get_sel_type(self) -> List[int]: diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index b13a968a61..f75052166b 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -54,6 +54,9 @@ DEVICE, GLOBAL_PT_FLOAT_PRECISION, ) +from deepmd.pt.utils.utils import ( + to_torch_tensor, +) if TYPE_CHECKING: import ase.neighborlist @@ -228,8 +231,6 @@ def eval( The output of the evaluation. The keys are the names of the output variables, and the values are the corresponding output arrays. """ - if fparam is not None or aparam is not None: - raise NotImplementedError # convert all of the input to numpy array atom_types = np.array(atom_types, dtype=np.int32) coords = np.array(coords) @@ -240,7 +241,12 @@ def eval( ) request_defs = self._get_request_defs(atomic) out = self._eval_func(self._eval_model, numb_test, natoms)( - coords, cells, atom_types, request_defs + coords, + cells, + atom_types, + fparam, + aparam, + request_defs, ) return dict( zip( @@ -330,6 +336,8 @@ def _eval_model( coords: np.ndarray, cells: Optional[np.ndarray], atom_types: np.ndarray, + fparam: Optional[np.ndarray], + aparam: Optional[np.ndarray], request_defs: List[OutputVariableDef], ): model = self.dp.to(DEVICE) @@ -355,12 +363,26 @@ def _eval_model( ) else: box_input = None - + if fparam is not None: + fparam_input = to_torch_tensor(fparam.reshape(-1, self.get_dim_fparam())) + else: + fparam_input = None + if aparam is not None: + aparam_input = to_torch_tensor( + aparam.reshape(-1, natoms, self.get_dim_aparam()) + ) + else: + aparam_input = None do_atomic_virial = any( x.category == OutputVariableCategory.DERV_C_REDU for x in request_defs ) batch_output = model( - coord_input, type_input, box=box_input, do_atomic_virial=do_atomic_virial + coord_input, + type_input, + box=box_input, + do_atomic_virial=do_atomic_virial, + fparam=fparam_input, + aparam=aparam_input, ) if isinstance(batch_output, tuple): batch_output = batch_output[0] diff --git a/deepmd/pt/train/wrapper.py b/deepmd/pt/train/wrapper.py index 2207f111a0..74b4a83ce7 100644 --- a/deepmd/pt/train/wrapper.py +++ b/deepmd/pt/train/wrapper.py @@ -164,6 +164,8 @@ def forward( task_key: Optional[torch.Tensor] = None, inference_only=False, do_atomic_virial=False, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, ): if not self.multi_task: task_key = "Default" @@ -172,7 +174,12 @@ def forward( task_key is not None ), f"Multitask model must specify the inference task! Supported tasks are {list(self.model.keys())}." model_pred = self.model[task_key]( - coord, atype, box=box, do_atomic_virial=do_atomic_virial + coord, + atype, + box=box, + do_atomic_virial=do_atomic_virial, + fparam=fparam, + aparam=aparam, ) natoms = atype.shape[-1] if not self.inference_only and not inference_only: diff --git a/source/tests/infer/fparam_aparam.pbtxt b/source/tests/infer/fparam_aparam.pbtxt index a89596961e..8c2e884090 100644 --- a/source/tests/infer/fparam_aparam.pbtxt +++ b/source/tests/infer/fparam_aparam.pbtxt @@ -35,7 +35,7 @@ node { dtype: DT_STRING tensor_shape { } - string_val: "{\"model\":{\"data_stat_nbatch\":1,\"descriptor\":{\"type\":\"se_e2_a\",\"sel\":[60],\"rcut_smth\":1.8,\"rcut\":6.0,\"neuron\":[5,10,20],\"resnet_dt\":false,\"axis_neuron\":8,\"seed\":1,\"activation_function\":\"tanh\",\"type_one_side\":false,\"precision\":\"default\",\"trainable\":true,\"exclude_types\":[],\"set_davg_zero\":false},\"fitting_net\":{\"neuron\":[5,5,5],\"resnet_dt\":true,\"numb_fparam\":1,\"numb_aparam\":1,\"seed\":1,\"type\":\"ener\",\"activation_function\":\"tanh\",\"precision\":\"default\",\"trainable\":true,\"rcond\":0.001,\"atom_ener\":[],\"use_aparam_as_mask\":false},\"data_stat_protect\":0.01,\"data_bias_nsample\":10},\"loss\":{\"start_pref_e\":0.02,\"limit_pref_e\":1,\"start_pref_f\":1000,\"limit_pref_f\":1,\"start_pref_v\":0,\"limit_pref_v\":0,\"type\":\"ener\",\"start_pref_ae\":0.0,\"limit_pref_ae\":0.0,\"start_pref_pf\":0.0,\"limit_pref_pf\":0.0,\"enable_atom_ener_coeff\":false},\"learning_rate\":{\"start_lr\":0.001,\"stop_lr\":3e-08,\"decay_steps\":5000,\"scale_by_worker\":\"linear\",\"type\":\"exp\"},\"training\":{\"training_data\":{\"systems\":[\"../data/e3000_i2000/\",\"../data/e8000_i2000/\"],\"set_prefix\":\"set\",\"batch_size\":1,\"auto_prob\":\"prob_sys_size\",\"sys_probs\":null},\"seed\":1,\"disp_file\":\"lcurve.out\",\"disp_freq\":100,\"save_freq\":1000,\"save_ckpt\":\"model.ckpt\",\"disp_training\":true,\"time_training\":true,\"profiling\":false,\"profiling_file\":\"timeline.json\",\"numb_steps\":1000,\"validation_data\":null,\"enable_profiler\":false,\"tensorboard\":false,\"tensorboard_log_dir\":\"log\",\"tensorboard_freq\":1}}" + string_val: "{\"model\":{\"data_stat_nbatch\":1,\"type_map\":[\"O\"],\"descriptor\":{\"type\":\"se_e2_a\",\"sel\":[60],\"rcut_smth\":1.8,\"rcut\":6.0,\"neuron\":[5,10,20],\"resnet_dt\":false,\"axis_neuron\":8,\"seed\":1,\"activation_function\":\"tanh\",\"type_one_side\":false,\"precision\":\"default\",\"trainable\":true,\"exclude_types\":[],\"set_davg_zero\":false},\"fitting_net\":{\"neuron\":[5,5,5],\"resnet_dt\":true,\"numb_fparam\":1,\"numb_aparam\":1,\"seed\":1,\"type\":\"ener\",\"activation_function\":\"tanh\",\"precision\":\"default\",\"trainable\":true,\"rcond\":0.001,\"atom_ener\":[],\"use_aparam_as_mask\":false},\"data_stat_protect\":0.01,\"data_bias_nsample\":10},\"loss\":{\"start_pref_e\":0.02,\"limit_pref_e\":1,\"start_pref_f\":1000,\"limit_pref_f\":1,\"start_pref_v\":0,\"limit_pref_v\":0,\"type\":\"ener\",\"start_pref_ae\":0.0,\"limit_pref_ae\":0.0,\"start_pref_pf\":0.0,\"limit_pref_pf\":0.0,\"enable_atom_ener_coeff\":false},\"learning_rate\":{\"start_lr\":0.001,\"stop_lr\":3e-08,\"decay_steps\":5000,\"scale_by_worker\":\"linear\",\"type\":\"exp\"},\"training\":{\"training_data\":{\"systems\":[\"../data/e3000_i2000/\",\"../data/e8000_i2000/\"],\"set_prefix\":\"set\",\"batch_size\":1,\"auto_prob\":\"prob_sys_size\",\"sys_probs\":null},\"seed\":1,\"disp_file\":\"lcurve.out\",\"disp_freq\":100,\"save_freq\":1000,\"save_ckpt\":\"model.ckpt\",\"disp_training\":true,\"time_training\":true,\"profiling\":false,\"profiling_file\":\"timeline.json\",\"numb_steps\":1000,\"validation_data\":null,\"enable_profiler\":false,\"tensorboard\":false,\"tensorboard_log_dir\":\"log\",\"tensorboard_freq\":1}}" } } } diff --git a/source/tests/infer/fparam_aparam.pth b/source/tests/infer/fparam_aparam.pth new file mode 100644 index 0000000000000000000000000000000000000000..7b0204cdd3abbb1ac39e729ca0ed902d814ca6a1 GIT binary patch literal 105871 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 z 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 n{~Tz%mAe>YX%u`86Cq6-{ Date: Thu, 29 Feb 2024 10:03:35 +0800 Subject: [PATCH 141/686] fix: cast input and output types at model's interface (#3352) Co-authored-by: Han Wang --- deepmd/dpmodel/common.py | 20 ++- deepmd/dpmodel/model/make_model.py | 112 +++++++++++++++-- deepmd/dpmodel/model/transform_output.py | 8 +- deepmd/pt/model/model/make_model.py | 117 +++++++++++++++--- deepmd/pt/model/model/transform_output.py | 13 +- deepmd/pt/utils/env.py | 4 + deepmd/pt/utils/exclude_mask.py | 14 +++ .../dpmodel/case_single_frame_with_nlist.py | 23 ++++ source/tests/common/dpmodel/test_dp_model.py | 99 ++++++++++++++- source/tests/pt/model/test_dp_model.py | 96 ++++++++++++++ 10 files changed, 475 insertions(+), 31 deletions(-) diff --git a/deepmd/dpmodel/common.py b/deepmd/dpmodel/common.py index 982a4eb834..761db2f6aa 100644 --- a/deepmd/dpmodel/common.py +++ b/deepmd/dpmodel/common.py @@ -6,7 +6,8 @@ import numpy as np -from deepmd.common import ( +from deepmd.env import ( + GLOBAL_ENER_FLOAT_PRECISION, GLOBAL_NP_FLOAT_PRECISION, ) @@ -21,6 +22,13 @@ "int64": np.int64, "default": GLOBAL_NP_FLOAT_PRECISION, } +RESERVED_PRECISON_DICT = { + np.float16: "float16", + np.float32: "float32", + np.float64: "float64", + np.int32: "int32", + np.int64: "int64", +} DEFAULT_PRECISION = "float64" @@ -35,3 +43,13 @@ def call(self, *args, **kwargs): def __call__(self, *args, **kwargs): """Forward pass in NumPy implementation.""" return self.call(*args, **kwargs) + + +__all__ = [ + "GLOBAL_NP_FLOAT_PRECISION", + "GLOBAL_ENER_FLOAT_PRECISION", + "PRECISION_DICT", + "RESERVED_PRECISON_DICT", + "DEFAULT_PRECISION", + "NativeOP", +] diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py index 7928644061..1261906148 100644 --- a/deepmd/dpmodel/model/make_model.py +++ b/deepmd/dpmodel/model/make_model.py @@ -1,17 +1,25 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( Dict, + List, Optional, + Tuple, ) import numpy as np from deepmd.dpmodel.common import ( + GLOBAL_ENER_FLOAT_PRECISION, + GLOBAL_NP_FLOAT_PRECISION, + PRECISION_DICT, + RESERVED_PRECISON_DICT, NativeOP, ) from deepmd.dpmodel.output_def import ( ModelOutputDef, OutputVariableCategory, + OutputVariableOperation, + check_operation_applied, ) from deepmd.dpmodel.utils import ( build_neighbor_list, @@ -59,6 +67,10 @@ def __init__( *args, **kwargs, ) + self.precision_dict = PRECISION_DICT + self.reverse_precision_dict = RESERVED_PRECISON_DICT + self.global_np_float_precision = GLOBAL_NP_FLOAT_PRECISION + self.global_ener_float_precision = GLOBAL_ENER_FLOAT_PRECISION def model_output_def(self): """Get the output def for the model.""" @@ -115,15 +127,19 @@ def call( """ nframes, nloc = atype.shape[:2] - if box is not None: + cc, bb, fp, ap, input_prec = self.input_type_cast( + coord, box=box, fparam=fparam, aparam=aparam + ) + del coord, box, fparam, aparam + if bb is not None: coord_normalized = normalize_coord( - coord.reshape(nframes, nloc, 3), - box.reshape(nframes, 3, 3), + cc.reshape(nframes, nloc, 3), + bb.reshape(nframes, 3, 3), ) else: - coord_normalized = coord.copy() + coord_normalized = cc.copy() extended_coord, extended_atype, mapping = extend_coord_with_ghosts( - coord_normalized, atype, box, self.get_rcut() + coord_normalized, atype, bb, self.get_rcut() ) nlist = build_neighbor_list( extended_coord, @@ -139,8 +155,8 @@ def call( extended_atype, nlist, mapping, - fparam=fparam, - aparam=aparam, + fparam=fp, + aparam=ap, do_atomic_virial=do_atomic_virial, ) model_predict = communicate_extended_output( @@ -149,6 +165,7 @@ def call( mapping, do_atomic_virial=do_atomic_virial, ) + model_predict = self.output_type_cast(model_predict, input_prec) return model_predict def call_lower( @@ -192,22 +209,95 @@ def call_lower( nframes, nall = extended_atype.shape[:2] extended_coord = extended_coord.reshape(nframes, -1, 3) nlist = self.format_nlist(extended_coord, extended_atype, nlist) + cc_ext, _, fp, ap, input_prec = self.input_type_cast( + extended_coord, fparam=fparam, aparam=aparam + ) + del extended_coord, fparam, aparam atomic_ret = self.forward_atomic( - extended_coord, + cc_ext, extended_atype, nlist, mapping=mapping, - fparam=fparam, - aparam=aparam, + fparam=fp, + aparam=ap, ) model_predict = fit_output_to_model_output( atomic_ret, self.fitting_output_def(), - extended_coord, + cc_ext, do_atomic_virial=do_atomic_virial, ) + model_predict = self.output_type_cast(model_predict, input_prec) return model_predict + def input_type_cast( + self, + coord: np.ndarray, + box: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + ) -> Tuple[ + np.ndarray, + Optional[np.ndarray], + Optional[np.ndarray], + Optional[np.ndarray], + str, + ]: + """Cast the input data to global float type.""" + input_prec = self.reverse_precision_dict[ + self.precision_dict[coord.dtype.name] + ] + ### + ### type checking would not pass jit, convert to coord prec anyway + ### + _lst: List[Optional[np.ndarray]] = [ + vv.astype(coord.dtype) if vv is not None else None + for vv in [box, fparam, aparam] + ] + box, fparam, aparam = _lst + if ( + input_prec + == self.reverse_precision_dict[self.global_np_float_precision] + ): + return coord, box, fparam, aparam, input_prec + else: + pp = self.global_np_float_precision + return ( + coord.astype(pp), + box.astype(pp) if box is not None else None, + fparam.astype(pp) if fparam is not None else None, + aparam.astype(pp) if aparam is not None else None, + input_prec, + ) + + def output_type_cast( + self, + model_ret: Dict[str, np.ndarray], + input_prec: str, + ) -> Dict[str, np.ndarray]: + """Convert the model output to the input prec.""" + do_cast = ( + input_prec + != self.reverse_precision_dict[self.global_np_float_precision] + ) + pp = self.precision_dict[input_prec] + odef = self.model_output_def() + for kk in odef.keys(): + if kk not in model_ret.keys(): + # do not return energy_derv_c if not do_atomic_virial + continue + if check_operation_applied(odef[kk], OutputVariableOperation.REDU): + model_ret[kk] = ( + model_ret[kk].astype(self.global_ener_float_precision) + if model_ret[kk] is not None + else None + ) + elif do_cast: + model_ret[kk] = ( + model_ret[kk].astype(pp) if model_ret[kk] is not None else None + ) + return model_ret + def format_nlist( self, extended_coord: np.ndarray, diff --git a/deepmd/dpmodel/model/transform_output.py b/deepmd/dpmodel/model/transform_output.py index 49368849ca..c87c79f7d4 100644 --- a/deepmd/dpmodel/model/transform_output.py +++ b/deepmd/dpmodel/model/transform_output.py @@ -5,6 +5,9 @@ import numpy as np +from deepmd.dpmodel.common import ( + GLOBAL_ENER_FLOAT_PRECISION, +) from deepmd.dpmodel.output_def import ( FittingOutputDef, ModelOutputDef, @@ -30,7 +33,10 @@ def fit_output_to_model_output( atom_axis = -(len(shap) + 1) if vdef.reduciable: kk_redu = get_reduce_name(kk) - model_ret[kk_redu] = np.sum(vv, axis=atom_axis) + # cast to energy prec brefore reduction + model_ret[kk_redu] = np.sum( + vv.astype(GLOBAL_ENER_FLOAT_PRECISION), axis=atom_axis + ) if vdef.r_differentiable: kk_derv_r, kk_derv_c = get_deriv_name(kk) # name-holders diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index b6478d297f..3efd3fb046 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -3,6 +3,7 @@ Dict, List, Optional, + Tuple, ) import torch @@ -12,13 +13,18 @@ ) from deepmd.dpmodel.output_def import ( OutputVariableCategory, + OutputVariableOperation, + check_operation_applied, ) from deepmd.pt.model.model.transform_output import ( communicate_extended_output, fit_output_to_model_output, ) -from deepmd.pt.utils import ( - env, +from deepmd.pt.utils.env import ( + GLOBAL_PT_ENER_FLOAT_PRECISION, + GLOBAL_PT_FLOAT_PRECISION, + PRECISION_DICT, + RESERVED_PRECISON_DICT, ) from deepmd.pt.utils.nlist import ( extend_input_and_build_neighbor_list, @@ -59,6 +65,10 @@ def __init__( *args, **kwargs, ) + self.precision_dict = PRECISION_DICT + self.reverse_precision_dict = RESERVED_PRECISON_DICT + self.global_pt_float_precision = GLOBAL_PT_FLOAT_PRECISION + self.global_pt_ener_float_precision = GLOBAL_PT_ENER_FLOAT_PRECISION def model_output_def(self): """Get the output def for the model.""" @@ -118,21 +128,22 @@ def forward_common( The keys are defined by the `ModelOutputDef`. """ - coord = coord.to(env.GLOBAL_PT_FLOAT_PRECISION) - if box is not None: - box = box.to(env.GLOBAL_PT_FLOAT_PRECISION) + cc, bb, fp, ap, input_prec = self.input_type_cast( + coord, box=box, fparam=fparam, aparam=aparam + ) + del coord, box, fparam, aparam ( extended_coord, extended_atype, mapping, nlist, ) = extend_input_and_build_neighbor_list( - coord, + cc, atype, self.get_rcut(), self.get_sel(), mixed_types=self.mixed_types(), - box=box, + box=bb, ) model_predict_lower = self.forward_common_lower( extended_coord, @@ -140,8 +151,8 @@ def forward_common( nlist, mapping, do_atomic_virial=do_atomic_virial, - fparam=fparam, - aparam=aparam, + fparam=fp, + aparam=ap, ) model_predict = communicate_extended_output( model_predict_lower, @@ -149,6 +160,7 @@ def forward_common( mapping, do_atomic_virial=do_atomic_virial, ) + model_predict = self.output_type_cast(model_predict, input_prec) return model_predict def forward_common_lower( @@ -189,26 +201,103 @@ def forward_common_lower( the result dict, defined by the `FittingOutputDef`. """ - extended_coord = extended_coord.to(env.GLOBAL_PT_FLOAT_PRECISION) nframes, nall = extended_atype.shape[:2] extended_coord = extended_coord.view(nframes, -1, 3) nlist = self.format_nlist(extended_coord, extended_atype, nlist) + cc_ext, _, fp, ap, input_prec = self.input_type_cast( + extended_coord, fparam=fparam, aparam=aparam + ) + del extended_coord, fparam, aparam atomic_ret = self.forward_atomic( - extended_coord, + cc_ext, extended_atype, nlist, mapping=mapping, - fparam=fparam, - aparam=aparam, + fparam=fp, + aparam=ap, ) model_predict = fit_output_to_model_output( atomic_ret, self.fitting_output_def(), - extended_coord, + cc_ext, do_atomic_virial=do_atomic_virial, ) + model_predict = self.output_type_cast(model_predict, input_prec) return model_predict + def input_type_cast( + self, + coord: torch.Tensor, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + ) -> Tuple[ + torch.Tensor, + Optional[torch.Tensor], + Optional[torch.Tensor], + Optional[torch.Tensor], + str, + ]: + """Cast the input data to global float type.""" + input_prec = self.reverse_precision_dict[coord.dtype] + ### + ### type checking would not pass jit, convert to coord prec anyway + ### + # for vv, kk in zip([fparam, aparam], ["frame", "atomic"]): + # if vv is not None and self.reverse_precision_dict[vv.dtype] != input_prec: + # log.warning( + # f"type of {kk} parameter {self.reverse_precision_dict[vv.dtype]}" + # " does not match" + # f" that of the coordinate {input_prec}" + # ) + _lst: List[Optional[torch.Tensor]] = [ + vv.to(coord.dtype) if vv is not None else None + for vv in [box, fparam, aparam] + ] + box, fparam, aparam = _lst + if ( + input_prec + == self.reverse_precision_dict[self.global_pt_float_precision] + ): + return coord, box, fparam, aparam, input_prec + else: + pp = self.global_pt_float_precision + return ( + coord.to(pp), + box.to(pp) if box is not None else None, + fparam.to(pp) if fparam is not None else None, + aparam.to(pp) if aparam is not None else None, + input_prec, + ) + + def output_type_cast( + self, + model_ret: Dict[str, torch.Tensor], + input_prec: str, + ) -> Dict[str, torch.Tensor]: + """Convert the model output to the input prec.""" + do_cast = ( + input_prec + != self.reverse_precision_dict[self.global_pt_float_precision] + ) + pp = self.precision_dict[input_prec] + odef = self.model_output_def() + for kk in odef.keys(): + if kk not in model_ret.keys(): + # do not return energy_derv_c if not do_atomic_virial + continue + if check_operation_applied(odef[kk], OutputVariableOperation.REDU): + model_ret[kk] = ( + model_ret[kk].to(self.global_pt_ener_float_precision) + if model_ret[kk] is not None + else None + ) + elif do_cast: + model_ret[kk] = ( + model_ret[kk].to(pp) if model_ret[kk] is not None else None + ) + return model_ret + @torch.jit.export def format_nlist( self, diff --git a/deepmd/pt/model/model/transform_output.py b/deepmd/pt/model/model/transform_output.py index 312bb952b5..730e6b29d0 100644 --- a/deepmd/pt/model/model/transform_output.py +++ b/deepmd/pt/model/model/transform_output.py @@ -14,6 +14,9 @@ get_deriv_name, get_reduce_name, ) +from deepmd.pt.utils import ( + env, +) def atomic_virial_corr( @@ -148,6 +151,7 @@ def fit_output_to_model_output( the model output. """ + redu_prec = env.GLOBAL_PT_ENER_FLOAT_PRECISION model_ret = dict(fit_ret.items()) for kk, vv in fit_ret.items(): vdef = fit_output_def[kk] @@ -155,7 +159,7 @@ def fit_output_to_model_output( atom_axis = -(len(shap) + 1) if vdef.reduciable: kk_redu = get_reduce_name(kk) - model_ret[kk_redu] = torch.sum(vv, dim=atom_axis) + model_ret[kk_redu] = torch.sum(vv.to(redu_prec), dim=atom_axis) if vdef.r_differentiable: kk_derv_r, kk_derv_c = get_deriv_name(kk) dr, dc = take_deriv( @@ -171,7 +175,7 @@ def fit_output_to_model_output( assert dc is not None model_ret[kk_derv_c] = dc model_ret[kk_derv_c + "_redu"] = torch.sum( - model_ret[kk_derv_c], dim=1 + model_ret[kk_derv_c].to(redu_prec), dim=1 ) return model_ret @@ -186,6 +190,7 @@ def communicate_extended_output( local and ghost (extended) atoms to local atoms. """ + redu_prec = env.GLOBAL_PT_ENER_FLOAT_PRECISION new_ret = {} for kk in model_output_def.keys_outp(): vv = model_ret[kk] @@ -235,7 +240,9 @@ def communicate_extended_output( src=model_ret[kk_derv_c], reduce="sum", ) - new_ret[kk_derv_c + "_redu"] = torch.sum(new_ret[kk_derv_c], dim=1) + new_ret[kk_derv_c + "_redu"] = torch.sum( + new_ret[kk_derv_c].to(redu_prec), dim=1 + ) if not do_atomic_virial: # pop atomic virial, because it is not correctly calculated. new_ret.pop(kk_derv_c) diff --git a/deepmd/pt/utils/env.py b/deepmd/pt/utils/env.py index 7383cf5c49..0b92953255 100644 --- a/deepmd/pt/utils/env.py +++ b/deepmd/pt/utils/env.py @@ -42,6 +42,9 @@ "int64": torch.int64, } GLOBAL_PT_FLOAT_PRECISION = PRECISION_DICT[np.dtype(GLOBAL_NP_FLOAT_PRECISION).name] +GLOBAL_PT_ENER_FLOAT_PRECISION = PRECISION_DICT[ + np.dtype(GLOBAL_ENER_FLOAT_PRECISION).name +] PRECISION_DICT["default"] = GLOBAL_PT_FLOAT_PRECISION # cannot automatically generated RESERVED_PRECISON_DICT = { @@ -65,6 +68,7 @@ "GLOBAL_ENER_FLOAT_PRECISION", "GLOBAL_NP_FLOAT_PRECISION", "GLOBAL_PT_FLOAT_PRECISION", + "GLOBAL_PT_ENER_FLOAT_PRECISION", "DEFAULT_PRECISION", "PRECISION_DICT", "RESERVED_PRECISON_DICT", diff --git a/deepmd/pt/utils/exclude_mask.py b/deepmd/pt/utils/exclude_mask.py index 74b1d8dc41..6df8df8dd0 100644 --- a/deepmd/pt/utils/exclude_mask.py +++ b/deepmd/pt/utils/exclude_mask.py @@ -22,6 +22,13 @@ def __init__( exclude_types: List[int] = [], ): super().__init__() + self.reinit(ntypes, exclude_types) + + def reinit( + self, + ntypes: int, + exclude_types: List[int] = [], + ): self.ntypes = ntypes self.exclude_types = exclude_types self.type_mask = np.array( @@ -62,6 +69,13 @@ def __init__( exclude_types: List[Tuple[int, int]] = [], ): super().__init__() + self.reinit(ntypes, exclude_types) + + def reinit( + self, + ntypes: int, + exclude_types: List[Tuple[int, int]] = [], + ): self.ntypes = ntypes self._exclude_types: Set[Tuple[int, int]] = set() for tt in exclude_types: diff --git a/source/tests/common/dpmodel/case_single_frame_with_nlist.py b/source/tests/common/dpmodel/case_single_frame_with_nlist.py index df4f73efbd..ecdf3590a8 100644 --- a/source/tests/common/dpmodel/case_single_frame_with_nlist.py +++ b/source/tests/common/dpmodel/case_single_frame_with_nlist.py @@ -2,6 +2,27 @@ import numpy as np +class TestCaseSingleFrameWithoutNlist: + def setUp(self): + # nloc == 3, nall == 4 + self.nloc = 3 + self.nf, self.nt = 1, 2 + self.coord = np.array( + [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 1], + ], + dtype=np.float64, + ).reshape([1, self.nloc * 3]) + self.atype = np.array([0, 0, 1], dtype=int).reshape([1, self.nloc]) + self.cell = 2.0 * np.eye(3).reshape([1, 9]) + # sel = [5, 2] + self.sel = [5, 2] + self.rcut = 0.4 + self.rcut_smth = 2.2 + + class TestCaseSingleFrameWithNlist: def setUp(self): # nloc == 3, nall == 4 @@ -17,7 +38,9 @@ def setUp(self): ], dtype=np.float64, ).reshape([1, self.nall, 3]) + self.coord = self.coord_ext[:, : self.nloc, :] self.atype_ext = np.array([0, 0, 1, 0], dtype=int).reshape([1, self.nall]) + self.atype = self.atype_ext[:, : self.nloc] # sel = [5, 2] self.sel = [5, 2] self.nlist = np.array( diff --git a/source/tests/common/dpmodel/test_dp_model.py b/source/tests/common/dpmodel/test_dp_model.py index b982c9c2b5..c3de1f4cdf 100644 --- a/source/tests/common/dpmodel/test_dp_model.py +++ b/source/tests/common/dpmodel/test_dp_model.py @@ -15,10 +15,11 @@ from .case_single_frame_with_nlist import ( TestCaseSingleFrameWithNlist, + TestCaseSingleFrameWithoutNlist, ) -class TestDPModel(unittest.TestCase, TestCaseSingleFrameWithNlist): +class TestDPModelLower(unittest.TestCase, TestCaseSingleFrameWithNlist): def setUp(self): TestCaseSingleFrameWithNlist.setUp(self) @@ -47,3 +48,99 @@ def test_self_consistency( np.testing.assert_allclose(ret0["energy"], ret1["energy"]) np.testing.assert_allclose(ret0["energy_redu"], ret1["energy_redu"]) + + def test_prec_consistency(self): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + mixed_types=ds.mixed_types(), + ) + nfp, nap = 2, 3 + type_map = ["foo", "bar"] + # fparam, aparam are converted to coordinate precision by model + fparam = rng.normal(size=[self.nf, nfp]) + aparam = rng.normal(size=[self.nf, nloc, nap]) + + md1 = DPModel(ds, ft, type_map=type_map) + + args64 = [self.coord_ext, self.atype_ext, self.nlist] + args64[0] = args64[0].astype(np.float64) + args32 = [self.coord_ext, self.atype_ext, self.nlist] + args32[0] = args32[0].astype(np.float32) + + model_l_ret_64 = md1.call_lower(*args64, fparam=fparam, aparam=aparam) + model_l_ret_32 = md1.call_lower(*args32, fparam=fparam, aparam=aparam) + + for ii in model_l_ret_32.keys(): + if model_l_ret_32[ii] is None: + continue + if ii[-4:] == "redu": + self.assertEqual(model_l_ret_32[ii].dtype, np.float64) + else: + self.assertEqual(model_l_ret_32[ii].dtype, np.float32) + self.assertEqual(model_l_ret_64[ii].dtype, np.float64) + np.testing.assert_allclose( + model_l_ret_32[ii], + model_l_ret_64[ii], + ) + + +class TestDPModel(unittest.TestCase, TestCaseSingleFrameWithoutNlist): + def setUp(self): + TestCaseSingleFrameWithoutNlist.setUp(self) + + def test_prec_consistency(self): + rng = np.random.default_rng() + nf, nloc = self.atype.shape + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + mixed_types=ds.mixed_types(), + ) + nfp, nap = 2, 3 + type_map = ["foo", "bar"] + # fparam, aparam are converted to coordinate precision by model + fparam = rng.normal(size=[self.nf, nfp]) + aparam = rng.normal(size=[self.nf, nloc, nap]) + + md1 = DPModel(ds, ft, type_map=type_map) + + args64 = [self.coord, self.atype, self.cell] + args64[0] = args64[0].astype(np.float64) + args64[2] = args64[2].astype(np.float64) + args32 = [self.coord, self.atype, self.cell] + args32[0] = args32[0].astype(np.float32) + args32[2] = args32[2].astype(np.float32) + + model_l_ret_64 = md1.call(*args64, fparam=fparam, aparam=aparam) + model_l_ret_32 = md1.call(*args32, fparam=fparam, aparam=aparam) + + for ii in model_l_ret_32.keys(): + if model_l_ret_32[ii] is None: + continue + if ii[-4:] == "redu": + self.assertEqual(model_l_ret_32[ii].dtype, np.float64) + else: + self.assertEqual(model_l_ret_32[ii].dtype, np.float32) + self.assertEqual(model_l_ret_64[ii].dtype, np.float64) + self.assertEqual(model_l_ret_64[ii].dtype, np.float64) + np.testing.assert_allclose( + model_l_ret_32[ii], + model_l_ret_64[ii], + ) diff --git a/source/tests/pt/model/test_dp_model.py b/source/tests/pt/model/test_dp_model.py index 0a16d4672c..840ba284e2 100644 --- a/source/tests/pt/model/test_dp_model.py +++ b/source/tests/pt/model/test_dp_model.py @@ -186,6 +186,53 @@ def test_dp_consistency_nopbc(self): to_numpy_array(ret1["energy_redu"]), ) + def test_prec_consistency(self): + rng = np.random.default_rng() + nf, nloc = self.atype.shape + ds = DPDescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = DPInvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + mixed_types=ds.mixed_types(), + ) + nfp, nap = 2, 3 + type_map = ["foo", "bar"] + fparam = rng.normal(size=[self.nf, nfp]) + aparam = rng.normal(size=[self.nf, nloc, nap]) + + md0 = DPDPModel(ds, ft, type_map=type_map) + md1 = DPModel.deserialize(md0.serialize()).to(env.DEVICE) + + args64 = [to_torch_tensor(ii) for ii in [self.coord, self.atype, self.cell]] + args64[0] = args64[0].to(torch.float64) + args64[2] = args64[2].to(torch.float64) + args32 = [to_torch_tensor(ii) for ii in [self.coord, self.atype, self.cell]] + args32[0] = args32[0].to(torch.float32) + args32[2] = args32[2].to(torch.float32) + # fparam, aparam are converted to coordinate precision by model + fparam = to_torch_tensor(fparam) + aparam = to_torch_tensor(aparam) + + model_l_ret_64 = md1.forward_common(*args64, fparam=fparam, aparam=aparam) + model_l_ret_32 = md1.forward_common(*args32, fparam=fparam, aparam=aparam) + + for ii in model_l_ret_32.keys(): + if ii[-4:] == "redu": + self.assertEqual(model_l_ret_32[ii].dtype, torch.float64) + else: + self.assertEqual(model_l_ret_32[ii].dtype, torch.float32) + self.assertEqual(model_l_ret_64[ii].dtype, torch.float64) + np.testing.assert_allclose( + to_numpy_array(model_l_ret_32[ii]), + to_numpy_array(model_l_ret_64[ii]), + ) + class TestDPModelLower(unittest.TestCase, TestCaseSingleFrameWithNlist): def setUp(self): @@ -269,6 +316,55 @@ def test_dp_consistency(self): to_numpy_array(ret1["energy_redu"]), ) + def test_prec_consistency(self): + rng = np.random.default_rng() + nf, nloc, nnei = self.nlist.shape + ds = DPDescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = DPInvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + mixed_types=ds.mixed_types(), + ) + nfp, nap = 2, 3 + type_map = ["foo", "bar"] + fparam = rng.normal(size=[self.nf, nfp]) + aparam = rng.normal(size=[self.nf, nloc, nap]) + + md0 = DPDPModel(ds, ft, type_map=type_map) + md1 = DPModel.deserialize(md0.serialize()).to(env.DEVICE) + + args64 = [ + to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] + ] + args64[0] = args64[0].to(torch.float64) + args32 = [ + to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] + ] + args32[0] = args32[0].to(torch.float32) + # fparam, aparam are converted to coordinate precision by model + fparam = to_torch_tensor(fparam) + aparam = to_torch_tensor(aparam) + + model_l_ret_64 = md1.forward_common_lower(*args64, fparam=fparam, aparam=aparam) + model_l_ret_32 = md1.forward_common_lower(*args32, fparam=fparam, aparam=aparam) + + for ii in model_l_ret_32.keys(): + if ii[-4:] == "redu": + self.assertEqual(model_l_ret_32[ii].dtype, torch.float64) + else: + self.assertEqual(model_l_ret_32[ii].dtype, torch.float32) + self.assertEqual(model_l_ret_64[ii].dtype, torch.float64) + np.testing.assert_allclose( + to_numpy_array(model_l_ret_32[ii]), + to_numpy_array(model_l_ret_64[ii]), + ) + def test_jit(self): nf, nloc, nnei = self.nlist.shape ds = DescrptSeA( From 48c88180356d881cff00203e590cb62063fb25ef Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 28 Feb 2024 21:32:51 -0500 Subject: [PATCH 142/686] pt: fix se_a type_one_side performance degradation (#3361) The code in this PR is ugly, but applying a mask is causing performance degradation for ~3 ms/step. When applying a mask, `aten::nonzero` has a high host time, as it causes host-device synchronization: ![image](https://github.com/deepmodeling/deepmd-kit/assets/9496702/86b3518c-206d-410d-928e-2f605746147c) After fixing: ![image](https://github.com/deepmodeling/deepmd-kit/assets/9496702/af9e86fa-7908-4bbb-ace7-58b4602e167f) See https://github.com/pytorch/pytorch/issues/12461 for more information. Signed-off-by: Jinzhe Zeng --- deepmd/pt/model/descriptor/se_a.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 6c29636d6d..8a211c977d 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -472,23 +472,34 @@ def forward( if self.type_one_side: ii = embedding_idx # torch.jit is not happy with slice(None) - ti_mask = torch.ones(nfnl, dtype=torch.bool, device=dmatrix.device) + # ti_mask = torch.ones(nfnl, dtype=torch.bool, device=dmatrix.device) + # applying a mask seems to cause performance degradation + ti_mask = None else: # ti: center atom type, ii: neighbor type... ii = embedding_idx // self.ntypes ti = embedding_idx % self.ntypes ti_mask = atype.ravel().eq(ti) # nfnl x nt - mm = exclude_mask[ti_mask, self.sec[ii] : self.sec[ii + 1]] + if ti_mask is not None: + mm = exclude_mask[ti_mask, self.sec[ii] : self.sec[ii + 1]] + else: + mm = exclude_mask[:, self.sec[ii] : self.sec[ii + 1]] # nfnl x nt x 4 - rr = dmatrix[ti_mask, self.sec[ii] : self.sec[ii + 1], :] + if ti_mask is not None: + rr = dmatrix[ti_mask, self.sec[ii] : self.sec[ii + 1], :] + else: + rr = dmatrix[:, self.sec[ii] : self.sec[ii + 1], :] rr = rr * mm[:, :, None] ss = rr[:, :, :1] # nfnl x nt x ng gg = ll.forward(ss) # nfnl x 4 x ng gr = torch.matmul(rr.permute(0, 2, 1), gg) - xyz_scatter[ti_mask] += gr + if ti_mask is not None: + xyz_scatter[ti_mask] += gr + else: + xyz_scatter += gr xyz_scatter /= self.nnei xyz_scatter_1 = xyz_scatter.permute(0, 2, 1) From 581dea3dc4940ea6a486d2b35c59b4631c4ab27b Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 28 Feb 2024 21:33:22 -0500 Subject: [PATCH 143/686] docs: dpmodel, model conversion (#3360) Signed-off-by: Jinzhe Zeng --- doc/_static/logo_icon.svg | 1 + doc/backend.md | 29 +++++++++++++++++++++++++++-- doc/conf.py | 1 + doc/model/train-se-e2-a.md | 4 ++-- doc/model/train-se-e2-r.md | 4 ++-- 5 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 doc/_static/logo_icon.svg diff --git a/doc/_static/logo_icon.svg b/doc/_static/logo_icon.svg new file mode 100644 index 0000000000..d8f6893355 --- /dev/null +++ b/doc/_static/logo_icon.svg @@ -0,0 +1 @@ + diff --git a/doc/backend.md b/doc/backend.md index 41a0b4d2c8..8c720ac1c1 100644 --- a/doc/backend.md +++ b/doc/backend.md @@ -9,12 +9,33 @@ In the documentation, TensorFlow {{ tensorflow_icon }} and PyTorch {{ pytorch_ic ### TensorFlow {{ tensorflow_icon }} -TensorFlow 2.2 or above is required. +- Model filename extension: `.pb` +- Checkpoint filename extension: `.meta`, `.index`, `.data-00000-of-00001` + +[TensorFlow](https://tensorflow.org) 2.2 or above is required. DeePMD-kit does not use the TensorFlow v2 API but uses the TensorFlow v1 API (`tf.compat.v1`) in the graph mode. ### PyTorch {{ pytorch_icon }} -PyTorch 2.0 or above is required. +- Model filename extension: `.pth` +- Checkpoint filename extension: `.pt` + +[PyTorch](https://pytorch.org/) 2.0 or above is required. +While `.pth` and `.pt` are the same in the PyTorch package, they have different meanings in the DeePMD-kit to distinguish the model and the checkpoint. + +### DPModel {{ dpmodel_icon }} + +:::{note} +This backend is only for development and should not take into production. +::: + +- Model filename extension: `.dp` + +DPModel is a reference backend for development, which uses pure [NumPy](https://numpy.org/) to implement models without using any heavy deep-learning frameworks. +Due to the limitation of NumPy, it doesn't support gradient calculation and thus cannot be used for training. +As a reference backend, it is not aimed at the best performance, but only the correct results. +The DPModel backend uses [HDF5](https://docs.h5py.org/) to store model serialization data, which is backend-independent. +Only Python inference interface can load this format. ## Switch the backend @@ -26,3 +47,7 @@ When training and freezing a model, you can use `dp --tf` or `dp --pt` in the co When doing inference, DeePMD-kit detects the backend from the model filename. For example, when the model filename ends with `.pb` (the ProtoBuf file), DeePMD-kit will consider it using the TensorFlow backend. + +## Convert model files between backends + +If a model is supported by two backends, one can use [`dp convert-backend`](./cli.rst) to convert the model file between these two backends. diff --git a/doc/conf.py b/doc/conf.py index 3687695b36..c959a9e30e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -188,6 +188,7 @@ def setup(app): myst_substitutions = { "tensorflow_icon": """![TensorFlow](/_static/tensorflow.svg){class=platform-icon}""", "pytorch_icon": """![PyTorch](/_static/pytorch.svg){class=platform-icon}""", + "dpmodel_icon": """![DPModel](/_static/logo_icon.svg){class=platform-icon}""", } # -- Options for HTML output ------------------------------------------------- diff --git a/doc/model/train-se-e2-a.md b/doc/model/train-se-e2-a.md index 22e5c20cb9..6a5c59682d 100644 --- a/doc/model/train-se-e2-a.md +++ b/doc/model/train-se-e2-a.md @@ -1,7 +1,7 @@ -# Descriptor `"se_e2_a"` {{ tensorflow_icon }} {{ pytorch_icon }} +# Descriptor `"se_e2_a"` {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DPModel {{ dpmodel_icon }} ::: The notation of `se_e2_a` is short for the Deep Potential Smooth Edition (DeepPot-SE) constructed from all information (both angular and radial) of atomic configurations. The `e2` stands for the embedding with two-atoms information. This descriptor was described in detail in [the DeepPot-SE paper](https://arxiv.org/abs/1805.09003). diff --git a/doc/model/train-se-e2-r.md b/doc/model/train-se-e2-r.md index c2c5fcfcd9..5d15269618 100644 --- a/doc/model/train-se-e2-r.md +++ b/doc/model/train-se-e2-r.md @@ -1,7 +1,7 @@ -# Descriptor `"se_e2_r"` {{ tensorflow_icon }} +# Descriptor `"se_e2_r"` {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DPModel {{ dpmodel_icon }} ::: The notation of `se_e2_r` is short for the Deep Potential Smooth Edition (DeepPot-SE) constructed from the radial information of atomic configurations. The `e2` stands for the embedding with two-atom information. From 17bd1ec7f81cf7687224b976e16670bcd8893dd5 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 28 Feb 2024 23:35:46 -0500 Subject: [PATCH 144/686] pt: apply argcheck to pt (#3342) Signed-off-by: Jinzhe Zeng --- deepmd/pt/entrypoints/main.py | 11 + deepmd/pt/model/descriptor/dpa1.py | 26 +- deepmd/pt/model/descriptor/dpa2.py | 4 +- deepmd/pt/model/descriptor/repformer_layer.py | 4 +- deepmd/pt/model/descriptor/repformers.py | 6 +- deepmd/pt/model/descriptor/se_atten.py | 4 +- deepmd/tf/descriptor/se_atten.py | 28 ++ deepmd/utils/argcheck.py | 441 ++++++++++++++++-- examples/water/se_atten/input_torch.json | 8 +- source/tests/common/test_examples.py | 3 + source/tests/pt/model/models/dpa1.json | 2 +- source/tests/pt/model/models/dpa2_hyb.json | 2 +- source/tests/pt/model/test_jit.py | 18 +- source/tests/pt/model/test_permutation.py | 5 +- source/tests/pt/model/water/se_atten.json | 3 +- source/tests/pt/test_training.py | 18 +- 16 files changed, 506 insertions(+), 77 deletions(-) diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index a317cea6a9..5583ee0326 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -50,6 +50,12 @@ from deepmd.pt.utils.stat import ( make_stat_input, ) +from deepmd.utils.argcheck import ( + normalize, +) +from deepmd.utils.compat import ( + update_deepmd_input, +) from deepmd.utils.path import ( DPPath, ) @@ -67,6 +73,11 @@ def get_trainer( force_load=False, init_frz_model=None, ): + # argcheck + if "model_dict" not in config.get("model", {}): + config = update_deepmd_input(config, warning=True, dump="input_v2_compat.json") + config = normalize(config) + # Initialize DDP local_rank = os.environ.get("LOCAL_RANK") if local_rank is not None: diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index b616d20cd8..6850c550fe 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -43,7 +43,7 @@ def __init__( post_ln=True, ffn=False, ffn_embed_dim=1024, - activation="tanh", + activation_function="tanh", scaling_factor=1.0, head_num=1, normalize=True, @@ -51,8 +51,30 @@ def __init__( return_rot=False, concat_output_tebd: bool = True, type: Optional[str] = None, + # not implemented + resnet_dt: bool = False, + type_one_side: bool = True, + precision: str = "default", + trainable: bool = True, + exclude_types: Optional[List[List[int]]] = None, + stripped_type_embedding: bool = False, + smooth_type_embdding: bool = False, ): super().__init__() + if resnet_dt: + raise NotImplementedError("resnet_dt is not supported.") + if not type_one_side: + raise NotImplementedError("type_one_side is not supported.") + if precision != "default" and precision != "float64": + raise NotImplementedError("precison is not supported.") + if not trainable: + raise NotImplementedError("trainable == False is not supported.") + if exclude_types is not None and exclude_types != []: + raise NotImplementedError("exclude_types is not supported.") + if stripped_type_embedding: + raise NotImplementedError("stripped_type_embedding is not supported.") + if smooth_type_embdding: + raise NotImplementedError("smooth_type_embdding is not supported.") del type self.se_atten = DescrptBlockSeAtten( rcut, @@ -71,7 +93,7 @@ def __init__( post_ln=post_ln, ffn=ffn, ffn_embed_dim=ffn_embed_dim, - activation=activation, + activation_function=activation_function, scaling_factor=scaling_factor, head_num=head_num, normalize=normalize, diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index b1df56a004..55bb77b366 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -197,7 +197,7 @@ def __init__( tebd_input_mode="concat", # tebd_input_mode='dot_residual_s', set_davg_zero=repinit_set_davg_zero, - activation=repinit_activation, + activation_function=repinit_activation, ) self.repformers = DescrptBlockRepformers( repformer_rcut, @@ -223,7 +223,7 @@ def __init__( attn2_hidden=repformer_attn2_hidden, attn2_nhead=repformer_attn2_nhead, attn2_has_gate=repformer_attn2_has_gate, - activation=repformer_activation, + activation_function=repformer_activation, update_style=repformer_update_style, set_davg_zero=repformer_set_davg_zero, smooth=True, diff --git a/deepmd/pt/model/descriptor/repformer_layer.py b/deepmd/pt/model/descriptor/repformer_layer.py index 55a2cba708..08fcb17b09 100644 --- a/deepmd/pt/model/descriptor/repformer_layer.py +++ b/deepmd/pt/model/descriptor/repformer_layer.py @@ -313,7 +313,7 @@ def __init__( attn2_hidden: int = 16, attn2_nhead: int = 4, attn2_has_gate: bool = False, - activation: str = "tanh", + activation_function: str = "tanh", update_style: str = "res_avg", set_davg_zero: bool = True, # TODO smooth: bool = True, @@ -332,7 +332,7 @@ def __init__( self.set_davg_zero = set_davg_zero self.do_bn_mode = do_bn_mode self.bn_momentum = bn_momentum - self.act = get_activation_fn(activation) + self.act = get_activation_fn(activation_function) self.update_g1_has_grrg = update_g1_has_grrg self.update_g1_has_drrd = update_g1_has_drrd self.update_g1_has_conv = update_g1_has_conv diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py index ad523bcc2d..2425139e16 100644 --- a/deepmd/pt/model/descriptor/repformers.py +++ b/deepmd/pt/model/descriptor/repformers.py @@ -76,7 +76,7 @@ def __init__( attn2_hidden: int = 16, attn2_nhead: int = 4, attn2_has_gate: bool = False, - activation: str = "tanh", + activation_function: str = "tanh", update_style: str = "res_avg", set_davg_zero: bool = True, # TODO smooth: bool = True, @@ -109,7 +109,7 @@ def __init__( self.set_davg_zero = set_davg_zero self.g1_dim = g1_dim self.g2_dim = g2_dim - self.act = get_activation_fn(activation) + self.act = get_activation_fn(activation_function) self.direct_dist = direct_dist self.add_type_ebd_to_seq = add_type_ebd_to_seq @@ -140,7 +140,7 @@ def __init__( attn2_has_gate=attn2_has_gate, attn2_hidden=attn2_hidden, attn2_nhead=attn2_nhead, - activation=activation, + activation_function=activation_function, update_style=update_style, smooth=smooth, ) diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index 0b32bd9341..a2197213ad 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -53,7 +53,7 @@ def __init__( post_ln=True, ffn=False, ffn_embed_dim=1024, - activation="tanh", + activation_function="tanh", scaling_factor=1.0, head_num=1, normalize=True, @@ -86,7 +86,7 @@ def __init__( self.post_ln = post_ln self.ffn = ffn self.ffn_embed_dim = ffn_embed_dim - self.activation = activation + self.activation = activation_function # TODO: To be fixed: precision should be given from inputs self.prec = torch.float64 self.scaling_factor = scaling_factor diff --git a/deepmd/tf/descriptor/se_atten.py b/deepmd/tf/descriptor/se_atten.py index 1c3c48e484..35b354c8da 100644 --- a/deepmd/tf/descriptor/se_atten.py +++ b/deepmd/tf/descriptor/se_atten.py @@ -152,6 +152,16 @@ def __init__( multi_task: bool = False, stripped_type_embedding: bool = False, smooth_type_embdding: bool = False, + # not implemented + post_ln=True, + ffn=False, + ffn_embed_dim=1024, + scaling_factor=1.0, + head_num=1, + normalize=True, + temperature=None, + return_rot=False, + concat_output_tebd: bool = True, **kwargs, ) -> None: if not set_davg_zero and not (stripped_type_embedding and smooth_type_embdding): @@ -159,6 +169,24 @@ def __init__( "Set 'set_davg_zero' False in descriptor 'se_atten' " "may cause unexpected incontinuity during model inference!" ) + if not post_ln: + raise NotImplementedError("post_ln is not supported.") + if ffn: + raise NotImplementedError("ffn is not supported.") + if ffn_embed_dim != 1024: + raise NotImplementedError("ffn_embed_dim is not supported.") + if scaling_factor != 1.0: + raise NotImplementedError("scaling_factor is not supported.") + if head_num != 1: + raise NotImplementedError("head_num is not supported.") + if not normalize: + raise NotImplementedError("normalize is not supported.") + if temperature is not None: + raise NotImplementedError("temperature is not supported.") + if return_rot: + raise NotImplementedError("return_rot is not supported.") + if not concat_output_tebd: + raise NotImplementedError("concat_output_tebd is not supported.") DescrptSeA.__init__( self, rcut, diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index dbe4881952..8366f7bb38 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -45,6 +45,9 @@ "bfloat16": None, } +doc_only_tf_supported = "(Supported Backend: TensorFlow) " +doc_only_pt_supported = "(Supported Backend: PyTorch) " + def list_to_doc(xx): items = [] @@ -109,7 +112,7 @@ def __init__(self) -> None: self.__plugin = Plugin() def register( - self, name: str, alias: Optional[List[str]] = None + self, name: str, alias: Optional[List[str]] = None, doc: str = "" ) -> Callable[[], List[Argument]]: """Register a descriptor argument plugin. @@ -135,7 +138,7 @@ def descrpt_some_descrpt_args(): # convert alias to hashed item if isinstance(alias, list): alias = tuple(alias) - return self.__plugin.register((name, alias)) + return self.__plugin.register((name, alias, doc)) def get_all_argument(self, exclude_hybrid: bool = False) -> List[Argument]: """Get all arguments. @@ -151,11 +154,11 @@ def get_all_argument(self, exclude_hybrid: bool = False) -> List[Argument]: all arguments """ arguments = [] - for (name, alias), metd in self.__plugin.plugins.items(): + for (name, alias, doc), metd in self.__plugin.plugins.items(): if exclude_hybrid and name == "hybrid": continue arguments.append( - Argument(name=name, dtype=dict, sub_fields=metd(), alias=alias) + Argument(name=name, dtype=dict, sub_fields=metd(), alias=alias, doc=doc) ) return arguments @@ -163,7 +166,7 @@ def get_all_argument(self, exclude_hybrid: bool = False) -> List[Argument]: descrpt_args_plugin = ArgsPlugin() -@descrpt_args_plugin.register("loc_frame") +@descrpt_args_plugin.register("loc_frame", doc=doc_only_tf_supported) def descrpt_local_frame_args(): doc_sel_a = "A list of integers. The length of the list should be the same as the number of atom types in the system. `sel_a[i]` gives the selected number of type-i neighbors. The full relative coordinates of the neighbors are used by the descriptor." doc_sel_r = "A list of integers. The length of the list should be the same as the number of atom types in the system. `sel_r[i]` gives the selected number of type-i neighbors. Only relative distance of the neighbors are used by the descriptor. sel_a[i] + sel_r[i] is recommended to be larger than the maximally possible number of type-i neighbors in the cut-off radius." @@ -244,7 +247,9 @@ def descrpt_se_a_args(): ] -@descrpt_args_plugin.register("se_e3", alias=["se_at", "se_a_3be", "se_t"]) +@descrpt_args_plugin.register( + "se_e3", alias=["se_at", "se_a_3be", "se_t"], doc=doc_only_tf_supported +) def descrpt_se_t_args(): doc_sel = 'This parameter set the number of selected neighbors for each type of atom. It can be:\n\n\ - `List[int]`. The length of the list should be the same as the number of atom types in the system. `sel[i]` gives the selected number of type-i neighbors. `sel[i]` is recommended to be larger than the maximally possible number of type-i neighbors in the cut-off radius. It is noted that the total sel value must be less than 4096 in a GPU environment.\n\n\ @@ -283,7 +288,7 @@ def descrpt_se_t_args(): ] -@descrpt_args_plugin.register("se_a_tpe", alias=["se_a_ebd"]) +@descrpt_args_plugin.register("se_a_tpe", alias=["se_a_ebd"], doc=doc_only_tf_supported) def descrpt_se_a_tpe_args(): doc_type_nchanl = "number of channels for type embedding" doc_type_nlayer = "number of hidden layers of type embedding net" @@ -348,7 +353,7 @@ def descrpt_se_r_args(): ] -@descrpt_args_plugin.register("hybrid") +@descrpt_args_plugin.register("hybrid", doc=doc_only_tf_supported) def descrpt_hybrid_args(): doc_list = "A list of descriptor definitions" @@ -376,12 +381,25 @@ def descrpt_se_atten_common_args(): doc_neuron = "Number of neurons in each hidden layers of the embedding net. When two layers are of the same size or one layer is twice as large as the previous layer, a skip connection is built." doc_axis_neuron = "Size of the submatrix of G (embedding matrix)." doc_activation_function = f'The activation function in the embedding net. Supported activation functions are {list_to_doc(ACTIVATION_FN_DICT.keys())} Note that "gelu" denotes the custom operator version, and "gelu_tf" denotes the TF standard version. If you set "None" or "none" here, no activation function will be used.' - doc_resnet_dt = 'Whether to use a "Timestep" in the skip connection' - doc_type_one_side = r"If true, the embedding network parameters vary by types of neighbor atoms only, so there will be $N_\text{types}$ sets of embedding network parameters. Otherwise, the embedding network parameters vary by types of centric atoms and types of neighbor atoms, so there will be $N_\text{types}^2$ sets of embedding network parameters." - doc_precision = f"The precision of the embedding net parameters, supported options are {list_to_doc(PRECISION_DICT.keys())} Default follows the interface precision." - doc_trainable = "If the parameters in the embedding net is trainable" + doc_resnet_dt = ( + doc_only_tf_supported + 'Whether to use a "Timestep" in the skip connection' + ) + doc_type_one_side = ( + doc_only_tf_supported + + r"If true, the embedding network parameters vary by types of neighbor atoms only, so there will be $N_\text{types}$ sets of embedding network parameters. Otherwise, the embedding network parameters vary by types of centric atoms and types of neighbor atoms, so there will be $N_\text{types}^2$ sets of embedding network parameters." + ) + doc_precision = ( + doc_only_tf_supported + + f"The precision of the embedding net parameters, supported options are {list_to_doc(PRECISION_DICT.keys())} Default follows the interface precision." + ) + doc_trainable = ( + doc_only_tf_supported + "If the parameters in the embedding net is trainable" + ) doc_seed = "Random seed for parameter initialization" - doc_exclude_types = "The excluded pairs of types which have no interaction with each other. For example, `[[0, 1]]` means no interaction between type 0 and type 1." + doc_exclude_types = ( + doc_only_tf_supported + + "The excluded pairs of types which have no interaction with each other. For example, `[[0, 1]]` means no interaction between type 0 and type 1." + ) doc_attn = "The length of hidden vectors in attention layers" doc_attn_layer = "The number of attention layers. Note that model compression of `se_atten` is only enabled when attn_layer==0 and stripped_type_embedding is True" doc_attn_dotr = "Whether to do dot product with the normalized relative coordinates" @@ -432,7 +450,7 @@ def descrpt_se_atten_common_args(): ] -@descrpt_args_plugin.register("se_atten") +@descrpt_args_plugin.register("se_atten", alias=["dpa1"]) def descrpt_se_atten_args(): doc_stripped_type_embedding = "Whether to strip the type embedding into a separated embedding network. Setting it to `False` will fall back to the previous version of `se_atten` which is non-compressible." doc_smooth_type_embdding = "When using stripped type embedding, whether to dot smooth factor on the network output of type embedding to keep the network smooth, instead of setting `set_davg_zero` to be True." @@ -445,22 +463,60 @@ def descrpt_se_atten_args(): bool, optional=True, default=False, - doc=doc_stripped_type_embedding, + doc=doc_only_tf_supported + doc_stripped_type_embedding, ), Argument( "smooth_type_embdding", bool, optional=True, default=False, - doc=doc_smooth_type_embdding, + doc=doc_only_tf_supported + doc_smooth_type_embdding, ), Argument( "set_davg_zero", bool, optional=True, default=True, doc=doc_set_davg_zero ), + # pt only + Argument("tebd_dim", int, optional=True, default=8, doc=doc_only_pt_supported), + Argument( + "tebd_input_mode", + str, + optional=True, + default="concat", + doc=doc_only_pt_supported, + ), + Argument( + "post_ln", bool, optional=True, default=True, doc=doc_only_pt_supported + ), + Argument("ffn", bool, optional=True, default=False, doc=doc_only_pt_supported), + Argument( + "ffn_embed_dim", int, optional=True, default=1024, doc=doc_only_pt_supported + ), + Argument( + "scaling_factor", + float, + optional=True, + default=1.0, + doc=doc_only_pt_supported, + ), + Argument("head_num", int, optional=True, default=1, doc=doc_only_pt_supported), + Argument( + "normalize", bool, optional=True, default=True, doc=doc_only_pt_supported + ), + Argument("temperature", float, optional=True, doc=doc_only_pt_supported), + Argument( + "return_rot", bool, optional=True, default=False, doc=doc_only_pt_supported + ), + Argument( + "concat_output_tebd", + bool, + optional=True, + default=True, + doc=doc_only_pt_supported, + ), ] -@descrpt_args_plugin.register("se_atten_v2") +@descrpt_args_plugin.register("se_atten_v2", doc=doc_only_tf_supported) def descrpt_se_atten_v2_args(): doc_set_davg_zero = "Set the normalization average to zero. This option should be set when `se_atten` descriptor or `atom_ener` in the energy fitting is used" @@ -472,12 +528,272 @@ def descrpt_se_atten_v2_args(): ] -@descrpt_args_plugin.register("se_a_ebd_v2", alias=["se_a_tpe_v2"]) +@descrpt_args_plugin.register("dpa2", doc=doc_only_pt_supported) +def descrpt_dpa2_args(): + # Generate by GitHub Copilot + doc_repinit_rcut = "The cut-off radius of the repinit block" + doc_repinit_rcut_smth = "From this position the inverse distance smoothly decays to 0 at the cut-off. Use in the repinit block." + doc_repinit_nsel = "Maximally possible number of neighbors for repinit block." + doc_repformer_rcut = "The cut-off radius of the repformer block" + doc_repformer_rcut_smth = "From this position the inverse distance smoothly decays to 0 at the cut-off. Use in the repformer block." + doc_repformer_nsel = "Maximally possible number of neighbors for repformer block." + doc_tebd_dim = "The dimension of atom type embedding" + doc_concat_output_tebd = ( + "Whether to concat type embedding at the output of the descriptor." + ) + doc_repinit_neuron = "repinit block: the number of neurons in the embedding net." + doc_repinit_axis_neuron = ( + "repinit block: the number of dimension of split in the symmetrization op." + ) + doc_repinit_activation = ( + "repinit block: the activation function in the embedding net" + ) + doc_repformer_nlayers = "repformers block: the number of repformer layers" + doc_repformer_g1_dim = "repformers block: the dimension of single-atom rep" + doc_repformer_g2_dim = "repformers block: the dimension of invariant pair-atom rep" + doc_repformer_axis_dim = ( + "repformers block: the number of dimension of split in the symmetrization ops." + ) + doc_repformer_do_bn_mode = "repformers block: do batch norm in the repformer layers" + doc_repformer_bn_momentum = "repformers block: moment in the batch normalization" + doc_repformer_update_g1_has_conv = ( + "repformers block: update the g1 rep with convolution term" + ) + doc_repformer_update_g1_has_drrd = ( + "repformers block: update the g1 rep with the drrd term" + ) + doc_repformer_update_g1_has_grrg = ( + "repformers block: update the g1 rep with the grrg term" + ) + doc_repformer_update_g1_has_attn = ( + "repformers block: update the g1 rep with the localized self-attention" + ) + doc_repformer_update_g2_has_g1g1 = ( + "repformers block: update the g2 rep with the g1xg1 term" + ) + doc_repformer_update_g2_has_attn = ( + "repformers block: update the g2 rep with the gated self-attention" + ) + doc_repformer_update_h2 = "repformers block: update the h2 rep" + doc_repformer_attn1_hidden = ( + "repformers block: the hidden dimension of localized self-attention" + ) + doc_repformer_attn1_nhead = ( + "repformers block: the number of heads in localized self-attention" + ) + doc_repformer_attn2_hidden = ( + "repformers block: the hidden dimension of gated self-attention" + ) + doc_repformer_attn2_nhead = ( + "repformers block: the number of heads in gated self-attention" + ) + doc_repformer_attn2_has_gate = ( + "repformers block: has gate in the gated self-attention" + ) + doc_repformer_activation = "repformers block: the activation function in the MLPs." + doc_repformer_update_style = "repformers block: style of update a rep. can be res_avg or res_incr. res_avg updates a rep `u` with: u = 1/\\sqrt{n+1} (u + u_1 + u_2 + ... + u_n) res_incr updates a rep `u` with: u = u + 1/\\sqrt{n} (u_1 + u_2 + ... + u_n)" + doc_repformer_set_davg_zero = "repformers block: set the avg to zero in statistics" + doc_repformer_add_type_ebd_to_seq = ( + "repformers block: concatenate the type embedding at the output" + ) + return [ + Argument("repinit_rcut", float, doc=doc_repinit_rcut), + Argument("repinit_rcut_smth", float, doc=doc_repinit_rcut_smth), + Argument("repinit_nsel", int, doc=doc_repinit_nsel), + Argument("repformer_rcut", float, doc=doc_repformer_rcut), + Argument("repformer_rcut_smth", float, doc=doc_repformer_rcut_smth), + Argument("repformer_nsel", int, doc=doc_repformer_nsel), + Argument("tebd_dim", int, optional=True, default=8, doc=doc_tebd_dim), + Argument( + "concat_output_tebd", + bool, + optional=True, + default=True, + doc=doc_concat_output_tebd, + ), + Argument( + "repinit_neuron", + list, + optional=True, + default=[25, 50, 100], + doc=doc_repinit_neuron, + ), + Argument( + "repinit_axis_neuron", + int, + optional=True, + default=16, + doc=doc_repinit_axis_neuron, + ), + Argument("repinit_set_davg_zero", bool, optional=True, default=True), + Argument( + "repinit_activation", + str, + optional=True, + default="tanh", + doc=doc_repinit_activation, + ), + Argument( + "repformer_nlayers", + int, + optional=True, + default=3, + doc=doc_repformer_nlayers, + ), + Argument( + "repformer_g1_dim", + int, + optional=True, + default=128, + doc=doc_repformer_g1_dim, + ), + Argument( + "repformer_g2_dim", int, optional=True, default=16, doc=doc_repformer_g2_dim + ), + Argument( + "repformer_axis_dim", + int, + optional=True, + default=4, + doc=doc_repformer_axis_dim, + ), + Argument( + "repformer_do_bn_mode", + str, + optional=True, + default="no", + doc=doc_repformer_do_bn_mode, + ), + Argument( + "repformer_bn_momentum", + float, + optional=True, + default=0.1, + doc=doc_repformer_bn_momentum, + ), + Argument( + "repformer_update_g1_has_conv", + bool, + optional=True, + default=True, + doc=doc_repformer_update_g1_has_conv, + ), + Argument( + "repformer_update_g1_has_drrd", + bool, + optional=True, + default=True, + doc=doc_repformer_update_g1_has_drrd, + ), + Argument( + "repformer_update_g1_has_grrg", + bool, + optional=True, + default=True, + doc=doc_repformer_update_g1_has_grrg, + ), + Argument( + "repformer_update_g1_has_attn", + bool, + optional=True, + default=True, + doc=doc_repformer_update_g1_has_attn, + ), + Argument( + "repformer_update_g2_has_g1g1", + bool, + optional=True, + default=True, + doc=doc_repformer_update_g2_has_g1g1, + ), + Argument( + "repformer_update_g2_has_attn", + bool, + optional=True, + default=True, + doc=doc_repformer_update_g2_has_attn, + ), + Argument( + "repformer_update_h2", + bool, + optional=True, + default=False, + doc=doc_repformer_update_h2, + ), + Argument( + "repformer_attn1_hidden", + int, + optional=True, + default=64, + doc=doc_repformer_attn1_hidden, + ), + Argument( + "repformer_attn1_nhead", + int, + optional=True, + default=4, + doc=doc_repformer_attn1_nhead, + ), + Argument( + "repformer_attn2_hidden", + int, + optional=True, + default=16, + doc=doc_repformer_attn2_hidden, + ), + Argument( + "repformer_attn2_nhead", + int, + optional=True, + default=4, + doc=doc_repformer_attn2_nhead, + ), + Argument( + "repformer_attn2_has_gate", + bool, + optional=True, + default=False, + doc=doc_repformer_attn2_has_gate, + ), + Argument( + "repformer_activation", + str, + optional=True, + default="tanh", + doc=doc_repformer_activation, + ), + Argument( + "repformer_update_style", + str, + optional=True, + default="res_avg", + doc=doc_repformer_update_style, + ), + Argument( + "repformer_set_davg_zero", + bool, + optional=True, + default=True, + doc=doc_repformer_set_davg_zero, + ), + Argument( + "repformer_add_type_ebd_to_seq", + bool, + optional=True, + default=False, + doc=doc_repformer_add_type_ebd_to_seq, + ), + ] + + +@descrpt_args_plugin.register( + "se_a_ebd_v2", alias=["se_a_tpe_v2"], doc=doc_only_tf_supported +) def descrpt_se_a_ebd_v2_args(): return descrpt_se_a_args() -@descrpt_args_plugin.register("se_a_mask") +@descrpt_args_plugin.register("se_a_mask", doc=doc_only_tf_supported) def descrpt_se_a_mask_args(): doc_sel = 'This parameter sets the number of selected neighbors for each type of atom. It can be:\n\n\ - `List[int]`. The length of the list should be the same as the number of atom types in the system. `sel[i]` gives the selected number of type-i neighbors. `sel[i]` is recommended to be larger than the maximally possible number of type-i neighbors in the cut-off radius. It is noted that the total sel value must be less than 4096 in a GPU environment.\n\n\ @@ -637,7 +953,7 @@ def fitting_ener(): ] -@fitting_args_plugin.register("dos") +@fitting_args_plugin.register("dos", doc=doc_only_tf_supported) def fitting_dos(): doc_numb_fparam = "The dimension of the frame parameter. If set to >0, file `fparam.npy` should be included to provided the input fparams." doc_numb_aparam = "The dimension of the atomic parameter. If set to >0, file `aparam.npy` should be included to provided the input aparams." @@ -684,7 +1000,7 @@ def fitting_dos(): ] -@fitting_args_plugin.register("polar") +@fitting_args_plugin.register("polar", doc=doc_only_tf_supported) def fitting_polar(): doc_neuron = "The number of neurons in each hidden layers of the fitting net. When two hidden layers are of the same size, a skip connection is built." doc_activation_function = f'The activation function in the fitting net. Supported activation functions are {list_to_doc(ACTIVATION_FN_DICT.keys())} Note that "gelu" denotes the custom operator version, and "gelu_tf" denotes the TF standard version. If you set "None" or "none" here, no activation function will be used.' @@ -738,7 +1054,7 @@ def fitting_polar(): # return fitting_polar() -@fitting_args_plugin.register("dipole") +@fitting_args_plugin.register("dipole", doc=doc_only_tf_supported) def fitting_dipole(): doc_neuron = "The number of neurons in each hidden layers of the fitting net. When two hidden layers are of the same size, a skip connection is built." doc_activation_function = f'The activation function in the fitting net. Supported activation functions are {list_to_doc(ACTIVATION_FN_DICT.keys())} Note that "gelu" denotes the custom operator version, and "gelu_tf" denotes the TF standard version. If you set "None" or "none" here, no activation function will be used.' @@ -900,16 +1216,30 @@ def model_args(exclude_hybrid=False): default=10, doc=doc_data_bias_nsample, ), - Argument("use_srtab", str, optional=True, doc=doc_use_srtab), - Argument("smin_alpha", float, optional=True, doc=doc_smin_alpha), - Argument("sw_rmin", float, optional=True, doc=doc_sw_rmin), - Argument("sw_rmax", float, optional=True, doc=doc_sw_rmax), + Argument( + "use_srtab", + str, + optional=True, + doc=doc_only_tf_supported + doc_use_srtab, + ), + Argument( + "smin_alpha", + float, + optional=True, + doc=doc_only_tf_supported + doc_smin_alpha, + ), + Argument( + "sw_rmin", float, optional=True, doc=doc_only_tf_supported + doc_sw_rmin + ), + Argument( + "sw_rmax", float, optional=True, doc=doc_only_tf_supported + doc_sw_rmax + ), Argument( "srtab_add_bias", bool, optional=True, default=True, - doc=doc_srtab_add_bias, + doc=doc_only_tf_supported + doc_srtab_add_bias, ), Argument( "type_embedding", @@ -917,7 +1247,7 @@ def model_args(exclude_hybrid=False): type_embedding_args(), [], optional=True, - doc=doc_type_embedding, + doc=doc_only_tf_supported + doc_type_embedding, ), Argument( "modifier", @@ -925,7 +1255,7 @@ def model_args(exclude_hybrid=False): [], [modifier_variant_type_args()], optional=True, - doc=doc_modifier, + doc=doc_only_tf_supported + doc_modifier, ), Argument( "compress", @@ -933,7 +1263,7 @@ def model_args(exclude_hybrid=False): [], [model_compression_type_args()], optional=True, - doc=doc_compress_config, + doc=doc_only_tf_supported + doc_compress_config, fold_subdoc=True, ), Argument("spin", dict, spin_args(), [], optional=True, doc=doc_spin), @@ -997,7 +1327,7 @@ def multi_model_args() -> Argument: ), Argument("fitting_net_dict", dict, doc=doc_fitting_net_dict), ], - doc="Multiple-task model.", + doc=doc_only_tf_supported + "Multiple-task model.", ) return ca @@ -1016,6 +1346,7 @@ def pairwise_dprc() -> Argument: qm_model_args, qmmm_model_args, ], + doc=doc_only_tf_supported, ) return ca @@ -1028,6 +1359,7 @@ def frozen_model_args() -> Argument: [ Argument("model_file", str, optional=False, doc=doc_model_file), ], + doc=doc_only_tf_supported, ) return ca @@ -1047,7 +1379,7 @@ def pairtab_model_args() -> Argument: Argument("rcut", float, optional=False, doc=doc_rcut), Argument("sel", [int, List[int], str], optional=False, doc=doc_sel), ], - doc="Pairwise tabulation energy model.", + doc=doc_only_tf_supported + "Pairwise tabulation energy model.", ) return ca @@ -1076,6 +1408,7 @@ def linear_ener_model_args() -> Argument: doc=doc_weights, ), ], + doc=doc_only_tf_supported, ) return ca @@ -1390,7 +1723,7 @@ def loss_ener_spin(): ] -@loss_args_plugin.register("dos") +@loss_args_plugin.register("dos", doc=doc_only_tf_supported) def loss_dos(): doc_start_pref_dos = start_pref("Density of State (DOS)") doc_limit_pref_dos = limit_pref("Density of State (DOS)") @@ -1465,7 +1798,7 @@ def loss_dos(): # YWolfeee: Modified to support tensor type of loss args. -@loss_args_plugin.register("tensor") +@loss_args_plugin.register("tensor", doc=doc_only_tf_supported) def loss_tensor(): # doc_global_weight = "The prefactor of the weight of global loss. It should be larger than or equal to 0. If only `pref` is provided or both are not provided, training will be global mode, i.e. the shape of 'polarizability.npy` or `dipole.npy` should be #frams x [9 or 3]." # doc_local_weight = "The prefactor of the weight of atomic loss. It should be larger than or equal to 0. If only `pref_atomic` is provided, training will be atomic mode, i.e. the shape of `polarizability.npy` or `dipole.npy` should be #frames x ([9 or 3] x #selected atoms). If both `pref` and `pref_atomic` are provided, training will be combined mode, and atomic label should be provided as well." @@ -1746,13 +2079,19 @@ def training_args(): # ! modified by Ziyao: data configuration isolated. Argument( "time_training", bool, optional=True, default=True, doc=doc_time_training ), - Argument("profiling", bool, optional=True, default=False, doc=doc_profiling), + Argument( + "profiling", + bool, + optional=True, + default=False, + doc=doc_only_tf_supported + doc_profiling, + ), Argument( "profiling_file", str, optional=True, default="timeline.json", - doc=doc_profiling_file, + doc=doc_only_tf_supported + doc_profiling_file, ), Argument( "enable_profiler", @@ -1776,10 +2115,38 @@ def training_args(): # ! modified by Ziyao: data configuration isolated. ), Argument("data_dict", dict, optional=True, doc=doc_data_dict), Argument("fitting_weight", dict, optional=True, doc=doc_fitting_weight), + Argument("warmup_steps", int, optional=True, doc=doc_only_pt_supported), + Argument("gradient_max_norm", float, optional=True, doc=doc_only_pt_supported), + Argument("stat_file", str, optional=True, doc=doc_only_pt_supported), + ] + variants = [ + Variant( + "opt_type", + choices=[ + Argument("Adam", dict, [], [], optional=True), + Argument( + "LKF", + dict, + [ + Argument( + "kf_blocksize", + int, + optional=True, + doc=doc_only_pt_supported, + ), + ], + [], + optional=True, + ), + ], + optional=True, + default_tag="Adam", + doc=doc_only_pt_supported, + ) ] doc_training = "The training options." - return Argument("training", dict, args, [], doc=doc_training) + return Argument("training", dict, args, variants, doc=doc_training) def make_index(keys): diff --git a/examples/water/se_atten/input_torch.json b/examples/water/se_atten/input_torch.json index bc948cc2a0..7e9cf06f35 100644 --- a/examples/water/se_atten/input_torch.json +++ b/examples/water/se_atten/input_torch.json @@ -17,6 +17,7 @@ ], "tebd_dim": 8, "axis_neuron": 16, + "type_one_side": true, "attn": 128, "attn_layer": 2, "attn_dotr": true, @@ -24,7 +25,7 @@ "post_ln": true, "ffn": false, "ffn_embed_dim": 1024, - "activation": "tanh", + "activation_function": "tanh", "scaling_factor": 1.0, "head_num": 1, "normalize": true, @@ -78,11 +79,6 @@ "numb_btch": 3, "_comment": "that's all" }, - "wandb_config": { - "wandb_enabled": false, - "entity": "dp_model_engineering", - "project": "DPA" - }, "numb_steps": 1000000, "seed": 10, "disp_file": "lcurve.out", diff --git a/source/tests/common/test_examples.py b/source/tests/common/test_examples.py index ad06925eab..49abcf2f90 100644 --- a/source/tests/common/test_examples.py +++ b/source/tests/common/test_examples.py @@ -42,6 +42,9 @@ p_examples / "dprc" / "normal" / "input.json", p_examples / "dprc" / "pairwise" / "input.json", p_examples / "dprc" / "generalized_force" / "input.json", + p_examples / "water" / "se_e2_a" / "input_torch.json", + p_examples / "water" / "se_atten" / "input_torch.json", + p_examples / "water" / "dpa2" / "input_torch.json", ) diff --git a/source/tests/pt/model/models/dpa1.json b/source/tests/pt/model/models/dpa1.json index dd838ac692..5d2c65c214 100644 --- a/source/tests/pt/model/models/dpa1.json +++ b/source/tests/pt/model/models/dpa1.json @@ -21,7 +21,7 @@ "post_ln": true, "ffn": false, "ffn_embed_dim": 10, - "activation": "tanh", + "activation_function": "tanh", "scaling_factor": 1.0, "head_num": 1, "normalize": true, diff --git a/source/tests/pt/model/models/dpa2_hyb.json b/source/tests/pt/model/models/dpa2_hyb.json index b5d53b0246..ee69ed4d69 100644 --- a/source/tests/pt/model/models/dpa2_hyb.json +++ b/source/tests/pt/model/models/dpa2_hyb.json @@ -25,7 +25,7 @@ "post_ln": true, "ffn": false, "ffn_embed_dim": 10, - "activation": "tanh", + "activation_function": "tanh", "scaling_factor": 1.0, "head_num": 1, "normalize": true, diff --git a/source/tests/pt/model/test_jit.py b/source/tests/pt/model/test_jit.py index f13dade183..a1aa9658fc 100644 --- a/source/tests/pt/model/test_jit.py +++ b/source/tests/pt/model/test_jit.py @@ -85,15 +85,15 @@ def setUp(self): self.config["training"]["training_data"]["systems"] = data_file self.config["training"]["validation_data"]["systems"] = data_file self.config["model"] = deepcopy(model_dpa2) - self.config["model"]["descriptor"]["rcut"] = self.config["model"]["descriptor"][ - "repinit_rcut" - ] - self.config["model"]["descriptor"]["rcut_smth"] = self.config["model"][ - "descriptor" - ]["repinit_rcut_smth"] - self.config["model"]["descriptor"]["sel"] = self.config["model"]["descriptor"][ - "repinit_nsel" - ] + # self.config["model"]["descriptor"]["rcut"] = self.config["model"]["descriptor"][ + # "repinit_rcut" + # ] + # self.config["model"]["descriptor"]["rcut_smth"] = self.config["model"][ + # "descriptor" + # ]["repinit_rcut_smth"] + # self.config["model"]["descriptor"]["sel"] = self.config["model"]["descriptor"][ + # "repinit_nsel" + # ] self.config["training"]["numb_steps"] = 10 self.config["training"]["save_freq"] = 10 diff --git a/source/tests/pt/model/test_permutation.py b/source/tests/pt/model/test_permutation.py index b97cb349ad..45790bf43d 100644 --- a/source/tests/pt/model/test_permutation.py +++ b/source/tests/pt/model/test_permutation.py @@ -115,12 +115,13 @@ "post_ln": True, "ffn": False, "ffn_embed_dim": 512, - "activation": "tanh", + "activation_function": "tanh", "scaling_factor": 1.0, "head_num": 1, "normalize": False, "temperature": 1.0, "set_davg_zero": True, + "type_one_side": True, }, "fitting_net": { "neuron": [24, 24, 24], @@ -149,7 +150,7 @@ "post_ln": True, "ffn": False, "ffn_embed_dim": 1024, - "activation": "tanh", + "activation_function": "tanh", "scaling_factor": 1.0, "head_num": 1, "normalize": True, diff --git a/source/tests/pt/model/water/se_atten.json b/source/tests/pt/model/water/se_atten.json index 3ed80ae892..6b6fca50d3 100644 --- a/source/tests/pt/model/water/se_atten.json +++ b/source/tests/pt/model/water/se_atten.json @@ -16,6 +16,7 @@ 100 ], "axis_neuron": 16, + "type_one_side": true, "attn": 64, "attn_layer": 2, "attn_dotr": true, @@ -23,7 +24,7 @@ "post_ln": true, "ffn": false, "ffn_embed_dim": 512, - "activation": "tanh", + "activation_function": "tanh", "scaling_factor": 1.0, "head_num": 1, "normalize": false, diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index 2186467788..f86691cde6 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -79,15 +79,15 @@ def setUp(self): self.config["training"]["training_data"]["systems"] = data_file self.config["training"]["validation_data"]["systems"] = data_file self.config["model"] = deepcopy(model_dpa2) - self.config["model"]["descriptor"]["rcut"] = self.config["model"]["descriptor"][ - "repinit_rcut" - ] - self.config["model"]["descriptor"]["rcut_smth"] = self.config["model"][ - "descriptor" - ]["repinit_rcut_smth"] - self.config["model"]["descriptor"]["sel"] = self.config["model"]["descriptor"][ - "repinit_nsel" - ] + # self.config["model"]["descriptor"]["rcut"] = self.config["model"]["descriptor"][ + # "repinit_rcut" + # ] + # self.config["model"]["descriptor"]["rcut_smth"] = self.config["model"][ + # "descriptor" + # ]["repinit_rcut_smth"] + # self.config["model"]["descriptor"]["sel"] = self.config["model"]["descriptor"][ + # "repinit_nsel" + # ] self.config["training"]["numb_steps"] = 1 self.config["training"]["save_freq"] = 1 From 84d0576b1a7330bb1fdace2ca6358734c224ee5b Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 28 Feb 2024 23:50:13 -0500 Subject: [PATCH 145/686] docs: apply type_one_side=True to `se_a` and `se_r` (#3364) Fix #2265. Signed-off-by: Jinzhe Zeng --- doc/model/train-se-e2-r.md | 1 + doc/train/multi-task-training.md | 5 +++-- examples/dos/train/input.json | 1 + examples/fparam/train/input.json | 1 + examples/fparam/train/input_aparam.json | 1 + examples/nopbc/train/input.json | 1 + examples/spin/se_e2_a/input.json | 1 + examples/water/d3/input.json | 1 + examples/water/dplr/train/dw.json | 1 + examples/water/dplr/train/ener.json | 1 + examples/water/hybrid/input.json | 2 ++ examples/water/se_e2_a/input.json | 1 + examples/water/se_e2_a/input_torch.json | 1 + examples/water/se_e2_a_mixed_prec/input.json | 1 + examples/water/se_e2_r/input.json | 1 + examples/water/zbl/input.json | 1 + examples/water_multi_task/ener_dipole/input.json | 1 + examples/water_tensor/dipole/dipole_input.json | 1 + examples/water_tensor/polar/polar_input.json | 1 + examples/zinc_protein/zinc_se_a_mask.json | 1 + 20 files changed, 23 insertions(+), 2 deletions(-) diff --git a/doc/model/train-se-e2-r.md b/doc/model/train-se-e2-r.md index 5d15269618..1ec768017c 100644 --- a/doc/model/train-se-e2-r.md +++ b/doc/model/train-se-e2-r.md @@ -59,6 +59,7 @@ The training input script is very similar to that of [`se_e2_a`](train-se-e2-a.m "rcut_smth": 0.50, "rcut": 6.00, "neuron": [5, 10, 20], + "type_one_side": true, "resnet_dt": false, "seed": 1, "_comment": " that's all" diff --git a/doc/train/multi-task-training.md b/doc/train/multi-task-training.md index 974606190e..47fb1cc1da 100644 --- a/doc/train/multi-task-training.md +++ b/doc/train/multi-task-training.md @@ -75,15 +75,16 @@ You can first train a multi-task model using input script with the following {re "rcut_smth": 0.5, "rcut": 6.0, "neuron": [25, 50, 100], + "type_one_side": true }, "fitting_net_dict": { "water_dipole": { "type": "dipole", - "neuron": [100, 100, 100], + "neuron": [100, 100, 100] }, "water_ener": { "neuron": [240, 240, 240], - "resnet_dt": true, + "resnet_dt": true } }, } diff --git a/examples/dos/train/input.json b/examples/dos/train/input.json index f2094c18a6..327a9c3aff 100644 --- a/examples/dos/train/input.json +++ b/examples/dos/train/input.json @@ -17,6 +17,7 @@ ], "resnet_dt": false, "axis_neuron": 8, + "type_one_side": true, "precision": "float64", "seed": 1 }, diff --git a/examples/fparam/train/input.json b/examples/fparam/train/input.json index a81051f459..f881dff3ca 100644 --- a/examples/fparam/train/input.json +++ b/examples/fparam/train/input.json @@ -16,6 +16,7 @@ ], "resnet_dt": false, "axis_neuron": 8, + "type_one_side": true, "precision": "float64", "seed": 1 }, diff --git a/examples/fparam/train/input_aparam.json b/examples/fparam/train/input_aparam.json index fdc53706b9..93a34f7305 100644 --- a/examples/fparam/train/input_aparam.json +++ b/examples/fparam/train/input_aparam.json @@ -16,6 +16,7 @@ ], "resnet_dt": false, "axis_neuron": 8, + "type_one_side": true, "precision": "float64", "seed": 1 }, diff --git a/examples/nopbc/train/input.json b/examples/nopbc/train/input.json index 2c33791d45..491a7e1476 100644 --- a/examples/nopbc/train/input.json +++ b/examples/nopbc/train/input.json @@ -22,6 +22,7 @@ ], "resnet_dt": false, "axis_neuron": 12, + "type_one_side": true, "seed": 1, "_comment2": " that's all" }, diff --git a/examples/spin/se_e2_a/input.json b/examples/spin/se_e2_a/input.json index f9e0988163..8d124d1fc4 100644 --- a/examples/spin/se_e2_a/input.json +++ b/examples/spin/se_e2_a/input.json @@ -20,6 +20,7 @@ ], "resnet_dt": false, "axis_neuron": 16, + "type_one_side": true, "precision": "float64", "seed": 1, "_comment2": " that's all" diff --git a/examples/water/d3/input.json b/examples/water/d3/input.json index bbe7a2c8a9..e811920f5b 100644 --- a/examples/water/d3/input.json +++ b/examples/water/d3/input.json @@ -24,6 +24,7 @@ ], "resnet_dt": false, "axis_neuron": 16, + "type_one_side": true, "precision": "float64", "seed": 1, "_comment2": " that's all" diff --git a/examples/water/dplr/train/dw.json b/examples/water/dplr/train/dw.json index 401e6272f5..038e07abef 100644 --- a/examples/water/dplr/train/dw.json +++ b/examples/water/dplr/train/dw.json @@ -20,6 +20,7 @@ ], "resnet_dt": false, "axis_neuron": 8, + "type_one_side": true, "precision": "float64", "seed": 1, "_comment2": " that's all" diff --git a/examples/water/dplr/train/ener.json b/examples/water/dplr/train/ener.json index 7b47bfda55..809f1a5ece 100644 --- a/examples/water/dplr/train/ener.json +++ b/examples/water/dplr/train/ener.json @@ -20,6 +20,7 @@ ], "resnet_dt": false, "axis_neuron": 8, + "type_one_side": true, "precision": "float64", "seed": 3458359619, "_comment2": " that's all" diff --git a/examples/water/hybrid/input.json b/examples/water/hybrid/input.json index 2315d26444..dd29c15d9d 100644 --- a/examples/water/hybrid/input.json +++ b/examples/water/hybrid/input.json @@ -23,6 +23,7 @@ ], "resnet_dt": false, "axis_neuron": 4, + "type_one_side": true, "precision": "float64", "seed": 1, "_comment2": " that's all" @@ -41,6 +42,7 @@ 20 ], "resnet_dt": false, + "type_one_side": true, "precision": "float64", "seed": 1, "_comment3": " that's all" diff --git a/examples/water/se_e2_a/input.json b/examples/water/se_e2_a/input.json index 46c38ba834..0a24d11549 100644 --- a/examples/water/se_e2_a/input.json +++ b/examples/water/se_e2_a/input.json @@ -20,6 +20,7 @@ ], "resnet_dt": false, "axis_neuron": 16, + "type_one_side": true, "precision": "float64", "seed": 1, "_comment2": " that's all" diff --git a/examples/water/se_e2_a/input_torch.json b/examples/water/se_e2_a/input_torch.json index c686b49d45..fe424afed3 100644 --- a/examples/water/se_e2_a/input_torch.json +++ b/examples/water/se_e2_a/input_torch.json @@ -19,6 +19,7 @@ ], "resnet_dt": false, "axis_neuron": 16, + "type_one_side": true, "seed": 1, "_comment": " that's all" }, diff --git a/examples/water/se_e2_a_mixed_prec/input.json b/examples/water/se_e2_a_mixed_prec/input.json index 0382b80b30..f1993f6cc0 100644 --- a/examples/water/se_e2_a_mixed_prec/input.json +++ b/examples/water/se_e2_a_mixed_prec/input.json @@ -20,6 +20,7 @@ ], "resnet_dt": false, "axis_neuron": 16, + "type_one_side": true, "seed": 1, "_comment2": " that's all" }, diff --git a/examples/water/se_e2_r/input.json b/examples/water/se_e2_r/input.json index 7fdd1835c6..783b4c7bdb 100644 --- a/examples/water/se_e2_r/input.json +++ b/examples/water/se_e2_r/input.json @@ -19,6 +19,7 @@ 20 ], "resnet_dt": false, + "type_one_side": true, "precision": "float64", "seed": 1, "_comment2": " that's all" diff --git a/examples/water/zbl/input.json b/examples/water/zbl/input.json index 180a6cc8b5..cb5602d92d 100644 --- a/examples/water/zbl/input.json +++ b/examples/water/zbl/input.json @@ -24,6 +24,7 @@ ], "resnet_dt": false, "axis_neuron": 16, + "type_one_side": true, "precision": "float64", "seed": 1, "_comment2": " that's all" diff --git a/examples/water_multi_task/ener_dipole/input.json b/examples/water_multi_task/ener_dipole/input.json index 9d00adac2e..45b49c5d90 100644 --- a/examples/water_multi_task/ener_dipole/input.json +++ b/examples/water_multi_task/ener_dipole/input.json @@ -20,6 +20,7 @@ ], "resnet_dt": false, "axis_neuron": 16, + "type_one_side": true, "precision": "float64", "seed": 1, "_comment2": " that's all" diff --git a/examples/water_tensor/dipole/dipole_input.json b/examples/water_tensor/dipole/dipole_input.json index b42b9b8465..75497ecefb 100644 --- a/examples/water_tensor/dipole/dipole_input.json +++ b/examples/water_tensor/dipole/dipole_input.json @@ -20,6 +20,7 @@ ], "resnet_dt": false, "axis_neuron": 6, + "type_one_side": true, "precision": "float64", "seed": 1, "_comment2": " that's all" diff --git a/examples/water_tensor/polar/polar_input.json b/examples/water_tensor/polar/polar_input.json index ca53182e79..26e4bcf5e9 100644 --- a/examples/water_tensor/polar/polar_input.json +++ b/examples/water_tensor/polar/polar_input.json @@ -21,6 +21,7 @@ ], "resnet_dt": false, "axis_neuron": 16, + "type_one_side": true, "precision": "float64", "seed": 1, "_comment2": " that's all" diff --git a/examples/zinc_protein/zinc_se_a_mask.json b/examples/zinc_protein/zinc_se_a_mask.json index 04f63aa4ed..8d3c747e08 100644 --- a/examples/zinc_protein/zinc_se_a_mask.json +++ b/examples/zinc_protein/zinc_se_a_mask.json @@ -27,6 +27,7 @@ ], "resnet_dt": true, "axis_neuron": 16, + "type_one_side": true, "precision": "float64", "seed": 1, "_comment2": " that's all" From d09af56f3a10898283b2775ff0f2b071bfaf29fa Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 29 Feb 2024 00:25:17 -0500 Subject: [PATCH 146/686] feat: update sel by statistics (#3348) Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../descriptor/make_base_descriptor.py | 16 ++ deepmd/dpmodel/descriptor/se_e2_a.py | 17 ++ deepmd/dpmodel/descriptor/se_r.py | 17 ++ deepmd/dpmodel/model/base_model.py | 15 ++ deepmd/dpmodel/model/dp_model.py | 20 +- deepmd/dpmodel/utils/update_sel.py | 21 ++ deepmd/main.py | 2 +- deepmd/pt/entrypoints/main.py | 9 + deepmd/pt/model/descriptor/dpa1.py | 17 ++ deepmd/pt/model/descriptor/dpa2.py | 32 +++ deepmd/pt/model/descriptor/se_a.py | 17 ++ deepmd/pt/model/descriptor/se_r.py | 17 ++ deepmd/pt/model/model/dp_model.py | 20 ++ deepmd/pt/model/model/dp_zbl_model.py | 20 ++ deepmd/pt/utils/update_sel.py | 21 ++ deepmd/tf/descriptor/se.py | 9 +- deepmd/tf/descriptor/se_atten.py | 9 +- deepmd/tf/entrypoints/compress.py | 10 +- deepmd/tf/entrypoints/train.py | 208 +----------------- deepmd/tf/model/pairtab.py | 9 +- deepmd/tf/model/pairwise_dprc.py | 9 +- deepmd/tf/utils/update_sel.py | 34 +++ deepmd/utils/data_system.py | 77 +++++++ deepmd/utils/update_sel.py | 170 ++++++++++++++ source/tests/tf/test_train.py | 61 ++--- 25 files changed, 597 insertions(+), 260 deletions(-) create mode 100644 deepmd/dpmodel/utils/update_sel.py create mode 100644 deepmd/pt/utils/update_sel.py create mode 100644 deepmd/tf/utils/update_sel.py create mode 100644 deepmd/utils/update_sel.py diff --git a/deepmd/dpmodel/descriptor/make_base_descriptor.py b/deepmd/dpmodel/descriptor/make_base_descriptor.py index 18416ff16b..69f0da787f 100644 --- a/deepmd/dpmodel/descriptor/make_base_descriptor.py +++ b/deepmd/dpmodel/descriptor/make_base_descriptor.py @@ -124,6 +124,22 @@ def deserialize(cls, data: dict) -> "BD": return BD.get_class_by_type(data["type"]).deserialize(data) raise NotImplementedError("Not implemented in class %s" % cls.__name__) + @classmethod + @abstractmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict): + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + # call subprocess + cls = cls.get_class_by_type(j_get_type(local_jdata, cls.__name__)) + return cls.update_sel(global_jdata, local_jdata) + setattr(BD, fwd_method_name, BD.fwd) delattr(BD, "fwd") diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index b102933ac9..5e72653f1d 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -3,6 +3,9 @@ import numpy as np +from deepmd.dpmodel.utils.update_sel import ( + UpdateSel, +) from deepmd.env import ( GLOBAL_NP_FLOAT_PRECISION, ) @@ -388,3 +391,17 @@ def deserialize(cls, data: dict) -> "DescrptSeA": obj.embeddings = NetworkCollection.deserialize(embeddings) obj.env_mat = EnvMat.deserialize(env_mat) return obj + + @classmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict): + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + local_jdata_cpy = local_jdata.copy() + return UpdateSel().update_one_sel(global_jdata, local_jdata_cpy, False) diff --git a/deepmd/dpmodel/descriptor/se_r.py b/deepmd/dpmodel/descriptor/se_r.py index 5973c55353..98185c0117 100644 --- a/deepmd/dpmodel/descriptor/se_r.py +++ b/deepmd/dpmodel/descriptor/se_r.py @@ -1,6 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import numpy as np +from deepmd.dpmodel.utils.update_sel import ( + UpdateSel, +) from deepmd.utils.path import ( DPPath, ) @@ -324,3 +327,17 @@ def deserialize(cls, data: dict) -> "DescrptSeR": obj.embeddings = NetworkCollection.deserialize(embeddings) obj.env_mat = EnvMat.deserialize(env_mat) return obj + + @classmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict): + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + local_jdata_cpy = local_jdata.copy() + return UpdateSel().update_one_sel(global_jdata, local_jdata_cpy, False) diff --git a/deepmd/dpmodel/model/base_model.py b/deepmd/dpmodel/model/base_model.py index faf3e7cfff..e7cc8d9272 100644 --- a/deepmd/dpmodel/model/base_model.py +++ b/deepmd/dpmodel/model/base_model.py @@ -139,6 +139,21 @@ def get_nsel(self) -> int: """Returns the total number of selected neighboring atoms in the cut-off radius.""" pass + @classmethod + @abstractmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict): + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + cls = cls.get_class_by_type(local_jdata.get("type", "standard")) + return cls.update_sel(global_jdata, local_jdata) + return BaseBaseModel diff --git a/deepmd/dpmodel/model/dp_model.py b/deepmd/dpmodel/model/dp_model.py index 804ce51dfd..ef7866a6dd 100644 --- a/deepmd/dpmodel/model/dp_model.py +++ b/deepmd/dpmodel/model/dp_model.py @@ -2,6 +2,9 @@ from deepmd.dpmodel.atomic_model import ( DPAtomicModel, ) +from deepmd.dpmodel.descriptor.base_descriptor import ( + BaseDescriptor, +) from deepmd.dpmodel.model.base_model import ( BaseModel, ) @@ -14,4 +17,19 @@ # use "class" to resolve "Variable not allowed in type expression" @BaseModel.register("standard") class DPModel(make_model(DPAtomicModel), BaseModel): - pass + @classmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict): + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + local_jdata_cpy = local_jdata.copy() + local_jdata_cpy["descriptor"] = BaseDescriptor.update_sel( + global_jdata, local_jdata["descriptor"] + ) + return local_jdata_cpy diff --git a/deepmd/dpmodel/utils/update_sel.py b/deepmd/dpmodel/utils/update_sel.py new file mode 100644 index 0000000000..f36e63651d --- /dev/null +++ b/deepmd/dpmodel/utils/update_sel.py @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Type, +) + +from deepmd.dpmodel.utils.neighbor_stat import ( + NeighborStat, +) +from deepmd.utils.update_sel import ( + BaseUpdateSel, +) + + +class UpdateSel(BaseUpdateSel): + @property + def neighbor_stat(self) -> Type[NeighborStat]: + return NeighborStat + + def hook(self, min_nbor_dist, max_nbor_size): + # TODO: save to the model + pass diff --git a/deepmd/main.py b/deepmd/main.py index df5c99bb2d..5dab029d83 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -245,7 +245,7 @@ def main_parser() -> argparse.ArgumentParser: parser_train.add_argument( "--skip-neighbor-stat", action="store_true", - help="(Supported backend: TensorFlow) Skip calculating neighbor statistics. Sel checking, automatic sel, and model compression will be disabled.", + help="Skip calculating neighbor statistics. Sel checking, automatic sel, and model compression will be disabled.", ) parser_train.add_argument( # -m has been used by mpi-log diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 5583ee0326..0aed595eab 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -32,6 +32,9 @@ from deepmd.pt.infer import ( inference, ) +from deepmd.pt.model.model import ( + BaseModel, +) from deepmd.pt.train import ( training, ) @@ -249,6 +252,12 @@ def train(FLAGS): SummaryPrinter()() with open(FLAGS.INPUT) as fin: config = json.load(fin) + if not FLAGS.skip_neighbor_stat: + log.info( + "Calculate neighbor statistics... (add --skip-neighbor-stat to skip this step)" + ) + config["model"] = BaseModel.update_sel(config, config["model"]) + trainer = get_trainer( config, FLAGS.init_model, diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index 6850c550fe..0245179d8b 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -9,6 +9,9 @@ from deepmd.pt.model.network.network import ( TypeEmbedNet, ) +from deepmd.pt.utils.update_sel import ( + UpdateSel, +) from deepmd.utils.path import ( DPPath, ) @@ -215,3 +218,17 @@ def forward( g1 = torch.cat([g1, g1_inp], dim=-1) return g1, rot_mat, g2, h2, sw + + @classmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict): + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + local_jdata_cpy = local_jdata.copy() + return UpdateSel().update_one_sel(global_jdata, local_jdata_cpy, True) diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index 55bb77b366..20a7c74cda 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -15,6 +15,9 @@ build_multiple_neighbor_list, get_multiple_nlist_key, ) +from deepmd.pt.utils.update_sel import ( + UpdateSel, +) from deepmd.utils.path import ( DPPath, ) @@ -396,3 +399,32 @@ def forward( if self.concat_output_tebd: g1 = torch.cat([g1, g1_inp], dim=-1) return g1, rot_mat, g2, h2, sw + + @classmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict): + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + local_jdata_cpy = local_jdata.copy() + update_sel = UpdateSel() + local_jdata_cpy = update_sel.update_one_sel( + global_jdata, + local_jdata_cpy, + True, + rcut_key="repinit_rcut", + sel_key="repinit_nsel", + ) + local_jdata_cpy = update_sel.update_one_sel( + global_jdata, + local_jdata_cpy, + True, + rcut_key="repformer_rcut", + sel_key="repformer_nsel", + ) + return local_jdata_cpy diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 8a211c977d..bb3cd30ff9 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -25,6 +25,9 @@ from deepmd.pt.utils.env_mat_stat import ( EnvMatStatSe, ) +from deepmd.pt.utils.update_sel import ( + UpdateSel, +) from deepmd.utils.env_mat_stat import ( StatItem, ) @@ -228,6 +231,20 @@ def t_cvt(xx): obj.sea.filter_layers = NetworkCollection.deserialize(embeddings) return obj + @classmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict): + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + local_jdata_cpy = local_jdata.copy() + return UpdateSel().update_one_sel(global_jdata, local_jdata_cpy, False) + @DescriptorBlock.register("se_e2_a") class DescrptBlockSeA(DescriptorBlock): diff --git a/deepmd/pt/model/descriptor/se_r.py b/deepmd/pt/model/descriptor/se_r.py index bdb7dafe73..1debcc8caf 100644 --- a/deepmd/pt/model/descriptor/se_r.py +++ b/deepmd/pt/model/descriptor/se_r.py @@ -30,6 +30,9 @@ from deepmd.pt.utils.exclude_mask import ( PairExcludeMask, ) +from deepmd.pt.utils.update_sel import ( + UpdateSel, +) from deepmd.utils.env_mat_stat import ( StatItem, ) @@ -319,3 +322,17 @@ def t_cvt(xx): obj["dstd"] = t_cvt(variables["dstd"]) obj.filter_layers = NetworkCollection.deserialize(embeddings) return obj + + @classmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict): + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + local_jdata_cpy = local_jdata.copy() + return UpdateSel().update_one_sel(global_jdata, local_jdata_cpy, False) diff --git a/deepmd/pt/model/model/dp_model.py b/deepmd/pt/model/model/dp_model.py index 79c129334a..0df45d4f84 100644 --- a/deepmd/pt/model/model/dp_model.py +++ b/deepmd/pt/model/model/dp_model.py @@ -2,6 +2,9 @@ from deepmd.pt.model.atomic_model import ( DPAtomicModel, ) +from deepmd.pt.model.descriptor.base_descriptor import ( + BaseDescriptor, +) from deepmd.pt.model.model.model import ( BaseModel, ) @@ -47,3 +50,20 @@ def __new__(cls, descriptor, fitting, *args, **kwargs): cls = PolarModel # else: unknown fitting type, fall back to DPModel return super().__new__(cls) + + @classmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict): + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + local_jdata_cpy = local_jdata.copy() + local_jdata_cpy["descriptor"] = BaseDescriptor.update_sel( + global_jdata, local_jdata["descriptor"] + ) + return local_jdata_cpy diff --git a/deepmd/pt/model/model/dp_zbl_model.py b/deepmd/pt/model/model/dp_zbl_model.py index c8264f2007..f2af0fff52 100644 --- a/deepmd/pt/model/model/dp_zbl_model.py +++ b/deepmd/pt/model/model/dp_zbl_model.py @@ -6,6 +6,9 @@ import torch +from deepmd.dpmodel.model.dp_model import ( + DPModel, +) from deepmd.pt.model.atomic_model import ( DPZBLLinearAtomicModel, ) @@ -97,3 +100,20 @@ def forward_lower( model_predict["dforce"] = model_ret["dforce"] model_predict = model_ret return model_predict + + @classmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict): + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + local_jdata_cpy = local_jdata.copy() + local_jdata_cpy["dpmodel"] = DPModel.update_sel( + global_jdata, local_jdata["dpmodel"] + ) + return local_jdata_cpy diff --git a/deepmd/pt/utils/update_sel.py b/deepmd/pt/utils/update_sel.py new file mode 100644 index 0000000000..2d077acac1 --- /dev/null +++ b/deepmd/pt/utils/update_sel.py @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Type, +) + +from deepmd.pt.utils.neighbor_stat import ( + NeighborStat, +) +from deepmd.utils.update_sel import ( + BaseUpdateSel, +) + + +class UpdateSel(BaseUpdateSel): + @property + def neighbor_stat(self) -> Type[NeighborStat]: + return NeighborStat + + def hook(self, min_nbor_dist, max_nbor_size): + # TODO: save to the model + pass diff --git a/deepmd/tf/descriptor/se.py b/deepmd/tf/descriptor/se.py index 857c8b28df..4232503464 100644 --- a/deepmd/tf/descriptor/se.py +++ b/deepmd/tf/descriptor/se.py @@ -18,6 +18,9 @@ get_embedding_net_variables_from_graph_def, get_tensor_by_name_from_graph, ) +from deepmd.tf.utils.update_sel import ( + UpdateSel, +) from .descriptor import ( Descriptor, @@ -161,13 +164,9 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): local_jdata : dict The local data refer to the current class """ - from deepmd.tf.entrypoints.train import ( - update_one_sel, - ) - # default behavior is to update sel which is a list local_jdata_cpy = local_jdata.copy() - return update_one_sel(global_jdata, local_jdata_cpy, False) + return UpdateSel().update_one_sel(global_jdata, local_jdata_cpy, False) def serialize_network( self, diff --git a/deepmd/tf/descriptor/se_atten.py b/deepmd/tf/descriptor/se_atten.py index 35b354c8da..4be5cbd164 100644 --- a/deepmd/tf/descriptor/se_atten.py +++ b/deepmd/tf/descriptor/se_atten.py @@ -57,6 +57,9 @@ from deepmd.tf.utils.tabulate import ( DPTabulate, ) +from deepmd.tf.utils.update_sel import ( + UpdateSel, +) from .descriptor import ( Descriptor, @@ -1453,9 +1456,5 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): local_jdata : dict The local data refer to the current class """ - from deepmd.tf.entrypoints.train import ( - update_one_sel, - ) - local_jdata_cpy = local_jdata.copy() - return update_one_sel(global_jdata, local_jdata_cpy, True) + return UpdateSel().update_one_sel(global_jdata, local_jdata_cpy, True) diff --git a/deepmd/tf/entrypoints/compress.py b/deepmd/tf/entrypoints/compress.py index b1273b92e1..1f2bbc93a0 100644 --- a/deepmd/tf/entrypoints/compress.py +++ b/deepmd/tf/entrypoints/compress.py @@ -29,13 +29,14 @@ get_tensor_by_name_from_graph, load_graph_def, ) +from deepmd.tf.utils.update_sel import ( + UpdateSel, +) from .freeze import ( freeze, ) from .train import ( - get_min_nbor_dist, - get_rcut, train, ) @@ -115,7 +116,10 @@ def compress( log.info("stage 0: compute the min_nbor_dist") jdata = j_loader(training_script) jdata = update_deepmd_input(jdata) - t_min_nbor_dist = get_min_nbor_dist(jdata, get_rcut(jdata)) + update_sel = UpdateSel() + t_min_nbor_dist = update_sel.get_min_nbor_dist( + jdata, update_sel.get_rcut(jdata) + ) _check_compress_type(graph) diff --git a/deepmd/tf/entrypoints/train.py b/deepmd/tf/entrypoints/train.py index 3759ff9331..e573423fc3 100755 --- a/deepmd/tf/entrypoints/train.py +++ b/deepmd/tf/entrypoints/train.py @@ -14,13 +14,10 @@ ) from deepmd.tf.common import ( - data_requirement, - expand_sys_str, j_loader, j_must_have, ) from deepmd.tf.env import ( - GLOBAL_ENER_FLOAT_PRECISION, reset_default_tf_session_config, tf, ) @@ -43,20 +40,14 @@ from deepmd.tf.utils.compat import ( update_deepmd_input, ) -from deepmd.tf.utils.data_system import ( - DeepmdDataSystem, -) from deepmd.tf.utils.finetune import ( replace_model_params_with_pretrained_model, ) from deepmd.tf.utils.multi_init import ( replace_model_params_with_frz_multi_model, ) -from deepmd.tf.utils.neighbor_stat import ( - NeighborStat, -) -from deepmd.tf.utils.path import ( - DPPath, +from deepmd.utils.data_system import ( + get_data, ) __all__ = ["train"] @@ -285,53 +276,6 @@ def _do_work(jdata: Dict[str, Any], run_opt: RunOptions, is_compress: bool = Fal log.info("finished compressing") -def get_data(jdata: Dict[str, Any], rcut, type_map, modifier, multi_task_mode=False): - systems = j_must_have(jdata, "systems") - if isinstance(systems, str): - systems = expand_sys_str(systems) - elif isinstance(systems, list): - systems = systems.copy() - help_msg = "Please check your setting for data systems" - # check length of systems - if len(systems) == 0: - msg = "cannot find valid a data system" - log.fatal(msg) - raise OSError(msg, help_msg) - # rougly check all items in systems are valid - for ii in systems: - ii = DPPath(ii) - if not ii.is_dir(): - msg = f"dir {ii} is not a valid dir" - log.fatal(msg) - raise OSError(msg, help_msg) - if not (ii / "type.raw").is_file(): - msg = f"dir {ii} is not a valid data system dir" - log.fatal(msg) - raise OSError(msg, help_msg) - - batch_size = j_must_have(jdata, "batch_size") - sys_probs = jdata.get("sys_probs", None) - auto_prob = jdata.get("auto_prob", "prob_sys_size") - optional_type_map = not multi_task_mode - - data = DeepmdDataSystem( - systems=systems, - batch_size=batch_size, - test_size=1, # to satisfy the old api - shuffle_test=True, # to satisfy the old api - rcut=rcut, - type_map=type_map, - optional_type_map=optional_type_map, - modifier=modifier, - trn_all_set=True, # sample from all sets - sys_probs=sys_probs, - auto_prob_style=auto_prob, - ) - data.add_dict(data_requirement) - - return data - - def get_modifier(modi_data=None): modifier: Optional[DipoleChargeModifier] if modi_data is not None: @@ -350,154 +294,6 @@ def get_modifier(modi_data=None): return modifier -def get_rcut(jdata): - if jdata["model"].get("type") == "pairwise_dprc": - return max( - jdata["model"]["qm_model"]["descriptor"]["rcut"], - jdata["model"]["qmmm_model"]["descriptor"]["rcut"], - ) - descrpt_data = jdata["model"]["descriptor"] - rcut_list = [] - if descrpt_data["type"] == "hybrid": - for ii in descrpt_data["list"]: - rcut_list.append(ii["rcut"]) - else: - rcut_list.append(descrpt_data["rcut"]) - return max(rcut_list) - - -def get_type_map(jdata): - return jdata["model"].get("type_map", None) - - -def get_nbor_stat(jdata, rcut, mixed_type: bool = False): - # it seems that DeepmdDataSystem does not need rcut - # it's not clear why there is an argument... - # max_rcut = get_rcut(jdata) - max_rcut = rcut - type_map = get_type_map(jdata) - - if type_map and len(type_map) == 0: - type_map = None - multi_task_mode = "data_dict" in jdata["training"] - if not multi_task_mode: - train_data = get_data( - jdata["training"]["training_data"], max_rcut, type_map, None - ) - train_data.get_batch() - else: - assert ( - type_map is not None - ), "Data stat in multi-task mode must have available type_map! " - train_data = None - for systems in jdata["training"]["data_dict"]: - tmp_data = get_data( - jdata["training"]["data_dict"][systems]["training_data"], - max_rcut, - type_map, - None, - ) - tmp_data.get_batch() - assert tmp_data.get_type_map(), f"In multi-task mode, 'type_map.raw' must be defined in data systems {systems}! " - if train_data is None: - train_data = tmp_data - else: - train_data.system_dirs += tmp_data.system_dirs - train_data.data_systems += tmp_data.data_systems - train_data.natoms += tmp_data.natoms - train_data.natoms_vec += tmp_data.natoms_vec - train_data.default_mesh += tmp_data.default_mesh - data_ntypes = train_data.get_ntypes() - if type_map is not None: - map_ntypes = len(type_map) - else: - map_ntypes = data_ntypes - ntypes = max([map_ntypes, data_ntypes]) - - neistat = NeighborStat(ntypes, rcut, mixed_type=mixed_type) - - min_nbor_dist, max_nbor_size = neistat.get_stat(train_data) - - # moved from traier.py as duplicated - # TODO: this is a simple fix but we should have a clear - # architecture to call neighbor stat - tf.constant( - min_nbor_dist, - name="train_attr/min_nbor_dist", - dtype=GLOBAL_ENER_FLOAT_PRECISION, - ) - tf.constant(max_nbor_size, name="train_attr/max_nbor_size", dtype=tf.int32) - return min_nbor_dist, max_nbor_size - - -def get_sel(jdata, rcut, mixed_type: bool = False): - _, max_nbor_size = get_nbor_stat(jdata, rcut, mixed_type=mixed_type) - return max_nbor_size - - -def get_min_nbor_dist(jdata, rcut): - min_nbor_dist, _ = get_nbor_stat(jdata, rcut) - return min_nbor_dist - - -def parse_auto_sel(sel): - if not isinstance(sel, str): - return False - words = sel.split(":") - if words[0] == "auto": - return True - else: - return False - - -def parse_auto_sel_ratio(sel): - if not parse_auto_sel(sel): - raise RuntimeError(f"invalid auto sel format {sel}") - else: - words = sel.split(":") - if len(words) == 1: - ratio = 1.1 - elif len(words) == 2: - ratio = float(words[1]) - else: - raise RuntimeError(f"invalid auto sel format {sel}") - return ratio - - -def wrap_up_4(xx): - return 4 * ((int(xx) + 3) // 4) - - -def update_one_sel(jdata, descriptor, mixed_type: bool = False): - rcut = descriptor["rcut"] - tmp_sel = get_sel( - jdata, - rcut, - mixed_type=mixed_type, - ) - sel = descriptor["sel"] - if isinstance(sel, int): - # convert to list and finnally convert back to int - sel = [sel] - if parse_auto_sel(descriptor["sel"]): - ratio = parse_auto_sel_ratio(descriptor["sel"]) - descriptor["sel"] = sel = [int(wrap_up_4(ii * ratio)) for ii in tmp_sel] - else: - # sel is set by user - for ii, (tt, dd) in enumerate(zip(tmp_sel, sel)): - if dd and tt > dd: - # we may skip warning for sel=0, where the user is likely - # to exclude such type in the descriptor - log.warning( - "sel of type %d is not enough! The expected value is " - "not less than %d, but you set it to %d. The accuracy" - " of your model may get worse." % (ii, tt, dd) - ) - if mixed_type: - descriptor["sel"] = sel = sum(sel) - return descriptor - - def update_sel(jdata): log.info( "Calculate neighbor statistics... (add --skip-neighbor-stat to skip this step)" diff --git a/deepmd/tf/model/pairtab.py b/deepmd/tf/model/pairtab.py index 2cb0dc6e52..3cc1114f81 100644 --- a/deepmd/tf/model/pairtab.py +++ b/deepmd/tf/model/pairtab.py @@ -29,6 +29,9 @@ from deepmd.tf.utils.pair_tab import ( PairTab, ) +from deepmd.tf.utils.update_sel import ( + UpdateSel, +) @Model.register("pairtab") @@ -281,9 +284,5 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict) -> dict: dict The updated local data """ - from deepmd.tf.entrypoints.train import ( - update_one_sel, - ) - local_jdata_cpy = local_jdata.copy() - return update_one_sel(global_jdata, local_jdata_cpy, True) + return UpdateSel().update_one_sel(global_jdata, local_jdata_cpy, True) diff --git a/deepmd/tf/model/pairwise_dprc.py b/deepmd/tf/model/pairwise_dprc.py index 5a377cdfa4..a67696ba97 100644 --- a/deepmd/tf/model/pairwise_dprc.py +++ b/deepmd/tf/model/pairwise_dprc.py @@ -31,6 +31,9 @@ from deepmd.tf.utils.type_embed import ( TypeEmbedNet, ) +from deepmd.tf.utils.update_sel import ( + UpdateSel, +) @Model.register("pairwise_dprc") @@ -413,13 +416,9 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): local_jdata : dict The local data refer to the current class """ - from deepmd.tf.entrypoints.train import ( - get_min_nbor_dist, - ) - # do not update sel; only find min distance # rcut is not important here - get_min_nbor_dist(global_jdata, 6.0) + UpdateSel().get_min_nbor_dist(global_jdata, 6.0) return local_jdata diff --git a/deepmd/tf/utils/update_sel.py b/deepmd/tf/utils/update_sel.py new file mode 100644 index 0000000000..bed6274f56 --- /dev/null +++ b/deepmd/tf/utils/update_sel.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Type, +) + +from deepmd.env import ( + GLOBAL_ENER_FLOAT_PRECISION, +) +from deepmd.tf.env import ( + tf, +) +from deepmd.tf.utils.neighbor_stat import ( + NeighborStat, +) +from deepmd.utils.update_sel import ( + BaseUpdateSel, +) + + +class UpdateSel(BaseUpdateSel): + @property + def neighbor_stat(self) -> Type[NeighborStat]: + return NeighborStat + + def hook(self, min_nbor_dist, max_nbor_size): + # moved from traier.py as duplicated + # TODO: this is a simple fix but we should have a clear + # architecture to call neighbor stat + tf.constant( + min_nbor_dist, + name="train_attr/min_nbor_dist", + dtype=GLOBAL_ENER_FLOAT_PRECISION, + ) + tf.constant(max_nbor_size, name="train_attr/max_nbor_size", dtype=tf.int32) diff --git a/deepmd/utils/data_system.py b/deepmd/utils/data_system.py index 592b1f9748..90b600548f 100644 --- a/deepmd/utils/data_system.py +++ b/deepmd/utils/data_system.py @@ -6,6 +6,8 @@ lru_cache, ) from typing import ( + Any, + Dict, List, Optional, ) @@ -14,6 +16,9 @@ import deepmd.utils.random as dp_random from deepmd.common import ( + data_requirement, + expand_sys_str, + j_must_have, make_default_mesh, ) from deepmd.env import ( @@ -25,6 +30,9 @@ from deepmd.utils.out_stat import ( compute_stats_from_redu, ) +from deepmd.utils.path import ( + DPPath, +) log = logging.getLogger(__name__) @@ -657,3 +665,72 @@ def prob_sys_size_ext(keywords, nsystems, nbatch): tmp_prob = [float(i) for i in nbatch_block] / np.sum(nbatch_block) sys_probs[block_stt[ii] : block_end[ii]] = tmp_prob * block_probs[ii] return sys_probs + + +def get_data( + jdata: Dict[str, Any], rcut, type_map, modifier, multi_task_mode=False +) -> DeepmdDataSystem: + """Get the data system. + + Parameters + ---------- + jdata + The json data + rcut + The cut-off radius, not used + type_map + The type map + modifier + The data modifier + multi_task_mode + If in multi task mode + + Returns + ------- + DeepmdDataSystem + The data system + """ + systems = j_must_have(jdata, "systems") + if isinstance(systems, str): + systems = expand_sys_str(systems) + elif isinstance(systems, list): + systems = systems.copy() + help_msg = "Please check your setting for data systems" + # check length of systems + if len(systems) == 0: + msg = "cannot find valid a data system" + log.fatal(msg) + raise OSError(msg, help_msg) + # rougly check all items in systems are valid + for ii in systems: + ii = DPPath(ii) + if not ii.is_dir(): + msg = f"dir {ii} is not a valid dir" + log.fatal(msg) + raise OSError(msg, help_msg) + if not (ii / "type.raw").is_file(): + msg = f"dir {ii} is not a valid data system dir" + log.fatal(msg) + raise OSError(msg, help_msg) + + batch_size = j_must_have(jdata, "batch_size") + sys_probs = jdata.get("sys_probs", None) + auto_prob = jdata.get("auto_prob", "prob_sys_size") + optional_type_map = not multi_task_mode + + data = DeepmdDataSystem( + systems=systems, + batch_size=batch_size, + test_size=1, # to satisfy the old api + shuffle_test=True, # to satisfy the old api + rcut=rcut, + type_map=type_map, + optional_type_map=optional_type_map, + modifier=modifier, + trn_all_set=True, # sample from all sets + sys_probs=sys_probs, + auto_prob_style=auto_prob, + ) + data.add_dict(data_requirement) + + return data diff --git a/deepmd/utils/update_sel.py b/deepmd/utils/update_sel.py new file mode 100644 index 0000000000..d1be8e8138 --- /dev/null +++ b/deepmd/utils/update_sel.py @@ -0,0 +1,170 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging +from abc import ( + abstractmethod, +) +from typing import ( + Type, +) + +from deepmd.utils.data_system import ( + get_data, +) +from deepmd.utils.neighbor_stat import ( + NeighborStat, +) + +log = logging.getLogger(__name__) + + +class BaseUpdateSel: + """Update the sel field in the descriptor.""" + + def update_one_sel( + self, + jdata, + descriptor, + mixed_type: bool = False, + rcut_key="rcut", + sel_key="sel", + ): + rcut = descriptor[rcut_key] + tmp_sel = self.get_sel( + jdata, + rcut, + mixed_type=mixed_type, + ) + sel = descriptor[sel_key] + if isinstance(sel, int): + # convert to list and finnally convert back to int + sel = [sel] + if self.parse_auto_sel(descriptor[sel_key]): + ratio = self.parse_auto_sel_ratio(descriptor[sel_key]) + descriptor[sel_key] = sel = [ + int(self.wrap_up_4(ii * ratio)) for ii in tmp_sel + ] + else: + # sel is set by user + for ii, (tt, dd) in enumerate(zip(tmp_sel, sel)): + if dd and tt > dd: + # we may skip warning for sel=0, where the user is likely + # to exclude such type in the descriptor + log.warning( + "sel of type %d is not enough! The expected value is " + "not less than %d, but you set it to %d. The accuracy" + " of your model may get worse." % (ii, tt, dd) + ) + if mixed_type: + descriptor[sel_key] = sum(sel) + return descriptor + + def parse_auto_sel(self, sel): + if not isinstance(sel, str): + return False + words = sel.split(":") + if words[0] == "auto": + return True + else: + return False + + def parse_auto_sel_ratio(self, sel): + if not self.parse_auto_sel(sel): + raise RuntimeError(f"invalid auto sel format {sel}") + else: + words = sel.split(":") + if len(words) == 1: + ratio = 1.1 + elif len(words) == 2: + ratio = float(words[1]) + else: + raise RuntimeError(f"invalid auto sel format {sel}") + return ratio + + def wrap_up_4(self, xx): + return 4 * ((int(xx) + 3) // 4) + + def get_sel(self, jdata, rcut, mixed_type: bool = False): + _, max_nbor_size = self.get_nbor_stat(jdata, rcut, mixed_type=mixed_type) + return max_nbor_size + + def get_rcut(self, jdata): + if jdata["model"].get("type") == "pairwise_dprc": + return max( + jdata["model"]["qm_model"]["descriptor"]["rcut"], + jdata["model"]["qmmm_model"]["descriptor"]["rcut"], + ) + descrpt_data = jdata["model"]["descriptor"] + rcut_list = [] + if descrpt_data["type"] == "hybrid": + for ii in descrpt_data["list"]: + rcut_list.append(ii["rcut"]) + else: + rcut_list.append(descrpt_data["rcut"]) + return max(rcut_list) + + def get_type_map(self, jdata): + return jdata["model"].get("type_map", None) + + def get_nbor_stat(self, jdata, rcut, mixed_type: bool = False): + # it seems that DeepmdDataSystem does not need rcut + # it's not clear why there is an argument... + # max_rcut = get_rcut(jdata) + max_rcut = rcut + type_map = self.get_type_map(jdata) + + if type_map and len(type_map) == 0: + type_map = None + multi_task_mode = "data_dict" in jdata["training"] + if not multi_task_mode: + train_data = get_data( + jdata["training"]["training_data"], max_rcut, type_map, None + ) + train_data.get_batch() + else: + assert ( + type_map is not None + ), "Data stat in multi-task mode must have available type_map! " + train_data = None + for systems in jdata["training"]["data_dict"]: + tmp_data = get_data( + jdata["training"]["data_dict"][systems]["training_data"], + max_rcut, + type_map, + None, + ) + tmp_data.get_batch() + assert tmp_data.get_type_map(), f"In multi-task mode, 'type_map.raw' must be defined in data systems {systems}! " + if train_data is None: + train_data = tmp_data + else: + train_data.system_dirs += tmp_data.system_dirs + train_data.data_systems += tmp_data.data_systems + train_data.natoms += tmp_data.natoms + train_data.natoms_vec += tmp_data.natoms_vec + train_data.default_mesh += tmp_data.default_mesh + data_ntypes = train_data.get_ntypes() + if type_map is not None: + map_ntypes = len(type_map) + else: + map_ntypes = data_ntypes + ntypes = max([map_ntypes, data_ntypes]) + + neistat = self.neighbor_stat(ntypes, rcut, mixed_type=mixed_type) + + min_nbor_dist, max_nbor_size = neistat.get_stat(train_data) + self.hook(min_nbor_dist, max_nbor_size) + + return min_nbor_dist, max_nbor_size + + @property + @abstractmethod + def neighbor_stat(self) -> Type[NeighborStat]: + pass + + @abstractmethod + def hook(self, min_nbor_dist, max_nbor_size): + pass + + def get_min_nbor_dist(self, jdata, rcut): + min_nbor_dist, _ = self.get_nbor_stat(jdata, rcut) + return min_nbor_dist diff --git a/source/tests/tf/test_train.py b/source/tests/tf/test_train.py index 3da62475ea..3e22dc57bc 100644 --- a/source/tests/tf/test_train.py +++ b/source/tests/tf/test_train.py @@ -5,46 +5,49 @@ ) from deepmd.tf.entrypoints.train import ( - parse_auto_sel, - parse_auto_sel_ratio, - update_one_sel, update_sel, - wrap_up_4, +) +from deepmd.tf.utils.update_sel import ( + UpdateSel, ) class TestTrain(unittest.TestCase): + def setUp(self) -> None: + self.update_sel = UpdateSel() + return super().setUp() + def test_train_parse_auto_sel(self): - self.assertTrue(parse_auto_sel("auto")) - self.assertTrue(parse_auto_sel("auto:12")) - self.assertTrue(parse_auto_sel("auto:12:13")) - self.assertFalse(parse_auto_sel([1, 2])) - self.assertFalse(parse_auto_sel("abc:12:13")) + self.assertTrue(self.update_sel.parse_auto_sel("auto")) + self.assertTrue(self.update_sel.parse_auto_sel("auto:12")) + self.assertTrue(self.update_sel.parse_auto_sel("auto:12:13")) + self.assertFalse(self.update_sel.parse_auto_sel([1, 2])) + self.assertFalse(self.update_sel.parse_auto_sel("abc:12:13")) def test_train_parse_auto_sel_ratio(self): - self.assertEqual(parse_auto_sel_ratio("auto"), 1.1) - self.assertEqual(parse_auto_sel_ratio("auto:1.2"), 1.2) + self.assertEqual(self.update_sel.parse_auto_sel_ratio("auto"), 1.1) + self.assertEqual(self.update_sel.parse_auto_sel_ratio("auto:1.2"), 1.2) with self.assertRaises(RuntimeError): - parse_auto_sel_ratio("auto:1.2:1.3") + self.update_sel.parse_auto_sel_ratio("auto:1.2:1.3") with self.assertRaises(RuntimeError): - parse_auto_sel_ratio("abc") + self.update_sel.parse_auto_sel_ratio("abc") with self.assertRaises(RuntimeError): - parse_auto_sel_ratio([1, 2, 3]) + self.update_sel.parse_auto_sel_ratio([1, 2, 3]) - @patch("deepmd.tf.entrypoints.train.get_sel") + @patch("deepmd.tf.utils.update_sel.UpdateSel.get_sel") def test_update_one_sel(self, sel_mock): sel_mock.return_value = [10, 20] jdata = {} descriptor = {"type": "se_e2_a", "rcut": 6, "sel": "auto"} - descriptor = update_one_sel(jdata, descriptor) + descriptor = self.update_sel.update_one_sel(jdata, descriptor) # self.assertEqual(descriptor['sel'], [11,22]) self.assertEqual(descriptor["sel"], [12, 24]) descriptor = {"type": "se_e2_a", "rcut": 6, "sel": "auto:1.5"} - descriptor = update_one_sel(jdata, descriptor) + descriptor = self.update_sel.update_one_sel(jdata, descriptor) # self.assertEqual(descriptor['sel'], [15,30]) self.assertEqual(descriptor["sel"], [16, 32]) - @patch("deepmd.tf.entrypoints.train.get_sel") + @patch("deepmd.tf.utils.update_sel.UpdateSel.get_sel") def test_update_sel_hybrid(self, sel_mock): sel_mock.return_value = [10, 20] jdata = { @@ -72,7 +75,7 @@ def test_update_sel_hybrid(self, sel_mock): jdata = update_sel(jdata) self.assertEqual(jdata, expected_out) - @patch("deepmd.tf.entrypoints.train.get_sel") + @patch("deepmd.tf.utils.update_sel.UpdateSel.get_sel") def test_update_sel(self, sel_mock): sel_mock.return_value = [10, 20] jdata = {"model": {"descriptor": {"type": "se_e2_a", "rcut": 6, "sel": "auto"}}} @@ -82,7 +85,7 @@ def test_update_sel(self, sel_mock): jdata = update_sel(jdata) self.assertEqual(jdata, expected_out) - @patch("deepmd.tf.entrypoints.train.get_sel") + @patch("deepmd.tf.utils.update_sel.UpdateSel.get_sel") def test_update_sel_atten_auto(self, sel_mock): sel_mock.return_value = [25] jdata = { @@ -106,7 +109,7 @@ def test_update_sel_atten_auto(self, sel_mock): jdata = update_sel(jdata) self.assertEqual(jdata, expected_out) - @patch("deepmd.tf.entrypoints.train.get_sel") + @patch("deepmd.tf.utils.update_sel.UpdateSel.get_sel") def test_update_sel_atten_int(self, sel_mock): sel_mock.return_value = [25] jdata = { @@ -130,7 +133,7 @@ def test_update_sel_atten_int(self, sel_mock): jdata = update_sel(jdata) self.assertEqual(jdata, expected_out) - @patch("deepmd.tf.entrypoints.train.get_sel") + @patch("deepmd.tf.utils.update_sel.UpdateSel.get_sel") def test_update_sel_atten_list(self, sel_mock): sel_mock.return_value = [25] jdata = { @@ -200,7 +203,7 @@ def test_skip_linear_frozen(self): jdata = update_sel(jdata) self.assertEqual(jdata, expected_out) - @patch("deepmd.tf.entrypoints.train.get_min_nbor_dist") + @patch("deepmd.tf.utils.update_sel.UpdateSel.get_min_nbor_dist") def test_pairwise_dprc(self, sel_mock): sel_mock.return_value = 0.5 jdata = { @@ -219,9 +222,9 @@ def test_pairwise_dprc(self, sel_mock): self.assertEqual(jdata, expected_out) def test_wrap_up_4(self): - self.assertEqual(wrap_up_4(12), 3 * 4) - self.assertEqual(wrap_up_4(13), 4 * 4) - self.assertEqual(wrap_up_4(14), 4 * 4) - self.assertEqual(wrap_up_4(15), 4 * 4) - self.assertEqual(wrap_up_4(16), 4 * 4) - self.assertEqual(wrap_up_4(17), 5 * 4) + self.assertEqual(self.update_sel.wrap_up_4(12), 3 * 4) + self.assertEqual(self.update_sel.wrap_up_4(13), 4 * 4) + self.assertEqual(self.update_sel.wrap_up_4(14), 4 * 4) + self.assertEqual(self.update_sel.wrap_up_4(15), 4 * 4) + self.assertEqual(self.update_sel.wrap_up_4(16), 4 * 4) + self.assertEqual(self.update_sel.wrap_up_4(17), 5 * 4) From 342b0c3da07e7b416f24028cd607cf47109eea10 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Thu, 29 Feb 2024 14:08:42 +0800 Subject: [PATCH 147/686] feat: support exclude atypes in atomic model (#3357) With this pr, we do not need to implement the exclude types for each descriptor and fitting. --------- Co-authored-by: Han Wang --- .../dpmodel/atomic_model/base_atomic_model.py | 83 ++++++++++++++++++- .../dpmodel/atomic_model/dp_atomic_model.py | 30 ++++--- .../atomic_model/linear_atomic_model.py | 33 ++++---- .../atomic_model/make_base_atomic_model.py | 10 +++ .../atomic_model/pairtab_atomic_model.py | 28 ++++--- deepmd/dpmodel/model/make_model.py | 2 +- deepmd/dpmodel/model/model.py | 2 + .../model/atomic_model/base_atomic_model.py | 79 ++++++++++++++++++ .../pt/model/atomic_model/dp_atomic_model.py | 41 ++++++--- .../model/atomic_model/linear_atomic_model.py | 36 ++++---- .../atomic_model/pairtab_atomic_model.py | 33 +++++--- deepmd/pt/model/descriptor/se_a.py | 18 +++- deepmd/pt/model/model/__init__.py | 15 +++- deepmd/pt/model/model/make_model.py | 2 +- deepmd/pt/model/task/fitting.py | 12 ++- deepmd/tf/model/model.py | 5 ++ deepmd/utils/argcheck.py | 17 ++++ .../common/dpmodel/test_dp_atomic_model.py | 15 ++-- source/tests/consistent/io/test_io.py | 9 ++ source/tests/consistent/model/test_ener.py | 20 +++++ source/tests/pt/model/test_deeppot.py | 6 ++ source/tests/pt/model/test_dp_atomic_model.py | 78 ++++++++++++++--- source/tests/pt/test_dp_test.py | 3 +- 23 files changed, 470 insertions(+), 107 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/base_atomic_model.py b/deepmd/dpmodel/atomic_model/base_atomic_model.py index b9521cde8e..09d33203a1 100644 --- a/deepmd/dpmodel/atomic_model/base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/base_atomic_model.py @@ -1,8 +1,89 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + List, + Optional, + Tuple, +) + import numpy as np +from deepmd.dpmodel.utils import ( + AtomExcludeMask, + PairExcludeMask, +) + from .make_base_atomic_model import ( make_base_atomic_model, ) -BaseAtomicModel = make_base_atomic_model(np.ndarray) +BaseAtomicModel_ = make_base_atomic_model(np.ndarray) + + +class BaseAtomicModel(BaseAtomicModel_): + def __init__( + self, + atom_exclude_types: List[int] = [], + pair_exclude_types: List[Tuple[int, int]] = [], + ): + super().__init__() + self.reinit_atom_exclude(atom_exclude_types) + self.reinit_pair_exclude(pair_exclude_types) + + def reinit_atom_exclude( + self, + exclude_types: List[int] = [], + ): + self.atom_exclude_types = exclude_types + if exclude_types == []: + self.atom_excl = None + else: + self.atom_excl = AtomExcludeMask(self.get_ntypes(), self.atom_exclude_types) + + def reinit_pair_exclude( + self, + exclude_types: List[Tuple[int, int]] = [], + ): + self.pair_exclude_types = exclude_types + if exclude_types == []: + self.pair_excl = None + else: + self.pair_excl = PairExcludeMask(self.get_ntypes(), self.pair_exclude_types) + + def forward_common_atomic( + self, + extended_coord: np.ndarray, + extended_atype: np.ndarray, + nlist: np.ndarray, + mapping: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + ) -> Dict[str, np.ndarray]: + _, nloc, _ = nlist.shape + atype = extended_atype[:, :nloc] + if self.pair_excl is not None: + pair_mask = self.pair_excl.build_type_exclude_mask(nlist, extended_atype) + # exclude neighbors in the nlist + nlist = np.where(pair_mask == 1, nlist, -1) + + ret_dict = self.forward_atomic( + extended_coord, + extended_atype, + nlist, + mapping=mapping, + fparam=fparam, + aparam=aparam, + ) + + if self.atom_excl is not None: + atom_mask = self.atom_excl.build_type_exclude_mask(atype) + for kk in ret_dict.keys(): + ret_dict[kk] = ret_dict[kk] * atom_mask[:, :, None] + + return ret_dict + + def serialize(self) -> dict: + return { + "atom_exclude_types": self.atom_exclude_types, + "pair_exclude_types": self.pair_exclude_types, + } diff --git a/deepmd/dpmodel/atomic_model/dp_atomic_model.py b/deepmd/dpmodel/atomic_model/dp_atomic_model.py index cd349749fa..96ef6d30ae 100644 --- a/deepmd/dpmodel/atomic_model/dp_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/dp_atomic_model.py @@ -46,11 +46,12 @@ def __init__( descriptor, fitting, type_map: Optional[List[str]] = None, + **kwargs, ): - super().__init__() self.type_map = type_map self.descriptor = descriptor self.fitting = fitting + super().__init__(**kwargs) def fitting_output_def(self) -> FittingOutputDef: """Get the output def of the fitting net.""" @@ -132,14 +133,18 @@ def forward_atomic( return ret def serialize(self) -> dict: - return { - "@class": "Model", - "type": "standard", - "@version": 1, - "type_map": self.type_map, - "descriptor": self.descriptor.serialize(), - "fitting": self.fitting.serialize(), - } + dd = super().serialize() + dd.update( + { + "@class": "Model", + "type": "standard", + "@version": 1, + "type_map": self.type_map, + "descriptor": self.descriptor.serialize(), + "fitting": self.fitting.serialize(), + } + ) + return dd @classmethod def deserialize(cls, data) -> "DPAtomicModel": @@ -147,9 +152,10 @@ def deserialize(cls, data) -> "DPAtomicModel": check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") - descriptor_obj = BaseDescriptor.deserialize(data["descriptor"]) - fitting_obj = BaseFitting.deserialize(data["fitting"]) - obj = cls(descriptor_obj, fitting_obj, type_map=data["type_map"]) + descriptor_obj = BaseDescriptor.deserialize(data.pop("descriptor")) + fitting_obj = BaseFitting.deserialize(data.pop("fitting")) + type_map = data.pop("type_map", None) + obj = cls(descriptor_obj, fitting_obj, type_map=type_map, **data) return obj def get_dim_fparam(self) -> int: diff --git a/deepmd/dpmodel/atomic_model/linear_atomic_model.py b/deepmd/dpmodel/atomic_model/linear_atomic_model.py index 6d8aea499e..03c1249d4b 100644 --- a/deepmd/dpmodel/atomic_model/linear_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/linear_atomic_model.py @@ -52,9 +52,9 @@ def __init__( models: List[BaseAtomicModel], **kwargs, ): - super().__init__() self.models = models self.mixed_types_list = [model.mixed_types() for model in self.models] + super().__init__(**kwargs) def mixed_types(self) -> bool: """If true, the model @@ -273,15 +273,19 @@ def __init__( self.smin_alpha = smin_alpha def serialize(self) -> dict: - return { - "@class": "Model", - "type": "zbl", - "@version": 1, - "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), - "sw_rmin": self.sw_rmin, - "sw_rmax": self.sw_rmax, - "smin_alpha": self.smin_alpha, - } + dd = BaseAtomicModel.serialize(self) + dd.update( + { + "@class": "Model", + "type": "zbl", + "@version": 1, + "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), + "sw_rmin": self.sw_rmin, + "sw_rmax": self.sw_rmax, + "smin_alpha": self.smin_alpha, + } + ) + return dd @classmethod def deserialize(cls, data) -> "DPZBLLinearAtomicModel": @@ -289,11 +293,11 @@ def deserialize(cls, data) -> "DPZBLLinearAtomicModel": check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") - sw_rmin = data["sw_rmin"] - sw_rmax = data["sw_rmax"] - smin_alpha = data["smin_alpha"] + sw_rmin = data.pop("sw_rmin") + sw_rmax = data.pop("sw_rmax") + smin_alpha = data.pop("smin_alpha") - dp_model, zbl_model = LinearAtomicModel.deserialize(data["models"]) + dp_model, zbl_model = LinearAtomicModel.deserialize(data.pop("models")) return cls( dp_model=dp_model, @@ -301,6 +305,7 @@ def deserialize(cls, data) -> "DPZBLLinearAtomicModel": sw_rmin=sw_rmin, sw_rmax=sw_rmax, smin_alpha=smin_alpha, + **data, ) def _compute_weight( diff --git a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py index d4186c990d..df6e39dd2e 100644 --- a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py @@ -48,6 +48,16 @@ def get_rcut(self) -> float: def get_type_map(self) -> Optional[List[str]]: """Get the type map.""" + def get_ntypes(self) -> int: + """Get the number of atom types.""" + tmap = self.get_type_map() + if tmap is not None: + return len(tmap) + else: + raise ValueError( + "cannot infer the number of types from a None type map" + ) + @abstractmethod def get_sel(self) -> List[int]: """Returns the number of selected atoms for each type.""" diff --git a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py index ddece80f2d..5469ee80d2 100644 --- a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py @@ -109,14 +109,18 @@ def mixed_types(self) -> bool: return True def serialize(self) -> dict: - return { - "@class": "Model", - "type": "pairtab", - "@version": 1, - "tab": self.tab.serialize(), - "rcut": self.rcut, - "sel": self.sel, - } + dd = BaseAtomicModel.serialize(self) + dd.update( + { + "@class": "Model", + "type": "pairtab", + "@version": 1, + "tab": self.tab.serialize(), + "rcut": self.rcut, + "sel": self.sel, + } + ) + return dd @classmethod def deserialize(cls, data) -> "PairTabAtomicModel": @@ -124,10 +128,10 @@ def deserialize(cls, data) -> "PairTabAtomicModel": check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") - rcut = data["rcut"] - sel = data["sel"] - tab = PairTab.deserialize(data["tab"]) - tab_model = cls(None, rcut, sel) + rcut = data.pop("rcut") + sel = data.pop("sel") + tab = PairTab.deserialize(data.pop("tab")) + tab_model = cls(None, rcut, sel, **data) tab_model.tab = tab tab_model.tab_info = tab_model.tab.tab_info tab_model.tab_data = tab_model.tab.tab_data diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py index 1261906148..e8b1ecc390 100644 --- a/deepmd/dpmodel/model/make_model.py +++ b/deepmd/dpmodel/model/make_model.py @@ -213,7 +213,7 @@ def call_lower( extended_coord, fparam=fparam, aparam=aparam ) del extended_coord, fparam, aparam - atomic_ret = self.forward_atomic( + atomic_ret = self.forward_common_atomic( cc_ext, extended_atype, nlist, diff --git a/deepmd/dpmodel/model/model.py b/deepmd/dpmodel/model/model.py index 4a6e269f25..6f06785c56 100644 --- a/deepmd/dpmodel/model/model.py +++ b/deepmd/dpmodel/model/model.py @@ -38,4 +38,6 @@ def get_model(data: dict) -> DPModel: descriptor=descriptor, fitting=fitting, type_map=data["type_map"], + atom_exclude_types=data.get("atom_exclude_types", []), + pair_exclude_types=data.get("pair_exclude_types", []), ) diff --git a/deepmd/pt/model/atomic_model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py index 1e5f976baf..d6de3dfc88 100644 --- a/deepmd/pt/model/atomic_model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -1,16 +1,56 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + List, + Optional, + Tuple, +) + import torch from deepmd.dpmodel.atomic_model import ( make_base_atomic_model, ) +from deepmd.pt.utils import ( + AtomExcludeMask, + PairExcludeMask, +) BaseAtomicModel_ = make_base_atomic_model(torch.Tensor) class BaseAtomicModel(BaseAtomicModel_): + def __init__( + self, + atom_exclude_types: List[int] = [], + pair_exclude_types: List[Tuple[int, int]] = [], + ): + super().__init__() + self.reinit_atom_exclude(atom_exclude_types) + self.reinit_pair_exclude(pair_exclude_types) + + def reinit_atom_exclude( + self, + exclude_types: List[int] = [], + ): + self.atom_exclude_types = exclude_types + if exclude_types == []: + self.atom_excl = None + else: + self.atom_excl = AtomExcludeMask(self.get_ntypes(), self.atom_exclude_types) + + def reinit_pair_exclude( + self, + exclude_types: List[Tuple[int, int]] = [], + ): + self.pair_exclude_types = exclude_types + if exclude_types == []: + self.pair_excl = None + else: + self.pair_excl = PairExcludeMask(self.get_ntypes(), self.pair_exclude_types) + # export public methods that are not abstract get_nsel = torch.jit.export(BaseAtomicModel_.get_nsel) get_nnei = torch.jit.export(BaseAtomicModel_.get_nnei) @@ -18,3 +58,42 @@ class BaseAtomicModel(BaseAtomicModel_): @torch.jit.export def get_model_def_script(self) -> str: return self.model_def_script + + def forward_common_atomic( + self, + extended_coord: torch.Tensor, + extended_atype: torch.Tensor, + nlist: torch.Tensor, + mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + ) -> Dict[str, torch.Tensor]: + _, nloc, _ = nlist.shape + atype = extended_atype[:, :nloc] + + if self.pair_excl is not None: + pair_mask = self.pair_excl(nlist, extended_atype) + # exclude neighbors in the nlist + nlist = torch.where(pair_mask == 1, nlist, -1) + + ret_dict = self.forward_atomic( + extended_coord, + extended_atype, + nlist, + mapping=mapping, + fparam=fparam, + aparam=aparam, + ) + + if self.atom_excl is not None: + atom_mask = self.atom_excl(atype) + for kk in ret_dict.keys(): + ret_dict[kk] = ret_dict[kk] * atom_mask[:, :, None] + + return ret_dict + + def serialize(self) -> dict: + return { + "atom_exclude_types": self.atom_exclude_types, + "pair_exclude_types": self.pair_exclude_types, + } diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index d2c1743d30..63e91ff428 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -49,8 +49,14 @@ class DPAtomicModel(torch.nn.Module, BaseAtomicModel): For example `type_map[1]` gives the name of the type 1. """ - def __init__(self, descriptor, fitting, type_map: Optional[List[str]]): - super().__init__() + def __init__( + self, + descriptor, + fitting, + type_map: Optional[List[str]], + **kwargs, + ): + torch.nn.Module.__init__(self) self.model_def_script = "" ntypes = len(type_map) self.type_map = type_map @@ -59,6 +65,8 @@ def __init__(self, descriptor, fitting, type_map: Optional[List[str]]): self.rcut = self.descriptor.get_rcut() self.sel = self.descriptor.get_sel() self.fitting_net = fitting + # order matters ntypes and type_map should be initialized first. + BaseAtomicModel.__init__(self, **kwargs) def fitting_output_def(self) -> FittingOutputDef: """Get the output def of the fitting net.""" @@ -95,22 +103,29 @@ def mixed_types(self) -> bool: return self.descriptor.mixed_types() def serialize(self) -> dict: - return { - "@class": "Model", - "type": "standard", - "@version": 1, - "type_map": self.type_map, - "descriptor": self.descriptor.serialize(), - "fitting": self.fitting_net.serialize(), - } + dd = BaseAtomicModel.serialize(self) + dd.update( + { + "@class": "Model", + "@version": 1, + "type": "standard", + "type_map": self.type_map, + "descriptor": self.descriptor.serialize(), + "fitting": self.fitting_net.serialize(), + } + ) + return dd @classmethod def deserialize(cls, data) -> "DPAtomicModel": data = copy.deepcopy(data) check_version_compatibility(data.pop("@version", 1), 1, 1) - descriptor_obj = BaseDescriptor.deserialize(data["descriptor"]) - fitting_obj = BaseFitting.deserialize(data["fitting"]) - obj = cls(descriptor_obj, fitting_obj, type_map=data["type_map"]) + data.pop("@class", None) + data.pop("type", None) + descriptor_obj = BaseDescriptor.deserialize(data.pop("descriptor")) + fitting_obj = BaseFitting.deserialize(data.pop("fitting")) + type_map = data.pop("type_map", None) + obj = cls(descriptor_obj, fitting_obj, type_map=type_map, **data) return obj def forward_atomic( diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 52f5f1d13c..5efbe533da 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -54,10 +54,11 @@ def __init__( models: List[BaseAtomicModel], **kwargs, ): - super().__init__() + torch.nn.Module.__init__(self) self.models = torch.nn.ModuleList(models) self.atomic_bias = None self.mixed_types_list = [model.mixed_types() for model in self.models] + BaseAtomicModel.__init__(self, **kwargs) def mixed_types(self) -> bool: """If true, the model @@ -307,32 +308,39 @@ def __init__( self.zbl_weight = torch.empty(0, dtype=torch.float64, device=env.DEVICE) def serialize(self) -> dict: - return { - "@class": "Model", - "type": "zbl", - "@version": 1, - "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), - "sw_rmin": self.sw_rmin, - "sw_rmax": self.sw_rmax, - "smin_alpha": self.smin_alpha, - } + dd = BaseAtomicModel.serialize(self) + dd.update( + { + "@class": "Model", + "@version": 1, + "type": "zbl", + "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), + "sw_rmin": self.sw_rmin, + "sw_rmax": self.sw_rmax, + "smin_alpha": self.smin_alpha, + } + ) + return dd @classmethod def deserialize(cls, data) -> "DPZBLLinearAtomicModel": data = copy.deepcopy(data) check_version_compatibility(data.pop("@version", 1), 1, 1) - sw_rmin = data["sw_rmin"] - sw_rmax = data["sw_rmax"] - smin_alpha = data["smin_alpha"] + sw_rmin = data.pop("sw_rmin") + sw_rmax = data.pop("sw_rmax") + smin_alpha = data.pop("smin_alpha") - dp_model, zbl_model = LinearAtomicModel.deserialize(data["models"]) + dp_model, zbl_model = LinearAtomicModel.deserialize(data.pop("models")) + data.pop("@class", None) + data.pop("type", None) return cls( dp_model=dp_model, zbl_model=zbl_model, sw_rmin=sw_rmin, sw_rmax=sw_rmax, smin_alpha=smin_alpha, + **data, ) def _compute_weight( diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index c0b7c65d7a..47a20d3be9 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -52,11 +52,12 @@ class PairTabAtomicModel(torch.nn.Module, BaseAtomicModel): def __init__( self, tab_file: str, rcut: float, sel: Union[int, List[int]], **kwargs ): - super().__init__() + torch.nn.Module.__init__(self) self.model_def_script = "" self.tab_file = tab_file self.rcut = rcut self.tab = self._set_pairtab(tab_file, rcut) + BaseAtomicModel.__init__(self, **kwargs) # handle deserialization with no input file if self.tab_file is not None: @@ -125,23 +126,29 @@ def mixed_types(self) -> bool: return True def serialize(self) -> dict: - return { - "@class": "Model", - "type": "pairtab", - "@version": 1, - "tab": self.tab.serialize(), - "rcut": self.rcut, - "sel": self.sel, - } + dd = BaseAtomicModel.serialize(self) + dd.update( + { + "@class": "Model", + "@version": 1, + "type": "pairtab", + "tab": self.tab.serialize(), + "rcut": self.rcut, + "sel": self.sel, + } + ) + return dd @classmethod def deserialize(cls, data) -> "PairTabAtomicModel": data = copy.deepcopy(data) check_version_compatibility(data.pop("@version", 1), 1, 1) - rcut = data["rcut"] - sel = data["sel"] - tab = PairTab.deserialize(data["tab"]) - tab_model = cls(None, rcut, sel) + rcut = data.pop("rcut") + sel = data.pop("sel") + tab = PairTab.deserialize(data.pop("tab")) + data.pop("@class", None) + data.pop("type", None) + tab_model = cls(None, rcut, sel, **data) tab_model.tab = tab tab_model.register_buffer("tab_info", torch.from_numpy(tab_model.tab.tab_info)) tab_model.register_buffer("tab_data", torch.from_numpy(tab_model.tab.tab_data)) diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index bb3cd30ff9..fc2cf60531 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -136,6 +136,13 @@ def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None) """Update mean and stddev for descriptor elements.""" return self.sea.compute_input_stats(merged, path) + def reinit_exclude( + self, + exclude_types: List[Tuple[int, int]] = [], + ): + """Update the type exclusions.""" + self.sea.reinit_exclude(exclude_types) + def forward( self, coord_ext: torch.Tensor, @@ -288,10 +295,10 @@ def __init__( self.prec = PRECISION_DICT[self.precision] self.resnet_dt = resnet_dt self.old_impl = old_impl - self.exclude_types = exclude_types self.ntypes = len(sel) - self.emask = PairExcludeMask(len(sel), exclude_types=exclude_types) self.type_one_side = type_one_side + # order matters, placed after the assignment of self.ntypes + self.reinit_exclude(exclude_types) self.sel = sel self.sec = torch.tensor( @@ -424,6 +431,13 @@ def get_stats(self) -> Dict[str, StatItem]: ) return self.stats + def reinit_exclude( + self, + exclude_types: List[Tuple[int, int]] = [], + ): + self.exclude_types = exclude_types + self.emask = PairExcludeMask(self.ntypes, exclude_types=exclude_types) + def forward( self, nlist: torch.Tensor, diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index b823a051f5..87eb391a7e 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -71,11 +71,15 @@ def get_zbl_model(model_params): rmin = model_params["sw_rmin"] rmax = model_params["sw_rmax"] + atom_exclude_types = model_params.get("atom_exclude_types", []) + pair_exclude_types = model_params.get("pair_exclude_types", []) return DPZBLModel( dp_model, pt_model, rmin, rmax, + atom_exclude_types=atom_exclude_types, + pair_exclude_types=pair_exclude_types, ) @@ -98,7 +102,16 @@ def get_model(model_params): if "ener" in fitting_net["type"]: fitting_net["return_energy"] = True fitting = BaseFitting(**fitting_net) - model = DPModel(descriptor, fitting, type_map=model_params["type_map"]) + atom_exclude_types = model_params.get("atom_exclude_types", []) + pair_exclude_types = model_params.get("pair_exclude_types", []) + + model = DPModel( + descriptor, + fitting, + type_map=model_params["type_map"], + atom_exclude_types=atom_exclude_types, + pair_exclude_types=pair_exclude_types, + ) model.model_def_script = json.dumps(model_params) return model diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 3efd3fb046..98f0a18241 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -208,7 +208,7 @@ def forward_common_lower( extended_coord, fparam=fparam, aparam=aparam ) del extended_coord, fparam, aparam - atomic_ret = self.forward_atomic( + atomic_ret = self.forward_common_atomic( cc_ext, extended_atype, nlist, diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 20876d9be7..8e8338210f 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -280,9 +280,8 @@ def __init__( self.precision = precision self.prec = PRECISION_DICT[self.precision] self.rcond = rcond - self.exclude_types = exclude_types - - self.emask = AtomExcludeMask(self.ntypes, self.exclude_types) + # order matters, should be place after the assignment of ntypes + self.reinit_exclude(exclude_types) net_dim_out = self._net_out_dim() # init constants @@ -358,6 +357,13 @@ def __init__( log.info("Set seed to %d in fitting net.", seed) torch.manual_seed(seed) + def reinit_exclude( + self, + exclude_types: List[int] = [], + ): + self.exclude_types = exclude_types + self.emask = AtomExcludeMask(self.ntypes, self.exclude_types) + def serialize(self) -> dict: """Serialize the fitting to dict.""" return { diff --git a/deepmd/tf/model/model.py b/deepmd/tf/model/model.py index 889f7ccc4d..ca660f8e95 100644 --- a/deepmd/tf/model/model.py +++ b/deepmd/tf/model/model.py @@ -784,6 +784,8 @@ def deserialize(cls, data: dict, suffix: str = "") -> "Descriptor": check_version_compatibility(data.pop("@version", 1), 1, 1) descriptor = Descriptor.deserialize(data.pop("descriptor"), suffix=suffix) fitting = Fitting.deserialize(data.pop("fitting"), suffix=suffix) + data.pop("atom_exclude_types") + data.pop("pair_exclude_types") return cls( descriptor=descriptor, fitting_net=fitting, @@ -814,4 +816,7 @@ def serialize(self, suffix: str = "") -> dict: "type_map": self.type_map, "descriptor": self.descrpt.serialize(suffix=suffix), "fitting": self.fitting.serialize(suffix=suffix), + # not supported yet + "atom_exclude_types": [], + "pair_exclude_types": [], } diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 8366f7bb38..8e3196cba1 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -1182,6 +1182,9 @@ def model_args(exclude_hybrid=False): doc_srtab_add_bias = "Whether add energy bias from the statistics of the data to short-range tabulated atomic energy. It only takes effect when `use_srtab` is provided." doc_compress_config = "Model compression configurations" doc_spin = "The settings for systems with spin." + doc_atom_exclude_types = "Exclude the atomic contribution of the listed atom types" + doc_pair_exclude_types = "The atom pairs of the listed types are not treated to be neighbors, i.e. they do not see each other." + hybrid_models = [] if not exclude_hybrid: hybrid_models.extend( @@ -1234,6 +1237,20 @@ def model_args(exclude_hybrid=False): Argument( "sw_rmax", float, optional=True, doc=doc_only_tf_supported + doc_sw_rmax ), + Argument( + "pair_exclude_types", + list, + optional=True, + default=[], + doc=doc_only_pt_supported + doc_pair_exclude_types, + ), + Argument( + "atom_exclude_types", + list, + optional=True, + default=[], + doc=doc_only_pt_supported + doc_atom_exclude_types, + ), Argument( "srtab_add_bias", bool, diff --git a/source/tests/common/dpmodel/test_dp_atomic_model.py b/source/tests/common/dpmodel/test_dp_atomic_model.py index b32c8ae11a..f97299cf72 100644 --- a/source/tests/common/dpmodel/test_dp_atomic_model.py +++ b/source/tests/common/dpmodel/test_dp_atomic_model.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import itertools import unittest import numpy as np @@ -38,10 +39,14 @@ def test_self_consistency( mixed_types=ds.mixed_types(), ) type_map = ["foo", "bar"] - md0 = DPAtomicModel(ds, ft, type_map=type_map) - md1 = DPAtomicModel.deserialize(md0.serialize()) - ret0 = md0.forward_atomic(self.coord_ext, self.atype_ext, self.nlist) - ret1 = md1.forward_atomic(self.coord_ext, self.atype_ext, self.nlist) + for atom_excl, pair_excl in itertools.product([[], [1]], [[], [[0, 1]]]): + md0 = DPAtomicModel(ds, ft, type_map=type_map) + md0.reinit_atom_exclude(atom_excl) + md0.reinit_pair_exclude(pair_excl) + md1 = DPAtomicModel.deserialize(md0.serialize()) - np.testing.assert_allclose(ret0["energy"], ret1["energy"]) + ret0 = md0.forward_common_atomic(self.coord_ext, self.atype_ext, self.nlist) + ret1 = md1.forward_common_atomic(self.coord_ext, self.atype_ext, self.nlist) + + np.testing.assert_allclose(ret0["energy"], ret1["energy"]) diff --git a/source/tests/consistent/io/test_io.py b/source/tests/consistent/io/test_io.py index be599b0805..71e4002128 100644 --- a/source/tests/consistent/io/test_io.py +++ b/source/tests/consistent/io/test_io.py @@ -57,6 +57,12 @@ def save_data_to_model(self, model_file: str, data: dict) -> None: out_hook = out_backend.deserialize_hook out_hook(model_file, data) + def tearDown(self): + prefix = "test_consistent_io_" + self.__class__.__name__.lower() + for ii in Path(".").glob(prefix + ".*"): + if Path(ii).exists(): + Path(ii).unlink() + def test_data_equal(self): prefix = "test_consistent_io_" + self.__class__.__name__.lower() for backend_name in ("tensorflow", "pytorch", "dpmodel"): @@ -173,3 +179,6 @@ def setUp(self): "backend": "test", "model_def_script": model_def_script, } + + def tearDown(self): + IOTest.tearDown(self) diff --git a/source/tests/consistent/model/test_ener.py b/source/tests/consistent/model/test_ener.py index b3aa778ca0..da5033a3b6 100644 --- a/source/tests/consistent/model/test_ener.py +++ b/source/tests/consistent/model/test_ener.py @@ -17,6 +17,7 @@ INSTALLED_PT, INSTALLED_TF, CommonTest, + parameterized, ) from .common import ( ModelTest, @@ -37,11 +38,24 @@ ) +@parameterized( + ( + [], + [[0, 1]], + ), + ( + [], + [1], + ), +) class TestEner(CommonTest, ModelTest, unittest.TestCase): @property def data(self) -> dict: + pair_exclude_types, atom_exclude_types = self.param return { "type_map": ["O", "H"], + "pair_exclude_types": pair_exclude_types, + "atom_exclude_types": atom_exclude_types, "descriptor": { "type": "se_e2_a", "sel": [20, 20], @@ -73,6 +87,12 @@ def data(self) -> dict: pt_class = EnergyModelPT args = model_args() + def skip_tf(self): + return ( + self.data["pair_exclude_types"] != [] + or self.data["atom_exclude_types"] != [] + ) + def pass_data_to_cls(self, cls, data) -> Any: """Pass data to the class.""" data = data.copy() diff --git a/source/tests/pt/model/test_deeppot.py b/source/tests/pt/model/test_deeppot.py index 102e1f6b0c..697ebb6411 100644 --- a/source/tests/pt/model/test_deeppot.py +++ b/source/tests/pt/model/test_deeppot.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import json +import os import unittest from argparse import ( Namespace, @@ -53,6 +54,11 @@ def setUp(self): trainer.wrapper(**input_dict, label=label_dict, cur_lr=1.0) self.model = "model.pt" + def tearDown(self): + for f in os.listdir("."): + if f in ["lcurve.out", self.input_json]: + os.remove(f) + def test_dp_test(self): dp = DeepPot(str(self.model)) cell = np.array( diff --git a/source/tests/pt/model/test_dp_atomic_model.py b/source/tests/pt/model/test_dp_atomic_model.py index bb0d20ab02..88bb3ab763 100644 --- a/source/tests/pt/model/test_dp_atomic_model.py +++ b/source/tests/pt/model/test_dp_atomic_model.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import itertools import unittest import numpy as np @@ -50,17 +51,27 @@ def test_self_consistency(self): mixed_types=ds.mixed_types(), ).to(env.DEVICE) type_map = ["foo", "bar"] - md0 = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) - md1 = DPAtomicModel.deserialize(md0.serialize()).to(env.DEVICE) - args = [ - to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] - ] - ret0 = md0.forward_atomic(*args) - ret1 = md1.forward_atomic(*args) - np.testing.assert_allclose( - to_numpy_array(ret0["energy"]), - to_numpy_array(ret1["energy"]), - ) + + # test the case of exclusion + for atom_excl, pair_excl in itertools.product([[], [1]], [[], [[0, 1]]]): + md0 = DPAtomicModel( + ds, + ft, + type_map=type_map, + ).to(env.DEVICE) + md0.reinit_atom_exclude(atom_excl) + md0.reinit_pair_exclude(pair_excl) + md1 = DPAtomicModel.deserialize(md0.serialize()).to(env.DEVICE) + args = [ + to_torch_tensor(ii) + for ii in [self.coord_ext, self.atype_ext, self.nlist] + ] + ret0 = md0.forward_common_atomic(*args) + ret1 = md1.forward_common_atomic(*args) + np.testing.assert_allclose( + to_numpy_array(ret0["energy"]), + to_numpy_array(ret1["energy"]), + ) def test_dp_consistency(self): rng = np.random.default_rng() @@ -84,8 +95,8 @@ def test_dp_consistency(self): args1 = [ to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] ] - ret0 = md0.forward_atomic(*args0) - ret1 = md1.forward_atomic(*args1) + ret0 = md0.forward_common_atomic(*args0) + ret1 = md1.forward_common_atomic(*args1) np.testing.assert_allclose( ret0["energy"], to_numpy_array(ret1["energy"]), @@ -110,3 +121,44 @@ def test_jit(self): md0 = torch.jit.script(md0) self.assertEqual(md0.get_rcut(), self.rcut) self.assertEqual(md0.get_type_map(), type_map) + + def test_excl_consistency(self): + type_map = ["foo", "bar"] + + # test the case of exclusion + for atom_excl, pair_excl in itertools.product([[], [1]], [[], [[0, 1]]]): + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ).to(env.DEVICE) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + mixed_types=ds.mixed_types(), + ).to(env.DEVICE) + md0 = DPAtomicModel( + ds, + ft, + type_map=type_map, + ).to(env.DEVICE) + md1 = DPAtomicModel.deserialize(md0.serialize()).to(env.DEVICE) + + md0.reinit_atom_exclude(atom_excl) + md0.reinit_pair_exclude(pair_excl) + # hacking! + md1.descriptor.reinit_exclude(pair_excl) + md1.fitting_net.reinit_exclude(atom_excl) + + args = [ + to_torch_tensor(ii) + for ii in [self.coord_ext, self.atype_ext, self.nlist] + ] + ret0 = md0.forward_common_atomic(*args) + ret1 = md1.forward_common_atomic(*args) + np.testing.assert_allclose( + to_numpy_array(ret0["energy"]), + to_numpy_array(ret1["energy"]), + ) diff --git a/source/tests/pt/test_dp_test.py b/source/tests/pt/test_dp_test.py index 8d7dc9cd58..08bd2ce623 100644 --- a/source/tests/pt/test_dp_test.py +++ b/source/tests/pt/test_dp_test.py @@ -62,11 +62,10 @@ def tearDown(self): for f in os.listdir("."): if f.startswith("model") and f.endswith(".pt"): os.remove(f) - if f in ["lcurve.out"]: + if f in ["lcurve.out", self.input_json]: os.remove(f) if f in ["stat_files"]: shutil.rmtree(f) - os.remove(self.input_json) if __name__ == "__main__": From 665d7169f44b4d3fcd44552f2c995ce891a750f7 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 29 Feb 2024 03:18:58 -0500 Subject: [PATCH 148/686] feat(pt): support fparam/aparam in C++ DeepPot (#3358) Signed-off-by: Jinzhe Zeng --- source/api_cc/include/DeepPotPT.h | 12 +- source/api_cc/src/DeepPotPT.cc | 74 +++- .../tests/test_deeppot_a_fparam_aparam_pt.cc | 384 ++++++++++++++++++ source/tests/infer/fparam_aparam.pth | Bin 105871 -> 108888 bytes 4 files changed, 448 insertions(+), 22 deletions(-) create mode 100644 source/api_cc/tests/test_deeppot_a_fparam_aparam_pt.cc diff --git a/source/api_cc/include/DeepPotPT.h b/source/api_cc/include/DeepPotPT.h index 1b757069c3..d50d338d33 100644 --- a/source/api_cc/include/DeepPotPT.h +++ b/source/api_cc/include/DeepPotPT.h @@ -69,9 +69,9 @@ class DeepPotPT : public DeepPotBase { std::vector& atom_virial, const std::vector& coord, const std::vector& atype, - const std::vector& box); - // const std::vector& fparam = std::vector(), - // const std::vector& aparam = std::vector()); + const std::vector& box, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); /** * @brief Evaluate the energy, force, virial, atomic energy, and atomic virial *by using this DP. @@ -108,9 +108,9 @@ class DeepPotPT : public DeepPotBase { const std::vector& box, // const int nghost, const InputNlist& lmp_list, - const int& ago); - // const std::vector& fparam = std::vector(), - // const std::vector& aparam = std::vector()); + const int& ago, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); /** * @brief Evaluate the energy, force, and virial with the mixed type *by using this DP. diff --git a/source/api_cc/src/DeepPotPT.cc b/source/api_cc/src/DeepPotPT.cc index f05e27b9b2..9514a9769c 100644 --- a/source/api_cc/src/DeepPotPT.cc +++ b/source/api_cc/src/DeepPotPT.cc @@ -62,9 +62,9 @@ void DeepPotPT::init(const std::string& model, rcut = static_cast(rcut_); ntypes = 0; ntypes_spin = 0; - dfparam = 0; - daparam = 0; - aparam_nall = false; + dfparam = module.run_method("get_dim_fparam").toInt(); + daparam = module.run_method("get_dim_aparam").toInt(); + aparam_nall = module.run_method("is_aparam_nall").toBool(); inited = true; } DeepPotPT::~DeepPotPT() {} @@ -79,7 +79,9 @@ void DeepPotPT::compute(ENERGYVTYPE& ener, const std::vector& atype, const std::vector& box, const InputNlist& lmp_list, - const int& ago) { + const int& ago, + const std::vector& fparam, + const std::vector& aparam) { torch::Device device(torch::kCUDA, gpu_id); if (!gpu_enabled) { device = torch::Device(torch::kCPU); @@ -109,11 +111,27 @@ void DeepPotPT::compute(ENERGYVTYPE& ener, firstneigh_tensor = firstneigh.to(torch::kInt64).to(device); bool do_atom_virial_tensor = true; c10::optional optional_tensor; + c10::optional fparam_tensor; + if (!fparam.empty()) { + fparam_tensor = + torch::from_blob(const_cast(fparam.data()), + {1, static_cast(fparam.size())}, options) + .to(device); + } + c10::optional aparam_tensor; + if (!aparam.empty()) { + aparam_tensor = + torch::from_blob(const_cast(aparam.data()), + {1, lmp_list.inum, + static_cast(aparam.size()) / lmp_list.inum}, + options) + .to(device); + } c10::Dict outputs = module .run_method("forward_lower", coord_wrapped_Tensor, atype_Tensor, - firstneigh_tensor, optional_tensor, optional_tensor, - optional_tensor, do_atom_virial_tensor) + firstneigh_tensor, optional_tensor, fparam_tensor, + aparam_tensor, do_atom_virial_tensor) .toGenericDict(); c10::IValue energy_ = outputs.at("energy"); c10::IValue force_ = outputs.at("extended_force"); @@ -156,7 +174,9 @@ template void DeepPotPT::compute>( const std::vector& atype, const std::vector& box, const InputNlist& lmp_list, - const int& ago); + const int& ago, + const std::vector& fparam, + const std::vector& aparam); template void DeepPotPT::compute>( std::vector& ener, std::vector& force, @@ -167,7 +187,9 @@ template void DeepPotPT::compute>( const std::vector& atype, const std::vector& box, const InputNlist& lmp_list, - const int& ago); + const int& ago, + const std::vector& fparam, + const std::vector& aparam); template void DeepPotPT::compute(ENERGYVTYPE& ener, std::vector& force, @@ -176,7 +198,9 @@ void DeepPotPT::compute(ENERGYVTYPE& ener, std::vector& atom_virial, const std::vector& coord, const std::vector& atype, - const std::vector& box) { + const std::vector& box, + const std::vector& fparam, + const std::vector& aparam) { torch::Device device(torch::kCUDA, gpu_id); if (!gpu_enabled) { device = torch::Device(torch::kCPU); @@ -207,8 +231,21 @@ void DeepPotPT::compute(ENERGYVTYPE& ener, } inputs.push_back(box_Tensor); c10::optional fparam_tensor; + if (!fparam.empty()) { + fparam_tensor = + torch::from_blob(const_cast(fparam.data()), + {1, static_cast(fparam.size())}, options) + .to(device); + } inputs.push_back(fparam_tensor); c10::optional aparam_tensor; + if (!aparam.empty()) { + aparam_tensor = + torch::from_blob( + const_cast(aparam.data()), + {1, natoms, static_cast(aparam.size()) / natoms}, options) + .to(device); + } inputs.push_back(aparam_tensor); bool do_atom_virial_tensor = true; inputs.push_back(do_atom_virial_tensor); @@ -253,7 +290,9 @@ template void DeepPotPT::compute>( std::vector& atom_virial, const std::vector& coord, const std::vector& atype, - const std::vector& box); + const std::vector& box, + const std::vector& fparam, + const std::vector& aparam); template void DeepPotPT::compute>( std::vector& ener, std::vector& force, @@ -262,7 +301,9 @@ template void DeepPotPT::compute>( std::vector& atom_virial, const std::vector& coord, const std::vector& atype, - const std::vector& box); + const std::vector& box, + const std::vector& fparam, + const std::vector& aparam); void DeepPotPT::get_type_map(std::string& type_map) { auto ret = module.run_method("get_type_map").toList(); for (const torch::IValue& element : ret) { @@ -282,7 +323,8 @@ void DeepPotPT::computew(std::vector& ener, const std::vector& box, const std::vector& fparam, const std::vector& aparam) { - compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box); + compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box, + fparam, aparam); } void DeepPotPT::computew(std::vector& ener, std::vector& force, @@ -294,7 +336,8 @@ void DeepPotPT::computew(std::vector& ener, const std::vector& box, const std::vector& fparam, const std::vector& aparam) { - compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box); + compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box, + fparam, aparam); } void DeepPotPT::computew(std::vector& ener, std::vector& force, @@ -309,9 +352,8 @@ void DeepPotPT::computew(std::vector& ener, const int& ago, const std::vector& fparam, const std::vector& aparam) { - // TODO: atomic compute unsupported compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box, - inlist, ago); + inlist, ago, fparam, aparam); } void DeepPotPT::computew(std::vector& ener, std::vector& force, @@ -327,7 +369,7 @@ void DeepPotPT::computew(std::vector& ener, const std::vector& fparam, const std::vector& aparam) { compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box, - inlist, ago); + inlist, ago, fparam, aparam); } void DeepPotPT::computew_mixed_type(std::vector& ener, std::vector& force, diff --git a/source/api_cc/tests/test_deeppot_a_fparam_aparam_pt.cc b/source/api_cc/tests/test_deeppot_a_fparam_aparam_pt.cc new file mode 100644 index 0000000000..dfaf0abc06 --- /dev/null +++ b/source/api_cc/tests/test_deeppot_a_fparam_aparam_pt.cc @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "DeepPot.h" +#include "neighbor_list.h" +#include "test_utils.h" + +// 1e-10 cannot pass; unclear bug or not +#undef EPSILON +#define EPSILON (std::is_same::value ? 1e-7 : 1e-4) + +template +class TestInferDeepPotAFParamAParamPt : public ::testing::Test { + protected: + std::vector coord = {12.83, 2.56, 2.18, 12.09, 2.87, 2.74, + 00.25, 3.32, 1.68, 3.36, 3.00, 1.81, + 3.51, 2.51, 2.60, 4.27, 3.22, 1.56}; + std::vector atype = {0, 0, 0, 0, 0, 0}; + std::vector box = {13., 0., 0., 0., 13., 0., 0., 0., 13.}; + std::vector fparam = {0.25852028}; + std::vector aparam = {0.25852028, 0.25852028, 0.25852028, + 0.25852028, 0.25852028, 0.25852028}; + std::vector expected_e = { + -1.038271183039953804e-01, -7.285433575272914908e-02, + -9.467600174099155552e-02, -1.467050086239614082e-01, + -7.660561620618722145e-02, -7.277295998502930630e-02}; + std::vector expected_f = { + 6.622266817497907132e-02, 5.278739055693523058e-02, + 2.265727495541422845e-02, -2.606047850915838363e-02, + -4.538811686410718776e-02, 1.058247569147072187e-02, + 1.679392490937766935e-01, -2.257828022687320690e-03, + -4.490145670355452645e-02, -1.148364103573685929e-01, + -1.169790466695089237e-02, 6.140402504113953025e-02, + -8.078778132132799494e-02, -5.838878056243369807e-02, + 6.773639989682191109e-02, -1.247724708090079161e-02, + 6.494523955924384750e-02, -1.174787188812918687e-01}; + std::vector expected_v = { + -1.589185553287162656e-01, 2.586163333170100279e-03, + -1.575127933809472624e-04, -1.855360380105876630e-02, + 1.949822090859933826e-02, -1.006552056166355388e-02, + 3.177029853276916449e-02, 1.714349636720383010e-03, + -1.290389175187874483e-03, -8.553510339477603253e-02, + -5.654637257232508415e-03, -1.286954833787038420e-02, + 2.464156457499515687e-02, -2.398202886026797043e-02, + -1.957110465239037672e-02, 2.233492928605742764e-02, + 6.107843207824020099e-03, 1.707078295947736047e-03, + -1.653994088976195043e-01, 3.894358678172111371e-02, + -2.169595969759342477e-02, 6.819704294738503786e-03, + -5.018242039618424008e-03, 2.640664428663210429e-03, + -1.985298275686078057e-03, -3.638421609610945767e-02, + 2.342932331075030239e-02, -8.501331914753691710e-02, + -2.181253413538992297e-03, 4.311300069651782287e-03, + -1.910329328333908129e-03, -1.808810159508548836e-03, + -1.540075281450827612e-03, -1.173703213175551763e-02, + -2.596306629910121507e-03, 6.705025662372287101e-03, + -9.038455005073858795e-02, 3.011717773578577451e-02, + -5.083054073419784880e-02, -2.951210292616929069e-03, + 2.342445652898489383e-02, -4.091207474993674431e-02, + -1.648470649301832236e-02, -2.872261885460645689e-02, + 4.763924972552112391e-02, -8.300036532764677732e-02, + 1.020429228955421243e-03, -1.026734151199098881e-03, + 5.678534096113684732e-02, 1.273635718045938205e-02, + -1.530143225195957322e-02, -1.061671865629566225e-01, + -2.486859433265622629e-02, 2.875323131744185121e-02}; + int natoms; + double expected_tot_e; + std::vector expected_tot_v; + + deepmd::DeepPot dp; + + void SetUp() override { + dp.init("../../tests/infer/fparam_aparam.pth"); + + natoms = expected_e.size(); + EXPECT_EQ(natoms * 3, expected_f.size()); + EXPECT_EQ(natoms * 9, expected_v.size()); + expected_tot_e = 0.; + expected_tot_v.resize(9); + std::fill(expected_tot_v.begin(), expected_tot_v.end(), 0.); + for (int ii = 0; ii < natoms; ++ii) { + expected_tot_e += expected_e[ii]; + } + for (int ii = 0; ii < natoms; ++ii) { + for (int dd = 0; dd < 9; ++dd) { + expected_tot_v[dd] += expected_v[ii * 9 + dd]; + } + } + }; + + void TearDown() override { remove("fparam_aparam.pb"); }; +}; + +TYPED_TEST_SUITE(TestInferDeepPotAFParamAParamPt, ValueTypes); + +TYPED_TEST(TestInferDeepPotAFParamAParamPt, cpu_build_nlist) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& fparam = this->fparam; + std::vector& aparam = this->aparam; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + double ener; + std::vector force, virial; + dp.compute(ener, force, virial, coord, atype, box, fparam, aparam); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAFParamAParamPt, cpu_build_nlist_atomic) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& fparam = this->fparam; + std::vector& aparam = this->aparam; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + double ener; + std::vector force, virial, atom_ener, atom_vir; + dp.compute(ener, force, virial, atom_ener, atom_vir, coord, atype, box, + fparam, aparam); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + EXPECT_EQ(atom_ener.size(), natoms); + EXPECT_EQ(atom_vir.size(), natoms * 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + for (int ii = 0; ii < natoms; ++ii) { + EXPECT_LT(fabs(atom_ener[ii] - expected_e[ii]), EPSILON); + } + for (int ii = 0; ii < natoms * 9; ++ii) { + EXPECT_LT(fabs(atom_vir[ii] - expected_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAFParamAParamPt, cpu_lmp_nlist) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& fparam = this->fparam; + std::vector& aparam = this->aparam; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + float rc = dp.cutoff(); + int nloc = coord.size() / 3; + std::vector coord_cpy; + std::vector atype_cpy, mapping; + std::vector > nlist_data; + _build_nlist(nlist_data, coord_cpy, atype_cpy, mapping, coord, + atype, box, rc); + int nall = coord_cpy.size() / 3; + std::vector ilist(nloc), numneigh(nloc); + std::vector firstneigh(nloc); + deepmd::InputNlist inlist(nloc, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + + double ener; + std::vector force_, virial; + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 0, fparam, aparam); + std::vector force; + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + + ener = 0.; + std::fill(force_.begin(), force_.end(), 0.0); + std::fill(virial.begin(), virial.end(), 0.0); + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 1, fparam, aparam); + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAFParamAParamPt, cpu_lmp_nlist_atomic) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& fparam = this->fparam; + std::vector& aparam = this->aparam; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + float rc = dp.cutoff(); + int nloc = coord.size() / 3; + std::vector coord_cpy; + std::vector atype_cpy, mapping; + std::vector > nlist_data; + _build_nlist(nlist_data, coord_cpy, atype_cpy, mapping, coord, + atype, box, rc); + int nall = coord_cpy.size() / 3; + std::vector ilist(nloc), numneigh(nloc); + std::vector firstneigh(nloc); + deepmd::InputNlist inlist(nloc, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + + double ener; + std::vector force_, atom_ener_, atom_vir_, virial; + std::vector force, atom_ener, atom_vir; + dp.compute(ener, force_, virial, atom_ener_, atom_vir_, coord_cpy, atype_cpy, + box, nall - nloc, inlist, 0, fparam, aparam); + _fold_back(force, force_, mapping, nloc, nall, 3); + _fold_back(atom_ener, atom_ener_, mapping, nloc, nall, 1); + _fold_back(atom_vir, atom_vir_, mapping, nloc, nall, 9); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + EXPECT_EQ(atom_ener.size(), natoms); + EXPECT_EQ(atom_vir.size(), natoms * 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + for (int ii = 0; ii < natoms; ++ii) { + EXPECT_LT(fabs(atom_ener[ii] - expected_e[ii]), EPSILON); + } + for (int ii = 0; ii < natoms * 9; ++ii) { + EXPECT_LT(fabs(atom_vir[ii] - expected_v[ii]), EPSILON); + } + + ener = 0.; + std::fill(force_.begin(), force_.end(), 0.0); + std::fill(virial.begin(), virial.end(), 0.0); + std::fill(atom_ener_.begin(), atom_ener_.end(), 0.0); + std::fill(atom_vir_.begin(), atom_vir_.end(), 0.0); + dp.compute(ener, force_, virial, atom_ener_, atom_vir_, coord_cpy, atype_cpy, + box, nall - nloc, inlist, 1, fparam, aparam); + _fold_back(force, force_, mapping, nloc, nall, 3); + _fold_back(atom_ener, atom_ener_, mapping, nloc, nall, 1); + _fold_back(atom_vir, atom_vir_, mapping, nloc, nall, 9); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + EXPECT_EQ(atom_ener.size(), natoms); + EXPECT_EQ(atom_vir.size(), natoms * 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + for (int ii = 0; ii < natoms; ++ii) { + EXPECT_LT(fabs(atom_ener[ii] - expected_e[ii]), EPSILON); + } + for (int ii = 0; ii < natoms * 9; ++ii) { + EXPECT_LT(fabs(atom_vir[ii] - expected_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAFParamAParamPt, cpu_lmp_nlist_2rc) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& fparam = this->fparam; + std::vector& aparam = this->aparam; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + float rc = dp.cutoff(); + int nloc = coord.size() / 3; + std::vector coord_cpy; + std::vector atype_cpy, mapping; + std::vector > nlist_data; + _build_nlist(nlist_data, coord_cpy, atype_cpy, mapping, coord, + atype, box, rc * 2); + int nall = coord_cpy.size() / 3; + std::vector ilist(nloc), numneigh(nloc); + std::vector firstneigh(nloc); + deepmd::InputNlist inlist(nloc, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + + double ener; + std::vector force_(nall * 3, 0.0), virial(9, 0.0); + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 0, fparam, aparam); + std::vector force; + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + + ener = 0.; + std::fill(force_.begin(), force_.end(), 0.0); + std::fill(virial.begin(), virial.end(), 0.0); + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 1, fparam, aparam); + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} diff --git a/source/tests/infer/fparam_aparam.pth b/source/tests/infer/fparam_aparam.pth index 7b0204cdd3abbb1ac39e729ca0ed902d814ca6a1..c433ced49bc9523ce316d9d75245c185a1eafac7 100644 GIT binary patch delta 72912 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̑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

S|E~P6W1GIV5JAK#g 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

Date: Thu, 29 Feb 2024 17:45:17 +0800 Subject: [PATCH 149/686] Chore: Update doc for PyTorch Energy Fitting (#3362) This PR is to perform regression test on `PT backend` against `TF backend`, and update doc to provide instructions for using `PT backend`. ### Regression Test on EnergyFitting image image image image ### Checklist - [x] Se_a energy training both backends - [x] Se_r energy training both backends - [x] dpa1 energy training both backends - [x] dpa2 energy training pytorch - [x] backend-convert - [x] pb to pth (all 4 models above) ___`se_a`,`se_r` only___ - [x] pth to pb (all 4 models above) ___`se_a`,`se_r` only___ - [x] freeze - [x] test https://github.com/deepmodeling/deepmd-kit/issues/3363 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/backend.md | 4 ++++ doc/freeze/freeze.md | 22 +++++++++++++++++-- doc/model/pairtab.md | 4 ++++ doc/model/train-se-atten.md | 43 +++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/doc/backend.md b/doc/backend.md index 8c720ac1c1..0b49f1ca00 100644 --- a/doc/backend.md +++ b/doc/backend.md @@ -51,3 +51,7 @@ For example, when the model filename ends with `.pb` (the ProtoBuf file), DeePMD ## Convert model files between backends If a model is supported by two backends, one can use [`dp convert-backend`](./cli.rst) to convert the model file between these two backends. + +:::{warning} +Currently, only the `se_e2_a` model fully supports the backend conversion between TensorFlow {{ tensorflow_icon }} and PyTorch {{ pytorch_icon }}. +::: diff --git a/doc/freeze/freeze.md b/doc/freeze/freeze.md index ba0cd44606..151c0b3b44 100644 --- a/doc/freeze/freeze.md +++ b/doc/freeze/freeze.md @@ -1,10 +1,28 @@ # Freeze a model The trained neural network is extracted from a checkpoint and dumped into a protobuf(.pb) file. This process is called "freezing" a model. The idea and part of our code are from [Morgan](https://blog.metaflow.fr/tensorflow-how-to-freeze-a-model-and-serve-it-with-a-python-api-d4f3596b3adc). To freeze a model, typically one does + +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} + +```bash +$ dp freeze -o model.pb +``` +in the folder where the model is trained. The output model is called `model.pb`. + +::: + +:::{tab-item} PyTorch {{ pytorch_icon }} + ```bash -$ dp freeze -o graph.pb +$ dp --pt freeze -o model.pth ``` -in the folder where the model is trained. The output model is called `graph.pb`. +in the folder where the model is trained. The output model is called `model.pth`. + +::: + +:::: In [multi-task mode](../train/multi-task-training.md): - This process will in default output several models, each of which contains the common descriptor and diff --git a/doc/model/pairtab.md b/doc/model/pairtab.md index 719bb95004..fee4d754a6 100644 --- a/doc/model/pairtab.md +++ b/doc/model/pairtab.md @@ -53,6 +53,10 @@ in the order of Type_0-Type_0, Type_0-Type_1, ..., Type_0-Type_N, Type_1-Type_1, The interaction should be smooth at the cut-off distance. +:::{note} +In instances where the interaction at the cut-off distance is not delineated within the table file, extrapolation will be conducted utilizing the available interaction data. This extrapolative procedure guarantees a smooth transition from the table-provided value to `0` whenever feasible. +::: + ## Interpolation with a short-range pairwise potential ```json diff --git a/doc/model/train-se-atten.md b/doc/model/train-se-atten.md index 1ac1b33519..745c0d1720 100644 --- a/doc/model/train-se-atten.md +++ b/doc/model/train-se-atten.md @@ -70,6 +70,11 @@ $deepmd_source_dir/examples/water/se_atten/input.json With the training input script, data are also provided in the example directory. One may train the model with the DeePMD-kit from the directory. An example of the DPA-1 descriptor is provided as follows + +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} + ```json "descriptor" :{ "type": "se_atten", @@ -86,6 +91,7 @@ An example of the DPA-1 descriptor is provided as follows "seed": 1 } ``` + * The {ref}`type ` of the descriptor is set to `"se_atten"`, which will use DPA-1 structures. * {ref}`rcut ` is the cut-off radius for neighbor searching, and the {ref}`rcut_smth ` gives where the smoothing starts. * **{ref}`sel `** gives the maximum possible number of neighbors in the cut-off radius. It is an int. Note that this number highly affects the efficiency of training, which we usually use less than 200. (We use 120 for training 56 elements in [OC2M dataset](https://github.com/Open-Catalyst-Project/ocp/blob/main/DATASET.md)) @@ -98,6 +104,43 @@ An example of the DPA-1 descriptor is provided as follows * {ref}`attn_mask ` determines whether to mask the diagonal in the attention weights and False is recommended. * {ref}`attn_dotr ` determines whether to dot the relative coordinates on the attention weights as a gated scheme, True is recommended. +::: + +:::{tab-item} PyTorch {{ pytorch_icon }} + +```json + "descriptor" :{ + "type": "dpa1", + "rcut_smth": 0.50, + "rcut": 6.00, + "sel": 120, + "neuron": [25, 50, 100], + "tebd_dim": 8, + "axis_neuron": 16, + "attn": 128, + "attn_layer": 2, + "attn_mask": false, + "attn_dotr": true, + "post_ln": true + } +``` + +* The {ref}`type ` of the descriptor is set to `"dpa1"`, which will use DPA-1 structures. +* {ref}`rcut ` is the cut-off radius for neighbor searching, and the {ref}`rcut_smth ` gives where the smoothing starts. +* **{ref}`sel `** gives the maximum possible number of neighbors in the cut-off radius. It is an int. Note that this number highly affects the efficiency of training, which we usually use less than 200. (We use 120 for training 56 elements in [OC2M dataset](https://github.com/Open-Catalyst-Project/ocp/blob/main/DATASET.md)) +* The {ref}`neuron ` specifies the size of the embedding net. From left to right the members denote the sizes of each hidden layer from the input end to the output end, respectively. If the outer layer is twice the size of the inner layer, then the inner layer is copied and concatenated, then a [ResNet architecture](https://arxiv.org/abs/1512.03385) is built between them. +* The {ref}`tebd_dim ` specifies the dimension of the type embedding. +* The {ref}`axis_neuron ` specifies the size of the submatrix of the embedding matrix, the axis matrix as explained in the [DeepPot-SE paper](https://arxiv.org/abs/1805.09003) +* {ref}`attn ` sets the length of a hidden vector during scale-dot attention computation. +* {ref}`attn_layer ` sets the number of layers in attention mechanism. +* {ref}`attn_mask ` determines whether to mask the diagonal in the attention weights and False is recommended. +* {ref}`attn_dotr ` determines whether to dot the relative coordinates on the attention weights as a gated scheme, True is recommended. +* {ref}`post_ln ` determines whether to perform post layer norm. + +::: + +:::: + ### Descriptor `"se_atten_v2"` We highly recommend using the version 2.0 of the attention-based descriptor `"se_atten_v2"`, which is inherited from `"se_atten"` but with the following parameter modifications: ```json From 19a4921f5c74963439734d4ea61367e70bc388be Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 29 Feb 2024 04:45:24 -0500 Subject: [PATCH 150/686] fix se_r consistency (#3366) Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/descriptor/se_r.py | 4 ++-- deepmd/pt/model/descriptor/se_r.py | 4 ++-- source/tests/consistent/descriptor/test_se_e2_a.py | 2 +- source/tests/consistent/descriptor/test_se_r.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deepmd/dpmodel/descriptor/se_r.py b/deepmd/dpmodel/descriptor/se_r.py index 98185c0117..a5dcfb16dd 100644 --- a/deepmd/dpmodel/descriptor/se_r.py +++ b/deepmd/dpmodel/descriptor/se_r.py @@ -276,9 +276,9 @@ def call( gg = self.cal_g(tr, tt) gg = np.mean(gg, axis=2) # nf x nloc x ng x 1 - xyz_scatter += gg + xyz_scatter += gg * (self.sel[tt] / self.nnei) - res_rescale = 1.0 / 10.0 + res_rescale = 1.0 / 5.0 res = xyz_scatter * res_rescale res = res.reshape(nf, nloc, -1).astype(GLOBAL_NP_FLOAT_PRECISION) return res, None, None, None, ww diff --git a/deepmd/pt/model/descriptor/se_r.py b/deepmd/pt/model/descriptor/se_r.py index 1debcc8caf..16721fbe5e 100644 --- a/deepmd/pt/model/descriptor/se_r.py +++ b/deepmd/pt/model/descriptor/se_r.py @@ -258,9 +258,9 @@ def forward( # nfnl x nt x ng gg = ll.forward(ss) gg = torch.mean(gg, dim=1).unsqueeze(1) - xyz_scatter += gg + xyz_scatter += gg * (self.sel[ii] / self.nnei) - res_rescale = 1.0 / 10.0 + res_rescale = 1.0 / 5.0 result = xyz_scatter * res_rescale result = result.view(-1, nloc, self.filter_neuron[-1]) return ( diff --git a/source/tests/consistent/descriptor/test_se_e2_a.py b/source/tests/consistent/descriptor/test_se_e2_a.py index 0243a77044..b8f4205d09 100644 --- a/source/tests/consistent/descriptor/test_se_e2_a.py +++ b/source/tests/consistent/descriptor/test_se_e2_a.py @@ -51,7 +51,7 @@ def data(self) -> dict: precision, ) = self.param return { - "sel": [10, 10], + "sel": [9, 10], "rcut_smth": 5.80, "rcut": 6.00, "neuron": [6, 12, 24], diff --git a/source/tests/consistent/descriptor/test_se_r.py b/source/tests/consistent/descriptor/test_se_r.py index 354ae1cc99..8b835f3b5c 100644 --- a/source/tests/consistent/descriptor/test_se_r.py +++ b/source/tests/consistent/descriptor/test_se_r.py @@ -51,7 +51,7 @@ def data(self) -> dict: precision, ) = self.param return { - "sel": [10, 10], + "sel": [9, 10], "rcut_smth": 5.80, "rcut": 6.00, "neuron": [6, 12, 24], From 2511e8b6ffa2614e10ca86e632e0aabcdb45ef40 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Thu, 29 Feb 2024 21:42:01 +0800 Subject: [PATCH 151/686] pt: Fix multitask neighbor stat (#3367) --- deepmd/pt/entrypoints/main.py | 36 +++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 0aed595eab..c4b5a4cf44 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -3,6 +3,9 @@ import json import logging import os +from copy import ( + deepcopy, +) from pathlib import ( Path, ) @@ -75,9 +78,11 @@ def get_trainer( model_branch="", force_load=False, init_frz_model=None, + shared_links=None, ): + multi_task = "model_dict" in config.get("model", {}) # argcheck - if "model_dict" not in config.get("model", {}): + if not multi_task: config = update_deepmd_input(config, warning=True, dump="input_v2_compat.json") config = normalize(config) @@ -88,7 +93,6 @@ def get_trainer( assert dist.is_nccl_available() dist.init_process_group(backend="nccl") - multi_task = "model_dict" in config["model"] ckpt = init_model if init_model is not None else restart_model config["model"] = change_finetune_model_params( ckpt, @@ -98,9 +102,6 @@ def get_trainer( model_branch=model_branch, ) config["model"]["resuming"] = (finetune_model is not None) or (ckpt is not None) - shared_links = None - if multi_task: - config["model"], shared_links = preprocess_shared_params(config["model"]) def prepare_trainer_input_single( model_params_single, data_dict_single, loss_dict_single, suffix="" @@ -252,11 +253,33 @@ def train(FLAGS): SummaryPrinter()() with open(FLAGS.INPUT) as fin: config = json.load(fin) + + # update multitask config + multi_task = "model_dict" in config["model"] + shared_links = None + if multi_task: + config["model"], shared_links = preprocess_shared_params(config["model"]) + + # do neighbor stat if not FLAGS.skip_neighbor_stat: log.info( "Calculate neighbor statistics... (add --skip-neighbor-stat to skip this step)" ) - config["model"] = BaseModel.update_sel(config, config["model"]) + if not multi_task: + config["model"] = BaseModel.update_sel(config, config["model"]) + else: + training_jdata = deepcopy(config["training"]) + training_jdata.pop("data_dict", {}) + training_jdata.pop("model_prob", {}) + for model_item in config["model"]["model_dict"]: + fake_global_jdata = { + "model": deepcopy(config["model"]["model_dict"][model_item]), + "training": deepcopy(config["training"]["data_dict"][model_item]), + } + fake_global_jdata["training"].update(training_jdata) + config["model"]["model_dict"][model_item] = BaseModel.update_sel( + fake_global_jdata, config["model"]["model_dict"][model_item] + ) trainer = get_trainer( config, @@ -266,6 +289,7 @@ def train(FLAGS): FLAGS.model_branch, FLAGS.force_load, FLAGS.init_frz_model, + shared_links=shared_links, ) trainer.run() From fd600d729fa62f356bcd3fab6689fc910d0f3847 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 29 Feb 2024 19:28:57 -0500 Subject: [PATCH 152/686] Hybrid descriptor (#3365) Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/descriptor/__init__.py | 4 + deepmd/dpmodel/descriptor/hybrid.py | 242 ++++++++++++++++++ deepmd/pt/model/descriptor/__init__.py | 2 + deepmd/pt/model/descriptor/hybrid.py | 239 +++++++++++++++++ deepmd/pt/model/descriptor/se_atten.py | 2 +- deepmd/pt/utils/utils.py | 2 +- deepmd/tf/descriptor/hybrid.py | 38 ++- deepmd/utils/argcheck.py | 2 +- doc/model/train-hybrid.md | 4 +- .../consistent/descriptor/test_hybrid.py | 137 ++++++++++ .../tests/pt/model/test_descriptor_hybrid.py | 93 +++++++ 11 files changed, 758 insertions(+), 7 deletions(-) create mode 100644 deepmd/dpmodel/descriptor/hybrid.py create mode 100644 source/tests/consistent/descriptor/test_hybrid.py create mode 100644 source/tests/pt/model/test_descriptor_hybrid.py diff --git a/deepmd/dpmodel/descriptor/__init__.py b/deepmd/dpmodel/descriptor/__init__.py index 08f8eb5052..a19a2aa034 100644 --- a/deepmd/dpmodel/descriptor/__init__.py +++ b/deepmd/dpmodel/descriptor/__init__.py @@ -1,4 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from .hybrid import ( + DescrptHybrid, +) from .make_base_descriptor import ( make_base_descriptor, ) @@ -12,5 +15,6 @@ __all__ = [ "DescrptSeA", "DescrptSeR", + "DescrptHybrid", "make_base_descriptor", ] diff --git a/deepmd/dpmodel/descriptor/hybrid.py b/deepmd/dpmodel/descriptor/hybrid.py new file mode 100644 index 0000000000..d2620fdcf7 --- /dev/null +++ b/deepmd/dpmodel/descriptor/hybrid.py @@ -0,0 +1,242 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Any, + Dict, + List, + Optional, + Union, +) + +import numpy as np + +from deepmd.dpmodel.common import ( + NativeOP, +) +from deepmd.dpmodel.descriptor.base_descriptor import ( + BaseDescriptor, +) +from deepmd.dpmodel.utils.nlist import ( + nlist_distinguish_types, +) +from deepmd.utils.path import ( + DPPath, +) +from deepmd.utils.version import ( + check_version_compatibility, +) + + +@BaseDescriptor.register("hybrid") +class DescrptHybrid(BaseDescriptor, NativeOP): + """Concate a list of descriptors to form a new descriptor. + + Parameters + ---------- + list : list : List[Union[BaseDescriptor, Dict[str, Any]]] + Build a descriptor from the concatenation of the list of descriptors. + The descriptor can be either an object or a dictionary. + """ + + def __init__( + self, + list: List[Union[BaseDescriptor, Dict[str, Any]]], + ) -> None: + super().__init__() + # warning: list is conflict with built-in list + descrpt_list = list + if descrpt_list == [] or descrpt_list is None: + raise RuntimeError( + "cannot build descriptor from an empty list of descriptors." + ) + formatted_descript_list = [] + for ii in descrpt_list: + if isinstance(ii, BaseDescriptor): + formatted_descript_list.append(ii) + elif isinstance(ii, dict): + formatted_descript_list.append(BaseDescriptor(**ii)) + else: + raise NotImplementedError + self.descrpt_list = formatted_descript_list + self.numb_descrpt = len(self.descrpt_list) + for ii in range(1, self.numb_descrpt): + assert ( + self.descrpt_list[ii].get_ntypes() == self.descrpt_list[0].get_ntypes() + ), f"number of atom types in {ii}th descrptor {self.descrpt_list[0].__class__.__name__} does not match others" + # if hybrid sel is larger than sub sel, the nlist needs to be cut for each type + hybrid_sel = self.get_sel() + self.nlist_cut_idx: List[np.ndarray] = [] + if self.mixed_types() and not all( + descrpt.mixed_types() for descrpt in self.descrpt_list + ): + self.sel_no_mixed_types = np.max( + [ + descrpt.get_sel() + for descrpt in self.descrpt_list + if not descrpt.mixed_types() + ], + axis=0, + ).tolist() + else: + self.sel_no_mixed_types = None + for ii in range(self.numb_descrpt): + if self.mixed_types() == self.descrpt_list[ii].mixed_types(): + hybrid_sel = self.get_sel() + else: + assert self.sel_no_mixed_types is not None + hybrid_sel = self.sel_no_mixed_types + sub_sel = self.descrpt_list[ii].get_sel() + start_idx = np.cumsum(np.pad(hybrid_sel, (1, 0), "constant"))[:-1] + end_idx = start_idx + np.array(sub_sel) + cut_idx = np.concatenate( + [range(ss, ee) for ss, ee in zip(start_idx, end_idx)] + ) + self.nlist_cut_idx.append(cut_idx) + + def get_rcut(self) -> float: + """Returns the cut-off radius.""" + return np.max([descrpt.get_rcut() for descrpt in self.descrpt_list]).item() + + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + if self.mixed_types(): + return [ + np.max( + [descrpt.get_nsel() for descrpt in self.descrpt_list], axis=0 + ).item() + ] + else: + return np.max( + [descrpt.get_sel() for descrpt in self.descrpt_list], axis=0 + ).tolist() + + def get_ntypes(self) -> int: + """Returns the number of element types.""" + return self.descrpt_list[0].get_ntypes() + + def get_dim_out(self) -> int: + """Returns the output dimension.""" + return np.sum([descrpt.get_dim_out() for descrpt in self.descrpt_list]).item() + + def get_dim_emb(self) -> int: + """Returns the output dimension.""" + return np.sum([descrpt.get_dim_emb() for descrpt in self.descrpt_list]).item() + + def mixed_types(self): + """Returns if the descriptor requires a neighbor list that distinguish different + atomic types or not. + """ + return any(descrpt.mixed_types() for descrpt in self.descrpt_list) + + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): + """Update mean and stddev for descriptor elements.""" + for descrpt in self.descrpt_list: + descrpt.compute_input_stats(merged, path) + + def call( + self, + coord_ext, + atype_ext, + nlist, + mapping: Optional[np.ndarray] = None, + ): + """Compute the descriptor. + + Parameters + ---------- + coord_ext + The extended coordinates of atoms. shape: nf x (nallx3) + atype_ext + The extended aotm types. shape: nf x nall + nlist + The neighbor list. shape: nf x nloc x nnei + mapping + The index mapping, not required by this descriptor. + + Returns + ------- + descriptor + The descriptor. shape: nf x nloc x (ng x axis_neuron) + gr + The rotationally equivariant and permutationally invariant single particle + representation. shape: nf x nloc x ng x 3. + g2 + The rotationally invariant pair-partical representation. + h2 + The rotationally equivariant pair-partical representation. + sw + The smooth switch function. + """ + out_descriptor = [] + out_gr = [] + out_g2 = [] + out_h2 = None + out_sw = None + if self.sel_no_mixed_types is not None: + nl_distinguish_types = nlist_distinguish_types( + nlist, + atype_ext, + self.sel_no_mixed_types, + ) + else: + nl_distinguish_types = None + for descrpt, nci in zip(self.descrpt_list, self.nlist_cut_idx): + # cut the nlist to the correct length + if self.mixed_types() == descrpt.mixed_types(): + nl = nlist[:, :, nci] + else: + # mixed_types is True, but descrpt.mixed_types is False + assert nl_distinguish_types is not None + nl = nl_distinguish_types[:, :, nci] + odescriptor, gr, g2, h2, sw = descrpt(coord_ext, atype_ext, nl, mapping) + out_descriptor.append(odescriptor) + if gr is not None: + out_gr.append(gr) + if g2 is not None: + out_g2.append(g2) + if self.get_rcut() == descrpt.get_rcut(): + out_h2 = h2 + out_sw = sw + + out_descriptor = np.concatenate(out_descriptor, axis=-1) + out_gr = np.concatenate(out_gr, axis=-2) if out_gr else None + out_g2 = np.concatenate(out_g2, axis=-1) if out_g2 else None + return out_descriptor, out_gr, out_g2, out_h2, out_sw + + @classmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict) -> dict: + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + local_jdata_cpy = local_jdata.copy() + local_jdata_cpy["list"] = [ + BaseDescriptor.update_sel(global_jdata, sub_jdata) + for sub_jdata in local_jdata["list"] + ] + return local_jdata_cpy + + def serialize(self) -> dict: + return { + "@class": "Descriptor", + "type": "hybrid", + "@version": 1, + "list": [descrpt.serialize() for descrpt in self.descrpt_list], + } + + @classmethod + def deserialize(cls, data: dict) -> "DescrptHybrid": + data = data.copy() + class_name = data.pop("@class") + assert class_name == "Descriptor" + class_type = data.pop("type") + assert class_type == "hybrid" + check_version_compatibility(data.pop("@version"), 1, 1) + obj = cls( + list=[BaseDescriptor.deserialize(ii) for ii in data["list"]], + ) + return obj diff --git a/deepmd/pt/model/descriptor/__init__.py b/deepmd/pt/model/descriptor/__init__.py index 5fd644f149..72f734de04 100644 --- a/deepmd/pt/model/descriptor/__init__.py +++ b/deepmd/pt/model/descriptor/__init__.py @@ -18,6 +18,7 @@ ) from .hybrid import ( DescrptBlockHybrid, + DescrptHybrid, ) from .repformers import ( DescrptBlockRepformers, @@ -39,6 +40,7 @@ "DescrptSeR", "DescrptDPA1", "DescrptDPA2", + "DescrptHybrid", "prod_env_mat", "DescrptGaussianLcc", "DescrptBlockHybrid", diff --git a/deepmd/pt/model/descriptor/hybrid.py b/deepmd/pt/model/descriptor/hybrid.py index 688d448b81..5aa83ef534 100644 --- a/deepmd/pt/model/descriptor/hybrid.py +++ b/deepmd/pt/model/descriptor/hybrid.py @@ -1,21 +1,260 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( + Any, + Dict, List, Optional, + Union, ) +import numpy as np import torch from deepmd.pt.model.descriptor import ( DescriptorBlock, ) +from deepmd.pt.model.descriptor.base_descriptor import ( + BaseDescriptor, +) from deepmd.pt.model.network.network import ( Identity, Linear, ) +from deepmd.pt.utils.nlist import ( + nlist_distinguish_types, +) +from deepmd.pt.utils.utils import ( + to_torch_tensor, +) from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) + + +@BaseDescriptor.register("hybrid") +class DescrptHybrid(BaseDescriptor, torch.nn.Module): + """Concate a list of descriptors to form a new descriptor. + + Parameters + ---------- + list : list : List[Union[BaseDescriptor, Dict[str, Any]]] + Build a descriptor from the concatenation of the list of descriptors. + The descriptor can be either an object or a dictionary. + """ + + def __init__( + self, + list: List[Union[BaseDescriptor, Dict[str, Any]]], + **kwargs, + ) -> None: + super().__init__() + # warning: list is conflict with built-in list + descrpt_list = list + if descrpt_list == [] or descrpt_list is None: + raise RuntimeError( + "cannot build descriptor from an empty list of descriptors." + ) + formatted_descript_list: List[BaseDescriptor] = [] + for ii in descrpt_list: + if isinstance(ii, BaseDescriptor): + formatted_descript_list.append(ii) + elif isinstance(ii, dict): + formatted_descript_list.append( + # pass other arguments (e.g. ntypes) to the descriptor + BaseDescriptor(**ii, **kwargs) + ) + else: + raise NotImplementedError + self.descrpt_list = torch.nn.ModuleList(formatted_descript_list) + self.numb_descrpt = len(self.descrpt_list) + for ii in range(1, self.numb_descrpt): + assert ( + self.descrpt_list[ii].get_ntypes() == self.descrpt_list[0].get_ntypes() + ), f"number of atom types in {ii}th descrptor does not match others" + # if hybrid sel is larger than sub sel, the nlist needs to be cut for each type + self.nlist_cut_idx: List[torch.Tensor] = [] + if self.mixed_types() and not all( + descrpt.mixed_types() for descrpt in self.descrpt_list + ): + self.sel_no_mixed_types = np.max( + [ + descrpt.get_sel() + for descrpt in self.descrpt_list + if not descrpt.mixed_types() + ], + axis=0, + ).tolist() + else: + self.sel_no_mixed_types = None + for ii in range(self.numb_descrpt): + if self.mixed_types() == self.descrpt_list[ii].mixed_types(): + hybrid_sel = self.get_sel() + else: + assert self.sel_no_mixed_types is not None + hybrid_sel = self.sel_no_mixed_types + sub_sel = self.descrpt_list[ii].get_sel() + start_idx = np.cumsum(np.pad(hybrid_sel, (1, 0), "constant"))[:-1] + end_idx = start_idx + np.array(sub_sel) + cut_idx = np.concatenate( + [range(ss, ee) for ss, ee in zip(start_idx, end_idx)] + ).astype(np.int64) + self.nlist_cut_idx.append(to_torch_tensor(cut_idx)) + + def get_rcut(self) -> float: + """Returns the cut-off radius.""" + # do not use numpy here - jit is not happy + return max([descrpt.get_rcut() for descrpt in self.descrpt_list]) + + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + if self.mixed_types(): + return [ + np.max( + [descrpt.get_nsel() for descrpt in self.descrpt_list], axis=0 + ).item() + ] + else: + return np.max( + [descrpt.get_sel() for descrpt in self.descrpt_list], axis=0 + ).tolist() + + def get_ntypes(self) -> int: + """Returns the number of element types.""" + return self.descrpt_list[0].get_ntypes() + + def get_dim_out(self) -> int: + """Returns the output dimension.""" + return sum([descrpt.get_dim_out() for descrpt in self.descrpt_list]) + + def get_dim_emb(self) -> int: + """Returns the output dimension.""" + return sum([descrpt.get_dim_emb() for descrpt in self.descrpt_list]) + + def mixed_types(self): + """Returns if the descriptor requires a neighbor list that distinguish different + atomic types or not. + """ + return any(descrpt.mixed_types() for descrpt in self.descrpt_list) + + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): + """Update mean and stddev for descriptor elements.""" + for descrpt in self.descrpt_list: + descrpt.compute_input_stats(merged, path) + + def forward( + self, + coord_ext: torch.Tensor, + atype_ext: torch.Tensor, + nlist: torch.Tensor, + mapping: Optional[torch.Tensor] = None, + ): + """Compute the descriptor. + + Parameters + ---------- + coord_ext + The extended coordinates of atoms. shape: nf x (nallx3) + atype_ext + The extended aotm types. shape: nf x nall + nlist + The neighbor list. shape: nf x nloc x nnei + mapping + The index mapping, not required by this descriptor. + + Returns + ------- + descriptor + The descriptor. shape: nf x nloc x (ng x axis_neuron) + gr + The rotationally equivariant and permutationally invariant single particle + representation. shape: nf x nloc x ng x 3. This descriptor returns None + g2 + The rotationally invariant pair-partical representation. + this descriptor returns None + h2 + The rotationally equivariant pair-partical representation. + this descriptor returns None + sw + The smooth switch function. this descriptor returns None + """ + out_descriptor = [] + out_gr = [] + out_g2 = [] + out_h2: Optional[torch.Tensor] = None + out_sw: Optional[torch.Tensor] = None + if self.sel_no_mixed_types is not None: + nl_distinguish_types = nlist_distinguish_types( + nlist, + atype_ext, + self.sel_no_mixed_types, + ) + else: + nl_distinguish_types = None + # make jit happy + # for descrpt, nci in zip(self.descrpt_list, self.nlist_cut_idx): + for ii, descrpt in enumerate(self.descrpt_list): + # cut the nlist to the correct length + if self.mixed_types() == descrpt.mixed_types(): + nl = nlist[:, :, self.nlist_cut_idx[ii]] + else: + # mixed_types is True, but descrpt.mixed_types is False + assert nl_distinguish_types is not None + nl = nl_distinguish_types[:, :, self.nlist_cut_idx[ii]] + odescriptor, gr, g2, h2, sw = descrpt(coord_ext, atype_ext, nl, mapping) + out_descriptor.append(odescriptor) + if gr is not None: + out_gr.append(gr) + if g2 is not None: + out_g2.append(g2) + if self.get_rcut() == descrpt.get_rcut(): + out_h2 = h2 + out_sw = sw + out_descriptor = torch.cat(out_descriptor, dim=-1) + out_gr = torch.cat(out_gr, dim=-2) if out_gr else None + out_g2 = torch.cat(out_g2, dim=-1) if out_g2 else None + return out_descriptor, out_gr, out_g2, out_h2, out_sw + + @classmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict) -> dict: + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + local_jdata_cpy = local_jdata.copy() + local_jdata_cpy["list"] = [ + BaseDescriptor.update_sel(global_jdata, sub_jdata) + for sub_jdata in local_jdata["list"] + ] + return local_jdata_cpy + + def serialize(self) -> dict: + return { + "@class": "Descriptor", + "type": "hybrid", + "@version": 1, + "list": [descrpt.serialize() for descrpt in self.descrpt_list], + } + + @classmethod + def deserialize(cls, data: dict) -> "DescrptHybrid": + data = data.copy() + class_name = data.pop("@class") + assert class_name == "Descriptor" + class_type = data.pop("type") + assert class_type == "hybrid" + check_version_compatibility(data.pop("@version"), 1, 1) + obj = cls( + list=[BaseDescriptor.deserialize(ii) for ii in data["list"]], + ) + return obj @DescriptorBlock.register("hybrid") diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index a2197213ad..c815cda013 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -303,7 +303,7 @@ def forward( result.view(-1, nloc, self.filter_neuron[-1] * self.axis_neuron), ret.view(-1, nloc, self.nnei, self.filter_neuron[-1]), dmatrix.view(-1, nloc, self.nnei, 4)[..., 1:], - rot_mat.view(-1, self.filter_neuron[-1], 3), + rot_mat.view(-1, nloc, self.filter_neuron[-1], 3), sw, ) diff --git a/deepmd/pt/utils/utils.py b/deepmd/pt/utils/utils.py index 852c42cd0c..f5a4cd84b6 100644 --- a/deepmd/pt/utils/utils.py +++ b/deepmd/pt/utils/utils.py @@ -97,7 +97,7 @@ def to_torch_tensor( # Create a reverse mapping of NP_PRECISION_DICT reverse_precision_dict = {v: k for k, v in NP_PRECISION_DICT.items()} # Use the reverse mapping to find keys with the desired value - prec = reverse_precision_dict.get(type(xx.flat[0]), None) + prec = reverse_precision_dict.get(xx.dtype.type, None) prec = PT_PRECISION_DICT.get(prec, None) if prec is None: raise ValueError(f"unknown precision {xx.dtype}") diff --git a/deepmd/tf/descriptor/hybrid.py b/deepmd/tf/descriptor/hybrid.py index 8ce8acc4db..4e7eaa2c92 100644 --- a/deepmd/tf/descriptor/hybrid.py +++ b/deepmd/tf/descriptor/hybrid.py @@ -1,8 +1,11 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( + Any, + Dict, List, Optional, Tuple, + Union, ) import numpy as np @@ -14,6 +17,9 @@ from deepmd.tf.utils.spin import ( Spin, ) +from deepmd.utils.version import ( + check_version_compatibility, +) # from deepmd.tf.descriptor import DescrptLocFrame # from deepmd.tf.descriptor import DescrptSeA @@ -32,13 +38,14 @@ class DescrptHybrid(Descriptor): Parameters ---------- - list : list + list : list : List[Union[Descriptor, Dict[str, Any]]] Build a descriptor from the concatenation of the list of descriptors. + The descriptor can be either an object or a dictionary. """ def __init__( self, - list: list, + list: List[Union[Descriptor, Dict[str, Any]]], multi_task: bool = False, ntypes: Optional[int] = None, spin: Optional[Spin] = None, @@ -434,3 +441,30 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): for sub_jdata in local_jdata["list"] ] return local_jdata_cpy + + def serialize(self, suffix: str = "") -> dict: + return { + "@class": "Descriptor", + "type": "hybrid", + "@version": 1, + "list": [ + descrpt.serialize(suffix=f"{suffix}_{idx}") + for idx, descrpt in enumerate(self.descrpt_list) + ], + } + + @classmethod + def deserialize(cls, data: dict, suffix: str = "") -> "DescrptHybrid": + data = data.copy() + class_name = data.pop("@class") + assert class_name == "Descriptor" + class_type = data.pop("type") + assert class_type == "hybrid" + check_version_compatibility(data.pop("@version"), 1, 1) + obj = cls( + list=[ + Descriptor.deserialize(ii, suffix=f"{suffix}_{idx}") + for idx, ii in enumerate(data["list"]) + ], + ) + return obj diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 8e3196cba1..89b341491e 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -353,7 +353,7 @@ def descrpt_se_r_args(): ] -@descrpt_args_plugin.register("hybrid", doc=doc_only_tf_supported) +@descrpt_args_plugin.register("hybrid") def descrpt_hybrid_args(): doc_list = "A list of descriptor definitions" diff --git a/doc/model/train-hybrid.md b/doc/model/train-hybrid.md index 1db3f49a1f..3014aa869f 100644 --- a/doc/model/train-hybrid.md +++ b/doc/model/train-hybrid.md @@ -1,7 +1,7 @@ -# Descriptor `"hybrid"` {{ tensorflow_icon }} +# Descriptor `"hybrid"` {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DPModel {{ dpmodel_icon }} ::: This descriptor hybridizes multiple descriptors to form a new descriptor. For example, we have a list of descriptors denoted by $\mathcal D_1$, $\mathcal D_2$, ..., $\mathcal D_N$, the hybrid descriptor this the concatenation of the list, i.e. $\mathcal D = (\mathcal D_1, \mathcal D_2, \cdots, \mathcal D_N)$. diff --git a/source/tests/consistent/descriptor/test_hybrid.py b/source/tests/consistent/descriptor/test_hybrid.py new file mode 100644 index 0000000000..7cfb627d54 --- /dev/null +++ b/source/tests/consistent/descriptor/test_hybrid.py @@ -0,0 +1,137 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest +from typing import ( + Any, + Tuple, +) + +import numpy as np + +from deepmd.dpmodel.descriptor.hybrid import DescrptHybrid as DescrptHybridDP +from deepmd.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) + +from ..common import ( + INSTALLED_PT, + INSTALLED_TF, + CommonTest, +) +from .common import ( + DescriptorTest, +) + +if INSTALLED_PT: + from deepmd.pt.model.descriptor.hybrid import DescrptHybrid as DescrptHybridPT +else: + DescrptHybridPT = None +if INSTALLED_TF: + from deepmd.tf.descriptor.hybrid import DescrptHybrid as DescrptHybridTF +else: + DescrptHybridTF = None +from deepmd.utils.argcheck import ( + descrpt_hybrid_args, +) + + +class TestHybrid(CommonTest, DescriptorTest, unittest.TestCase): + @property + def data(self) -> dict: + return { + "list": [ + { + "type": "se_e2_r", + # test the case that sel are different! + "sel": [10, 10], + "rcut_smth": 5.80, + "rcut": 6.00, + "neuron": [6, 12, 24], + "resnet_dt": False, + "type_one_side": True, + "precision": "float64", + "seed": 20240229, + }, + { + "type": "se_e2_a", + "sel": [9, 11], + "rcut_smth": 2.80, + "rcut": 3.00, + "neuron": [6, 12, 24], + "axis_neuron": 3, + "resnet_dt": True, + "type_one_side": True, + "precision": "float64", + "seed": 20240229, + }, + ] + } + + tf_class = DescrptHybridTF + dp_class = DescrptHybridDP + pt_class = DescrptHybridPT + args = descrpt_hybrid_args() + + def setUp(self): + CommonTest.setUp(self) + + self.ntypes = 2 + self.coords = np.array( + [ + 12.83, + 2.56, + 2.18, + 12.09, + 2.87, + 2.74, + 00.25, + 3.32, + 1.68, + 3.36, + 3.00, + 1.81, + 3.51, + 2.51, + 2.60, + 4.27, + 3.22, + 1.56, + ], + dtype=GLOBAL_NP_FLOAT_PRECISION, + ) + self.atype = np.array([0, 1, 1, 0, 1, 1], dtype=np.int32) + self.box = np.array( + [13.0, 0.0, 0.0, 0.0, 13.0, 0.0, 0.0, 0.0, 13.0], + dtype=GLOBAL_NP_FLOAT_PRECISION, + ) + self.natoms = np.array([6, 6, 2, 4], dtype=np.int32) + + def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: + return self.build_tf_descriptor( + obj, + self.natoms, + self.coords, + self.atype, + self.box, + suffix, + ) + + def eval_dp(self, dp_obj: Any) -> Any: + return self.eval_dp_descriptor( + dp_obj, + self.natoms, + self.coords, + self.atype, + self.box, + ) + + def eval_pt(self, pt_obj: Any) -> Any: + return self.eval_pt_descriptor( + pt_obj, + self.natoms, + self.coords, + self.atype, + self.box, + ) + + def extract_ret(self, ret: Any, backend) -> Tuple[np.ndarray, ...]: + return (ret[0],) diff --git a/source/tests/pt/model/test_descriptor_hybrid.py b/source/tests/pt/model/test_descriptor_hybrid.py new file mode 100644 index 0000000000..6742388bd9 --- /dev/null +++ b/source/tests/pt/model/test_descriptor_hybrid.py @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np +import torch + +from deepmd.pt.model.descriptor.dpa1 import ( + DescrptDPA1, +) +from deepmd.pt.model.descriptor.hybrid import ( + DescrptHybrid, +) +from deepmd.pt.model.descriptor.se_a import ( + DescrptSeA, +) +from deepmd.pt.model.descriptor.se_r import ( + DescrptSeR, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.utils import ( + to_torch_tensor, +) + +from .test_env_mat import ( + TestCaseSingleFrameWithNlist, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION + + +class TestDescrptHybrid(unittest.TestCase, TestCaseSingleFrameWithNlist): + def setUp(self): + TestCaseSingleFrameWithNlist.setUp(self) + + def test_jit( + self, + ): + ddsub0 = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + old_impl=False, + ) + ddsub1 = DescrptSeR( + self.rcut, + self.rcut_smth, + self.sel, + ) + dd0 = DescrptHybrid(list=[ddsub0, ddsub1]) + dd1 = DescrptHybrid.deserialize(dd0.serialize()) + dd0 = torch.jit.script(dd0) + dd1 = torch.jit.script(dd1) + + def test_hybrid_mixed_and_no_mixed(self): + coord_ext = to_torch_tensor(self.coord_ext) + atype_ext = to_torch_tensor(self.atype_ext) + nlist1 = to_torch_tensor(self.nlist) + nlist2 = to_torch_tensor(-np.sort(-self.nlist, axis=-1)) + ddsub0 = DescrptSeA( + rcut=self.rcut, + rcut_smth=self.rcut_smth, + sel=self.sel, + ) + ddsub1 = DescrptDPA1( + rcut=self.rcut, + rcut_smth=self.rcut_smth, + sel=np.sum(self.sel).item() - 1, + ntypes=len(self.sel), + ) + ddsub2 = DescrptSeR( + rcut=self.rcut / 2, + rcut_smth=self.rcut_smth, + sel=[3, 1], + ) + dd = DescrptHybrid(list=[ddsub0, ddsub1, ddsub2]) + ret = dd( + coord_ext, + atype_ext, + nlist2, + ) + ret0 = ddsub0( + coord_ext, + atype_ext, + nlist1, + ) + ret1 = ddsub1(coord_ext, atype_ext, nlist2[:, :, :-1]) + ret2 = ddsub2(coord_ext, atype_ext, nlist1[:, :, [0, 1, 2, self.sel[0]]]) + torch.testing.assert_close( + ret[0], + torch.cat([ret0[0], ret1[0], ret2[0]], dim=2), + ) From 54efc03455a77b49af8626cffe4969f8b7799cae Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 29 Feb 2024 19:29:22 -0500 Subject: [PATCH 153/686] bump scikit-build-core to 0.8 (#3369) Signed-off-by: Jinzhe Zeng --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8615cd12be..f6feefbbb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ requires = [ # dynamic metadata API is still unstable # TODO: unpin the upper bound when it is stable - "scikit-build-core>=0.5,<0.8,!=0.6.0", + "scikit-build-core>=0.5,<0.9,!=0.6.0", "packaging", ] build-backend = "backend.dp_backend" From 16c6db623246816b540c7d79467c5d754b6d6884 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Fri, 1 Mar 2024 14:42:36 +0800 Subject: [PATCH 154/686] pt: refact training code (#3359) This PR - add data_requirement for dataloader - reformat `make_stat_input` and related training code - support single-task & multi-task training --------- Signed-off-by: Duo <50307526+iProzd@users.noreply.github.com> Signed-off-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- deepmd/dpmodel/descriptor/hybrid.py | 8 + .../descriptor/make_base_descriptor.py | 15 +- deepmd/dpmodel/descriptor/se_e2_a.py | 8 + deepmd/dpmodel/descriptor/se_r.py | 8 + deepmd/dpmodel/model/dp_model.py | 1 + deepmd/pt/entrypoints/main.py | 70 ++---- deepmd/pt/loss/ener.py | 107 +++++++- deepmd/pt/loss/loss.py | 20 +- .../pt/model/atomic_model/dp_atomic_model.py | 17 +- deepmd/pt/model/descriptor/__init__.py | 4 + deepmd/pt/model/descriptor/descriptor.py | 30 ++- deepmd/pt/model/descriptor/dpa1.py | 47 +++- deepmd/pt/model/descriptor/dpa2.py | 73 +++++- deepmd/pt/model/descriptor/hybrid.py | 58 ++++- deepmd/pt/model/descriptor/repformers.py | 35 ++- deepmd/pt/model/descriptor/se_a.py | 76 +++++- deepmd/pt/model/descriptor/se_atten.py | 35 ++- deepmd/pt/model/descriptor/se_r.py | 68 ++++- deepmd/pt/model/network/network.py | 5 + deepmd/pt/model/task/dipole.py | 31 ++- deepmd/pt/model/task/ener.py | 42 +++- deepmd/pt/model/task/fitting.py | 18 +- deepmd/pt/model/task/polarizability.py | 30 ++- deepmd/pt/train/training.py | 232 ++++++++++-------- deepmd/pt/train/wrapper.py | 10 +- deepmd/pt/utils/dataloader.py | 10 +- deepmd/pt/utils/dataset.py | 24 +- deepmd/pt/utils/finetune.py | 4 +- deepmd/pt/utils/multi_task.py | 101 +++++--- deepmd/pt/utils/stat.py | 40 ++- deepmd/utils/data.py | 72 ++++++ deepmd/utils/env_mat_stat.py | 5 + source/tests/pt/model/test_descriptor.py | 4 + source/tests/pt/model/test_dipole_fitting.py | 20 +- source/tests/pt/model/test_embedding_net.py | 5 + source/tests/pt/model/test_model.py | 5 + .../pt/model/test_polarizability_fitting.py | 24 +- source/tests/pt/model/water/multitask.json | 139 +++++++++++ source/tests/pt/test_loss.py | 4 + source/tests/pt/test_multitask.py | 181 ++++++++++++++ source/tests/pt/test_stat.py | 45 +++- source/tests/pt/test_training.py | 9 - 42 files changed, 1409 insertions(+), 331 deletions(-) create mode 100644 source/tests/pt/model/water/multitask.json create mode 100644 source/tests/pt/test_multitask.py diff --git a/deepmd/dpmodel/descriptor/hybrid.py b/deepmd/dpmodel/descriptor/hybrid.py index d2620fdcf7..46f2616b84 100644 --- a/deepmd/dpmodel/descriptor/hybrid.py +++ b/deepmd/dpmodel/descriptor/hybrid.py @@ -127,6 +127,14 @@ def mixed_types(self): """ return any(descrpt.mixed_types() for descrpt in self.descrpt_list) + def share_params(self, base_class, shared_level, resume=False): + """ + Share the parameters of self to the base_class with shared_level during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ + raise NotImplementedError + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): """Update mean and stddev for descriptor elements.""" for descrpt in self.descrpt_list: diff --git a/deepmd/dpmodel/descriptor/make_base_descriptor.py b/deepmd/dpmodel/descriptor/make_base_descriptor.py index 69f0da787f..940bd0cd27 100644 --- a/deepmd/dpmodel/descriptor/make_base_descriptor.py +++ b/deepmd/dpmodel/descriptor/make_base_descriptor.py @@ -4,8 +4,10 @@ abstractmethod, ) from typing import ( + Callable, List, Optional, + Union, ) from deepmd.common import ( @@ -84,8 +86,19 @@ def mixed_types(self) -> bool: """ pass + @abstractmethod + def share_params(self, base_class, shared_level, resume=False): + """ + Share the parameters of self to the base_class with shared_level during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ + pass + def compute_input_stats( - self, merged: List[dict], path: Optional[DPPath] = None + self, + merged: Union[Callable[[], List[dict]], List[dict]], + path: Optional[DPPath] = None, ): """Update mean and stddev for descriptor elements.""" raise NotImplementedError diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index 5e72653f1d..f6b1c5677e 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -243,6 +243,14 @@ def mixed_types(self): """ return False + def share_params(self, base_class, shared_level, resume=False): + """ + Share the parameters of self to the base_class with shared_level during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ + raise NotImplementedError + def get_ntypes(self) -> int: """Returns the number of element types.""" return self.ntypes diff --git a/deepmd/dpmodel/descriptor/se_r.py b/deepmd/dpmodel/descriptor/se_r.py index a5dcfb16dd..2dbf495d14 100644 --- a/deepmd/dpmodel/descriptor/se_r.py +++ b/deepmd/dpmodel/descriptor/se_r.py @@ -203,6 +203,14 @@ def mixed_types(self): """ return False + def share_params(self, base_class, shared_level, resume=False): + """ + Share the parameters of self to the base_class with shared_level during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ + raise NotImplementedError + def get_ntypes(self) -> int: """Returns the number of element types.""" return self.ntypes diff --git a/deepmd/dpmodel/model/dp_model.py b/deepmd/dpmodel/model/dp_model.py index ef7866a6dd..15f9027d4c 100644 --- a/deepmd/dpmodel/model/dp_model.py +++ b/deepmd/dpmodel/model/dp_model.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later + from deepmd.dpmodel.atomic_model import ( DPAtomicModel, ) diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index c4b5a4cf44..023bc5305e 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -53,9 +53,6 @@ from deepmd.pt.utils.multi_task import ( preprocess_shared_params, ) -from deepmd.pt.utils.stat import ( - make_stat_input, -) from deepmd.utils.argcheck import ( normalize, ) @@ -104,36 +101,23 @@ def get_trainer( config["model"]["resuming"] = (finetune_model is not None) or (ckpt is not None) def prepare_trainer_input_single( - model_params_single, data_dict_single, loss_dict_single, suffix="" + model_params_single, data_dict_single, loss_dict_single, suffix="", rank=0 ): training_dataset_params = data_dict_single["training_data"] type_split = False if model_params_single["descriptor"]["type"] in ["se_e2_a"]: type_split = True - validation_dataset_params = data_dict_single["validation_data"] + validation_dataset_params = data_dict_single.get("validation_data", None) + validation_systems = ( + validation_dataset_params["systems"] if validation_dataset_params else None + ) training_systems = training_dataset_params["systems"] - validation_systems = validation_dataset_params["systems"] - - # noise params - noise_settings = None - if loss_dict_single.get("type", "ener") == "denoise": - noise_settings = { - "noise_type": loss_dict_single.pop("noise_type", "uniform"), - "noise": loss_dict_single.pop("noise", 1.0), - "noise_mode": loss_dict_single.pop("noise_mode", "fix_num"), - "mask_num": loss_dict_single.pop("mask_num", 8), - "mask_prob": loss_dict_single.pop("mask_prob", 0.15), - "same_mask": loss_dict_single.pop("same_mask", False), - "mask_coord": loss_dict_single.pop("mask_coord", False), - "mask_type": loss_dict_single.pop("mask_type", False), - "max_fail_num": loss_dict_single.pop("max_fail_num", 10), - "mask_type_idx": len(model_params_single["type_map"]) - 1, - } - # noise_settings = None # stat files stat_file_path_single = data_dict_single.get("stat_file", None) - if stat_file_path_single is not None: + if rank != 0: + stat_file_path_single = None + elif stat_file_path_single is not None: if Path(stat_file_path_single).is_dir(): raise ValueError( f"stat_file should be a file, not a directory: {stat_file_path_single}" @@ -144,10 +128,14 @@ def prepare_trainer_input_single( stat_file_path_single = DPPath(stat_file_path_single, "a") # validation and training data - validation_data_single = DpLoaderSet( - validation_systems, - validation_dataset_params["batch_size"], - model_params_single, + validation_data_single = ( + DpLoaderSet( + validation_systems, + validation_dataset_params["batch_size"], + model_params_single, + ) + if validation_systems + else None ) if ckpt or finetune_model: train_data_single = DpLoaderSet( @@ -155,60 +143,48 @@ def prepare_trainer_input_single( training_dataset_params["batch_size"], model_params_single, ) - sampled_single = None else: train_data_single = DpLoaderSet( training_systems, training_dataset_params["batch_size"], model_params_single, ) - data_stat_nbatch = model_params_single.get("data_stat_nbatch", 10) - sampled_single = make_stat_input( - train_data_single.systems, - train_data_single.dataloaders, - data_stat_nbatch, - ) - if noise_settings is not None: - train_data_single = DpLoaderSet( - training_systems, - training_dataset_params["batch_size"], - model_params_single, - ) return ( train_data_single, validation_data_single, - sampled_single, stat_file_path_single, ) + rank = dist.get_rank() if dist.is_initialized() else 0 if not multi_task: ( train_data, validation_data, - sampled, stat_file_path, ) = prepare_trainer_input_single( - config["model"], config["training"], config["loss"] + config["model"], + config["training"], + config["loss"], + rank=rank, ) else: - train_data, validation_data, sampled, stat_file_path = {}, {}, {}, {} + train_data, validation_data, stat_file_path = {}, {}, {} for model_key in config["model"]["model_dict"]: ( train_data[model_key], validation_data[model_key], - sampled[model_key], stat_file_path[model_key], ) = prepare_trainer_input_single( config["model"]["model_dict"][model_key], config["training"]["data_dict"][model_key], config["loss_dict"][model_key], suffix=f"_{model_key}", + rank=rank, ) trainer = training.Trainer( config, train_data, - sampled=sampled, stat_file_path=stat_file_path, validation_data=validation_data, init_model=init_model, diff --git a/deepmd/pt/loss/ener.py b/deepmd/pt/loss/ener.py index 4ed765cf69..2834733112 100644 --- a/deepmd/pt/loss/ener.py +++ b/deepmd/pt/loss/ener.py @@ -1,4 +1,8 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + List, +) + import torch import torch.nn.functional as F @@ -11,6 +15,9 @@ from deepmd.pt.utils.env import ( GLOBAL_PT_FLOAT_PRECISION, ) +from deepmd.utils.data import ( + DataRequirementItem, +) class EnergyStdLoss(TaskLoss): @@ -23,16 +30,57 @@ def __init__( limit_pref_f=0.0, start_pref_v=0.0, limit_pref_v=0.0, + start_pref_ae: float = 0.0, + limit_pref_ae: float = 0.0, + start_pref_pf: float = 0.0, + limit_pref_pf: float = 0.0, use_l1_all: bool = False, inference=False, **kwargs, ): - """Construct a layer to compute loss on energy, force and virial.""" + r"""Construct a layer to compute loss on energy, force and virial. + + Parameters + ---------- + starter_learning_rate : float + The learning rate at the start of the training. + start_pref_e : float + The prefactor of energy loss at the start of the training. + limit_pref_e : float + The prefactor of energy loss at the end of the training. + start_pref_f : float + The prefactor of force loss at the start of the training. + limit_pref_f : float + The prefactor of force loss at the end of the training. + start_pref_v : float + The prefactor of virial loss at the start of the training. + limit_pref_v : float + The prefactor of virial loss at the end of the training. + start_pref_ae : float + The prefactor of atomic energy loss at the start of the training. + limit_pref_ae : float + The prefactor of atomic energy loss at the end of the training. + start_pref_pf : float + The prefactor of atomic prefactor force loss at the start of the training. + limit_pref_pf : float + The prefactor of atomic prefactor force loss at the end of the training. + use_l1_all : bool + Whether to use L1 loss, if False (default), it will use L2 loss. + inference : bool + If true, it will output all losses found in output, ignoring the pre-factors. + **kwargs + Other keyword arguments. + """ super().__init__() self.starter_learning_rate = starter_learning_rate self.has_e = (start_pref_e != 0.0 and limit_pref_e != 0.0) or inference self.has_f = (start_pref_f != 0.0 and limit_pref_f != 0.0) or inference self.has_v = (start_pref_v != 0.0 and limit_pref_v != 0.0) or inference + + # TODO need support for atomic energy and atomic pref + self.has_ae = (start_pref_ae != 0.0 and limit_pref_ae != 0.0) or inference + self.has_pf = (start_pref_pf != 0.0 and limit_pref_pf != 0.0) or inference + self.start_pref_e = start_pref_e self.limit_pref_e = limit_pref_e self.start_pref_f = start_pref_f @@ -153,3 +201,60 @@ def forward(self, model_pred, label, natoms, learning_rate, mae=False): if not self.inference: more_loss["rmse"] = torch.sqrt(loss.detach()) return loss, more_loss + + @property + def label_requirement(self) -> List[DataRequirementItem]: + """Return data label requirements needed for this loss calculation.""" + label_requirement = [] + if self.has_e: + label_requirement.append( + DataRequirementItem( + "energy", + ndof=1, + atomic=False, + must=False, + high_prec=True, + ) + ) + if self.has_f: + label_requirement.append( + DataRequirementItem( + "force", + ndof=3, + atomic=True, + must=False, + high_prec=False, + ) + ) + if self.has_v: + label_requirement.append( + DataRequirementItem( + "virial", + ndof=9, + atomic=False, + must=False, + high_prec=False, + ) + ) + if self.has_ae: + label_requirement.append( + DataRequirementItem( + "atom_ener", + ndof=1, + atomic=True, + must=False, + high_prec=False, + ) + ) + if self.has_pf: + label_requirement.append( + DataRequirementItem( + "atom_pref", + ndof=1, + atomic=True, + must=False, + high_prec=False, + repeat=3, + ) + ) + return label_requirement diff --git a/deepmd/pt/loss/loss.py b/deepmd/pt/loss/loss.py index 9f2c3a7ed7..925ff8f4ef 100644 --- a/deepmd/pt/loss/loss.py +++ b/deepmd/pt/loss/loss.py @@ -1,8 +1,20 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from abc import ( + ABC, + abstractmethod, +) +from typing import ( + List, +) + import torch +from deepmd.utils.data import ( + DataRequirementItem, +) + -class TaskLoss(torch.nn.Module): +class TaskLoss(torch.nn.Module, ABC): def __init__(self, **kwargs): """Construct loss.""" super().__init__() @@ -10,3 +22,9 @@ def __init__(self, **kwargs): def forward(self, model_pred, label, natoms, learning_rate): """Return loss .""" raise NotImplementedError + + @property + @abstractmethod + def label_requirement(self) -> List[DataRequirementItem]: + """Return data label requirements needed for this loss calculation.""" + pass diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index 63e91ff428..7f6c3076d8 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -18,9 +18,6 @@ from deepmd.pt.model.task.base_fitting import ( BaseFitting, ) -from deepmd.pt.utils.utils import ( - dict_to_device, -) from deepmd.utils.path import ( DPPath, ) @@ -185,7 +182,7 @@ def forward_atomic( def compute_or_load_stat( self, - sampled, + sampled_func, stat_file_path: Optional[DPPath] = None, ): """ @@ -198,8 +195,8 @@ def compute_or_load_stat( Parameters ---------- - sampled - The sampled data frames from different data systems. + sampled_func + The lazy sampled function to get data frames from different data systems. stat_file_path The dictionary of paths to the statistics files. """ @@ -207,13 +204,9 @@ def compute_or_load_stat( # descriptors and fitting net with different type_map # should not share the same parameters stat_file_path /= " ".join(self.type_map) - for data_sys in sampled: - dict_to_device(data_sys) - if sampled is None: - sampled = [] - self.descriptor.compute_input_stats(sampled, stat_file_path) + self.descriptor.compute_input_stats(sampled_func, stat_file_path) if self.fitting_net is not None: - self.fitting_net.compute_output_stats(sampled, stat_file_path) + self.fitting_net.compute_output_stats(sampled_func, stat_file_path) @torch.jit.export def get_dim_fparam(self) -> int: diff --git a/deepmd/pt/model/descriptor/__init__.py b/deepmd/pt/model/descriptor/__init__.py index 72f734de04..325cf29e42 100644 --- a/deepmd/pt/model/descriptor/__init__.py +++ b/deepmd/pt/model/descriptor/__init__.py @@ -1,4 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from .base_descriptor import ( + BaseDescriptor, +) from .descriptor import ( DescriptorBlock, make_default_type_embedding, @@ -32,6 +35,7 @@ ) __all__ = [ + "BaseDescriptor", "DescriptorBlock", "make_default_type_embedding", "DescrptBlockSeA", diff --git a/deepmd/pt/model/descriptor/descriptor.py b/deepmd/pt/model/descriptor/descriptor.py index 964cdb01eb..24c1ef4dab 100644 --- a/deepmd/pt/model/descriptor/descriptor.py +++ b/deepmd/pt/model/descriptor/descriptor.py @@ -5,9 +5,11 @@ abstractmethod, ) from typing import ( + Callable, Dict, List, Optional, + Union, ) import torch @@ -86,8 +88,27 @@ def get_dim_emb(self) -> int: """Returns the embedding dimension.""" pass - def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): - """Update mean and stddev for DescriptorBlock elements.""" + def compute_input_stats( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + path: Optional[DPPath] = None, + ): + """ + Compute the input statistics (e.g. mean and stddev) for the descriptors from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + path : Optional[DPPath] + The path to the stat file. + + """ raise NotImplementedError def get_stats(self) -> Dict[str, StatItem]: @@ -95,6 +116,11 @@ def get_stats(self) -> Dict[str, StatItem]: raise NotImplementedError def share_params(self, base_class, shared_level, resume=False): + """ + Share the parameters of self to the base_class with shared_level during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ assert ( self.__class__ == base_class.__class__ ), "Only descriptors of the same type can share params!" diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index 0245179d8b..224a24d60e 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -1,7 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( + Callable, List, Optional, + Union, ) import torch @@ -145,6 +147,29 @@ def mixed_types(self) -> bool: """ return self.se_atten.mixed_types() + def share_params(self, base_class, shared_level, resume=False): + """ + Share the parameters of self to the base_class with shared_level during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ + assert ( + self.__class__ == base_class.__class__ + ), "Only descriptors of the same type can share params!" + # For DPA1 descriptors, the user-defined share-level + # shared_level: 0 + # share all parameters in both type_embedding and se_atten + if shared_level == 0: + self._modules["type_embedding"] = base_class._modules["type_embedding"] + self.se_atten.share_params(base_class.se_atten, 0, resume=resume) + # shared_level: 1 + # share all parameters in type_embedding + elif shared_level == 1: + self._modules["type_embedding"] = base_class._modules["type_embedding"] + # Other shared levels + else: + raise NotImplementedError + @property def dim_out(self): return self.get_dim_out() @@ -153,7 +178,27 @@ def dim_out(self): def dim_emb(self): return self.get_dim_emb() - def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): + def compute_input_stats( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + path: Optional[DPPath] = None, + ): + """ + Compute the input statistics (e.g. mean and stddev) for the descriptors from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + path : Optional[DPPath] + The path to the stat file. + + """ return self.se_atten.compute_input_stats(merged, path) def serialize(self) -> dict: diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index 20a7c74cda..dcb381d53a 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -1,7 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( + Callable, List, Optional, + Union, ) import torch @@ -289,6 +291,46 @@ def mixed_types(self) -> bool: """ return True + def share_params(self, base_class, shared_level, resume=False): + """ + Share the parameters of self to the base_class with shared_level during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ + assert ( + self.__class__ == base_class.__class__ + ), "Only descriptors of the same type can share params!" + # For DPA2 descriptors, the user-defined share-level + # shared_level: 0 + # share all parameters in type_embedding, repinit and repformers + if shared_level == 0: + self._modules["type_embedding"] = base_class._modules["type_embedding"] + self.repinit.share_params(base_class.repinit, 0, resume=resume) + self._modules["g1_shape_tranform"] = base_class._modules[ + "g1_shape_tranform" + ] + self.repformers.share_params(base_class.repformers, 0, resume=resume) + # shared_level: 1 + # share all parameters in type_embedding and repinit + elif shared_level == 1: + self._modules["type_embedding"] = base_class._modules["type_embedding"] + self.repinit.share_params(base_class.repinit, 0, resume=resume) + # shared_level: 2 + # share all parameters in type_embedding and repformers + elif shared_level == 2: + self._modules["type_embedding"] = base_class._modules["type_embedding"] + self._modules["g1_shape_tranform"] = base_class._modules[ + "g1_shape_tranform" + ] + self.repformers.share_params(base_class.repformers, 0, resume=resume) + # shared_level: 3 + # share all parameters in type_embedding + elif shared_level == 3: + self._modules["type_embedding"] = base_class._modules["type_embedding"] + # Other shared levels + else: + raise NotImplementedError + @property def dim_out(self): return self.get_dim_out() @@ -298,16 +340,29 @@ def dim_emb(self): """Returns the embedding dimension g2.""" return self.get_dim_emb() - def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): + def compute_input_stats( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + path: Optional[DPPath] = None, + ): + """ + Compute the input statistics (e.g. mean and stddev) for the descriptors from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + path : Optional[DPPath] + The path to the stat file. + + """ for ii, descrpt in enumerate([self.repinit, self.repformers]): - merged_tmp = [ - { - key: item[key] if not isinstance(item[key], list) else item[key][ii] - for key in item - } - for item in merged - ] - descrpt.compute_input_stats(merged_tmp, path) + descrpt.compute_input_stats(merged, path) def serialize(self) -> dict: """Serialize the obj to dict.""" diff --git a/deepmd/pt/model/descriptor/hybrid.py b/deepmd/pt/model/descriptor/hybrid.py index 5aa83ef534..b53adca462 100644 --- a/deepmd/pt/model/descriptor/hybrid.py +++ b/deepmd/pt/model/descriptor/hybrid.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( Any, + Callable, Dict, List, Optional, @@ -139,6 +140,23 @@ def mixed_types(self): """ return any(descrpt.mixed_types() for descrpt in self.descrpt_list) + def share_params(self, base_class, shared_level, resume=False): + """ + Share the parameters of self to the base_class with shared_level during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ + assert ( + self.__class__ == base_class.__class__ + ), "Only descriptors of the same type can share params!" + if shared_level == 0: + for ii, des in enumerate(self.descrpt_list): + self.descrpt_list[ii].share_params( + base_class.descrpt_list[ii], shared_level, resume=resume + ) + else: + raise NotImplementedError + def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): """Update mean and stddev for descriptor elements.""" for descrpt in self.descrpt_list: @@ -383,6 +401,11 @@ def dim_emb(self): raise RuntimeError def share_params(self, base_class, shared_level, resume=False): + """ + Share the parameters of self to the base_class with shared_level during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ assert ( self.__class__ == base_class.__class__ ), "Only descriptors of the same type can share params!" @@ -391,22 +414,33 @@ def share_params(self, base_class, shared_level, resume=False): self.descriptor_list[ii].share_params( base_class.descriptor_list[ii], shared_level, resume=resume ) - if self.hybrid_mode == "sequential": - self.sequential_transform = base_class.sequential_transform else: raise NotImplementedError - def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): - """Update mean and stddev for descriptor elements.""" + def compute_input_stats( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + path: Optional[DPPath] = None, + ): + """ + Compute the input statistics (e.g. mean and stddev) for the descriptors from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + path : Optional[DPPath] + The path to the stat file. + + """ for ii, descrpt in enumerate(self.descriptor_list): - merged_tmp = [ - { - key: item[key] if not isinstance(item[key], list) else item[key][ii] - for key in item - } - for item in merged - ] - descrpt.compute_input_stats(merged_tmp, path) + # need support for hybrid descriptors + descrpt.compute_input_stats(merged, path) def forward( self, diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py index 2425139e16..3e8bf72f77 100644 --- a/deepmd/pt/model/descriptor/repformers.py +++ b/deepmd/pt/model/descriptor/repformers.py @@ -1,8 +1,10 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( + Callable, Dict, List, Optional, + Union, ) import torch @@ -278,12 +280,39 @@ def forward( return g1, g2, h2, rot_mat.view(-1, nloc, self.dim_emb, 3), sw - def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): - """Update mean and stddev for descriptor elements.""" + def compute_input_stats( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + path: Optional[DPPath] = None, + ): + """ + Compute the input statistics (e.g. mean and stddev) for the descriptors from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + path : Optional[DPPath] + The path to the stat file. + + """ env_mat_stat = EnvMatStatSe(self) if path is not None: path = path / env_mat_stat.get_hash() - env_mat_stat.load_or_compute_stats(merged, path) + if path is None or not path.is_dir(): + if callable(merged): + # only get data for once + sampled = merged() + else: + sampled = merged + else: + sampled = [] + env_mat_stat.load_or_compute_stats(sampled, path) self.stats = env_mat_stat.stats mean, stddev = env_mat_stat() if not self.set_davg_zero: diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index fc2cf60531..d836b48992 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -1,11 +1,13 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import itertools from typing import ( + Callable, ClassVar, Dict, List, Optional, Tuple, + Union, ) import numpy as np @@ -127,13 +129,50 @@ def mixed_types(self): """ return self.sea.mixed_types() + def share_params(self, base_class, shared_level, resume=False): + """ + Share the parameters of self to the base_class with shared_level during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ + assert ( + self.__class__ == base_class.__class__ + ), "Only descriptors of the same type can share params!" + # For SeA descriptors, the user-defined share-level + # shared_level: 0 + # share all parameters in sea + if shared_level == 0: + self.sea.share_params(base_class.sea, 0, resume=resume) + # Other shared levels + else: + raise NotImplementedError + @property def dim_out(self): """Returns the output dimension of this descriptor.""" return self.sea.dim_out - def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): - """Update mean and stddev for descriptor elements.""" + def compute_input_stats( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + path: Optional[DPPath] = None, + ): + """ + Compute the input statistics (e.g. mean and stddev) for the descriptors from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + path : Optional[DPPath] + The path to the stat file. + + """ return self.sea.compute_input_stats(merged, path) def reinit_exclude( @@ -411,12 +450,39 @@ def __getitem__(self, key): else: raise KeyError(key) - def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): - """Update mean and stddev for descriptor elements.""" + def compute_input_stats( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + path: Optional[DPPath] = None, + ): + """ + Compute the input statistics (e.g. mean and stddev) for the descriptors from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + path : Optional[DPPath] + The path to the stat file. + + """ env_mat_stat = EnvMatStatSe(self) if path is not None: path = path / env_mat_stat.get_hash() - env_mat_stat.load_or_compute_stats(merged, path) + if path is None or not path.is_dir(): + if callable(merged): + # only get data for once + sampled = merged() + else: + sampled = merged + else: + sampled = [] + env_mat_stat.load_or_compute_stats(sampled, path) self.stats = env_mat_stat.stats mean, stddev = env_mat_stat() if not self.set_davg_zero: diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index c815cda013..db9202c7fc 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -1,8 +1,10 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( + Callable, Dict, List, Optional, + Union, ) import numpy as np @@ -200,12 +202,39 @@ def dim_emb(self): """Returns the output dimension of embedding.""" return self.get_dim_emb() - def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): - """Update mean and stddev for descriptor elements.""" + def compute_input_stats( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + path: Optional[DPPath] = None, + ): + """ + Compute the input statistics (e.g. mean and stddev) for the descriptors from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + path : Optional[DPPath] + The path to the stat file. + + """ env_mat_stat = EnvMatStatSe(self) if path is not None: path = path / env_mat_stat.get_hash() - env_mat_stat.load_or_compute_stats(merged, path) + if path is None or not path.is_dir(): + if callable(merged): + # only get data for once + sampled = merged() + else: + sampled = merged + else: + sampled = [] + env_mat_stat.load_or_compute_stats(sampled, path) self.stats = env_mat_stat.stats mean, stddev = env_mat_stat() if not self.set_davg_zero: diff --git a/deepmd/pt/model/descriptor/se_r.py b/deepmd/pt/model/descriptor/se_r.py index 16721fbe5e..27e459d861 100644 --- a/deepmd/pt/model/descriptor/se_r.py +++ b/deepmd/pt/model/descriptor/se_r.py @@ -1,9 +1,11 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( + Callable, Dict, List, Optional, Tuple, + Union, ) import numpy as np @@ -151,12 +153,72 @@ def mixed_types(self) -> bool: """ return False - def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None): - """Update mean and stddev for descriptor elements.""" + def share_params(self, base_class, shared_level, resume=False): + """ + Share the parameters of self to the base_class with shared_level during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ + assert ( + self.__class__ == base_class.__class__ + ), "Only descriptors of the same type can share params!" + # For SeR descriptors, the user-defined share-level + # shared_level: 0 + if shared_level == 0: + # link buffers + if hasattr(self, "mean") and not resume: + # in case of change params during resume + base_env = EnvMatStatSe(base_class) + base_env.stats = base_class.stats + for kk in base_class.get_stats(): + base_env.stats[kk] += self.get_stats()[kk] + mean, stddev = base_env() + if not base_class.set_davg_zero: + base_class.mean.copy_(torch.tensor(mean, device=env.DEVICE)) + base_class.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) + self.mean = base_class.mean + self.stddev = base_class.stddev + # self.load_state_dict(base_class.state_dict()) # this does not work, because it only inits the model + # the following will successfully link all the params except buffers + for item in self._modules: + self._modules[item] = base_class._modules[item] + # Other shared levels + else: + raise NotImplementedError + + def compute_input_stats( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + path: Optional[DPPath] = None, + ): + """ + Compute the input statistics (e.g. mean and stddev) for the descriptors from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + path : Optional[DPPath] + The path to the stat file. + + """ env_mat_stat = EnvMatStatSe(self) if path is not None: path = path / env_mat_stat.get_hash() - env_mat_stat.load_or_compute_stats(merged, path) + if path is None or not path.is_dir(): + if callable(merged): + # only get data for once + sampled = merged() + else: + sampled = merged + else: + sampled = [] + env_mat_stat.load_or_compute_stats(sampled, path) self.stats = env_mat_stat.stats mean, stddev = env_mat_stat() if not self.set_davg_zero: diff --git a/deepmd/pt/model/network/network.py b/deepmd/pt/model/network/network.py index 9ef7b3366a..10d0364c9b 100644 --- a/deepmd/pt/model/network/network.py +++ b/deepmd/pt/model/network/network.py @@ -575,6 +575,11 @@ def forward(self, atype): return self.embedding(atype) def share_params(self, base_class, shared_level, resume=False): + """ + Share the parameters of self to the base_class with shared_level during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ assert ( self.__class__ == base_class.__class__ ), "Only TypeEmbedNet of the same type can share params!" diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py index 9df3a5fb32..7d2dd221db 100644 --- a/deepmd/pt/model/task/dipole.py +++ b/deepmd/pt/model/task/dipole.py @@ -1,8 +1,10 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging from typing import ( + Callable, List, Optional, + Union, ) import torch @@ -20,6 +22,9 @@ from deepmd.pt.utils.env import ( DEFAULT_PRECISION, ) +from deepmd.utils.path import ( + DPPath, +) log = logging.getLogger(__name__) @@ -67,7 +72,6 @@ class DipoleFittingNet(GeneralFitting): def __init__( self, - var_name: str, ntypes: int, dim_descrpt: int, embedding_width: int, @@ -89,7 +93,7 @@ def __init__( self.r_differentiable = r_differentiable self.c_differentiable = c_differentiable super().__init__( - var_name=var_name, + var_name=kwargs.pop("var_name", "dipole"), ntypes=ntypes, dim_descrpt=dim_descrpt, neuron=neuron, @@ -132,6 +136,29 @@ def output_def(self) -> FittingOutputDef: ] ) + def compute_output_stats( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + stat_file_path: Optional[DPPath] = None, + ): + """ + Compute the output statistics (e.g. energy bias) for the fitting net from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + stat_file_path : Optional[DPPath] + The path to the stat file. + + """ + raise NotImplementedError + def forward( self, descriptor: torch.Tensor, diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index ff7ae6f8ec..29ed5acaad 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -2,9 +2,11 @@ import copy import logging from typing import ( + Callable, List, Optional, Tuple, + Union, ) import numpy as np @@ -138,18 +140,43 @@ def serialize(self) -> dict: data["atom_ener"] = self.atom_ener return data - def compute_output_stats(self, merged, stat_file_path: Optional[DPPath] = None): - energy = [item[self.var_name] for item in merged] - data_mixed_type = "real_natoms_vec" in merged[0] - if data_mixed_type: - input_natoms = [item["real_natoms_vec"] for item in merged] - else: - input_natoms = [item["natoms"] for item in merged] + def compute_output_stats( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + stat_file_path: Optional[DPPath] = None, + ): + """ + Compute the output statistics (e.g. energy bias) for the fitting net from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + stat_file_path : Optional[DPPath] + The path to the stat file. + + """ if stat_file_path is not None: stat_file_path = stat_file_path / "bias_atom_e" if stat_file_path is not None and stat_file_path.is_file(): bias_atom_e = stat_file_path.load_numpy() else: + if callable(merged): + # only get data for once + sampled = merged() + else: + sampled = merged + energy = [item["energy"] for item in sampled] + data_mixed_type = "real_natoms_vec" in sampled[0] + if data_mixed_type: + input_natoms = [item["real_natoms_vec"] for item in sampled] + else: + input_natoms = [item["natoms"] for item in sampled] # shape: (nframes, ndim) merged_energy = to_numpy_array(torch.cat(energy)) # shape: (nframes, ntypes) @@ -320,7 +347,6 @@ def __init__( self.filter_layers = torch.nn.ModuleList(filter_layers) if "seed" in kwargs: - log.info("Set seed to %d in fitting net.", kwargs["seed"]) torch.manual_seed(kwargs["seed"]) def output_def(self): diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 8e8338210f..47535580db 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -62,6 +62,11 @@ def __new__(cls, *args, **kwargs): return super().__new__(cls) def share_params(self, base_class, shared_level, resume=False): + """ + Share the parameters of self to the base_class with shared_level during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ assert ( self.__class__ == base_class.__class__ ), "Only fitting nets of the same type can share params!" @@ -77,18 +82,6 @@ def share_params(self, base_class, shared_level, resume=False): # the following will successfully link all the params except buffers, which need manually link. for item in self._modules: self._modules[item] = base_class._modules[item] - elif shared_level == 2: - # share all the layers before final layer - # the following will successfully link all the params except buffers, which need manually link. - self._modules["filter_layers"][0].deep_layers = base_class._modules[ - "filter_layers" - ][0].deep_layers - elif shared_level == 3: - # share the first layers - # the following will successfully link all the params except buffers, which need manually link. - self._modules["filter_layers"][0].deep_layers[0] = base_class._modules[ - "filter_layers" - ][0].deep_layers[0] else: raise NotImplementedError @@ -354,7 +347,6 @@ def __init__( self.filter_layers_old = None if seed is not None: - log.info("Set seed to %d in fitting net.", seed) torch.manual_seed(seed) def reinit_exclude( diff --git a/deepmd/pt/model/task/polarizability.py b/deepmd/pt/model/task/polarizability.py index 1bc4798c48..9483d1eb4a 100644 --- a/deepmd/pt/model/task/polarizability.py +++ b/deepmd/pt/model/task/polarizability.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging from typing import ( + Callable, List, Optional, Union, @@ -24,6 +25,9 @@ from deepmd.pt.utils.utils import ( to_numpy_array, ) +from deepmd.utils.path import ( + DPPath, +) log = logging.getLogger(__name__) @@ -72,7 +76,6 @@ class PolarFittingNet(GeneralFitting): def __init__( self, - var_name: str, ntypes: int, dim_descrpt: int, embedding_width: int, @@ -112,7 +115,7 @@ def __init__( ).view(ntypes, 1) self.shift_diag = shift_diag super().__init__( - var_name=var_name, + var_name=kwargs.pop("var_name", "polar"), ntypes=ntypes, dim_descrpt=dim_descrpt, neuron=neuron, @@ -160,6 +163,29 @@ def output_def(self) -> FittingOutputDef: ] ) + def compute_output_stats( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + stat_file_path: Optional[DPPath] = None, + ): + """ + Compute the output statistics (e.g. energy bias) for the fitting net from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + stat_file_path : Optional[DPPath] + The path to the stat file. + + """ + raise NotImplementedError + def forward( self, descriptor: torch.Tensor, diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 152c69a444..ef8a53e656 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import functools import logging import time from copy import ( @@ -49,6 +50,9 @@ from deepmd.pt.utils.learning_rate import ( LearningRateExp, ) +from deepmd.pt.utils.stat import ( + make_stat_input, +) if torch.__version__.startswith("2"): import torch._dynamo @@ -59,6 +63,10 @@ DataLoader, ) +from deepmd.utils.path import ( + DPH5Path, +) + log = logging.getLogger(__name__) @@ -67,7 +75,6 @@ def __init__( self, config: Dict[str, Any], training_data, - sampled=None, stat_file_path=None, validation_data=None, init_model=None, @@ -82,7 +89,15 @@ def __init__( Args: - config: The Dict-like configuration with training options. """ - resume_model = init_model if init_model is not None else restart_model + if init_model is not None: + resume_model = init_model + elif restart_model is not None: + resume_model = restart_model + elif finetune_model is not None: + resume_model = finetune_model + else: + resume_model = None + resuming = resume_model is not None self.restart_training = restart_model is not None model_params = config["model"] training_params = config["training"] @@ -93,8 +108,6 @@ def __init__( self.model_keys = ( list(model_params["model_dict"]) if self.multi_task else ["Default"] ) - if self.multi_task and sampled is None: - sampled = {key: None for key in self.model_keys} self.rank = dist.get_rank() if dist.is_initialized() else 0 self.world_size = dist.get_world_size() if dist.is_initialized() else 1 self.num_model = len(self.model_keys) @@ -119,62 +132,51 @@ def get_opt_param(params): return opt_type, opt_param def get_data_loader(_training_data, _validation_data, _training_params): - if "auto_prob" in _training_params["training_data"]: - train_sampler = get_weighted_sampler( - _training_data, _training_params["training_data"]["auto_prob"] - ) - elif "sys_probs" in _training_params["training_data"]: - train_sampler = get_weighted_sampler( - _training_data, - _training_params["training_data"]["sys_probs"], - sys_prob=True, + def get_dataloader_and_buffer(_data, _params): + if "auto_prob" in _training_params["training_data"]: + _sampler = get_weighted_sampler( + _data, _params["training_data"]["auto_prob"] + ) + elif "sys_probs" in _training_params["training_data"]: + _sampler = get_weighted_sampler( + _data, + _params["training_data"]["sys_probs"], + sys_prob=True, + ) + else: + _sampler = get_weighted_sampler(_data, "prob_sys_size") + + if _sampler is None: + log.warning( + "Sampler not specified!" + ) # None sampler will lead to a premature stop iteration. Replacement should be True in attribute of the sampler to produce expected number of items in one iteration. + _dataloader = DataLoader( + _data, + sampler=_sampler, + batch_size=None, + num_workers=NUM_WORKERS, # setting to 0 diverges the behavior of its iterator; should be >=1 + drop_last=False, + pin_memory=True, ) - else: - train_sampler = get_weighted_sampler(_training_data, "prob_sys_size") + with torch.device("cpu"): + _data_buffered = BufferedIterator(iter(_dataloader)) + return _dataloader, _data_buffered - if "auto_prob" in _training_params["validation_data"]: - valid_sampler = get_weighted_sampler( - _validation_data, _training_params["validation_data"]["auto_prob"] - ) - elif "sys_probs" in _training_params["validation_data"]: - valid_sampler = get_weighted_sampler( - _validation_data, - _training_params["validation_data"]["sys_probs"], - sys_prob=True, - ) - else: - valid_sampler = get_weighted_sampler(_validation_data, "prob_sys_size") - - if train_sampler is None or valid_sampler is None: - log.warning( - "Sampler not specified!" - ) # None sampler will lead to a premature stop iteration. Replacement should be True in attribute of the sampler to produce expected number of items in one iteration. - training_dataloader = DataLoader( - _training_data, - sampler=train_sampler, - batch_size=None, - num_workers=NUM_WORKERS, # setting to 0 diverges the behavior of its iterator; should be >=1 - drop_last=False, - pin_memory=True, - ) - with torch.device("cpu"): - training_data_buffered = BufferedIterator(iter(training_dataloader)) - validation_dataloader = DataLoader( - _validation_data, - sampler=valid_sampler, - batch_size=None, - num_workers=min(NUM_WORKERS, 1), - drop_last=False, - pin_memory=True, + training_dataloader, training_data_buffered = get_dataloader_and_buffer( + _training_data, _training_params ) - with torch.device("cpu"): - validation_data_buffered = BufferedIterator(iter(validation_dataloader)) - if _training_params.get("validation_data", None) is not None: + if _validation_data is not None: + ( + validation_dataloader, + validation_data_buffered, + ) = get_dataloader_and_buffer(_validation_data, _training_params) valid_numb_batch = _training_params["validation_data"].get( "numb_btch", 1 ) else: + validation_dataloader = None + validation_data_buffered = None valid_numb_batch = 1 return ( training_dataloader, @@ -184,13 +186,34 @@ def get_data_loader(_training_data, _validation_data, _training_params): valid_numb_batch, ) - def get_single_model(_model_params, _sampled, _stat_file_path): + def get_single_model( + _model_params, + _training_data, + _validation_data, + _stat_file_path, + _data_requirement, + ): model = get_model(deepcopy(_model_params)).to(DEVICE) - if not model_params.get("resuming", False): + _training_data.add_data_requirement(_data_requirement) + if _validation_data is not None: + _validation_data.add_data_requirement(_data_requirement) + if not resuming and self.rank == 0: + + @functools.lru_cache + def get_sample(): + sampled = make_stat_input( + _training_data.systems, + _training_data.dataloaders, + _model_params.get("data_stat_nbatch", 10), + ) + return sampled + model.compute_or_load_stat( - sampled=_sampled, + sampled_func=get_sample, stat_file_path=_stat_file_path, ) + if isinstance(_stat_file_path, DPH5Path): + _stat_file_path.root.close() return model def get_lr(lr_params): @@ -230,9 +253,34 @@ def get_loss(loss_params, start_lr, _ntypes): else: self.opt_type, self.opt_param = get_opt_param(training_params) + # Loss + if not self.multi_task: + self.loss = get_loss( + config["loss"], + config["learning_rate"]["start_lr"], + len(model_params["type_map"]), + ) + else: + self.loss = {} + for model_key in self.model_keys: + loss_param = config["loss_dict"][model_key] + if config.get("learning_rate_dict", None) is not None: + lr_param = config["learning_rate_dict"][model_key]["start_lr"] + else: + lr_param = config["learning_rate"]["start_lr"] + ntypes = len(model_params["model_dict"][model_key]["type_map"]) + self.loss[model_key] = get_loss(loss_param, lr_param, ntypes) + # Data + Model dp_random.seed(training_params["seed"]) if not self.multi_task: + self.model = get_single_model( + model_params, + training_data, + validation_data, + stat_file_path, + self.loss.label_requirement, + ) ( self.training_dataloader, self.training_data, @@ -240,7 +288,6 @@ def get_loss(loss_params, start_lr, _ntypes): self.validation_data, self.valid_numb_batch, ) = get_data_loader(training_data, validation_data, training_params) - self.model = get_single_model(model_params, sampled, stat_file_path) else: ( self.training_dataloader, @@ -251,6 +298,13 @@ def get_loss(loss_params, start_lr, _ntypes): self.model, ) = {}, {}, {}, {}, {}, {} for model_key in self.model_keys: + self.model[model_key] = get_single_model( + model_params["model_dict"][model_key], + training_data[model_key], + validation_data[model_key], + stat_file_path[model_key], + self.loss[model_key].label_requirement, + ) ( self.training_dataloader[model_key], self.training_data[model_key], @@ -262,11 +316,6 @@ def get_loss(loss_params, start_lr, _ntypes): validation_data[model_key], training_params["data_dict"][model_key], ) - self.model[model_key] = get_single_model( - model_params["model_dict"][model_key], - sampled[model_key], - stat_file_path[model_key], - ) # Learning rate self.warmup_steps = training_params.get("warmup_steps", 0) @@ -281,24 +330,6 @@ def get_loss(loss_params, start_lr, _ntypes): else: self.lr_exp = get_lr(config["learning_rate"]) - # Loss - if not self.multi_task: - self.loss = get_loss( - config["loss"], - config["learning_rate"]["start_lr"], - len(model_params["type_map"]), - ) - else: - self.loss = {} - for model_key in self.model_keys: - loss_param = config["loss_dict"][model_key] - if config.get("learning_rate_dict", None) is not None: - lr_param = config["learning_rate_dict"][model_key]["start_lr"] - else: - lr_param = config["learning_rate"]["start_lr"] - ntypes = len(model_params["model_dict"][model_key]["type_map"]) - self.loss[model_key] = get_loss(loss_param, lr_param, ntypes) - # JIT if JIT: self.model = torch.jit.script(self.model) @@ -309,7 +340,7 @@ def get_loss(loss_params, start_lr, _ntypes): # resuming and finetune optimizer_state_dict = None - if model_params["resuming"]: + if resuming: ntest = model_params.get("data_bias_nsample", 1) origin_model = ( finetune_model if finetune_model is not None else resume_model @@ -404,7 +435,7 @@ def get_loss(loss_params, start_lr, _ntypes): # Multi-task share params if shared_links is not None: - self.wrapper.share_params(shared_links, resume=model_params["resuming"]) + self.wrapper.share_params(shared_links, resume=resuming or self.rank != 0) if dist.is_initialized(): torch.cuda.set_device(LOCAL_RANK) @@ -617,6 +648,9 @@ def log_loss_valid(_task_key="Default"): input_dict, label_dict, _ = self.get_data( is_train=False, task_key=_task_key ) + if input_dict == {}: + # no validation data + return "", None _, loss, more_loss = self.wrapper( **input_dict, cur_lr=pref_lr, @@ -778,6 +812,8 @@ def get_data(self, is_train=True, task_key="Default"): ) batch_data = next(iter(self.training_data)) else: + if self.validation_data is None: + return {}, {}, {} try: batch_data = next(iter(self.validation_data)) except StopIteration: @@ -796,6 +832,8 @@ def get_data(self, is_train=True, task_key="Default"): ) batch_data = next(iter(self.training_data[task_key])) else: + if self.validation_data[task_key] is None: + return {}, {}, {} try: batch_data = next(iter(self.validation_data[task_key])) except StopIteration: @@ -812,28 +850,24 @@ def get_data(self, is_train=True, task_key="Default"): batch_data[key] = batch_data[key].to(DEVICE) else: batch_data[key] = [item.to(DEVICE) for item in batch_data[key]] - input_dict = {} - for item in [ + # we may need a better way to classify which are inputs and which are labels + # now wrapper only supports the following inputs: + input_keys = [ "coord", "atype", "box", - ]: - if item in batch_data: - input_dict[item] = batch_data[item] - else: - input_dict[item] = None + "spin", + "fparam", + "aparam", + ] + input_dict = {item_key: None for item_key in input_keys} label_dict = {} - for item in [ - "energy", - "force", - "virial", - "clean_coord", - "clean_type", - "coord_mask", - "type_mask", - ]: - if item in batch_data: - label_dict[item] = batch_data[item] + for item_key in batch_data: + if item_key in input_keys: + input_dict[item_key] = batch_data[item_key] + else: + if item_key not in ["sid", "fid"] and "find_" not in item_key: + label_dict[item_key] = batch_data[item_key] log_dict = {} if "fid" in batch_data: log_dict["fid"] = batch_data["fid"] diff --git a/deepmd/pt/train/wrapper.py b/deepmd/pt/train/wrapper.py index 74b4a83ce7..67f8043653 100644 --- a/deepmd/pt/train/wrapper.py +++ b/deepmd/pt/train/wrapper.py @@ -61,7 +61,7 @@ def __init__( self.inference_only = self.loss is None def set_trainable_params(self): - supported_types = ["type_embedding", "descriptor", "fitting_net"] + supported_types = ["descriptor", "fitting_net"] for model_item in self.model: for net_type in supported_types: trainable = True @@ -83,7 +83,12 @@ def set_trainable_params(self): param.requires_grad = trainable def share_params(self, shared_links, resume=False): - supported_types = ["type_embedding", "descriptor", "fitting_net"] + """ + Share the parameters of classes following rules defined in shared_links during multitask training. + If not start from checkpoint (resume is False), + some seperated parameters (e.g. mean and stddev) will be re-calculated across different classes. + """ + supported_types = ["descriptor", "fitting_net"] for shared_item in shared_links: class_name = shared_links[shared_item]["type"] shared_base = shared_links[shared_item]["links"][0] @@ -159,6 +164,7 @@ def forward( coord, atype, box: Optional[torch.Tensor] = None, + spin: Optional[torch.Tensor] = None, cur_lr: Optional[torch.Tensor] = None, label: Optional[torch.Tensor] = None, task_key: Optional[torch.Tensor] = None, diff --git a/deepmd/pt/utils/dataloader.py b/deepmd/pt/utils/dataloader.py index 2125f9cdee..65a96418c9 100644 --- a/deepmd/pt/utils/dataloader.py +++ b/deepmd/pt/utils/dataloader.py @@ -35,6 +35,9 @@ from deepmd.pt.utils.dataset import ( DeepmdDataSetForLoader, ) +from deepmd.utils.data import ( + DataRequirementItem, +) from deepmd.utils.data_system import ( prob_sys_size_ext, process_sys_probs, @@ -147,6 +150,11 @@ def __getitem__(self, idx): batch["sid"] = idx return batch + def add_data_requirement(self, data_requirement: List[DataRequirementItem]): + """Add data requirement for each system in multiple systems.""" + for system in self.systems: + system.add_data_requirement(data_requirement) + _sentinel = object() QUEUESIZE = 32 @@ -248,7 +256,7 @@ def get_weighted_sampler(training_data, prob_style, sys_prob=False): probs = prob_sys_size_ext(style, len(training_data), training_data.index) else: probs = process_sys_probs(prob_style, training_data.index) - log.info("Generated weighted sampler with prob array: " + str(probs)) + log.debug("Generated weighted sampler with prob array: " + str(probs)) # training_data.total_batch is the size of one epoch, you can increase it to avoid too many rebuilding of iteraters len_sampler = training_data.total_batch * max(env.NUM_WORKERS, 1) with torch.device("cpu"): diff --git a/deepmd/pt/utils/dataset.py b/deepmd/pt/utils/dataset.py index 4619b6417f..40a513acdf 100644 --- a/deepmd/pt/utils/dataset.py +++ b/deepmd/pt/utils/dataset.py @@ -1,10 +1,16 @@ # SPDX-License-Identifier: LGPL-3.0-or-later + +from typing import ( + List, +) + from torch.utils.data import ( Dataset, ) from deepmd.utils.data import ( + DataRequirementItem, DeepmdData, ) @@ -27,9 +33,6 @@ def __init__( self._data_system = DeepmdData( sys_path=system, shuffle_test=shuffle, type_map=self._type_map ) - self._data_system.add("energy", 1, atomic=False, must=False, high_prec=True) - self._data_system.add("force", 3, atomic=True, must=False, high_prec=False) - self._data_system.add("virial", 9, atomic=False, must=False, high_prec=False) self.mixed_type = self._data_system.mixed_type self._ntypes = self._data_system.get_ntypes() self._natoms = self._data_system.get_natoms() @@ -43,3 +46,18 @@ def __getitem__(self, index): b_data = self._data_system.get_item_torch(index) b_data["natoms"] = self._natoms_vec return b_data + + def add_data_requirement(self, data_requirement: List[DataRequirementItem]): + """Add data requirement for this data system.""" + for data_item in data_requirement: + self._data_system.add( + data_item["key"], + data_item["ndof"], + atomic=data_item["atomic"], + must=data_item["must"], + high_prec=data_item["high_prec"], + type_sel=data_item["type_sel"], + repeat=data_item["repeat"], + default=data_item["default"], + dtype=data_item["dtype"], + ) diff --git a/deepmd/pt/utils/finetune.py b/deepmd/pt/utils/finetune.py index 13749da151..c8fa1e5185 100644 --- a/deepmd/pt/utils/finetune.py +++ b/deepmd/pt/utils/finetune.py @@ -19,9 +19,7 @@ def change_finetune_model_params( - ckpt & finetune_model: origin model. - config: Read from json file. """ - if multi_task: - # TODO - log.error("finetune mode need modification for multitask mode!") + # TODO need support for multitask mode if finetune_model is not None: state_dict = torch.load(finetune_model, map_location=env.DEVICE) if "model" in state_dict: diff --git a/deepmd/pt/utils/multi_task.py b/deepmd/pt/utils/multi_task.py index f97a826b03..ae3933a101 100644 --- a/deepmd/pt/utils/multi_task.py +++ b/deepmd/pt/utils/multi_task.py @@ -4,17 +4,10 @@ ) from deepmd.pt.model.descriptor import ( - DescrptDPA1, - DescrptDPA2, - DescrptSeA, -) -from deepmd.pt.model.network.network import ( - TypeEmbedNet, + BaseDescriptor, ) from deepmd.pt.model.task import ( - EnergyFittingNet, - EnergyFittingNetDirect, - FittingNetAttenLcc, + BaseFitting, ) @@ -37,9 +30,68 @@ def preprocess_shared_params(model_config): - "shared_level": Shared level (int) of this item in this model. Lower for more params to share, 0 means to share all params in this item. This list are sorted by "shared_level". + For example, if one has `model_config` like this: + "model": { + "shared_dict": { + "my_type_map": ["foo", "bar"], + "my_des1": { + "type": "se_e2_a", + "neuron": [10, 20, 40] + }, + }, + "model_dict": { + "model_1": { + "type_map": "my_type_map", + "descriptor": "my_des1", + "fitting_net": { + "neuron": [100, 100, 100] + } + }, + "model_2": { + "type_map": "my_type_map", + "descriptor": "my_des1", + "fitting_net": { + "neuron": [100, 100, 100] + } + } + "model_3": { + "type_map": "my_type_map", + "descriptor": "my_des1:1", + "fitting_net": { + "neuron": [100, 100, 100] + } + } + } + } + The above config will init three model branches named `model_1` and `model_2` and `model_3`, + in which: + - `model_2` and `model_3` will have the same `type_map` as that in `model_1`. + - `model_2` will share all the parameters of `descriptor` with `model_1`, + while `model_3` will share part of parameters of `descriptor` with `model_1` + on human-defined share-level `1` (default is `0`, meaning share all the parameters). + - `model_1`, `model_2` and `model_3` have three different `fitting_net`s. + The returned `model_config` will automatically fulfill the input `model_config` as if there's no sharing, + and the `shared_links` will keep all the sharing information with looking: + { + 'my_des1': { + 'type': 'DescrptSeA', + 'links': [ + {'model_key': 'model_1', + 'shared_type': 'descriptor', + 'shared_level': 0}, + {'model_key': 'model_2', + 'shared_type': 'descriptor', + 'shared_level': 0}, + {'model_key': 'model_3', + 'shared_type': 'descriptor', + 'shared_level': 1} + ] + } + } + """ assert "model_dict" in model_config, "only multi-task model can use this method!" - supported_types = ["type_map", "type_embedding", "descriptor", "fitting_net"] + supported_types = ["type_map", "descriptor", "fitting_net"] shared_dict = model_config.get("shared_dict", {}) shared_links = {} type_map_keys = [] @@ -98,32 +150,9 @@ def replace_one_item(params_dict, key_type, key_in_dict, suffix="", index=None): def get_class_name(item_key, item_params): - if item_key == "type_embedding": - return TypeEmbedNet.__name__ - elif item_key == "descriptor": - item_type = item_params.get("type", "se_e2_a") - if item_type == "se_e2_a": - return DescrptSeA.__name__ - elif item_type in ["se_atten", "dpa1"]: - return DescrptDPA1.__name__ - elif item_type in ["dpa2"]: - return DescrptDPA2.__name__ - # todo add support for other combination - # elif item_type == "gaussian_lcc": - # return DescrptGaussianLcc.__name__ - # elif item_type == "hybrid": - # return DescrptHybrid.__name__ - else: - raise RuntimeError(f"Unknown descriptor type {item_type}") + if item_key == "descriptor": + return BaseDescriptor.get_class_by_type(item_params.get("type", "se_e2_a")) elif item_key == "fitting_net": - item_type = item_params.get("type", "ener") - if item_type == "ener": - return EnergyFittingNet.__name__ - elif item_type in ["direct_force", "direct_force_ener"]: - return EnergyFittingNetDirect.__name__ - elif item_type == "atten_vec_lcc": - return FittingNetAttenLcc.__name__ - else: - raise RuntimeError(f"Unknown fitting_net type {item_type}") + return BaseFitting.get_class_by_type(item_params.get("type", "ener")) else: raise RuntimeError(f"Unknown class_name type {item_key}") diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index 4c769f019e..3b246a0ec2 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -3,6 +3,10 @@ import torch +from deepmd.pt.utils.utils import ( + dict_to_device, +) + log = logging.getLogger(__name__) @@ -18,19 +22,9 @@ def make_stat_input(datasets, dataloaders, nbatches): - a list of dicts, each of which contains data from a system """ lst = [] - keys = [ - "coord", - "force", - "energy", - "atype", - "box", - "natoms", - ] - if datasets[0].mixed_type: - keys.append("real_natoms_vec") log.info(f"Packing data for statistics from {len(datasets)} systems") for i in range(len(datasets)): - sys_stat = {key: [] for key in keys} + sys_stat = {} with torch.device("cpu"): iterator = iter(dataloaders[i]) for _ in range(nbatches): @@ -40,19 +34,19 @@ def make_stat_input(datasets, dataloaders, nbatches): iterator = iter(dataloaders[i]) stat_data = next(iterator) for dd in stat_data: - if dd in keys: + if stat_data[dd] is None: + sys_stat[dd] = None + elif isinstance(stat_data[dd], torch.Tensor): + if dd not in sys_stat: + sys_stat[dd] = [] sys_stat[dd].append(stat_data[dd]) - for key in keys: - if not isinstance(sys_stat[key][0], list): - if sys_stat[key][0] is None: - sys_stat[key] = None - else: - sys_stat[key] = torch.cat(sys_stat[key], dim=0) + else: + pass + for key in sys_stat: + if sys_stat[key] is None or sys_stat[key][0] is None: + sys_stat[key] = None else: - sys_stat_list = [] - for ii, _ in enumerate(sys_stat[key][0]): - tmp_stat = [x[ii] for x in sys_stat[key]] - sys_stat_list.append(torch.cat(tmp_stat, dim=0)) - sys_stat[key] = sys_stat_list + sys_stat[key] = torch.cat(sys_stat[key], dim=0) + dict_to_device(sys_stat) lst.append(sys_stat) return lst diff --git a/deepmd/utils/data.py b/deepmd/utils/data.py index 6e0c47881f..03e39e1f21 100644 --- a/deepmd/utils/data.py +++ b/deepmd/utils/data.py @@ -490,6 +490,8 @@ def reformat_data_torch(self, data): if self.data_dict[kk]["atomic"]: data[kk] = data[kk].reshape(-1, self.data_dict[kk]["ndof"]) data["atype"] = data["type"] + if not self.pbc: + data["box"] = None return data def _load_set(self, set_name: DPPath): @@ -664,3 +666,73 @@ def _check_pbc(self, sys_path: DPPath): def _check_mode(self, set_path: DPPath): return (set_path / "real_atom_types.npy").is_file() + + +class DataRequirementItem: + """A class to store the data requirement for data systems. + + Parameters + ---------- + key + The key of the item. The corresponding data is stored in `sys_path/set.*/key.npy` + ndof + The number of dof + atomic + The item is an atomic property. + If False, the size of the data should be nframes x ndof + If True, the size of data should be nframes x natoms x ndof + must + The data file `sys_path/set.*/key.npy` must exist. + If must is False and the data file does not exist, the `data_dict[find_key]` is set to 0.0 + high_prec + Load the data and store in float64, otherwise in float32 + type_sel + Select certain type of atoms + repeat + The data will be repeated `repeat` times. + default : float, default=0. + default value of data + dtype : np.dtype, optional + the dtype of data, overwrites `high_prec` if provided + """ + + def __init__( + self, + key: str, + ndof: int, + atomic: bool = False, + must: bool = False, + high_prec: bool = False, + type_sel: Optional[List[int]] = None, + repeat: int = 1, + default: float = 0.0, + dtype: Optional[np.dtype] = None, + ) -> None: + self.key = key + self.ndof = ndof + self.atomic = atomic + self.must = must + self.high_prec = high_prec + self.type_sel = type_sel + self.repeat = repeat + self.default = default + self.dtype = dtype + self.dict = self.to_dict() + + def to_dict(self) -> dict: + return { + "key": self.key, + "ndof": self.ndof, + "atomic": self.atomic, + "must": self.must, + "high_prec": self.high_prec, + "type_sel": self.type_sel, + "repeat": self.repeat, + "default": self.default, + "dtype": self.dtype, + } + + def __getitem__(self, key: str): + if key not in self.dict: + raise KeyError(key) + return self.dict[key] diff --git a/deepmd/utils/env_mat_stat.py b/deepmd/utils/env_mat_stat.py index 2fa497b9b6..217c46844b 100644 --- a/deepmd/utils/env_mat_stat.py +++ b/deepmd/utils/env_mat_stat.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import logging from abc import ( ABC, abstractmethod, @@ -19,6 +20,8 @@ DPPath, ) +log = logging.getLogger(__name__) + class StatItem: """A class to store the statistics of the environment matrix. @@ -170,10 +173,12 @@ def load_or_compute_stats( """ if path is not None and path.is_dir(): self.load_stats(path) + log.info(f"Load stats from {path}.") else: self.compute_stats(data) if path is not None: self.save_stats(path) + log.info(f"Save stats to {path}.") def get_avg(self, default: float = 0) -> Dict[str, float]: """Get the average of the environment matrix. diff --git a/source/tests/pt/model/test_descriptor.py b/source/tests/pt/model/test_descriptor.py index ffad27201a..7d21d1c13d 100644 --- a/source/tests/pt/model/test_descriptor.py +++ b/source/tests/pt/model/test_descriptor.py @@ -38,6 +38,9 @@ op_module, ) +from ..test_stat import ( + energy_data_requirement, +) from .test_embedding_net import ( get_single_batch, ) @@ -114,6 +117,7 @@ def setUp(self): self.systems[0], model_config["type_map"], ) + ds.add_data_requirement(energy_data_requirement) self.np_batch, self.pt_batch = get_single_batch(ds) self.sec = np.cumsum(self.sel) self.ntypes = len(self.sel) diff --git a/source/tests/pt/model/test_dipole_fitting.py b/source/tests/pt/model/test_dipole_fitting.py index fcdd408726..fa4be9171c 100644 --- a/source/tests/pt/model/test_dipole_fitting.py +++ b/source/tests/pt/model/test_dipole_fitting.py @@ -79,7 +79,6 @@ def test_consistency( [0, 4], ): ft0 = DipoleFittingNet( - "foo", self.nt, self.dd0.dim_out, embedding_width=self.dd0.get_dim_emb(), @@ -115,12 +114,12 @@ def test_consistency( ) ret2 = ft2(rd0, atype, gr, fparam=ifp, aparam=iap) np.testing.assert_allclose( - to_numpy_array(ret0["foo"]), - ret1["foo"], + to_numpy_array(ret0["dipole"]), + ret1["dipole"], ) np.testing.assert_allclose( - to_numpy_array(ret0["foo"]), - to_numpy_array(ret2["foo"]), + to_numpy_array(ret0["dipole"]), + to_numpy_array(ret2["dipole"]), ) def test_jit( @@ -132,7 +131,6 @@ def test_jit( [0, 4], ): ft0 = DipoleFittingNet( - "foo", self.nt, self.dd0.dim_out, embedding_width=self.dd0.get_dim_emb(), @@ -168,7 +166,6 @@ def test_rot(self): [0, 4], ): ft0 = DipoleFittingNet( - "foo", 3, # ntype self.dd0.dim_out, # dim_descrpt embedding_width=self.dd0.get_dim_emb(), @@ -209,7 +206,7 @@ def test_rot(self): ) ret0 = ft0(rd0, extended_atype, gr0, fparam=ifp, aparam=iap) - res.append(ret0["foo"]) + res.append(ret0["dipole"]) np.testing.assert_allclose( to_numpy_array(res[1]), to_numpy_array(torch.matmul(res[0], rmat)) @@ -218,7 +215,6 @@ def test_rot(self): def test_permu(self): coord = torch.matmul(self.coord, self.cell) ft0 = DipoleFittingNet( - "foo", 3, # ntype self.dd0.dim_out, embedding_width=self.dd0.get_dim_emb(), @@ -245,7 +241,7 @@ def test_permu(self): ) ret0 = ft0(rd0, extended_atype, gr0, fparam=0, aparam=0) - res.append(ret0["foo"]) + res.append(ret0["dipole"]) np.testing.assert_allclose( to_numpy_array(res[0][:, idx_perm]), to_numpy_array(res[1]) @@ -260,7 +256,6 @@ def test_trans(self): self.cell, ) ft0 = DipoleFittingNet( - "foo", 3, # ntype self.dd0.dim_out, embedding_width=self.dd0.get_dim_emb(), @@ -286,7 +281,7 @@ def test_trans(self): ) ret0 = ft0(rd0, extended_atype, gr0, fparam=0, aparam=0) - res.append(ret0["foo"]) + res.append(ret0["dipole"]) np.testing.assert_allclose(to_numpy_array(res[0]), to_numpy_array(res[1])) @@ -305,7 +300,6 @@ def setUp(self): self.atype = torch.IntTensor([0, 0, 0, 1, 1], device="cpu").to(env.DEVICE) self.dd0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) self.ft0 = DipoleFittingNet( - "dipole", self.nt, self.dd0.dim_out, embedding_width=self.dd0.get_dim_emb(), diff --git a/source/tests/pt/model/test_embedding_net.py b/source/tests/pt/model/test_embedding_net.py index 87e8a97444..a1895718dd 100644 --- a/source/tests/pt/model/test_embedding_net.py +++ b/source/tests/pt/model/test_embedding_net.py @@ -39,6 +39,10 @@ ) from deepmd.tf.descriptor import DescrptSeA as DescrptSeA_tf +from ..test_stat import ( + energy_data_requirement, +) + CUR_DIR = os.path.dirname(__file__) @@ -128,6 +132,7 @@ def setUp(self): self.systems[0], model_config["type_map"], ) + ds.add_data_requirement(energy_data_requirement) self.filter_neuron = model_config["descriptor"]["neuron"] self.axis_neuron = model_config["descriptor"]["axis_neuron"] self.np_batch, self.torch_batch = get_single_batch(ds) diff --git a/source/tests/pt/model/test_model.py b/source/tests/pt/model/test_model.py index d8c7de39c3..69ec88f5d7 100644 --- a/source/tests/pt/model/test_model.py +++ b/source/tests/pt/model/test_model.py @@ -51,6 +51,10 @@ LearningRateExp, ) +from ..test_stat import ( + energy_data_requirement, +) + VariableState = collections.namedtuple("VariableState", ["value", "gradient"]) @@ -281,6 +285,7 @@ def test_consistency(self): "type_map": self.type_map, }, ) + my_ds.add_data_requirement(energy_data_requirement) my_model = get_model( model_params={ "descriptor": { diff --git a/source/tests/pt/model/test_polarizability_fitting.py b/source/tests/pt/model/test_polarizability_fitting.py index f76a9e28ac..b1a5e3f730 100644 --- a/source/tests/pt/model/test_polarizability_fitting.py +++ b/source/tests/pt/model/test_polarizability_fitting.py @@ -67,7 +67,6 @@ def test_consistency( [None, self.scale], ): ft0 = PolarFittingNet( - "foo", self.nt, self.dd0.dim_out, embedding_width=self.dd0.get_dim_emb(), @@ -113,16 +112,16 @@ def test_consistency( aparam=to_numpy_array(iap), ) np.testing.assert_allclose( - to_numpy_array(ret0["foo"]), - ret1["foo"], + to_numpy_array(ret0["polar"]), + ret1["polar"], ) np.testing.assert_allclose( - to_numpy_array(ret0["foo"]), - to_numpy_array(ret2["foo"]), + to_numpy_array(ret0["polar"]), + to_numpy_array(ret2["polar"]), ) np.testing.assert_allclose( - to_numpy_array(ret0["foo"]), - ret3["foo"], + to_numpy_array(ret0["polar"]), + ret3["polar"], ) def test_jit( @@ -135,7 +134,6 @@ def test_jit( [True, False], ): ft0 = PolarFittingNet( - "foo", self.nt, self.dd0.dim_out, embedding_width=self.dd0.get_dim_emb(), @@ -177,7 +175,6 @@ def test_rot(self): [None, self.scale], ): ft0 = PolarFittingNet( - "foo", self.nt, self.dd0.dim_out, # dim_descrpt embedding_width=self.dd0.get_dim_emb(), @@ -220,7 +217,7 @@ def test_rot(self): ) ret0 = ft0(rd0, extended_atype, gr0, fparam=ifp, aparam=iap) - res.append(ret0["foo"]) + res.append(ret0["polar"]) np.testing.assert_allclose( to_numpy_array(res[1]), to_numpy_array( @@ -235,7 +232,6 @@ def test_permu(self): coord = torch.matmul(self.coord, self.cell) for fit_diag, scale in itertools.product([True, False], [None, self.scale]): ft0 = PolarFittingNet( - "foo", self.nt, self.dd0.dim_out, embedding_width=self.dd0.get_dim_emb(), @@ -264,7 +260,7 @@ def test_permu(self): ) ret0 = ft0(rd0, extended_atype, gr0, fparam=None, aparam=None) - res.append(ret0["foo"]) + res.append(ret0["polar"]) np.testing.assert_allclose( to_numpy_array(res[0][:, idx_perm]), @@ -281,7 +277,6 @@ def test_trans(self): ) for fit_diag, scale in itertools.product([True, False], [None, self.scale]): ft0 = PolarFittingNet( - "foo", self.nt, self.dd0.dim_out, embedding_width=self.dd0.get_dim_emb(), @@ -309,7 +304,7 @@ def test_trans(self): ) ret0 = ft0(rd0, extended_atype, gr0, fparam=0, aparam=0) - res.append(ret0["foo"]) + res.append(ret0["polar"]) np.testing.assert_allclose(to_numpy_array(res[0]), to_numpy_array(res[1])) @@ -328,7 +323,6 @@ def setUp(self): self.atype = torch.IntTensor([0, 0, 0, 1, 1], device="cpu") self.dd0 = DescrptSeA(self.rcut, self.rcut_smth, self.sel).to(env.DEVICE) self.ft0 = PolarFittingNet( - "polar", self.nt, self.dd0.dim_out, embedding_width=self.dd0.get_dim_emb(), diff --git a/source/tests/pt/model/water/multitask.json b/source/tests/pt/model/water/multitask.json new file mode 100644 index 0000000000..6baddd672b --- /dev/null +++ b/source/tests/pt/model/water/multitask.json @@ -0,0 +1,139 @@ +{ + "model": { + "shared_dict": { + "my_type_map": [ + "O", + "H", + "B" + ], + "my_descriptor": { + "type": "se_e2_a", + "sel": [ + 46, + 92 + ], + "rcut_smth": 0.50, + "rcut": 6.00, + "neuron": [ + 25, + 50, + 100 + ], + "resnet_dt": false, + "axis_neuron": 16, + "seed": 1, + "_comment": " that's all" + }, + "_comment": "that's all" + }, + "model_dict": { + "model_1": { + "type_map": "my_type_map", + "descriptor": "my_descriptor", + "fitting_net": { + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "seed": 1, + "_comment": " that's all" + } + }, + "model_2": { + "type_map": "my_type_map", + "descriptor": "my_descriptor", + "fitting_net": { + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "seed": 1, + "_comment": " that's all" + } + } + } + }, + "learning_rate": { + "type": "exp", + "decay_steps": 5000, + "start_lr": 0.0002, + "decay_rate": 0.98, + "stop_lr": 3.51e-08, + "_comment": "that's all" + }, + "loss_dict": { + "_comment": " that's all", + "model_1": { + "type": "ener", + "start_pref_e": 0.02, + "limit_pref_e": 1, + "start_pref_f": 1000, + "limit_pref_f": 1, + "start_pref_v": 0, + "limit_pref_v": 0 + }, + "model_2": { + "type": "ener", + "start_pref_e": 0.02, + "limit_pref_e": 1, + "start_pref_f": 1000, + "limit_pref_f": 1, + "start_pref_v": 0, + "limit_pref_v": 0 + } + }, + "training": { + "model_prob": { + "model_1": 0.5, + "model_2": 0.5 + }, + "data_dict": { + "model_1": { + "stat_file": "./stat_files/model_1", + "training_data": { + "systems": [ + "pt/water/data/data_0" + ], + "batch_size": 1, + "_comment": "that's all" + }, + "validation_data": { + "systems": [ + "pt/water/data/data_0" + ], + "batch_size": 1, + "_comment": "that's all" + } + }, + "model_2": { + "stat_file": "./stat_files/model_2", + "training_data": { + "systems": [ + "pt/water/data/data_0" + ], + "batch_size": 1, + "_comment": "that's all" + }, + "validation_data": { + "systems": [ + "pt/water/data/data_0" + ], + "batch_size": 1, + "_comment": "that's all" + } + } + }, + "numb_steps": 100000, + "warmup_steps": 0, + "gradient_max_norm": 5.0, + "seed": 10, + "disp_file": "lcurve.out", + "disp_freq": 100, + "save_freq": 100, + "_comment": "that's all" + } +} diff --git a/source/tests/pt/test_loss.py b/source/tests/pt/test_loss.py index e117c7f05a..484d62a3ad 100644 --- a/source/tests/pt/test_loss.py +++ b/source/tests/pt/test_loss.py @@ -28,6 +28,9 @@ from .model.test_embedding_net import ( get_single_batch, ) +from .test_stat import ( + energy_data_requirement, +) CUR_DIR = os.path.dirname(__file__) @@ -47,6 +50,7 @@ def get_batch(): if isinstance(systems, str): systems = expand_sys_str(systems) dataset = DeepmdDataSetForLoader(systems[0], model_config["type_map"]) + dataset.add_data_requirement(energy_data_requirement) np_batch, pt_batch = get_single_batch(dataset) return np_batch, pt_batch diff --git a/source/tests/pt/test_multitask.py b/source/tests/pt/test_multitask.py new file mode 100644 index 0000000000..3c0240dbdc --- /dev/null +++ b/source/tests/pt/test_multitask.py @@ -0,0 +1,181 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import os +import shutil +import unittest +from copy import ( + deepcopy, +) +from pathlib import ( + Path, +) + +import torch + +from deepmd.pt.entrypoints.main import ( + get_trainer, +) +from deepmd.pt.utils.multi_task import ( + preprocess_shared_params, +) + +from .model.test_permutation import ( + model_dpa1, + model_dpa2, + model_se_e2_a, +) + +multitask_template_json = str(Path(__file__).parent / "water/multitask.json") +with open(multitask_template_json) as f: + multitask_template = json.load(f) + + +class MultiTaskTrainTest: + def test_multitask_train(self): + trainer = get_trainer(deepcopy(self.config), shared_links=self.shared_links) + trainer.run() + # check model keys + self.assertEqual(len(trainer.wrapper.model), 2) + self.assertIn("model_1", trainer.wrapper.model) + self.assertIn("model_2", trainer.wrapper.model) + + # check shared parameters + multi_state_dict = trainer.wrapper.model.state_dict() + for state_key in multi_state_dict: + if "model_1" in state_key: + self.assertIn(state_key.replace("model_1", "model_2"), multi_state_dict) + if "model_2" in state_key: + self.assertIn(state_key.replace("model_2", "model_1"), multi_state_dict) + if "model_1.descriptor" in state_key: + torch.testing.assert_allclose( + multi_state_dict[state_key], + multi_state_dict[state_key.replace("model_1", "model_2")], + ) + self.tearDown() + + def tearDown(self): + for f in os.listdir("."): + if f.startswith("model") and f.endswith(".pt"): + os.remove(f) + if f in ["lcurve.out"]: + os.remove(f) + if f in [self.stat_files]: + shutil.rmtree(f) + + +class TestMultiTaskSeA(unittest.TestCase, MultiTaskTrainTest): + def setUp(self): + multitask_se_e2_a = deepcopy(multitask_template) + multitask_se_e2_a["model"]["shared_dict"]["my_descriptor"] = model_se_e2_a[ + "descriptor" + ] + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.stat_files = "se_e2_a" + os.makedirs(self.stat_files, exist_ok=True) + self.config = multitask_se_e2_a + self.config["training"]["data_dict"]["model_1"]["training_data"][ + "systems" + ] = data_file + self.config["training"]["data_dict"]["model_1"]["validation_data"][ + "systems" + ] = data_file + self.config["training"]["data_dict"]["model_1"][ + "stat_file" + ] = f"{self.stat_files}/model_1" + self.config["training"]["data_dict"]["model_2"]["training_data"][ + "systems" + ] = data_file + self.config["training"]["data_dict"]["model_2"]["validation_data"][ + "systems" + ] = data_file + self.config["training"]["data_dict"]["model_2"][ + "stat_file" + ] = f"{self.stat_files}/model_2" + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + self.config["model"], self.shared_links = preprocess_shared_params( + self.config["model"] + ) + + def tearDown(self) -> None: + MultiTaskTrainTest.tearDown(self) + + +class TestMultiTaskDPA1(unittest.TestCase, MultiTaskTrainTest): + def setUp(self): + multitask_DPA1 = deepcopy(multitask_template) + multitask_DPA1["model"]["shared_dict"]["my_descriptor"] = model_dpa1[ + "descriptor" + ] + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.stat_files = "DPA1" + os.makedirs(self.stat_files, exist_ok=True) + self.config = multitask_DPA1 + self.config["training"]["data_dict"]["model_1"]["training_data"][ + "systems" + ] = data_file + self.config["training"]["data_dict"]["model_1"]["validation_data"][ + "systems" + ] = data_file + self.config["training"]["data_dict"]["model_1"][ + "stat_file" + ] = f"{self.stat_files}/model_1" + self.config["training"]["data_dict"]["model_2"]["training_data"][ + "systems" + ] = data_file + self.config["training"]["data_dict"]["model_2"]["validation_data"][ + "systems" + ] = data_file + self.config["training"]["data_dict"]["model_2"][ + "stat_file" + ] = f"{self.stat_files}/model_2" + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + self.config["model"], self.shared_links = preprocess_shared_params( + self.config["model"] + ) + + def tearDown(self) -> None: + MultiTaskTrainTest.tearDown(self) + + +class TestMultiTaskDPA2(unittest.TestCase, MultiTaskTrainTest): + def setUp(self): + multitask_DPA2 = deepcopy(multitask_template) + multitask_DPA2["model"]["shared_dict"]["my_descriptor"] = model_dpa2[ + "descriptor" + ] + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.stat_files = "DPA2" + os.makedirs(self.stat_files, exist_ok=True) + self.config = multitask_DPA2 + self.config["training"]["data_dict"]["model_1"]["training_data"][ + "systems" + ] = data_file + self.config["training"]["data_dict"]["model_1"]["validation_data"][ + "systems" + ] = data_file + self.config["training"]["data_dict"]["model_1"][ + "stat_file" + ] = f"{self.stat_files}/model_1" + self.config["training"]["data_dict"]["model_2"]["training_data"][ + "systems" + ] = data_file + self.config["training"]["data_dict"]["model_2"]["validation_data"][ + "systems" + ] = data_file + self.config["training"]["data_dict"]["model_2"][ + "stat_file" + ] = f"{self.stat_files}/model_2" + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + self.config["model"], self.shared_links = preprocess_shared_params( + self.config["model"] + ) + + def tearDown(self) -> None: + MultiTaskTrainTest.tearDown(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_stat.py b/source/tests/pt/test_stat.py index 98d4e59d95..3a09f82baf 100644 --- a/source/tests/pt/test_stat.py +++ b/source/tests/pt/test_stat.py @@ -44,9 +44,51 @@ from deepmd.tf.utils.data_system import ( DeepmdDataSystem, ) +from deepmd.utils.data import ( + DataRequirementItem, +) CUR_DIR = os.path.dirname(__file__) +energy_data_requirement = [ + DataRequirementItem( + "energy", + ndof=1, + atomic=False, + must=False, + high_prec=True, + ), + DataRequirementItem( + "force", + ndof=3, + atomic=True, + must=False, + high_prec=False, + ), + DataRequirementItem( + "virial", + ndof=9, + atomic=False, + must=False, + high_prec=False, + ), + DataRequirementItem( + "atom_ener", + ndof=1, + atomic=True, + must=False, + high_prec=False, + ), + DataRequirementItem( + "atom_pref", + ndof=1, + atomic=True, + must=False, + high_prec=False, + repeat=3, + ), +] + def compare(ut, base, given): if isinstance(base, list): @@ -111,6 +153,7 @@ def setUp(self): self.filter_neuron = model_config["descriptor"]["neuron"] self.axis_neuron = model_config["descriptor"]["axis_neuron"] self.n_neuron = model_config["fitting_net"]["neuron"] + self.my_dataset.add_data_requirement(energy_data_requirement) self.my_sampled = my_make( self.my_dataset.systems, self.my_dataset.dataloaders, self.data_stat_nbatch @@ -181,8 +224,6 @@ def test_descriptor(self): for sys in sampled: for key in [ "coord", - "force", - "energy", "atype", "natoms", "box", diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index f86691cde6..4e73fc4f8a 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -79,15 +79,6 @@ def setUp(self): self.config["training"]["training_data"]["systems"] = data_file self.config["training"]["validation_data"]["systems"] = data_file self.config["model"] = deepcopy(model_dpa2) - # self.config["model"]["descriptor"]["rcut"] = self.config["model"]["descriptor"][ - # "repinit_rcut" - # ] - # self.config["model"]["descriptor"]["rcut_smth"] = self.config["model"][ - # "descriptor" - # ]["repinit_rcut_smth"] - # self.config["model"]["descriptor"]["sel"] = self.config["model"]["descriptor"][ - # "repinit_nsel" - # ] self.config["training"]["numb_steps"] = 1 self.config["training"]["save_freq"] = 1 From 54b14ef64836d3850812f1a62c76cec4d43346ab Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 1 Mar 2024 02:04:43 -0500 Subject: [PATCH 155/686] sync descriptor alias (#3374) Add `se_a` to pt and dpmodel and add `dpa1` to tf. Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/descriptor/se_e2_a.py | 1 + deepmd/pt/model/descriptor/se_a.py | 1 + deepmd/tf/descriptor/se_atten.py | 1 + 3 files changed, 3 insertions(+) diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index f6b1c5677e..891f308edc 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -46,6 +46,7 @@ @BaseDescriptor.register("se_e2_a") +@BaseDescriptor.register("se_a") class DescrptSeA(NativeOP, BaseDescriptor): r"""DeepPot-SE constructed from all information (both angular and radial) of atomic configurations. The embedding takes the distance between atoms as input. diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index d836b48992..ee84da6bc2 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -65,6 +65,7 @@ @BaseDescriptor.register("se_e2_a") +@BaseDescriptor.register("se_a") class DescrptSeA(BaseDescriptor, torch.nn.Module): def __init__( self, diff --git a/deepmd/tf/descriptor/se_atten.py b/deepmd/tf/descriptor/se_atten.py index 4be5cbd164..8d80c10ba5 100644 --- a/deepmd/tf/descriptor/se_atten.py +++ b/deepmd/tf/descriptor/se_atten.py @@ -71,6 +71,7 @@ log = logging.getLogger(__name__) +@Descriptor.register("dpa1") @Descriptor.register("se_atten") class DescrptSeAtten(DescrptSeA): r"""Smooth version descriptor with attention. From f684be84a89fb1adddc4aa5f8f59e0c1f63f33e9 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 1 Mar 2024 02:17:11 -0500 Subject: [PATCH 156/686] feat: atom_ener in energy fitting (#3370) Also, fix the TF serialization issue (it tried to store a tensor instead of a NumPy array). --------- Signed-off-by: Jinzhe Zeng Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- deepmd/dpmodel/fitting/general_fitting.py | 33 +++++++++++++++++- deepmd/dpmodel/fitting/invar_fitting.py | 5 +-- deepmd/pt/model/task/ener.py | 3 ++ deepmd/pt/model/task/fitting.py | 36 +++++++++++++++++++- deepmd/tf/fit/ener.py | 2 +- source/tests/consistent/fitting/test_ener.py | 11 ++++++ 6 files changed, 85 insertions(+), 5 deletions(-) diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index 752a550849..5b4ca195b5 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -73,7 +73,10 @@ class GeneralFitting(NativeOP, BaseFitting): different fitting nets for different atom types. exclude_types: List[int] Atomic contributions of the excluded atom types are set zero. - + remove_vaccum_contribution: List[bool], optional + Remove vaccum contribution before the bias is added. The list assigned each + type. For `mixed_types` provide `[True]`, otherwise it should be a list of the same + length as `ntypes` signaling if or not removing the vaccum contribution for the atom types in the list. """ def __init__( @@ -95,6 +98,7 @@ def __init__( spin: Any = None, mixed_types: bool = True, exclude_types: List[int] = [], + remove_vaccum_contribution: Optional[List[bool]] = None, ): self.var_name = var_name self.ntypes = ntypes @@ -119,6 +123,7 @@ def __init__( self.exclude_types = exclude_types if self.spin is not None: raise NotImplementedError("spin is not supported") + self.remove_vaccum_contribution = remove_vaccum_contribution self.emask = AtomExcludeMask(self.ntypes, self.exclude_types) @@ -298,6 +303,14 @@ def _call_common( "which is not consistent with {self.dim_descrpt}." ) xx = descriptor + if self.remove_vaccum_contribution is not None: + # TODO: Idealy, the input for vaccum should be computed; + # we consider it as always zero for convenience. + # Needs a compute_input_stats for vaccum passed from the + # descriptor. + xx_zeros = np.zeros_like(xx) + else: + xx_zeros = None # check fparam dim, concate to input descriptor if self.numb_fparam > 0: assert fparam is not None, "fparam should not be None" @@ -312,6 +325,11 @@ def _call_common( [xx, fparam], axis=-1, ) + if xx_zeros is not None: + xx_zeros = np.concatenate( + [xx_zeros, fparam], + axis=-1, + ) # check aparam dim, concate to input descriptor if self.numb_aparam > 0: assert aparam is not None, "aparam should not be None" @@ -326,6 +344,11 @@ def _call_common( [xx, aparam], axis=-1, ) + if xx_zeros is not None: + xx_zeros = np.concatenate( + [xx_zeros, aparam], + axis=-1, + ) # calcualte the prediction if not self.mixed_types: @@ -335,11 +358,19 @@ def _call_common( (atype == type_i).reshape([nf, nloc, 1]), [1, 1, net_dim_out] ) atom_property = self.nets[(type_i,)](xx) + if self.remove_vaccum_contribution is not None and not ( + len(self.remove_vaccum_contribution) > type_i + and not self.remove_vaccum_contribution[type_i] + ): + assert xx_zeros is not None + atom_property -= self.nets[(type_i,)](xx_zeros) atom_property = atom_property + self.bias_atom_e[type_i] atom_property = atom_property * mask outs = outs + atom_property # Shape is [nframes, natoms[0], 1] else: outs = self.nets[()](xx) + self.bias_atom_e[atype] + if xx_zeros is not None: + outs -= self.nets[()](xx_zeros) # nf x nloc exclude_mask = self.emask.build_type_exclude_mask(atype) # nf x nloc x nod diff --git a/deepmd/dpmodel/fitting/invar_fitting.py b/deepmd/dpmodel/fitting/invar_fitting.py index 769dc45042..fd556ff074 100644 --- a/deepmd/dpmodel/fitting/invar_fitting.py +++ b/deepmd/dpmodel/fitting/invar_fitting.py @@ -136,8 +136,6 @@ def __init__( raise NotImplementedError("use_aparam_as_mask is not implemented") if layer_name is not None: raise NotImplementedError("layer_name is not implemented") - if atom_ener is not None and atom_ener != []: - raise NotImplementedError("atom_ener is not implemented") self.dim_out = dim_out self.atom_ener = atom_ener @@ -159,6 +157,9 @@ def __init__( spin=spin, mixed_types=mixed_types, exclude_types=exclude_types, + remove_vaccum_contribution=None + if atom_ener is None or len([x for x in atom_ener if x is not None]) == 0 + else [x is not None for x in atom_ener], ) def serialize(self) -> dict: diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 29ed5acaad..00bf049b97 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -126,6 +126,9 @@ def __init__( rcond=rcond, seed=seed, exclude_types=exclude_types, + remove_vaccum_contribution=None + if atom_ener is None or len([x for x in atom_ener if x is not None]) == 0 + else [x is not None for x in atom_ener], **kwargs, ) diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 47535580db..c41d445f66 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -239,7 +239,10 @@ class GeneralFitting(Fitting): Random seed. exclude_types: List[int] Atomic contributions of the excluded atom types are set zero. - + remove_vaccum_contribution: List[bool], optional + Remove vaccum contribution before the bias is added. The list assigned each + type. For `mixed_types` provide `[True]`, otherwise it should be a list of the same + length as `ntypes` signaling if or not removing the vaccum contribution for the atom types in the list. """ def __init__( @@ -258,6 +261,7 @@ def __init__( rcond: Optional[float] = None, seed: Optional[int] = None, exclude_types: List[int] = [], + remove_vaccum_contribution: Optional[List[bool]] = None, **kwargs, ): super().__init__() @@ -275,6 +279,7 @@ def __init__( self.rcond = rcond # order matters, should be place after the assignment of ntypes self.reinit_exclude(exclude_types) + self.remove_vaccum_contribution = remove_vaccum_contribution net_dim_out = self._net_out_dim() # init constants @@ -479,6 +484,14 @@ def _forward_common( aparam: Optional[torch.Tensor] = None, ): xx = descriptor + if self.remove_vaccum_contribution is not None: + # TODO: Idealy, the input for vaccum should be computed; + # we consider it as always zero for convenience. + # Needs a compute_input_stats for vaccum passed from the + # descriptor. + xx_zeros = torch.zeros_like(xx) + else: + xx_zeros = None nf, nloc, nd = xx.shape net_dim_out = self._net_out_dim() @@ -507,6 +520,11 @@ def _forward_common( [xx, fparam], dim=-1, ) + if xx_zeros is not None: + xx_zeros = torch.cat( + [xx_zeros, fparam], + dim=-1, + ) # check aparam dim, concate to input descriptor if self.numb_aparam > 0: assert aparam is not None, "aparam should not be None" @@ -526,6 +544,11 @@ def _forward_common( [xx, aparam], dim=-1, ) + if xx_zeros is not None: + xx_zeros = torch.cat( + [xx_zeros, aparam], + dim=-1, + ) outs = torch.zeros( (nf, nloc, net_dim_out), @@ -534,6 +557,7 @@ def _forward_common( ) # jit assertion if self.old_impl: assert self.filter_layers_old is not None + assert xx_zeros is None if self.mixed_types: atom_property = self.filter_layers_old[0](xx) + self.bias_atom_e[atype] outs = outs + atom_property # Shape is [nframes, natoms[0], 1] @@ -549,6 +573,8 @@ def _forward_common( atom_property = ( self.filter_layers.networks[0](xx) + self.bias_atom_e[atype] ) + if xx_zeros is not None: + atom_property -= self.filter_layers.networks[0](xx_zeros) outs = ( outs + atom_property ) # Shape is [nframes, natoms[0], net_dim_out] @@ -557,6 +583,14 @@ def _forward_common( mask = (atype == type_i).unsqueeze(-1) mask = torch.tile(mask, (1, 1, net_dim_out)) atom_property = ll(xx) + if xx_zeros is not None: + # must assert, otherwise jit is not happy + assert self.remove_vaccum_contribution is not None + if not ( + len(self.remove_vaccum_contribution) > type_i + and not self.remove_vaccum_contribution[type_i] + ): + atom_property -= ll(xx_zeros) atom_property = atom_property + self.bias_atom_e[type_i] atom_property = atom_property * mask outs = ( diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index a842df50bd..d605fbb0aa 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -1003,7 +1003,7 @@ def serialize(self, suffix: str = "") -> dict: "rcond": self.rcond, "tot_ener_zero": self.tot_ener_zero, "trainable": self.trainable, - "atom_ener": self.atom_ener, + "atom_ener": self.atom_ener_v, "activation_function": self.activation_function_name, "precision": self.fitting_precision.name, "layer_name": self.layer_name, diff --git a/source/tests/consistent/fitting/test_ener.py b/source/tests/consistent/fitting/test_ener.py index 994d967bc8..a22bcdb65f 100644 --- a/source/tests/consistent/fitting/test_ener.py +++ b/source/tests/consistent/fitting/test_ener.py @@ -43,6 +43,7 @@ ("float64", "float32"), # precision (True, False), # mixed_types (0, 1), # numb_fparam + ([], [-12345.6, None]), # atom_ener ) class TestEner(CommonTest, FittingTest, unittest.TestCase): @property @@ -52,6 +53,7 @@ def data(self) -> dict: precision, mixed_types, numb_fparam, + atom_ener, ) = self.param return { "neuron": [5, 5, 5], @@ -59,6 +61,7 @@ def data(self) -> dict: "precision": precision, "numb_fparam": numb_fparam, "seed": 20240217, + "atom_ener": atom_ener, } @property @@ -68,6 +71,7 @@ def skip_tf(self) -> bool: precision, mixed_types, numb_fparam, + atom_ener, ) = self.param # TODO: mixed_types return mixed_types or CommonTest.skip_pt @@ -79,6 +83,7 @@ def skip_pt(self) -> bool: precision, mixed_types, numb_fparam, + atom_ener, ) = self.param return CommonTest.skip_pt @@ -105,6 +110,7 @@ def addtional_data(self) -> dict: precision, mixed_types, numb_fparam, + atom_ener, ) = self.param return { "ntypes": self.ntypes, @@ -118,6 +124,7 @@ def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: precision, mixed_types, numb_fparam, + atom_ener, ) = self.param return self.build_tf_fitting( obj, @@ -134,6 +141,7 @@ def eval_pt(self, pt_obj: Any) -> Any: precision, mixed_types, numb_fparam, + atom_ener, ) = self.param return ( pt_obj( @@ -154,6 +162,7 @@ def eval_dp(self, dp_obj: Any) -> Any: precision, mixed_types, numb_fparam, + atom_ener, ) = self.param return dp_obj( self.inputs, @@ -175,6 +184,7 @@ def rtol(self) -> float: precision, mixed_types, numb_fparam, + atom_ener, ) = self.param if precision == "float64": return 1e-10 @@ -191,6 +201,7 @@ def atol(self) -> float: precision, mixed_types, numb_fparam, + atom_ener, ) = self.param if precision == "float64": return 1e-10 From 2594f098a8e0533f23d7a3c39a027a741f18f097 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 1 Mar 2024 05:26:44 -0500 Subject: [PATCH 157/686] docs: DPRc for PT, DPModel (#3373) It should be merged after #3370. Signed-off-by: Jinzhe Zeng --- doc/model/dprc.md | 51 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/doc/model/dprc.md b/doc/model/dprc.md index 48e18e8d89..ac1ab0e261 100644 --- a/doc/model/dprc.md +++ b/doc/model/dprc.md @@ -1,7 +1,7 @@ -# Deep Potential - Range Correction (DPRc) {{ tensorflow_icon }} +# Deep Potential - Range Correction (DPRc) {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DPModel {{ dpmodel_icon }} ::: Deep Potential - Range Correction (DPRc) is designed to combine with QM/MM method, and corrects energies from a low-level QM/MM method to a high-level QM/MM method: @@ -62,6 +62,10 @@ In a DPRc model, QM atoms and MM atoms have different atom types. Assuming we ha As described in the paper, the DPRc model only corrects $E_\text{QM}$ and $E_\text{QM/MM}$ within the cutoff, so we use a hybrid descriptor to describe them separatedly: +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} + ```json "descriptor" :{ "type": "hybrid", @@ -91,6 +95,45 @@ As described in the paper, the DPRc model only corrects $E_\text{QM}$ and $E_\te } ``` +::: + +:::{tab-item} PyTorch {{ pytorch_icon }} + +```json +"descriptor" :{ + "type": "hybrid", + "list" : [ + { + "type": "se_e2_a", + "sel": [6, 11, 0, 6, 0, 1], + "rcut_smth": 1.00, + "rcut": 9.00, + "neuron": [12, 25, 50], + "exclude_types": [[2, 2], [2, 4], [4, 4], [0, 2], [0, 4], [1, 2], [1, 4], [3, 2], [3, 4], [5, 2], [5, 4]], + "axis_neuron": 12, + "type_one_side": true, + "_comment": " QM/QM interaction" + }, + { + "type": "se_e2_a", + "sel": [6, 11, 100, 6, 50, 1], + "rcut_smth": 0.50, + "rcut": 6.00, + "neuron": [12, 25, 50], + "exclude_types": [[0, 0], [0, 1], [0, 3], [0, 5], [1, 1], [1, 3], [1, 5], [3, 3], [3, 5], [5, 5], [2, 2], [2, 4], [4, 4]], + "axis_neuron": 12, + "set_davg_zero": true, + "type_one_side": true, + "_comment": " QM/MM interaction" + } + ] +} +``` + +::: + +:::: + {ref}`exclude_types ` can be generated by the following Python script: ```py from itertools import combinations_with_replacement, product @@ -131,6 +174,10 @@ The DPRc model has the best practices with the [AMBER](../third-party/out-of-dee ## Pairwise DPRc +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} +::: + If one wants to correct from a low-level method into a full DFT level, and the system is too large to do full DFT calculation, one may try the experimental pairwise DPRc model. In a pairwise DPRc model, the total energy is divided into QM internal energy and the sum of QM/MM energy for each MM residue $l$: From 759bdcbb9f159e092c0f70bc4058f4c273c41bef Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 1 Mar 2024 06:07:29 -0500 Subject: [PATCH 158/686] pt: supprot `--output` in `dp train` (#3377) 1. Support `--output` in `dp train`; 2. move argcheck before neighbor-stat, which is consistent with TF. Signed-off-by: Jinzhe Zeng --- deepmd/main.py | 2 +- deepmd/pt/entrypoints/main.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/deepmd/main.py b/deepmd/main.py index 5dab029d83..870a04a088 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -240,7 +240,7 @@ def main_parser() -> argparse.ArgumentParser: "--output", type=str, default="out.json", - help="(Supported backend: TensorFlow) The output file of the parameters used in training.", + help="The output file of the parameters used in training.", ) parser_train.add_argument( "--skip-neighbor-stat", diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 023bc5305e..736e8dde09 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -78,10 +78,6 @@ def get_trainer( shared_links=None, ): multi_task = "model_dict" in config.get("model", {}) - # argcheck - if not multi_task: - config = update_deepmd_input(config, warning=True, dump="input_v2_compat.json") - config = normalize(config) # Initialize DDP local_rank = os.environ.get("LOCAL_RANK") @@ -236,6 +232,11 @@ def train(FLAGS): if multi_task: config["model"], shared_links = preprocess_shared_params(config["model"]) + # argcheck + if not multi_task: + config = update_deepmd_input(config, warning=True, dump="input_v2_compat.json") + config = normalize(config) + # do neighbor stat if not FLAGS.skip_neighbor_stat: log.info( @@ -257,6 +258,9 @@ def train(FLAGS): fake_global_jdata, config["model"]["model_dict"][model_item] ) + with open(FLAGS.output, "w") as fp: + json.dump(config, fp, indent=4) + trainer = get_trainer( config, FLAGS.init_model, From ee8b82ba78678f8fd3a8d2a1304f68b351c75726 Mon Sep 17 00:00:00 2001 From: Lysithea <52808607+CaRoLZhangxy@users.noreply.github.com> Date: Fri, 1 Mar 2024 19:12:57 +0800 Subject: [PATCH 159/686] pt: add real atoms select in c++ interface (#3375) select real atoms may be merged in DeepPot class for both backends in the future. --------- Signed-off-by: Lysithea <52808607+CaRoLZhangxy@users.noreply.github.com> Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jinzhe Zeng Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- .../model/atomic_model/base_atomic_model.py | 1 + deepmd/pt/model/model/make_model.py | 1 - source/api_cc/include/DeepPotPT.h | 6 +- source/api_cc/include/common.h | 1 + source/api_cc/include/commonPT.h | 24 ----- source/api_cc/src/DeepPotPT.cc | 82 +++++++++++++----- source/api_cc/src/common.cc | 10 +++ source/api_cc/src/commonPT.cc | 23 ----- source/api_cc/tests/test_deeppot_pt.cc | 2 - source/tests/infer/deeppot_sea.pth | Bin 123025 -> 127249 bytes source/tests/infer/fparam_aparam.pth | Bin 108888 -> 109203 bytes 11 files changed, 74 insertions(+), 76 deletions(-) delete mode 100644 source/api_cc/include/commonPT.h delete mode 100644 source/api_cc/src/commonPT.cc diff --git a/deepmd/pt/model/atomic_model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py index d6de3dfc88..f8b737b58e 100644 --- a/deepmd/pt/model/atomic_model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -54,6 +54,7 @@ def reinit_pair_exclude( # export public methods that are not abstract get_nsel = torch.jit.export(BaseAtomicModel_.get_nsel) get_nnei = torch.jit.export(BaseAtomicModel_.get_nnei) + get_ntypes = torch.jit.export(BaseAtomicModel_.get_ntypes) @torch.jit.export def get_model_def_script(self) -> str: diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 98f0a18241..4f35acb60e 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -298,7 +298,6 @@ def output_type_cast( ) return model_ret - @torch.jit.export def format_nlist( self, extended_coord: torch.Tensor, diff --git a/source/api_cc/include/DeepPotPT.h b/source/api_cc/include/DeepPotPT.h index d50d338d33..a7fc910b46 100644 --- a/source/api_cc/include/DeepPotPT.h +++ b/source/api_cc/include/DeepPotPT.h @@ -1,10 +1,10 @@ // SPDX-License-Identifier: LGPL-3.0-or-later #pragma once +#include #include #include "DeepPot.h" -#include "commonPT.h" namespace deepmd { /** @@ -106,7 +106,7 @@ class DeepPotPT : public DeepPotBase { const std::vector& coord, const std::vector& atype, const std::vector& box, - // const int nghost, + const int nghost, const InputNlist& lmp_list, const int& ago, const std::vector& fparam = std::vector(), @@ -322,7 +322,7 @@ class DeepPotPT : public DeepPotBase { // copy neighbor list info from host torch::jit::script::Module module; double rcut; - NeighborListDataPT nlist_data; + NeighborListData nlist_data; int max_num_neighbors; int gpu_id; bool gpu_enabled; diff --git a/source/api_cc/include/common.h b/source/api_cc/include/common.h index 72382169f8..4743336e0c 100644 --- a/source/api_cc/include/common.h +++ b/source/api_cc/include/common.h @@ -32,6 +32,7 @@ struct NeighborListData { void shuffle(const deepmd::AtomMap& map); void shuffle_exclude_empty(const std::vector& fwd_map); void make_inlist(InputNlist& inlist); + void padding(); }; /** diff --git a/source/api_cc/include/commonPT.h b/source/api_cc/include/commonPT.h deleted file mode 100644 index 57ffd5b295..0000000000 --- a/source/api_cc/include/commonPT.h +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -#include - -#include -#include -#include -#include - -#include "neighbor_list.h" -namespace deepmd { -struct NeighborListDataPT { - /// Array stores the core region atom's index - std::vector ilist; - /// Array stores the core region atom's neighbor index - std::vector jlist; - /// Array stores the number of neighbors of core region atoms - std::vector numneigh; - /// Array stores the the location of the first neighbor of core region atoms - std::vector firstneigh; - - public: - void copy_from_nlist(const InputNlist& inlist, int& max_num_neighbors); -}; -} // namespace deepmd diff --git a/source/api_cc/src/DeepPotPT.cc b/source/api_cc/src/DeepPotPT.cc index 9514a9769c..919d690bed 100644 --- a/source/api_cc/src/DeepPotPT.cc +++ b/source/api_cc/src/DeepPotPT.cc @@ -4,6 +4,17 @@ #include "common.h" using namespace deepmd; +torch::Tensor createNlistTensor(const std::vector>& data) { + std::vector row_tensors; + + for (const auto& row : data) { + torch::Tensor row_tensor = torch::tensor(row, torch::kInt32).unsqueeze(0); + row_tensors.push_back(row_tensor); + } + + torch::Tensor tensor = torch::cat(row_tensors, 0).unsqueeze(0); + return tensor; +} DeepPotPT::DeepPotPT() : inited(false) {} DeepPotPT::DeepPotPT(const std::string& model, const int& gpu_rank, @@ -60,7 +71,7 @@ void DeepPotPT::init(const std::string& model, auto rcut_ = module.run_method("get_rcut").toDouble(); rcut = static_cast(rcut_); - ntypes = 0; + ntypes = module.run_method("get_ntypes").toInt(); ntypes_spin = 0; dfparam = module.run_method("get_dim_fparam").toInt(); daparam = module.run_method("get_dim_aparam").toInt(); @@ -78,6 +89,7 @@ void DeepPotPT::compute(ENERGYVTYPE& ener, const std::vector& coord, const std::vector& atype, const std::vector& box, + const int nghost, const InputNlist& lmp_list, const int& ago, const std::vector& fparam, @@ -86,7 +98,6 @@ void DeepPotPT::compute(ENERGYVTYPE& ener, if (!gpu_enabled) { device = torch::Device(torch::kCPU); } - std::vector coord_wrapped = coord; int natoms = atype.size(); auto options = torch::TensorOptions().dtype(torch::kFloat64); torch::ScalarType floatType = torch::kFloat64; @@ -96,18 +107,29 @@ void DeepPotPT::compute(ENERGYVTYPE& ener, } auto int_options = torch::TensorOptions().dtype(torch::kInt64); auto int32_options = torch::TensorOptions().dtype(torch::kInt32); + + // select real atoms + std::vector dcoord, dforce, aparam_, datom_energy, datom_virial; + std::vector datype, fwd_map, bkw_map; + int nghost_real, nall_real, nloc_real; + int nall = natoms; + select_real_atoms_coord(dcoord, datype, aparam_, nghost_real, fwd_map, + bkw_map, nall_real, nloc_real, coord, atype, aparam, + nghost, ntypes, 1, daparam, nall, aparam_nall); + std::cout << datype.size() << std::endl; + std::vector coord_wrapped = dcoord; at::Tensor coord_wrapped_Tensor = - torch::from_blob(coord_wrapped.data(), {1, natoms, 3}, options) + torch::from_blob(coord_wrapped.data(), {1, nall_real, 3}, options) .to(device); - std::vector atype_64(atype.begin(), atype.end()); + std::vector atype_64(datype.begin(), datype.end()); at::Tensor atype_Tensor = - torch::from_blob(atype_64.data(), {1, natoms}, int_options).to(device); + torch::from_blob(atype_64.data(), {1, nall_real}, int_options).to(device); if (ago == 0) { - nlist_data.copy_from_nlist(lmp_list, max_num_neighbors); + nlist_data.copy_from_nlist(lmp_list); + nlist_data.shuffle_exclude_empty(fwd_map); + nlist_data.padding(); } - at::Tensor firstneigh = - torch::from_blob(nlist_data.jlist.data(), - {1, lmp_list.inum, max_num_neighbors}, int32_options); + at::Tensor firstneigh = createNlistTensor(nlist_data.jlist); firstneigh_tensor = firstneigh.to(torch::kInt64).to(device); bool do_atom_virial_tensor = true; c10::optional optional_tensor; @@ -119,13 +141,13 @@ void DeepPotPT::compute(ENERGYVTYPE& ener, .to(device); } c10::optional aparam_tensor; - if (!aparam.empty()) { - aparam_tensor = - torch::from_blob(const_cast(aparam.data()), - {1, lmp_list.inum, - static_cast(aparam.size()) / lmp_list.inum}, - options) - .to(device); + if (!aparam_.empty()) { + aparam_tensor = torch::from_blob( + const_cast(aparam_.data()), + {1, lmp_list.inum, + static_cast(aparam_.size()) / lmp_list.inum}, + options) + .to(device); } c10::Dict outputs = module @@ -145,14 +167,15 @@ void DeepPotPT::compute(ENERGYVTYPE& ener, torch::Tensor flat_atom_energy_ = atom_energy_.toTensor().view({-1}).to(floatType); torch::Tensor cpu_atom_energy_ = flat_atom_energy_.to(torch::kCPU); - atom_energy.resize(natoms, 0.0); // resize to nall to be consistenet with TF. - atom_energy.assign( + datom_energy.resize(nall_real, + 0.0); // resize to nall to be consistenet with TF. + datom_energy.assign( cpu_atom_energy_.data_ptr(), cpu_atom_energy_.data_ptr() + cpu_atom_energy_.numel()); torch::Tensor flat_force_ = force_.toTensor().view({-1}).to(floatType); torch::Tensor cpu_force_ = flat_force_.to(torch::kCPU); - force.assign(cpu_force_.data_ptr(), - cpu_force_.data_ptr() + cpu_force_.numel()); + dforce.assign(cpu_force_.data_ptr(), + cpu_force_.data_ptr() + cpu_force_.numel()); torch::Tensor flat_virial_ = virial_.toTensor().view({-1}).to(floatType); torch::Tensor cpu_virial_ = flat_virial_.to(torch::kCPU); virial.assign(cpu_virial_.data_ptr(), @@ -160,9 +183,20 @@ void DeepPotPT::compute(ENERGYVTYPE& ener, torch::Tensor flat_atom_virial_ = atom_virial_.toTensor().view({-1}).to(floatType); torch::Tensor cpu_atom_virial_ = flat_atom_virial_.to(torch::kCPU); - atom_virial.assign( + datom_virial.assign( cpu_atom_virial_.data_ptr(), cpu_atom_virial_.data_ptr() + cpu_atom_virial_.numel()); + int nframes = 1; + // bkw map + force.resize(static_cast(nframes) * fwd_map.size() * 3); + atom_energy.resize(static_cast(nframes) * fwd_map.size()); + atom_virial.resize(static_cast(nframes) * fwd_map.size() * 9); + select_map(force, dforce, bkw_map, 3, nframes, fwd_map.size(), + nall_real); + select_map(atom_energy, datom_energy, bkw_map, 1, nframes, + fwd_map.size(), nall_real); + select_map(atom_virial, datom_virial, bkw_map, 9, nframes, + fwd_map.size(), nall_real); } template void DeepPotPT::compute>( std::vector& ener, @@ -173,6 +207,7 @@ template void DeepPotPT::compute>( const std::vector& coord, const std::vector& atype, const std::vector& box, + const int nghost, const InputNlist& lmp_list, const int& ago, const std::vector& fparam, @@ -186,6 +221,7 @@ template void DeepPotPT::compute>( const std::vector& coord, const std::vector& atype, const std::vector& box, + const int nghost, const InputNlist& lmp_list, const int& ago, const std::vector& fparam, @@ -353,7 +389,7 @@ void DeepPotPT::computew(std::vector& ener, const std::vector& fparam, const std::vector& aparam) { compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box, - inlist, ago, fparam, aparam); + nghost, inlist, ago, fparam, aparam); } void DeepPotPT::computew(std::vector& ener, std::vector& force, @@ -369,7 +405,7 @@ void DeepPotPT::computew(std::vector& ener, const std::vector& fparam, const std::vector& aparam) { compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box, - inlist, ago, fparam, aparam); + nghost, inlist, ago, fparam, aparam); } void DeepPotPT::computew_mixed_type(std::vector& ener, std::vector& force, diff --git a/source/api_cc/src/common.cc b/source/api_cc/src/common.cc index d2923c8d9e..f104433468 100644 --- a/source/api_cc/src/common.cc +++ b/source/api_cc/src/common.cc @@ -293,6 +293,16 @@ void deepmd::NeighborListData::shuffle_exclude_empty( ilist = new_ilist; jlist = new_jlist; } +void deepmd::NeighborListData::padding() { + size_t max_length = 0; + for (const auto& row : jlist) { + max_length = std::max(max_length, row.size()); + } + + for (int i = 0; i < jlist.size(); i++) { + jlist[i].resize(max_length, -1); + } +} void deepmd::NeighborListData::make_inlist(InputNlist& inlist) { int nloc = ilist.size(); diff --git a/source/api_cc/src/commonPT.cc b/source/api_cc/src/commonPT.cc deleted file mode 100644 index 4ed3b21fe8..0000000000 --- a/source/api_cc/src/commonPT.cc +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -#ifdef BUILD_PYTORCH -#include "commonPT.h" -using namespace deepmd; -void NeighborListDataPT::copy_from_nlist(const InputNlist& inlist, - int& max_num_neighbors) { - int inum = inlist.inum; - ilist.resize(inum); - numneigh.resize(inum); - memcpy(&ilist[0], inlist.ilist, inum * sizeof(int)); - int* max_element = std::max_element(inlist.numneigh, inlist.numneigh + inum); - max_num_neighbors = *max_element; - unsigned long nlist_size = (unsigned long)inum * max_num_neighbors; - jlist.resize(nlist_size); - jlist.assign(nlist_size, -1); - for (int ii = 0; ii < inum; ++ii) { - int jnum = inlist.numneigh[ii]; - numneigh[ii] = inlist.numneigh[ii]; - memcpy(&jlist[(unsigned long)ii * max_num_neighbors], inlist.firstneigh[ii], - jnum * sizeof(int)); - } -} -#endif diff --git a/source/api_cc/tests/test_deeppot_pt.cc b/source/api_cc/tests/test_deeppot_pt.cc index e0e90ac75c..cc30e606c0 100644 --- a/source/api_cc/tests/test_deeppot_pt.cc +++ b/source/api_cc/tests/test_deeppot_pt.cc @@ -402,7 +402,6 @@ TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist_2rc) { } TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist_type_sel) { - GTEST_SKIP() << "Skipping this test for unsupported"; using VALUETYPE = TypeParam; std::vector& coord = this->coord; std::vector& atype = this->atype; @@ -465,7 +464,6 @@ TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist_type_sel) { } TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist_type_sel_atomic) { - GTEST_SKIP() << "Skipping this test for unsupported"; using VALUETYPE = TypeParam; std::vector& coord = this->coord; std::vector& atype = this->atype; diff --git a/source/tests/infer/deeppot_sea.pth b/source/tests/infer/deeppot_sea.pth index 98aaa8a2ad0ccb1f462ff3475f28fea7d28e24fb..c830f0df9e8ea8f682876e56519ce7e1310056c8 100644 GIT binary patch delta 76243 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 delta 72074 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%0DaCC%Zx%-X&spsP$|uuFrC_$z$fztt$aK&HU4H| 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 L1d{Pb!!ztZNYry6 From bdea3ce1c5abf62d0adc7320feb3536d6a99cb22 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Sat, 2 Mar 2024 09:34:52 +0800 Subject: [PATCH 160/686] Add trainable settings for pt (#3371) Signed-off-by: Duo <50307526+iProzd@users.noreply.github.com> --- deepmd/pt/model/descriptor/dpa1.py | 5 +++-- deepmd/pt/model/descriptor/dpa2.py | 6 ++++++ deepmd/pt/model/descriptor/se_a.py | 4 ++++ deepmd/pt/model/descriptor/se_r.py | 4 ++++ deepmd/pt/model/task/fitting.py | 16 +++++++++++++++- deepmd/pt/train/training.py | 3 --- deepmd/pt/train/wrapper.py | 22 ---------------------- deepmd/utils/argcheck.py | 4 ++-- source/tests/pt/test_training.py | 16 ++++++++++++++++ 9 files changed, 50 insertions(+), 30 deletions(-) diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index 224a24d60e..1b32467540 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -72,8 +72,6 @@ def __init__( raise NotImplementedError("type_one_side is not supported.") if precision != "default" and precision != "float64": raise NotImplementedError("precison is not supported.") - if not trainable: - raise NotImplementedError("trainable == False is not supported.") if exclude_types is not None and exclude_types != []: raise NotImplementedError("exclude_types is not supported.") if stripped_type_embedding: @@ -108,6 +106,9 @@ def __init__( self.type_embedding = TypeEmbedNet(ntypes, tebd_dim) self.tebd_dim = tebd_dim self.concat_output_tebd = concat_output_tebd + # set trainable + for param in self.parameters(): + param.requires_grad = trainable def get_rcut(self) -> float: """Returns the cut-off radius.""" diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index dcb381d53a..a80cc4a445 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -77,6 +77,7 @@ def __init__( repformer_update_style: str = "res_avg", repformer_set_davg_zero: bool = True, # TODO repformer_add_type_ebd_to_seq: bool = False, + trainable: bool = True, type: Optional[ str ] = None, # work around the bad design in get_trainer and DpLoaderSet! @@ -172,6 +173,8 @@ def __init__( repformers block: set the avg to zero in statistics repformer_add_type_ebd_to_seq : bool repformers block: concatenate the type embedding at the output. + trainable : bool + If the parameters in the descriptor are trainable. Returns ------- @@ -251,6 +254,9 @@ def __init__( self.rcut = self.repinit.get_rcut() self.ntypes = ntypes self.sel = self.repinit.sel + # set trainable + for param in self.parameters(): + param.requires_grad = trainable def get_rcut(self) -> float: """Returns the cut-off radius.""" diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index ee84da6bc2..3a18f150a4 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -312,6 +312,7 @@ def __init__( exclude_types: List[Tuple[int, int]] = [], old_impl: bool = False, type_one_side: bool = True, + trainable: bool = True, **kwargs, ): """Construct an embedding net of type `se_a`. @@ -384,6 +385,9 @@ def __init__( ) self.filter_layers = filter_layers self.stats = None + # set trainable + for param in self.parameters(): + param.requires_grad = trainable def get_rcut(self) -> float: """Returns the cut-off radius.""" diff --git a/deepmd/pt/model/descriptor/se_r.py b/deepmd/pt/model/descriptor/se_r.py index 27e459d861..5a4920b0e6 100644 --- a/deepmd/pt/model/descriptor/se_r.py +++ b/deepmd/pt/model/descriptor/se_r.py @@ -65,6 +65,7 @@ def __init__( resnet_dt: bool = False, exclude_types: List[Tuple[int, int]] = [], old_impl: bool = False, + trainable: bool = True, **kwargs, ): super().__init__() @@ -112,6 +113,9 @@ def __init__( ) self.filter_layers = filter_layers self.stats = None + # set trainable + for param in self.parameters(): + param.requires_grad = trainable def get_rcut(self) -> float: """Returns the cut-off radius.""" diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index c41d445f66..f79916b36e 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -7,6 +7,7 @@ from typing import ( List, Optional, + Union, ) import numpy as np @@ -239,6 +240,10 @@ class GeneralFitting(Fitting): Random seed. exclude_types: List[int] Atomic contributions of the excluded atom types are set zero. + trainable : Union[List[bool], bool] + If the parameters in the fitting net are trainable. + Now this only supports setting all the parameters in the fitting net at one state. + When in List[bool], the trainable will be True only if all the boolean parameters are True. remove_vaccum_contribution: List[bool], optional Remove vaccum contribution before the bias is added. The list assigned each type. For `mixed_types` provide `[True]`, otherwise it should be a list of the same @@ -261,6 +266,7 @@ def __init__( rcond: Optional[float] = None, seed: Optional[int] = None, exclude_types: List[int] = [], + trainable: Union[bool, List[bool]] = True, remove_vaccum_contribution: Optional[List[bool]] = None, **kwargs, ): @@ -279,6 +285,11 @@ def __init__( self.rcond = rcond # order matters, should be place after the assignment of ntypes self.reinit_exclude(exclude_types) + self.trainable = trainable + # need support for each layer settings + self.trainable = ( + all(self.trainable) if isinstance(self.trainable, list) else self.trainable + ) self.remove_vaccum_contribution = remove_vaccum_contribution net_dim_out = self._net_out_dim() @@ -353,6 +364,9 @@ def __init__( if seed is not None: torch.manual_seed(seed) + # set trainable + for param in self.parameters(): + param.requires_grad = self.trainable def reinit_exclude( self, @@ -394,7 +408,7 @@ def serialize(self) -> dict: # "spin": self.spin , ## NOTICE: not supported by far "tot_ener_zero": False, - "trainable": [True] * (len(self.neuron) + 1), + "trainable": [self.trainable] * (len(self.neuron) + 1), "layer_name": None, "use_aparam_as_mask": False, "spin": None, diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index ef8a53e656..07c8511cfe 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -430,9 +430,6 @@ def get_loss(loss_params, start_lr, _ntypes): frz_model = torch.jit.load(init_frz_model, map_location=DEVICE) self.model.load_state_dict(frz_model.state_dict()) - # Set trainable params - self.wrapper.set_trainable_params() - # Multi-task share params if shared_links is not None: self.wrapper.share_params(shared_links, resume=resuming or self.rank != 0) diff --git a/deepmd/pt/train/wrapper.py b/deepmd/pt/train/wrapper.py index 67f8043653..a455041526 100644 --- a/deepmd/pt/train/wrapper.py +++ b/deepmd/pt/train/wrapper.py @@ -60,28 +60,6 @@ def __init__( self.loss[task_key] = loss[task_key] self.inference_only = self.loss is None - def set_trainable_params(self): - supported_types = ["descriptor", "fitting_net"] - for model_item in self.model: - for net_type in supported_types: - trainable = True - if not self.multi_task: - if net_type in self.model_params: - trainable = self.model_params[net_type].get("trainable", True) - else: - if net_type in self.model_params["model_dict"][model_item]: - trainable = self.model_params["model_dict"][model_item][ - net_type - ].get("trainable", True) - if ( - hasattr(self.model[model_item], net_type) - and getattr(self.model[model_item], net_type) is not None - ): - for param in ( - self.model[model_item].__getattr__(net_type).parameters() - ): - param.requires_grad = trainable - def share_params(self, shared_links, resume=False): """ Share the parameters of classes following rules defined in shared_links during multitask training. diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 89b341491e..1f0064c460 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -885,9 +885,9 @@ def fitting_ener(): doc_activation_function = f'The activation function in the fitting net. Supported activation functions are {list_to_doc(ACTIVATION_FN_DICT.keys())} Note that "gelu" denotes the custom operator version, and "gelu_tf" denotes the TF standard version. If you set "None" or "none" here, no activation function will be used.' doc_precision = f"The precision of the fitting net parameters, supported options are {list_to_doc(PRECISION_DICT.keys())} Default follows the interface precision." doc_resnet_dt = 'Whether to use a "Timestep" in the skip connection' - doc_trainable = "Whether the parameters in the fitting net are trainable. This option can be\n\n\ + doc_trainable = f"Whether the parameters in the fitting net are trainable. This option can be\n\n\ - bool: True if all parameters of the fitting net are trainable, False otherwise.\n\n\ -- list of bool: Specifies if each layer is trainable. Since the fitting net is composed by hidden layers followed by a output layer, the length of this list should be equal to len(`neuron`)+1." +- list of bool{doc_only_tf_supported}: Specifies if each layer is trainable. Since the fitting net is composed by hidden layers followed by a output layer, the length of this list should be equal to len(`neuron`)+1." doc_rcond = "The condition number used to determine the inital energy shift for each type of atoms. See `rcond` in :py:meth:`numpy.linalg.lstsq` for more details." doc_seed = "Random seed for parameter initialization of the fitting net" doc_atom_ener = "Specify the atomic energy in vacuum for each type" diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index 4e73fc4f8a..f2a081610a 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -10,6 +10,8 @@ Path, ) +import torch + from deepmd.pt.entrypoints.main import ( get_trainer, ) @@ -28,6 +30,20 @@ def test_dp_train(self): trainer.run() self.tearDown() + def test_trainable(self): + fix_params = deepcopy(self.config) + fix_params["model"]["descriptor"]["trainable"] = False + fix_params["model"]["fitting_net"]["trainable"] = False + trainer_fix = get_trainer(fix_params) + model_dict_before_training = deepcopy(trainer_fix.model.state_dict()) + trainer_fix.run() + model_dict_after_training = deepcopy(trainer_fix.model.state_dict()) + for key in model_dict_before_training: + torch.testing.assert_allclose( + model_dict_before_training[key], model_dict_after_training[key] + ) + self.tearDown() + def tearDown(self): for f in os.listdir("."): if f.startswith("model") and f.endswith(".pt"): From 92ee6326aecec689882d49729f09b791309f9064 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 1 Mar 2024 21:09:26 -0500 Subject: [PATCH 161/686] tf: remove freeze warning for optional nodes (#3381) Fix #3334. --------- Signed-off-by: Jinzhe Zeng --- deepmd/tf/entrypoints/freeze.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/deepmd/tf/entrypoints/freeze.py b/deepmd/tf/entrypoints/freeze.py index 228f8466cb..c7ab1023fa 100755 --- a/deepmd/tf/entrypoints/freeze.py +++ b/deepmd/tf/entrypoints/freeze.py @@ -359,13 +359,21 @@ def freeze_graph( output_node = _make_node_names( freeze_type, modifier, out_suffix=out_suffix, node_names=node_names ) + # see #3334 + optional_node = [ + "train_attr/min_nbor_dist", + "fitting_attr/aparam_nall", + "spin_attr/ntypes_spin", + ] different_set = set(output_node) - set(input_node) if different_set: - log.warning( - "The following nodes are not in the graph: %s. " - "Skip freezeing these nodes. You may be freezing " - "a checkpoint generated by an old version." % different_set - ) + different_set -= set(optional_node) + if different_set: + log.warning( + "The following nodes are not in the graph: %s. " + "Skip freezeing these nodes. You may be freezing " + "a checkpoint generated by an old version." % different_set + ) # use intersection as output list output_node = list(set(output_node) & set(input_node)) log.info(f"The following nodes will be frozen: {output_node}") From 831610a501f9078523480e52b9f3b8d0e926e9f1 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 1 Mar 2024 21:19:41 -0500 Subject: [PATCH 162/686] fix: prevent deepmd.tf be imported globally (#3382) Add a ruff rule `TID253` to ensure it. --------- Signed-off-by: Jinzhe Zeng --- deepmd/utils/compat.py | 2 +- doc/conf.py | 4 +--- pyproject.toml | 22 ++++++++++++++++++++++ source/tests/common/test_uni_infer.py | 27 --------------------------- 4 files changed, 24 insertions(+), 31 deletions(-) delete mode 100644 source/tests/common/test_uni_infer.py diff --git a/deepmd/utils/compat.py b/deepmd/utils/compat.py index 3c48da27c6..5f9c14e6d8 100644 --- a/deepmd/utils/compat.py +++ b/deepmd/utils/compat.py @@ -16,7 +16,7 @@ import numpy as np -from deepmd.tf.common import ( +from deepmd.common import ( j_must_have, ) diff --git a/doc/conf.py b/doc/conf.py index c959a9e30e..22e97c974c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -17,11 +17,9 @@ date, ) -from deepmd.tf.common import ( +from deepmd.utils.argcheck import ( ACTIVATION_FN_DICT, PRECISION_DICT, -) -from deepmd.tf.utils.argcheck import ( list_to_doc, ) diff --git a/pyproject.toml b/pyproject.toml index f6feefbbb8..b1d110ff0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -237,6 +237,7 @@ select = [ "C4", # flake8-comprehensions "RUF", # ruff "NPY", # numpy + "TID253", # banned-module-level-imports ] ignore = [ @@ -256,9 +257,30 @@ ignore = [ ] ignore-init-module-imports = true +exclude = [ + "source/3rdparty/**", +] + [tool.ruff.lint.pydocstyle] convention = "numpy" +[tool.ruff.lint.flake8-tidy-imports] +banned-module-level-imports = [ + "deepmd.tf", + "deepmd.pt", + "tensorflow", + "torch", +] + +[tool.ruff.lint.extend-per-file-ignores] +# Also ignore `E402` in all `__init__.py` files. +"deepmd/tf/**" = ["TID253"] +"deepmd/pt/**" = ["TID253"] +"source/tests/tf/**" = ["TID253"] +"source/tests/pt/**" = ["TID253"] +"source/ipi/tests/**" = ["TID253"] +"source/lmp/tests/**" = ["TID253"] + [tool.pytest.ini_options] markers = "run" diff --git a/source/tests/common/test_uni_infer.py b/source/tests/common/test_uni_infer.py deleted file mode 100644 index 139d1f9ec9..0000000000 --- a/source/tests/common/test_uni_infer.py +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-License-Identifier: LGPL-3.0-or-later -"""Unit tests for the universal Python inference interface.""" - -import os -import unittest - -from deepmd.infer.deep_pot import DeepPot as DeepPot -from deepmd.tf.infer.deep_pot import DeepPot as DeepPotTF -from deepmd.tf.utils.convert import ( - convert_pbtxt_to_pb, -) - -from .common import ( - infer_path, -) - - -class TestUniversalInfer(unittest.TestCase): - @classmethod - def setUpClass(cls): - convert_pbtxt_to_pb( - str(infer_path / os.path.join("deeppot-r.pbtxt")), "deeppot.pb" - ) - - def test_deep_pot(self): - dp = DeepPot("deeppot.pb") - self.assertIsInstance(dp, DeepPotTF) From f4abe12336ec5affb38d072d0ed3d900824c6214 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Sat, 2 Mar 2024 12:45:16 +0800 Subject: [PATCH 163/686] Doc: Update the developer guide for the v3 (#3376) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/development/create-a-model-pt.md | 161 ++++++++++++++++++ ...create-a-model.md => create-a-model-tf.md} | 6 +- doc/index.rst | 3 +- 3 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 doc/development/create-a-model-pt.md rename doc/development/{create-a-model.md => create-a-model-tf.md} (83%) diff --git a/doc/development/create-a-model-pt.md b/doc/development/create-a-model-pt.md new file mode 100644 index 0000000000..fdb1defe8a --- /dev/null +++ b/doc/development/create-a-model-pt.md @@ -0,0 +1,161 @@ +# Create a model in PyTorch + +If you'd like to create a new model that isn't covered by the existing DeePMD-kit library, but reuse DeePMD-kit's other efficient modules such as data processing, trainner, etc, you may want to read this section. + +To incorporate your custom model you'll need to: +1. Register and implement new components (e.g. descriptor) in a Python file. +2. Register new arguments for user inputs. +3. Package new codes into a Python package. +4. Test new models. + +## Design a new component + +With DeePMD-kit v3, we have expanded support to include two additional backends alongside TensorFlow: the PyTorch backend and the framework-independent backend (dpmodel). The PyTorch backend adopts a highly modularized design to provide flexibility and extensibility. It ensures a consistent experience for both training and inference, aligning with the TensorFlow backend. + +The framework-independent backend is implemented in pure NumPy, serving as a reference backend to ensure consistency in tests. Its design pattern closely parallels that of the PyTorch backend. + +### New descriptors + +When creating a new descriptor, it is essential to inherit from both the {py:class}`deepmd.pt.model.descriptor.base_descriptor.BaseDescriptor` class and the {py:class}`torch.nn.Module` class. Abstract methods, including {py:class}`deepmd.pt.model.descriptor.base_descriptor.BaseDescriptor.forward`, must be implemented, while others remain optional. It is crucial to adhere to the original method arguments without any modifications. Once the implementation is complete, the next step involves registering the component with a designated key: + +```py +from deepmd.pt.model.descriptor.base_descriptor import ( + BaseDescriptor, +) + + +@BaseDescriptor.register("some_descrpt") +class SomeDescript(BaseDescriptor, torch.nn.Module): + def __init__(self, arg1: bool, arg2: float) -> None: + pass + + def get_rcut(self) -> float: + pass + + def get_nnei(self) -> int: + pass + + def get_ntypes(self) -> int: + pass + + def get_dim_out(self) -> int: + pass + + def get_dim_emb(self) -> int: + pass + + def mixed_types(self) -> bool: + pass + + def forward( + self, + coord_ext: torch.Tensor, + atype_ext: torch.Tensor, + nlist: torch.Tensor, + mapping: Optional[torch.Tensor] = None, + ): + pass + + def serialize(self) -> dict: + pass + + def deserialize(cls, data: dict) -> "SomeDescript": + pass + + def update_sel(cls, global_jdata: dict, local_jdata: dict): + pass +``` + +The serialize and deserialize methods are important for cross-backend model conversion. + +### New fitting nets + +In many instances, there is no requirement to create a new fitting net. For fitting user-defined scalar properties, the {py:class}`deepmd.pt.model.task.ener.InvarFitting` class can be utilized. However, if there is a need for a new fitting net, one should inherit from both the {py:class}`deepmd.pt.model.task.base_fitting.BaseFitting` class and the {py:class}`torch.nn.Module` class. Alternatively, for a more straightforward approach, inheritance from the {py:class}`deepmd.pt.model.task.fitting.GeneralFitting` class is also an option. + + +```py +from deepmd.pt.model.task.fitting import ( + GeneralFitting, +) +from deepmd.dpmodel import ( + FittingOutputDef, + fitting_check_output, +) + + +@GeneralFitting.register("some_fitting") +@fitting_check_output +class SomeFittingNet(GeneralFitting): + def __init__(self, arg1: bool, arg2: float) -> None: + pass + + def forward( + self, + descriptor: torch.Tensor, + atype: torch.Tensor, + gr: Optional[torch.Tensor] = None, + g2: Optional[torch.Tensor] = None, + h2: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + ): + pass + + def output_def(self) -> FittingOutputDef: + pass +``` +### New models +The PyTorch backend's model architecture is meticulously structured with multiple layers of abstraction, ensuring a high degree of flexibility. Typically, the process commences with an atomic model responsible for atom-wise property calculations. This atomic model inherits from both the {py:class}`deepmd.pt.model.atomic_model.base_atomic_model.BaseAtomicModel` class and the {py:class}`torch.nn.Module` class. + +Subsequently, the `AtomicModel` is encapsulated using the `make_model(AtomicModel)` function, which leverages the `deepmd.pt.model.model.make_model.make_model` function. The purpose of the `make_model` wrapper is to facilitate the translation between atomic property predictions and the extended property predictions and differentiation , e.g. the reduction of atomic energy contribution and the autodiff for calculating the forces and virial. The developers usually need to implement an `AtomicModel` not a `Model`. + +```py +from deepmd.pt.model.atomic_model.base_atomic_model import ( + BaseAtomicModel, +) + + +class SomeAtomicModel(BaseAtomicModel, torch.nn.Module): + def __init__(self, arg1: bool, arg2: float) -> None: + pass + + def forward_atomic(self): + pass +``` + +## Register new arguments + +To let someone uses your new component in their input file, you need to create a new method that returns some `Argument` of your new component, and then register new arguments. For example, the code below + +```py +from typing import List + +from dargs import Argument +from deepmd.utils.argcheck import descrpt_args_plugin + + +@descrpt_args_plugin.register("some_descrpt") +def descrpt_some_args() -> List[Argument]: + return [ + Argument("arg1", bool, optional=False, doc="balabala"), + Argument("arg2", float, optional=True, default=6.0, doc="haha"), + ] +``` + +allows one to use your new descriptor as below: + +```json +"descriptor" :{ + "type": "some_descrpt", + "arg1": true, + "arg2": 6.0 +} +``` + +The arguments here should be consistent with the class arguments of your new component. + +## Unit tests + +When transferring features from another backend to the PyTorch backend, it is essential to include a regression test in `/source/tests/consistent` to validate the consistency of the PyTorch backend with other backends. Presently, the regression tests cover self-consistency and cross-backend consistency between TensorFlow, PyTorch, and dpmodel (Numpy) through the serialization/deserialization technique. + +During the development of new components within the PyTorch backend, it is necessary to provide a dpmodel (Numpy) implementation and incorporate corresponding regression tests. For PyTorch components, developers are also required to include a unit test using `torch.jit`. diff --git a/doc/development/create-a-model.md b/doc/development/create-a-model-tf.md similarity index 83% rename from doc/development/create-a-model.md rename to doc/development/create-a-model-tf.md index d71a6e519a..7c4f5335ec 100644 --- a/doc/development/create-a-model.md +++ b/doc/development/create-a-model-tf.md @@ -1,4 +1,4 @@ -# Create a model +# Create a model in TensorFlow If you'd like to create a new model that isn't covered by the existing DeePMD-kit library, but reuse DeePMD-kit's other efficient modules such as data processing, trainner, etc, you may want to read this section. @@ -10,7 +10,7 @@ To incorporate your custom model you'll need to: ## Design a new component -When creating a new component, take descriptor as the example, you should inherit {py:class}`deepmd.tf.descriptor.descriptor.Descriptor` class and override several methods. Abstract methods such as {py:class}`deepmd.tf.descriptor.descriptor.Descriptor.build` must be implemented and others are not. You should keep arguments of these methods unchanged. +When creating a new component, take descriptor as the example, one should inherit from the {py:class}`deepmd.tf.descriptor.descriptor.Descriptor` class and override several methods. Abstract methods such as {py:class}`deepmd.tf.descriptor.descriptor.Descriptor.build` must be implemented and others are not. You should keep arguments of these methods unchanged. After implementation, you need to register the component with a key: ```py @@ -31,7 +31,7 @@ To let someone uses your new component in their input file, you need to create a from typing import List from dargs import Argument -from deepmd.tf.utils.argcheck import descrpt_args_plugin +from deepmd.utils.argcheck import descrpt_args_plugin @descrpt_args_plugin.register("some_descrpt") diff --git a/doc/index.rst b/doc/index.rst index d089507886..7bff8d3957 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -64,7 +64,8 @@ DeePMD-kit is a package written in Python/C++, designed to minimize the effort r :caption: Developer Guide development/cmake - development/create-a-model + development/create-a-model-tf + development/create-a-model-pt development/type-embedding development/coding-conventions development/cicd From bf4b473bef9cbfa69b0b201312fa521f539e825d Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 2 Mar 2024 02:42:08 -0500 Subject: [PATCH 164/686] pt: print data summary (#3383) Signed-off-by: Jinzhe Zeng --- deepmd/pt/train/training.py | 23 +++++++ deepmd/pt/utils/dataloader.py | 22 +++++++ deepmd/pt/utils/dataset.py | 1 + deepmd/utils/data_system.py | 112 +++++++++++++++++++++++----------- 4 files changed, 124 insertions(+), 34 deletions(-) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 07c8511cfe..97da0ce322 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -53,6 +53,9 @@ from deepmd.pt.utils.stat import ( make_stat_input, ) +from deepmd.pt.utils.utils import ( + to_numpy_array, +) if torch.__version__.startswith("2"): import torch._dynamo @@ -288,6 +291,14 @@ def get_loss(loss_params, start_lr, _ntypes): self.validation_data, self.valid_numb_batch, ) = get_data_loader(training_data, validation_data, training_params) + training_data.print_summary( + "training", to_numpy_array(self.training_dataloader.sampler.weights) + ) + if validation_data is not None: + validation_data.print_summary( + "validation", + to_numpy_array(self.validation_dataloader.sampler.weights), + ) else: ( self.training_dataloader, @@ -317,6 +328,18 @@ def get_loss(loss_params, start_lr, _ntypes): training_params["data_dict"][model_key], ) + training_data[model_key].print_summary( + f"training in {model_key}", + to_numpy_array(self.training_dataloader[model_key].sampler.weights), + ) + if validation_data is not None: + validation_data[model_key].print_summary( + f"validation in {model_key}", + to_numpy_array( + self.validation_dataloader[model_key].sampler.weights + ), + ) + # Learning rate self.warmup_steps = training_params.get("warmup_steps", 0) self.gradient_max_norm = training_params.get("gradient_max_norm", 0.0) diff --git a/deepmd/pt/utils/dataloader.py b/deepmd/pt/utils/dataloader.py index 65a96418c9..2715bced52 100644 --- a/deepmd/pt/utils/dataloader.py +++ b/deepmd/pt/utils/dataloader.py @@ -39,6 +39,7 @@ DataRequirementItem, ) from deepmd.utils.data_system import ( + print_summary, prob_sys_size_ext, process_sys_probs, ) @@ -91,6 +92,7 @@ def construct_dataset(system): self.total_batch = 0 self.dataloaders = [] + self.batch_sizes = [] for system in self.systems: if dist.is_initialized(): system_sampler = DistributedSampler(system) @@ -110,6 +112,7 @@ def construct_dataset(system): self.batch_size += 1 else: self.batch_size = batch_size + self.batch_sizes.append(self.batch_size) system_dataloader = DataLoader( dataset=system, batch_size=self.batch_size, @@ -155,6 +158,25 @@ def add_data_requirement(self, data_requirement: List[DataRequirementItem]): for system in self.systems: system.add_data_requirement(data_requirement) + def print_summary( + self, + name: str, + prob: List[float], + ): + print_summary( + name, + len(self.systems), + [ss.system for ss in self.systems], + [ss._natoms for ss in self.systems], + self.batch_sizes, + [ + ss._data_system.get_sys_numb_batch(self.batch_sizes[ii]) + for ii, ss in enumerate(self.systems) + ], + prob, + [ss._data_system.pbc for ss in self.systems], + ) + _sentinel = object() QUEUESIZE = 32 diff --git a/deepmd/pt/utils/dataset.py b/deepmd/pt/utils/dataset.py index 40a513acdf..67005b5ed3 100644 --- a/deepmd/pt/utils/dataset.py +++ b/deepmd/pt/utils/dataset.py @@ -29,6 +29,7 @@ def __init__( - batch_size: Max frame count in a batch. - type_map: Atom types. """ + self.system = system self._type_map = type_map self._data_system = DeepmdData( sys_path=system, shuffle_test=shuffle, type_map=self._type_map diff --git a/deepmd/utils/data_system.py b/deepmd/utils/data_system.py index 90b600548f..ba1041f113 100644 --- a/deepmd/utils/data_system.py +++ b/deepmd/utils/data_system.py @@ -556,40 +556,16 @@ def get_batch_size(self) -> int: """Get the batch size.""" return self.batch_size - def _format_name_length(self, name, width): - if len(name) <= width: - return "{: >{}}".format(name, width) - else: - name = name[-(width - 3) :] - name = "-- " + name - return name - - def print_summary(self, name): - # width 65 - sys_width = 42 - log.info( - f"---Summary of DataSystem: {name:13s}-----------------------------------------------" - ) - log.info("found %d system(s):" % self.nsystems) - log.info( - ("%s " % self._format_name_length("system", sys_width)) - + ("%6s %6s %6s %9s %3s" % ("natoms", "bch_sz", "n_bch", "prob", "pbc")) - ) - for ii in range(self.nsystems): - log.info( - "%s %6d %6d %6d %9.3e %3s" - % ( - self._format_name_length(self.system_dirs[ii], sys_width), - self.natoms[ii], - # TODO batch size * nbatches = number of structures - self.batch_size[ii], - self.nbatches[ii], - self.sys_probs[ii], - "T" if self.data_systems[ii].pbc else "F", - ) - ) - log.info( - "--------------------------------------------------------------------------------------" + def print_summary(self, name: str): + print_summary( + name, + self.nsystems, + self.system_dirs, + self.natoms, + self.batch_size, + self.nbatches, + self.sys_probs, + [ii.pbc for ii in self.data_systems], ) def _make_auto_bs(self, rule): @@ -625,6 +601,74 @@ def _check_type_map_consistency(self, type_map_list): return ret +def _format_name_length(name, width): + if len(name) <= width: + return "{: >{}}".format(name, width) + else: + name = name[-(width - 3) :] + name = "-- " + name + return name + + +def print_summary( + name: str, + nsystems: int, + system_dirs: List[str], + natoms: List[int], + batch_size: List[int], + nbatches: List[int], + sys_probs: List[float], + pbc: List[bool], +): + """Print summary of systems. + + Parameters + ---------- + name : str + The name of the system + nsystems : int + The number of systems + system_dirs : list of str + The directories of the systems + natoms : list of int + The number of atoms + batch_size : list of int + The batch size + nbatches : list of int + The number of batches + sys_probs : list of float + The probabilities + pbc : list of bool + The periodic boundary conditions + """ + # width 65 + sys_width = 42 + log.info( + f"---Summary of DataSystem: {name:13s}-----------------------------------------------" + ) + log.info("found %d system(s):" % nsystems) + log.info( + ("%s " % _format_name_length("system", sys_width)) + + ("%6s %6s %6s %9s %3s" % ("natoms", "bch_sz", "n_bch", "prob", "pbc")) + ) + for ii in range(nsystems): + log.info( + "%s %6d %6d %6d %9.3e %3s" + % ( + _format_name_length(system_dirs[ii], sys_width), + natoms[ii], + # TODO batch size * nbatches = number of structures + batch_size[ii], + nbatches[ii], + sys_probs[ii], + "T" if pbc[ii] else "F", + ) + ) + log.info( + "--------------------------------------------------------------------------------------" + ) + + def process_sys_probs(sys_probs, nbatch): sys_probs = np.array(sys_probs) type_filter = sys_probs >= 0 From c61ba883d8d6916723809b2ae2110b7d618e6a31 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 2 Mar 2024 02:43:51 -0500 Subject: [PATCH 165/686] pt: expand systems before training (#3384) Signed-off-by: Jinzhe Zeng --- deepmd/pt/entrypoints/main.py | 6 ++++ deepmd/utils/data_system.py | 55 ++++++++++++++++++++++++----------- deepmd/utils/path.py | 2 ++ 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 736e8dde09..0e5767cb4e 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -59,6 +59,9 @@ from deepmd.utils.compat import ( update_deepmd_input, ) +from deepmd.utils.data_system import ( + process_systems, +) from deepmd.utils.path import ( DPPath, ) @@ -108,6 +111,9 @@ def prepare_trainer_input_single( validation_dataset_params["systems"] if validation_dataset_params else None ) training_systems = training_dataset_params["systems"] + training_systems = process_systems(training_systems) + if validation_systems is not None: + validation_systems = process_systems(validation_systems) # stat files stat_file_path_single = data_dict_single.get("stat_file", None) diff --git a/deepmd/utils/data_system.py b/deepmd/utils/data_system.py index ba1041f113..da1dd04026 100644 --- a/deepmd/utils/data_system.py +++ b/deepmd/utils/data_system.py @@ -10,6 +10,7 @@ Dict, List, Optional, + Union, ) import numpy as np @@ -711,30 +712,22 @@ def prob_sys_size_ext(keywords, nsystems, nbatch): return sys_probs -def get_data( - jdata: Dict[str, Any], rcut, type_map, modifier, multi_task_mode=False -) -> DeepmdDataSystem: - """Get the data system. +def process_systems(systems: Union[str, List[str]]) -> List[str]: + """Process the user-input systems. + + If it is a single directory, search for all the systems in the directory. + Check if the systems are valid. Parameters ---------- - jdata - The json data - rcut - The cut-off radius, not used - type_map - The type map - modifier - The data modifier - multi_task_mode - If in multi task mode + systems : str or list of str + The user-input systems Returns ------- - DeepmdDataSystem - The data system + list of str + The valid systems """ - systems = j_must_have(jdata, "systems") if isinstance(systems, str): systems = expand_sys_str(systems) elif isinstance(systems, list): @@ -756,6 +749,34 @@ def get_data( msg = f"dir {ii} is not a valid data system dir" log.fatal(msg) raise OSError(msg, help_msg) + return systems + + +def get_data( + jdata: Dict[str, Any], rcut, type_map, modifier, multi_task_mode=False +) -> DeepmdDataSystem: + """Get the data system. + + Parameters + ---------- + jdata + The json data + rcut + The cut-off radius, not used + type_map + The type map + modifier + The data modifier + multi_task_mode + If in multi task mode + + Returns + ------- + DeepmdDataSystem + The data system + """ + systems = j_must_have(jdata, "systems") + systems = process_systems(systems) batch_size = j_must_have(jdata, "batch_size") sys_probs = jdata.get("sys_probs", None) diff --git a/deepmd/utils/path.py b/deepmd/utils/path.py index 79361b6c23..5887e91850 100644 --- a/deepmd/utils/path.py +++ b/deepmd/utils/path.py @@ -414,6 +414,8 @@ def is_file(self) -> bool: def is_dir(self) -> bool: """Check if self is directory.""" + if self._name == "/": + return True if self._name not in self._keys: return False return isinstance(self.root[self._name], h5py.Group) From d2b18b2b43c15e618c0eb4ee97c5a6f6944a65a5 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Sat, 2 Mar 2024 16:19:04 +0800 Subject: [PATCH 166/686] Doc: Update PT Multi-task (#3387) --- doc/train/index.rst | 3 +- doc/train/multi-task-training-pt.md | 81 +++++++++++ ...-training.md => multi-task-training-tf.md} | 0 .../pytorch_example/input_torch.json | 133 ++++++++++++++++++ 4 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 doc/train/multi-task-training-pt.md rename doc/train/{multi-task-training.md => multi-task-training-tf.md} (100%) create mode 100644 examples/water_multi_task/pytorch_example/input_torch.json diff --git a/doc/train/index.rst b/doc/train/index.rst index 92e84b3000..78ee31e5cb 100644 --- a/doc/train/index.rst +++ b/doc/train/index.rst @@ -8,7 +8,8 @@ Training training-advanced train-input parallel-training - multi-task-training + multi-task-training-tf + multi-task-training-pt tensorboard gpu-limitations finetuning diff --git a/doc/train/multi-task-training-pt.md b/doc/train/multi-task-training-pt.md new file mode 100644 index 0000000000..d1d0f9cd0d --- /dev/null +++ b/doc/train/multi-task-training-pt.md @@ -0,0 +1,81 @@ +# Multi-task training {{ pytorch_icon }} + +:::{note} +**Supported backends**: PyTorch {{ pytorch_icon }} +::: + + +## Theory + +The multi-task training process can simultaneously handle different datasets with properties that cannot be fitted in one network (e.g. properties from DFT calculations under different exchange-correlation functionals or different basis sets). +These datasets are denoted by $\boldsymbol x^{(1)}, \dots, \boldsymbol x^{(n_t)}$. +For each dataset, a training task is defined as +```math + \min_{\boldsymbol \theta} L^{(t)} (\boldsymbol x^{(t)}; \boldsymbol \theta^{(t)}, \tau), \quad t=1, \dots, n_t. +``` + +In the Pytorch implementation, during the multi-task training process, all tasks can share any portion of the model parameters. +A typical scenario is that each task shares the same descriptor with trainable parameters $\boldsymbol{\theta}_ {d}$, while each has its own fitting network with trainable parameters $\boldsymbol{\theta}_ f^{(t)}$, thus +$\boldsymbol{\theta}^{(t)} = \{ \boldsymbol{\theta}_ {d} , \boldsymbol{\theta}_ {f}^{(t)} \}$. +At each training step, a task will be randomly selected from ${1, \dots, n_t}$ according to the user-specified probability, +and the Adam optimizer is executed to minimize $L^{(t)}$ for one step to update the parameter $\boldsymbol \theta^{(t)}$. +In the case of multi-GPU parallel training, different GPUs will independently select their tasks. +In the DPA-2 model, this multi-task training framework is adopted.[^1] + +[^1] Duo Zhang, Xinzijian Liu, Xiangyu Zhang, Chengqian Zhang, Chun Cai, Hangrui Bi, Yiming Du, Xuejian Qin, Jiameng Huang, Bowen Li, Yifan Shan, Jinzhe Zeng, Yuzhi Zhang, Siyuan Liu, Yifan Li, Junhan Chang, Xinyan Wang, Shuo Zhou, Jianchuan Liu, Xiaoshan Luo, Zhenyu Wang, Wanrun Jiang, Jing Wu, Yudi Yang, Jiyuan Yang, Manyi Yang, Fu-Qiang Gong, Linshuang Zhang, Mengchao Shi, Fu-Zhi Dai, Darrin M. York, Shi Liu, Tong Zhu, Zhicheng Zhong, Jian Lv, Jun Cheng, Weile Jia, Mohan Chen, Guolin Ke, Weinan E, Linfeng Zhang, Han Wang,[arXiv preprint arXiv:2312.15492 (2023)](https://arxiv.org/abs/2312.15492) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). + +Compared with the previous TensorFlow implementation, the new support in PyTorch is more flexible and efficient. +In particular, it makes multi-GPU parallel training and even tasks beyond DFT possible, +enabling larger-scale and more general multi-task training to obtain more general pre-trained models. + +## Perform the multi-task training using PyTorch +Training on multiple data sets (each data set contains several data systems) can be performed in multi-task mode, +typically with one common descriptor and multiple specific fitting nets for each data set. +To proceed, one need to change the representation of the model definition in the input script. +The core idea is to replace the previous single model definition {ref}`model ` with multiple model definitions {ref}`model/model_dict/model_key `, +define the shared parameters of the model part {ref}`shared_dict `, and then expand other parts for multi-model settings. +Specifically, there are several parts that need to be modified: + +- {ref}`model/shared_dict `: The parameter definition of the shared part, including various descriptors, +type maps (or even fitting nets can be shared). Each module can be defined with a user-defined `part_key`, such as `my_descriptor`. +The content needs to align with the corresponding definition in the single-task training model component, such as the definition of the descriptor. + +- {ref}`model/model_dict `: The core definition of the model part and the explanation of sharing rules, +starting with user-defined model name keys `model_key`, such as `my_model_1`. +Each model part needs to align with the components of the single-task training {ref}`model `, but with the following sharing rules: +- - If you want to share the current model component with other tasks, which should be part of the {ref}`model/shared_dict `, +you can directly fill in the corresponding `part_key`, such as +```"descriptor": "my_descriptor", ``` +to replace the previous detailed parameters. Here, you can also specify the shared_level, such as +```"descriptor": "my_descriptor:shared_level", ``` +and use the user-defined integer `shared_level` in the code to share the corresponding module to varying degrees +(default is to share all parameters, i.e., `shared_level`=0). +The parts that are exclusive to each model can be written following the previous definition. + +- {ref}`loss_dict `: The loss settings corresponding to each task model, specified by the `model_key`. +Each {ref}`loss_dict/model_key ` contains the corresponding loss settings, +which are the same as the definition in single-task training {ref}``. + +- {ref}`training/data_dict `: The data settings corresponding to each task model, specified by the `model_key`. +Each `training/data_dict/model_key` contains the corresponding `training_data` and `validation_data` settings, +which are the same as the definition in single-task training {ref}`training_data ` and {ref}`validation_data `. + +- (Optional) {ref}`training/model_prob `: The sampling weight settings corresponding to each `model_key`, i.e., the probability weight in the training step. +You can specify any positive real number weight for each task. The higher the weight, the higher the probability of being sampled in each training. +This setting is optional, and if not set, tasks will be sampled with equal weights. + +An example input for multi-task training two models in water system is shown as following: +```{literalinclude} ../../examples/water_multi_task/pytorch_example/input_torch.json +:language: json +:linenos: +``` + +## Finetune from the pretrained multi-task model + +To finetune based on the checkpoint `model.pt` after the multi-task pre-training is completed, +users only need to prepare the normal input for single-task training `input_single.json`, +and then select one of the trained model's task names `model_key`. +Run the following command: +```bash +$ dp --pt train input_single.json --finetune model.pt --model-branch model_key +``` diff --git a/doc/train/multi-task-training.md b/doc/train/multi-task-training-tf.md similarity index 100% rename from doc/train/multi-task-training.md rename to doc/train/multi-task-training-tf.md diff --git a/examples/water_multi_task/pytorch_example/input_torch.json b/examples/water_multi_task/pytorch_example/input_torch.json new file mode 100644 index 0000000000..801848f077 --- /dev/null +++ b/examples/water_multi_task/pytorch_example/input_torch.json @@ -0,0 +1,133 @@ +{ + "_comment": "that's all", + "model": { + "shared_dict": { + "type_map_all": [ + "O", + "H" + ], + "sea_descriptor_1": { + "type": "se_e2_a", + "sel": [ + 46, + 92 + ], + "rcut_smth": 0.50, + "rcut": 6.00, + "neuron": [ + 25, + 50, + 100 + ], + "resnet_dt": false, + "axis_neuron": 16, + "type_one_side": true, + "seed": 1, + "_comment": " that's all" + }, + "_comment": "that's all" + }, + "model_dict": { + "water_1": { + "type_map": "type_map_all", + "descriptor": "sea_descriptor_1", + "fitting_net": { + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "seed": 1, + "_comment": " that's all" + } + }, + "water_2": { + "type_map": "type_map_all", + "descriptor": "sea_descriptor_1", + "fitting_net": { + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "seed": 1, + "_comment": " that's all" + } + } + } + }, + "learning_rate": { + "type": "exp", + "decay_steps": 5000, + "start_lr": 0.0002, + "decay_rate": 0.98, + "stop_lr": 3.51e-08, + "_comment": "that's all" + }, + "loss_dict": { + "_comment": " that's all", + "water_1": { + "type": "ener", + "start_pref_e": 0.02, + "limit_pref_e": 1, + "start_pref_f": 1000, + "limit_pref_f": 1, + "start_pref_v": 0, + "limit_pref_v": 0 + }, + "water_2": { + "type": "ener", + "start_pref_e": 0.02, + "limit_pref_e": 1, + "start_pref_f": 1000, + "limit_pref_f": 1, + "start_pref_v": 0, + "limit_pref_v": 0 + } + }, + "training": { + "model_prob": { + "water_1": 0.5, + "water_2": 0.5 + }, + "data_dict": { + "water_1": { + "training_data": { + "systems": [ + "../../water/data/data_0/", + "../../water/data/data_1/", + "../../water/data/data_2/" + ], + "batch_size": 1, + "_comment": "that's all" + }, + "validation_data": { + "systems": [ + "../../water/data/data_3/" + ], + "batch_size": 1, + "_comment": "that's all" + } + }, + "water_2": { + "training_data": { + "systems": [ + "../../water/data/data_0/", + "../../water/data/data_1/", + "../../water/data/data_2/" + ], + "batch_size": 1, + "_comment": "that's all" + } + } + }, + "numb_steps": 100000, + "seed": 10, + "disp_file": "lcurve.out", + "disp_freq": 100, + "save_freq": 100, + "_comment": "that's all" + } +} From 7aee42c2dc939e3cd833e736e6e69655cb6fd055 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 2 Mar 2024 04:28:55 -0500 Subject: [PATCH 167/686] pt: add fparam/aparam data requirements (#3386) Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/pt/train/training.py | 21 +++++++++++++++++++++ examples/fparam/train/input.json | 3 +++ source/tests/pt/test_training.py | 22 ++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 97da0ce322..b31822d0ee 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -56,6 +56,9 @@ from deepmd.pt.utils.utils import ( to_numpy_array, ) +from deepmd.utils.data import ( + DataRequirementItem, +) if torch.__version__.startswith("2"): import torch._dynamo @@ -200,6 +203,24 @@ def get_single_model( _training_data.add_data_requirement(_data_requirement) if _validation_data is not None: _validation_data.add_data_requirement(_data_requirement) + if model.get_dim_fparam() > 0: + fparam_requirement_items = [ + DataRequirementItem( + "fparam", model.get_dim_fparam(), atomic=False, must=True + ) + ] + _training_data.add_data_requirement(fparam_requirement_items) + if _validation_data is not None: + _validation_data.add_data_requirement(fparam_requirement_items) + if model.get_dim_aparam() > 0: + aparam_requirement_items = [ + DataRequirementItem( + "aparam", model.get_dim_aparam(), atomic=True, must=True + ) + ] + _training_data.add_data_requirement(aparam_requirement_items) + if _validation_data is not None: + _validation_data.add_data_requirement(aparam_requirement_items) if not resuming and self.rank == 0: @functools.lru_cache diff --git a/examples/fparam/train/input.json b/examples/fparam/train/input.json index f881dff3ca..1bb9b55f95 100644 --- a/examples/fparam/train/input.json +++ b/examples/fparam/train/input.json @@ -1,6 +1,9 @@ { "_comment1": " model parameters", "model": { + "type_map": [ + "Be" + ], "data_stat_nbatch": 1, "descriptor": { "type": "se_a", diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index f2a081610a..b9a42385dc 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -70,6 +70,28 @@ def tearDown(self) -> None: DPTrainTest.tearDown(self) +class TestFparam(unittest.TestCase, DPTrainTest): + """Test if `fparam` can be loaded correctly.""" + + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_se_e2_a) + self.config["model"]["fitting_net"]["numb_fparam"] = 1 + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + self.set_path = Path(__file__).parent / "water/data/data_0" / "set.000" + shutil.copyfile(self.set_path / "energy.npy", self.set_path / "fparam.npy") + + def tearDown(self) -> None: + (self.set_path / "fparam.npy").unlink(missing_ok=True) + DPTrainTest.tearDown(self) + + class TestEnergyModelDPA1(unittest.TestCase, DPTrainTest): def setUp(self): input_json = str(Path(__file__).parent / "water/se_atten.json") From 822be1edef3523b237744fe7dbfb4286368526ad Mon Sep 17 00:00:00 2001 From: Lysithea <52808607+CaRoLZhangxy@users.noreply.github.com> Date: Sat, 2 Mar 2024 17:30:41 +0800 Subject: [PATCH 168/686] doc: add PyTorch parallel training content (#3379) Signed-off-by: Lysithea <52808607+CaRoLZhangxy@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jinzhe Zeng --- doc/train/parallel-training.md | 109 ++++++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 7 deletions(-) diff --git a/doc/train/parallel-training.md b/doc/train/parallel-training.md index 4c707e5607..aba2836250 100644 --- a/doc/train/parallel-training.md +++ b/doc/train/parallel-training.md @@ -1,13 +1,14 @@ -# Parallel training {{ tensorflow_icon }} +# Parallel training {{ tensorflow_icon }} {{ pytorch_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }} ::: -Currently, parallel training is enabled in a synchronized way with help of [Horovod](https://github.com/horovod/horovod). +## TensorFlow Implementation {{ tensorflow_icon }} +Currently, parallel training in tensorflow version is enabled in a synchronized way with help of [Horovod](https://github.com/horovod/horovod). Depending on the number of training processes (according to MPI context) and the number of GPU cards available, DeePMD-kit will decide whether to launch the training in parallel (distributed) mode or in serial mode. Therefore, no additional options are specified in your JSON/YAML input file. -## Tuning learning rate +### Tuning learning rate Horovod works in the data-parallel mode, resulting in a larger global batch size. For example, the real batch size is 8 when {ref}`batch_size ` is set to 2 in the input file and you launch 4 workers. Thus, {ref}`learning_rate ` is automatically scaled by the number of workers for better convergence. Technical details of such heuristic rule are discussed at [Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour](https://arxiv.org/abs/1706.02677). @@ -21,7 +22,7 @@ In some cases, it won't work well when scaling the learning rate by worker count } ``` -## Scaling test +### Scaling test Testing `examples/water/se_e2_a` on an 8-GPU host, linear acceleration can be observed with the increasing number of cards. @@ -32,7 +33,7 @@ Testing `examples/water/se_e2_a` on an 8-GPU host, linear acceleration can be ob | 4 | 1.7635 | 56.71*4 | 3.29 | | 8 | 1.7267 | 57.91*8 | 6.72 | -## How to use +### How to use Training workers can be launched with `horovodrun`. The following command launches 4 processes on the same host: @@ -68,7 +69,7 @@ Whether distributed workers are initiated can be observed in the "Summary of the [0] DEEPMD INFO ----------------------------------------------------------------- ``` -## Logging +### Logging What's more, 2 command-line arguments are defined to control the logging behavior when performing parallel training with MPI. ``` @@ -84,3 +85,97 @@ optional arguments: means each process will output its own log (default: master) ``` + +## PyTorch Implementation {{ pytorch_icon }} + +Currently, parallel training in pytorch version is implemented in the form of PyTorch Distributed Data Parallelism [DDP](https://pytorch.org/docs/stable/generated/torch.nn.parallel.DistributedDataParallel.html). +DeePMD-kit will decide whether to launch the training in parallel (distributed) mode or in serial mode depending on your execution command. + +### Dataloader and Dataset +One of the major differences between two backends during training is that the PyTorch version employs a multi-threaded data loading utility [DataLoader](https://pytorch.org/docs/stable/data.html). +We utilize the PyTorch framework and have designed and implemented a multiprocessing data processing and loading system called DpLoaderSet based on torch DataLoader and Dataset. + + +First, we establish a DeepmdData class for each system, which is consistent with the TensorFlow version in this level. Then, we create a dataloader for each system, resulting in the same number of dataloaders as the number of systems. Next, we create a dataset for the dataloaders obtained in the previous step. This allows us to query the data for each system through this dataset, while the iteration pointers for each system are maintained by their respective dataloaders. Finally, a dataloader is created for the outermost dataset. + +We achieve custom sampling methods using a weighted sampler. The length of the sampler is set to total_batch_num * num_workers.The parameter "num_workers" defines the number of threads involved in multi-threaded loading, which can be modified by setting the environment variable NUM_WORKERS (default: min(8, ncpus)). + +> **Note** The underlying dataloader will use a distributed sampler to ensure that each GPU receives batches with different content in parallel mode, which will use sequential sampler in serial mode. In the TensorFlow version, Horovod shuffles the dataset using different random seeds for the same purpose.. +```mermaid +flowchart LR + + subgraph systems + subgraph system1 + direction LR + frame1[frame 1] + frame2[frame 2] + end + + subgraph system2 + direction LR + frame3[frame 3] + frame4[frame 4] + frame5[frame 5] + end + end + + subgraph dataset + dataset1[dataset 1] + dataset2[dataset 2] + end + system1 -- frames --> dataset1 + system2 --> dataset2 + + subgraph distribted sampler + ds1[distributed sampler 1] + ds2[distributed sampler 2] + end + dataset1 --> ds1 + dataset2 --> ds2 + + subgraph dataloader + dataloader1[dataloader 1] + dataloader2[dataloader 2] + end + ds1 -- mini batch --> dataloader1 + ds2 --> dataloader2 + + subgraph index[index on Rank 0] + dl11[dataloader 1, entry 1] + dl21[dataloader 2, entry 1] + dl22[dataloader 2, entry 2] + end + dataloader1 --> dl11 + dataloader2 --> dl21 + dataloader2 --> dl22 + + index -- for each step, choose 1 system --> WeightedSampler + --> dploaderset --> bufferedq[buffered queue] --> model +``` + +### How to use + +We use [`torchrun`](https://pytorch.org/docs/stable/elastic/run.html#usage) to launch a DDP training session. + +To start training with multiple GPUs in one node, set parameter `nproc_per_node` as the number of it: + +```bash +torchrun --nproc_per_node=4 --no-python dp --pt train input.json +# Not setting `nproc_per_node` uses only 1 GPU +torchrun --no-python dp --pt train input.json +``` + +To train a model with a cluster, one can manually launch the task using the commands below (usually this should be done by your job management system). Set `nnodes` as the number of available nodes, `node_rank` as the rank of the current node among all nodes (not the rank of processes!), and `nproc_per_node` as the number of available GPUs in one node. Please make sure that every node can access the rendezvous address and port (`rdzv_endpoint` in the command), and has a same amount of GPUs. + +```bash +# Running DDP on 2 nodes with 4 GPUs each +# On node 0: +torchrun --rdzv_endpoint=node0:12321 --nnodes=2 --nproc_per_node=4 --node_rank=0 --no_python dp --pt train tests/water/se_e2_a.json +# On node 1: +torchrun --rdzv_endpoint=node0:12321 --nnodes=2 --nproc_per_node=4 --node_rank=1 --no_python dp --pt train tests/water/se_e2_a.json +``` +> **Note** Set environment variables to tune [CPU specific optimizations](https://pytorch.org/tutorials/recipes/recipes/tuning_guide.html#cpu-specific-optimizations) in advance. + +> **Note** for developers: `torchrun` by default passes settings as environment variables [(list here)](https://pytorch.org/docs/stable/elastic/run.html#environment-variables). + +> To check forward, backward, and communication time, please set env var `TORCH_CPP_LOG_LEVEL=INFO TORCH_DISTRIBUTED_DEBUG=DETAIL`. More details can be found [here](https://pytorch.org/docs/stable/distributed.html#logging). From e9181060eb66d24d70c56053703d7362a27afc2f Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Sat, 2 Mar 2024 18:34:22 +0800 Subject: [PATCH 169/686] pt: fix params with no docstrs (#3388) Signed-off-by: Jinzhe Zeng Co-authored-by: Jinzhe Zeng --- deepmd/utils/argcheck.py | 114 +++++++++++++++++++++++++++++++++------ 1 file changed, 98 insertions(+), 16 deletions(-) diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 1f0064c460..22f71c8319 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -455,6 +455,20 @@ def descrpt_se_atten_args(): doc_stripped_type_embedding = "Whether to strip the type embedding into a separated embedding network. Setting it to `False` will fall back to the previous version of `se_atten` which is non-compressible." doc_smooth_type_embdding = "When using stripped type embedding, whether to dot smooth factor on the network output of type embedding to keep the network smooth, instead of setting `set_davg_zero` to be True." doc_set_davg_zero = "Set the normalization average to zero. This option should be set when `se_atten` descriptor or `atom_ener` in the energy fitting is used" + doc_tebd_dim = "The dimension of atom type embedding." + doc_temperature = "The scaling factor of normalization in calculations of attention weights, which is used to scale the matmul(Q, K)." + doc_scaling_factor = ( + "The scaling factor of normalization in calculations of attention weights, which is used to scale the matmul(Q, K). " + "If `temperature` is None, the scaling of attention weights is (N_hidden_dim * scaling_factor)**0.5. " + "Else, the scaling of attention weights is setting to `temperature`." + ) + doc_normalize = ( + "Whether to normalize the hidden vectors during attention calculation." + ) + doc_concat_output_tebd = ( + "Whether to concat type embedding at the output of the descriptor." + ) + doc_deprecated = "This feature will be removed in a future release." return [ *descrpt_se_atten_common_args(), @@ -476,42 +490,81 @@ def descrpt_se_atten_args(): "set_davg_zero", bool, optional=True, default=True, doc=doc_set_davg_zero ), # pt only - Argument("tebd_dim", int, optional=True, default=8, doc=doc_only_pt_supported), + Argument( + "tebd_dim", + int, + optional=True, + default=8, + doc=doc_only_pt_supported + doc_tebd_dim, + ), Argument( "tebd_input_mode", str, optional=True, default="concat", - doc=doc_only_pt_supported, + doc=doc_only_pt_supported + doc_deprecated, + ), + Argument( + "post_ln", + bool, + optional=True, + default=True, + doc=doc_only_pt_supported + doc_deprecated, ), Argument( - "post_ln", bool, optional=True, default=True, doc=doc_only_pt_supported + "ffn", + bool, + optional=True, + default=False, + doc=doc_only_pt_supported + doc_deprecated, ), - Argument("ffn", bool, optional=True, default=False, doc=doc_only_pt_supported), Argument( - "ffn_embed_dim", int, optional=True, default=1024, doc=doc_only_pt_supported + "ffn_embed_dim", + int, + optional=True, + default=1024, + doc=doc_only_pt_supported + doc_deprecated, ), Argument( "scaling_factor", float, optional=True, default=1.0, - doc=doc_only_pt_supported, + doc=doc_only_pt_supported + doc_scaling_factor, ), - Argument("head_num", int, optional=True, default=1, doc=doc_only_pt_supported), Argument( - "normalize", bool, optional=True, default=True, doc=doc_only_pt_supported + "head_num", + int, + optional=True, + default=1, + doc=doc_only_pt_supported + doc_deprecated, ), - Argument("temperature", float, optional=True, doc=doc_only_pt_supported), Argument( - "return_rot", bool, optional=True, default=False, doc=doc_only_pt_supported + "normalize", + bool, + optional=True, + default=True, + doc=doc_only_pt_supported + doc_normalize, + ), + Argument( + "temperature", + float, + optional=True, + doc=doc_only_pt_supported + doc_temperature, + ), + Argument( + "return_rot", + bool, + optional=True, + default=False, + doc=doc_only_pt_supported + doc_deprecated, ), Argument( "concat_output_tebd", bool, optional=True, default=True, - doc=doc_only_pt_supported, + doc=doc_only_pt_supported + doc_concat_output_tebd, ), ] @@ -2069,6 +2122,23 @@ def training_args(): # ! modified by Ziyao: data configuration isolated. "Weights will be normalized and minus ones will be ignored. " "If not set, each fitting net will be equally selected when training." ) + doc_warmup_steps = ( + "The number of steps for learning rate warmup. During warmup, " + "the learning rate begins at zero and progressively increases linearly to `start_lr`, " + "rather than starting directly from `start_lr`" + ) + doc_gradient_max_norm = ( + "Clips the gradient norm to a maximum value. " + "If the gradient norm exceeds this value, it will be clipped to this limit. " + "No gradient clipping will occur if set to 0." + ) + doc_stat_file = ( + "The file path for saving the data statistics results. " + "If set, the results will be saved and directly loaded during the next training session, " + "avoiding the need to recalculate the statistics" + ) + doc_opt_type = "The type of optimizer to use." + doc_kf_blocksize = "The blocksize for the Kalman filter." arg_training_data = training_data_args() arg_validation_data = validation_data_args() @@ -2132,9 +2202,21 @@ def training_args(): # ! modified by Ziyao: data configuration isolated. ), Argument("data_dict", dict, optional=True, doc=doc_data_dict), Argument("fitting_weight", dict, optional=True, doc=doc_fitting_weight), - Argument("warmup_steps", int, optional=True, doc=doc_only_pt_supported), - Argument("gradient_max_norm", float, optional=True, doc=doc_only_pt_supported), - Argument("stat_file", str, optional=True, doc=doc_only_pt_supported), + Argument( + "warmup_steps", + int, + optional=True, + doc=doc_only_pt_supported + doc_warmup_steps, + ), + Argument( + "gradient_max_norm", + float, + optional=True, + doc=doc_only_pt_supported + doc_gradient_max_norm, + ), + Argument( + "stat_file", str, optional=True, doc=doc_only_pt_supported + doc_stat_file + ), ] variants = [ Variant( @@ -2149,7 +2231,7 @@ def training_args(): # ! modified by Ziyao: data configuration isolated. "kf_blocksize", int, optional=True, - doc=doc_only_pt_supported, + doc=doc_only_pt_supported + doc_kf_blocksize, ), ], [], @@ -2158,7 +2240,7 @@ def training_args(): # ! modified by Ziyao: data configuration isolated. ], optional=True, default_tag="Adam", - doc=doc_only_pt_supported, + doc=doc_only_pt_supported + doc_opt_type, ) ] From cbeb1d56255c6ca7ec39d98edaeff1c97f832c65 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Sun, 3 Mar 2024 00:35:32 +0800 Subject: [PATCH 170/686] add mask to atomic model output when an atomic type exclusion presents. (#3389) This PR also - introduce atomic_output_def used wraps the fitting_output_def. - atomic_output_def will be used by make_model - add missing ut for pair and atom exclusions in dpmodel See also #3357 --------- Co-authored-by: Han Wang --- .../dpmodel/atomic_model/base_atomic_model.py | 24 +++++++ .../atomic_model/make_base_atomic_model.py | 11 +++- deepmd/dpmodel/descriptor/se_e2_a.py | 12 +++- deepmd/dpmodel/fitting/general_fitting.py | 12 +++- deepmd/dpmodel/model/make_model.py | 4 +- .../model/atomic_model/base_atomic_model.py | 24 +++++++ deepmd/pt/model/model/make_hessian_model.py | 10 +-- deepmd/pt/model/model/make_model.py | 4 +- .../common/dpmodel/test_dp_atomic_model.py | 65 +++++++++++++++++++ source/tests/pt/model/test_dp_atomic_model.py | 27 ++++++++ .../tests/pt/model/test_make_hessian_model.py | 4 +- 11 files changed, 180 insertions(+), 17 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/base_atomic_model.py b/deepmd/dpmodel/atomic_model/base_atomic_model.py index 09d33203a1..b8c4902d68 100644 --- a/deepmd/dpmodel/atomic_model/base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/base_atomic_model.py @@ -8,6 +8,10 @@ import numpy as np +from deepmd.dpmodel.output_def import ( + FittingOutputDef, + OutputVariableDef, +) from deepmd.dpmodel.utils import ( AtomExcludeMask, PairExcludeMask, @@ -50,6 +54,25 @@ def reinit_pair_exclude( else: self.pair_excl = PairExcludeMask(self.get_ntypes(), self.pair_exclude_types) + def atomic_output_def(self) -> FittingOutputDef: + old_def = self.fitting_output_def() + if self.atom_excl is None: + return old_def + else: + old_list = list(old_def.get_data().values()) + return FittingOutputDef( + old_list # noqa:RUF005 + + [ + OutputVariableDef( + name="mask", + shape=[1], + reduciable=False, + r_differentiable=False, + c_differentiable=False, + ) + ] + ) + def forward_common_atomic( self, extended_coord: np.ndarray, @@ -79,6 +102,7 @@ def forward_common_atomic( atom_mask = self.atom_excl.build_type_exclude_mask(atype) for kk in ret_dict.keys(): ret_dict[kk] = ret_dict[kk] * atom_mask[:, :, None] + ret_dict["mask"] = atom_mask return ret_dict diff --git a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py index df6e39dd2e..5548147d54 100644 --- a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py @@ -36,9 +36,18 @@ class BAM(ABC): @abstractmethod def fitting_output_def(self) -> FittingOutputDef: - """Get the fitting output def.""" + """Get the output def of developer implemented atomic models.""" pass + def atomic_output_def(self) -> FittingOutputDef: + """Get the output def of the atomic model. + + By default it is the same as FittingOutputDef, but it + allows model level wrapper of the output defined by the developer. + + """ + return self.fitting_output_def() + @abstractmethod def get_rcut(self) -> float: """Get the cut-off radius.""" diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index 891f308edc..a068a2e366 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -26,6 +26,7 @@ Any, List, Optional, + Tuple, ) from deepmd.dpmodel import ( @@ -168,12 +169,12 @@ def __init__( self.resnet_dt = resnet_dt self.trainable = trainable self.type_one_side = type_one_side - self.exclude_types = exclude_types self.set_davg_zero = set_davg_zero self.activation_function = activation_function self.precision = precision self.spin = spin - self.emask = PairExcludeMask(self.ntypes, self.exclude_types) + # order matters, placed after the assignment of self.ntypes + self.reinit_exclude(exclude_types) in_dim = 1 # not considiering type embedding self.embeddings = NetworkCollection( @@ -271,6 +272,13 @@ def cal_g( gg = self.embeddings[embedding_idx].call(ss) return gg + def reinit_exclude( + self, + exclude_types: List[Tuple[int, int]] = [], + ): + self.exclude_types = exclude_types + self.emask = PairExcludeMask(self.ntypes, exclude_types=exclude_types) + def call( self, coord_ext, diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index 5b4ca195b5..c004814b60 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -120,13 +120,12 @@ def __init__( self.use_aparam_as_mask = use_aparam_as_mask self.spin = spin self.mixed_types = mixed_types - self.exclude_types = exclude_types + # order matters, should be place after the assignment of ntypes + self.reinit_exclude(exclude_types) if self.spin is not None: raise NotImplementedError("spin is not supported") self.remove_vaccum_contribution = remove_vaccum_contribution - self.emask = AtomExcludeMask(self.ntypes, self.exclude_types) - net_dim_out = self._net_out_dim() # init constants self.bias_atom_e = np.zeros([self.ntypes, net_dim_out]) @@ -214,6 +213,13 @@ def __getitem__(self, key): else: raise KeyError(key) + def reinit_exclude( + self, + exclude_types: List[int] = [], + ): + self.exclude_types = exclude_types + self.emask = AtomExcludeMask(self.ntypes, self.exclude_types) + def serialize(self) -> dict: """Serialize the fitting to dict.""" return { diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py index e8b1ecc390..f30f6a4021 100644 --- a/deepmd/dpmodel/model/make_model.py +++ b/deepmd/dpmodel/model/make_model.py @@ -74,7 +74,7 @@ def __init__( def model_output_def(self): """Get the output def for the model.""" - return ModelOutputDef(self.fitting_output_def()) + return ModelOutputDef(self.atomic_output_def()) def model_output_type(self) -> str: """Get the output type for the model.""" @@ -223,7 +223,7 @@ def call_lower( ) model_predict = fit_output_to_model_output( atomic_ret, - self.fitting_output_def(), + self.atomic_output_def(), cc_ext, do_atomic_virial=do_atomic_virial, ) diff --git a/deepmd/pt/model/atomic_model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py index f8b737b58e..8827e3f18b 100644 --- a/deepmd/pt/model/atomic_model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -13,6 +13,10 @@ from deepmd.dpmodel.atomic_model import ( make_base_atomic_model, ) +from deepmd.dpmodel.output_def import ( + FittingOutputDef, + OutputVariableDef, +) from deepmd.pt.utils import ( AtomExcludeMask, PairExcludeMask, @@ -60,6 +64,25 @@ def reinit_pair_exclude( def get_model_def_script(self) -> str: return self.model_def_script + def atomic_output_def(self) -> FittingOutputDef: + old_def = self.fitting_output_def() + if self.atom_excl is None: + return old_def + else: + old_list = list(old_def.get_data().values()) + return FittingOutputDef( + old_list # noqa:RUF005 + + [ + OutputVariableDef( + name="mask", + shape=[1], + reduciable=False, + r_differentiable=False, + c_differentiable=False, + ) + ] + ) + def forward_common_atomic( self, extended_coord: torch.Tensor, @@ -90,6 +113,7 @@ def forward_common_atomic( atom_mask = self.atom_excl(atype) for kk in ret_dict.keys(): ret_dict[kk] = ret_dict[kk] * atom_mask[:, :, None] + ret_dict["mask"] = atom_mask return ret_dict diff --git a/deepmd/pt/model/model/make_hessian_model.py b/deepmd/pt/model/model/make_hessian_model.py index 0ed14b1931..9588348f53 100644 --- a/deepmd/pt/model/model/make_hessian_model.py +++ b/deepmd/pt/model/model/make_hessian_model.py @@ -25,7 +25,7 @@ def make_hessian_model(T_Model): Parameters ---------- T_Model - The model. Should provide the `forward_common` and `fitting_output_def` methods + The model. Should provide the `forward_common` and `atomic_output_def` methods Returns ------- @@ -43,7 +43,7 @@ def __init__( *args, **kwargs, ) - self.hess_fitting_def = copy.deepcopy(super().fitting_output_def()) + self.hess_fitting_def = copy.deepcopy(super().atomic_output_def()) def requires_hessian( self, @@ -56,7 +56,7 @@ def requires_hessian( if kk in keys: self.hess_fitting_def[kk].r_hessian = True - def fitting_output_def(self): + def atomic_output_def(self): """Get the fitting output def.""" return self.hess_fitting_def @@ -102,7 +102,7 @@ def forward_common( aparam=aparam, do_atomic_virial=do_atomic_virial, ) - vdef = self.fitting_output_def() + vdef = self.atomic_output_def() hess_yes = [vdef[kk].r_hessian for kk in vdef.keys()] if any(hess_yes): hess = self._cal_hessian_all( @@ -128,7 +128,7 @@ def _cal_hessian_all( box = box.view([nf, 9]) if box is not None else None fparam = fparam.view([nf, -1]) if fparam is not None else None aparam = aparam.view([nf, nloc, -1]) if aparam is not None else None - fdef = self.fitting_output_def() + fdef = self.atomic_output_def() # keys of values that require hessian hess_keys: List[str] = [] for kk in fdef.keys(): diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 4f35acb60e..60b71400fb 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -72,7 +72,7 @@ def __init__( def model_output_def(self): """Get the output def for the model.""" - return ModelOutputDef(self.fitting_output_def()) + return ModelOutputDef(self.atomic_output_def()) @torch.jit.export def model_output_type(self) -> str: @@ -218,7 +218,7 @@ def forward_common_lower( ) model_predict = fit_output_to_model_output( atomic_ret, - self.fitting_output_def(), + self.atomic_output_def(), cc_ext, do_atomic_virial=do_atomic_virial, ) diff --git a/source/tests/common/dpmodel/test_dp_atomic_model.py b/source/tests/common/dpmodel/test_dp_atomic_model.py index f97299cf72..ac49280b82 100644 --- a/source/tests/common/dpmodel/test_dp_atomic_model.py +++ b/source/tests/common/dpmodel/test_dp_atomic_model.py @@ -50,3 +50,68 @@ def test_self_consistency( ret1 = md1.forward_common_atomic(self.coord_ext, self.atype_ext, self.nlist) np.testing.assert_allclose(ret0["energy"], ret1["energy"]) + + def test_excl_consistency(self): + type_map = ["foo", "bar"] + + # test the case of exclusion + for atom_excl, pair_excl in itertools.product([[], [1]], [[], [[0, 1]]]): + ds = DescrptSeA( + self.rcut, + self.rcut_smth, + self.sel, + ) + ft = InvarFitting( + "energy", + self.nt, + ds.get_dim_out(), + 1, + mixed_types=ds.mixed_types(), + ) + md0 = DPAtomicModel( + ds, + ft, + type_map=type_map, + ) + md1 = DPAtomicModel.deserialize(md0.serialize()) + + md0.reinit_atom_exclude(atom_excl) + md0.reinit_pair_exclude(pair_excl) + # hacking! + md1.descriptor.reinit_exclude(pair_excl) + md1.fitting.reinit_exclude(atom_excl) + + # check energy consistency + args = [self.coord_ext, self.atype_ext, self.nlist] + ret0 = md0.forward_common_atomic(*args) + ret1 = md1.forward_common_atomic(*args) + np.testing.assert_allclose( + ret0["energy"], + ret1["energy"], + ) + + # check output def + out_names = [vv.name for vv in md0.atomic_output_def().get_data().values()] + if atom_excl == []: + self.assertEqual(out_names, ["energy"]) + else: + self.assertEqual(out_names, ["energy", "mask"]) + for ii in md0.atomic_output_def().get_data().values(): + if ii.name == "mask": + self.assertEqual(ii.shape, [1]) + self.assertFalse(ii.reduciable) + self.assertFalse(ii.r_differentiable) + self.assertFalse(ii.c_differentiable) + + # check mask + if atom_excl == []: + pass + elif atom_excl == [1]: + self.assertIn("mask", ret0.keys()) + expected = np.array([1, 1, 0], dtype=int) + expected = np.concatenate( + [expected, expected[self.perm[: self.nloc]]] + ).reshape(2, 3) + np.testing.assert_array_equal(ret0["mask"], expected) + else: + raise ValueError(f"not expected atom_excl {atom_excl}") diff --git a/source/tests/pt/model/test_dp_atomic_model.py b/source/tests/pt/model/test_dp_atomic_model.py index 88bb3ab763..6daaeef2ef 100644 --- a/source/tests/pt/model/test_dp_atomic_model.py +++ b/source/tests/pt/model/test_dp_atomic_model.py @@ -152,6 +152,7 @@ def test_excl_consistency(self): md1.descriptor.reinit_exclude(pair_excl) md1.fitting_net.reinit_exclude(atom_excl) + # check energy consistency args = [ to_torch_tensor(ii) for ii in [self.coord_ext, self.atype_ext, self.nlist] @@ -162,3 +163,29 @@ def test_excl_consistency(self): to_numpy_array(ret0["energy"]), to_numpy_array(ret1["energy"]), ) + + # check output def + out_names = [vv.name for vv in md0.atomic_output_def().get_data().values()] + if atom_excl == []: + self.assertEqual(out_names, ["energy"]) + else: + self.assertEqual(out_names, ["energy", "mask"]) + for ii in md0.atomic_output_def().get_data().values(): + if ii.name == "mask": + self.assertEqual(ii.shape, [1]) + self.assertFalse(ii.reduciable) + self.assertFalse(ii.r_differentiable) + self.assertFalse(ii.c_differentiable) + + # check mask + if atom_excl == []: + pass + elif atom_excl == [1]: + self.assertIn("mask", ret0.keys()) + expected = np.array([1, 1, 0], dtype=int) + expected = np.concatenate( + [expected, expected[self.perm[: self.nloc]]] + ).reshape(2, 3) + np.testing.assert_array_equal(to_numpy_array(ret0["mask"]), expected) + else: + raise ValueError(f"not expected atom_excl {atom_excl}") diff --git a/source/tests/pt/model/test_make_hessian_model.py b/source/tests/pt/model/test_make_hessian_model.py index 1fb7e6f53a..7d9ae2b810 100644 --- a/source/tests/pt/model/test_make_hessian_model.py +++ b/source/tests/pt/model/test_make_hessian_model.py @@ -166,8 +166,8 @@ def setUp(self): self.model_hess.requires_hessian("energy") def test_output_def(self): - self.assertTrue(self.model_hess.fitting_output_def()["energy"].r_hessian) - self.assertFalse(self.model_valu.fitting_output_def()["energy"].r_hessian) + self.assertTrue(self.model_hess.atomic_output_def()["energy"].r_hessian) + self.assertFalse(self.model_valu.atomic_output_def()["energy"].r_hessian) self.assertTrue(self.model_hess.model_output_def()["energy"].r_hessian) self.assertEqual( self.model_hess.model_output_def()["energy_derv_r_derv_r"].category, From 59d3b12d60d2a5046c6ee476882ca5841863f5b5 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 2 Mar 2024 12:00:46 -0500 Subject: [PATCH 171/686] breaking: change DeepTensor output dim from nsel_atoms to natoms (#3390) Signed-off-by: Jinzhe Zeng --- deepmd/entrypoints/test.py | 8 +++++++ deepmd/infer/deep_tensor.py | 10 ++------- deepmd/tf/infer/deep_eval.py | 12 +++++++++++ source/tests/tf/test_data_modifier_shuffle.py | 6 ++++-- source/tests/tf/test_deepdipole.py | 21 +++++++++++++------ source/tests/tf/test_deeppolar.py | 18 ++++++++++------ 6 files changed, 53 insertions(+), 22 deletions(-) diff --git a/deepmd/entrypoints/test.py b/deepmd/entrypoints/test.py index b9421bb198..efc75e31a7 100644 --- a/deepmd/entrypoints/test.py +++ b/deepmd/entrypoints/test.py @@ -860,6 +860,10 @@ def test_polar( rmse_fs = rmse_f / np.sqrt(sel_natoms) rmse_fa = rmse_f / sel_natoms else: + sel_mask = np.isin(atype, sel_type) + polar = polar.reshape((polar.shape[0], -1, 9))[:, sel_mask, :].reshape( + (polar.shape[0], -1) + ) rmse_f = rmse(polar - test_data["atomic_polarizability"][:numb_test]) log.info(f"# number of test data : {numb_test:d} ") @@ -996,6 +1000,10 @@ def test_dipole( rmse_fs = rmse_f / np.sqrt(sel_natoms) rmse_fa = rmse_f / sel_natoms else: + sel_mask = np.isin(atype, sel_type) + dipole = dipole.reshape((dipole.shape[0], -1, 3))[:, sel_mask, :].reshape( + (dipole.shape[0], -1) + ) rmse_f = rmse(dipole - test_data["atomic_dipole"][:numb_test]) log.info(f"# number of test data : {numb_test:d}") diff --git a/deepmd/infer/deep_tensor.py b/deepmd/infer/deep_tensor.py index 1bdc459920..14e13e7f84 100644 --- a/deepmd/infer/deep_tensor.py +++ b/deepmd/infer/deep_tensor.py @@ -104,11 +104,8 @@ def eval( aparam=aparam, **kwargs, ) - sel_natoms = self._get_sel_natoms(atom_types[0]) - if sel_natoms == 0: - sel_natoms = atom_types.shape[-1] # set to natoms if atomic: - return results[self.output_tensor_name].reshape(nframes, sel_natoms, -1) + return results[self.output_tensor_name].reshape(nframes, natoms, -1) else: return results[f"{self.output_tensor_name}_redu"].reshape(nframes, -1) @@ -187,9 +184,6 @@ def eval_full( **kwargs, ) - sel_natoms = self._get_sel_natoms(atom_types[0]) - if sel_natoms == 0: - sel_natoms = atom_types.shape[-1] # set to natoms energy = results[f"{self.output_tensor_name}_redu"].reshape(nframes, -1) force = results[f"{self.output_tensor_name}_derv_r"].reshape( nframes, -1, natoms, 3 @@ -199,7 +193,7 @@ def eval_full( ) if atomic: atomic_energy = results[self.output_tensor_name].reshape( - nframes, sel_natoms, -1 + nframes, natoms, -1 ) atomic_virial = results[f"{self.output_tensor_name}_derv_c"].reshape( nframes, -1, natoms, 9 diff --git a/deepmd/tf/infer/deep_eval.py b/deepmd/tf/infer/deep_eval.py index 27152b9f12..45eda3392f 100644 --- a/deepmd/tf/infer/deep_eval.py +++ b/deepmd/tf/infer/deep_eval.py @@ -967,6 +967,18 @@ def _eval_inner( v_out[ii] = self.reverse_map( np.reshape(v_out[ii], odef_shape), sel_imap[:natoms_real] ) + if nloc_sel < nloc: + # convert shape from nsel to nloc + # sel_atoms was applied before sort; see sort_input + # do not consider mixed_types here (as it is never supported) + sel_mask = np.isin(atom_types[0], self.sel_type) + out_nsel = v_out[ii] + out_nloc = np.zeros( + (nframes, nloc, *out_nsel.shape[2:]), dtype=out_nsel.dtype + ) + out_nloc[:, sel_mask] = out_nsel + v_out[ii] = out_nloc + odef_shape = self._get_output_shape(odef, nframes, nloc) v_out[ii] = np.reshape(v_out[ii], odef_shape) elif odef.category in ( OutputVariableCategory.REDU, diff --git a/source/tests/tf/test_data_modifier_shuffle.py b/source/tests/tf/test_data_modifier_shuffle.py index fb9da948f1..41086cc775 100644 --- a/source/tests/tf/test_data_modifier_shuffle.py +++ b/source/tests/tf/test_data_modifier_shuffle.py @@ -127,6 +127,8 @@ def _setUp_data(self): self.coords1 = np.reshape(self.coords1, [self.nframes, self.natoms * 3]) self.dipoles1 = self.dipoles0[:, self.sel_idx_map] self.box1 = self.box0 + self.sel_mask0 = np.isin(self.atom_types0, self.sel_type) + self.sel_mask1 = np.isin(self.atom_types1, self.sel_type) def _write_sys_data(self, dirname, atom_types, coords, dipoles, box): os.makedirs(dirname, exist_ok=True) @@ -185,8 +187,8 @@ def _setUp_jdata(self): def test_z_dipole(self): dd = DeepDipole(os.path.join(modifier_datapath, "dipole.pb")) - dv0 = dd.eval(self.coords0, self.box0, self.atom_types0) - dv1 = dd.eval(self.coords1, self.box1, self.atom_types1) + dv0 = dd.eval(self.coords0, self.box0, self.atom_types0)[:, self.sel_mask0] + dv1 = dd.eval(self.coords1, self.box1, self.atom_types1)[:, self.sel_mask1] dv01 = dv0.reshape([self.nframes, -1, 3]) dv01 = dv01[:, self.sel_idx_map, :] diff --git a/source/tests/tf/test_deepdipole.py b/source/tests/tf/test_deepdipole.py index 2c8ec7cc66..1e2f6dd45a 100644 --- a/source/tests/tf/test_deepdipole.py +++ b/source/tests/tf/test_deepdipole.py @@ -72,6 +72,7 @@ def setUp(self): 1.667785136187720063e00, ] ) + self.sel_mask = np.isin(self.atype, self.dp.get_sel_type()) @classmethod def tearDownClass(cls): @@ -85,7 +86,7 @@ def test_attrs(self): self.assertEqual(self.dp.get_sel_type(), [0]) def test_1frame_atm(self): - dd = self.dp.eval(self.coords, self.box, self.atype) + dd = self.dp.eval(self.coords, self.box, self.atype)[:, self.sel_mask] # check shape of the returns nframes = 1 natoms = len(self.atype) @@ -97,7 +98,7 @@ def test_1frame_atm(self): def test_2frame_atm(self): coords2 = np.concatenate((self.coords, self.coords)) box2 = np.concatenate((self.box, self.box)) - dd = self.dp.eval(coords2, box2, self.atype) + dd = self.dp.eval(coords2, box2, self.atype)[:, self.sel_mask] # check shape of the returns nframes = 2 natoms = len(self.atype) @@ -151,6 +152,7 @@ def setUp(self): 1.667798310054391e00, ] ) + self.sel_mask = np.isin(self.atype, self.dp.get_sel_type()) @classmethod def tearDownClass(cls): @@ -158,7 +160,7 @@ def tearDownClass(cls): cls.dp = None def test_1frame_atm(self): - dd = self.dp.eval(self.coords, None, self.atype) + dd = self.dp.eval(self.coords, None, self.atype)[:, self.sel_mask] # check shape of the returns nframes = 1 natoms = len(self.atype) @@ -168,7 +170,7 @@ def test_1frame_atm(self): np.testing.assert_almost_equal(dd.ravel(), self.expected_d, default_places) def test_1frame_atm_large_box(self): - dd = self.dp.eval(self.coords, self.box, self.atype) + dd = self.dp.eval(self.coords, self.box, self.atype)[:, self.sel_mask] # check shape of the returns nframes = 1 natoms = len(self.atype) @@ -455,6 +457,7 @@ def setUp(self): self.expected_gv = ( self.expected_v.reshape(1, self.nout, 6, 9).sum(-2).reshape(-1) ) + self.sel_mask = np.isin(self.atype, self.dp.get_sel_type()) @classmethod def tearDownClass(cls): @@ -476,7 +479,7 @@ def test_1frame_old(self): np.testing.assert_almost_equal(gt.ravel(), self.expected_gt, default_places) def test_1frame_old_atm(self): - at = self.dp.eval(self.coords, self.box, self.atype) + at = self.dp.eval(self.coords, self.box, self.atype)[:, self.sel_mask] # check shape of the returns nframes = 1 natoms = len(self.atype) @@ -488,7 +491,7 @@ def test_1frame_old_atm(self): def test_2frame_old_atm(self): coords2 = np.concatenate((self.coords, self.coords)) box2 = np.concatenate((self.box, self.box)) - at = self.dp.eval(coords2, box2, self.atype) + at = self.dp.eval(coords2, box2, self.atype)[:, self.sel_mask] # check shape of the returns nframes = 2 natoms = len(self.atype) @@ -515,6 +518,7 @@ def test_1frame_full_atm(self): gt, ff, vv, at, av = self.dp.eval_full( self.coords, self.box, self.atype, atomic=True ) + at = at[:, self.sel_mask] # check shape of the returns nframes = 1 natoms = len(self.atype) @@ -550,6 +554,7 @@ def test_1frame_full_atm_shuffle(self): self.atype[i_sf], atomic=True, ) + at = at[:, self.sel_mask[i_sf]] # check shape of the returns nframes = 1 natoms = len(self.atype) @@ -617,6 +622,7 @@ def test_2frame_full_atm(self): coords2 = np.concatenate((self.coords, self.coords)) box2 = np.concatenate((self.box, self.box)) gt, ff, vv, at, av = self.dp.eval_full(coords2, box2, self.atype, atomic=True) + at = at[:, self.sel_mask] # check shape of the returns nframes = 2 natoms = len(self.atype) @@ -949,6 +955,7 @@ def setUp(self): ) fake_target = fake_target - 13 * np.rint(fake_target / 13) self.target_t = fake_target.reshape(-1) + self.sel_mask = np.isin(self.atype, self.dp.get_sel_type()) @classmethod def tearDownClass(cls): @@ -966,6 +973,7 @@ def test_1frame_full_atm(self): gt, ff, vv, at, av = self.dp.eval_full( self.coords, self.box, self.atype, atomic=True ) + at = at[:, self.sel_mask] # check shape of the returns nframes = 1 natoms = len(self.atype) @@ -1001,6 +1009,7 @@ def test_1frame_full_atm_shuffle(self): self.atype[i_sf], atomic=True, ) + at = at[:, self.sel_mask[i_sf]] # check shape of the returns nframes = 1 natoms = len(self.atype) diff --git a/source/tests/tf/test_deeppolar.py b/source/tests/tf/test_deeppolar.py index cfa115c59f..b4f3fe7d0d 100644 --- a/source/tests/tf/test_deeppolar.py +++ b/source/tests/tf/test_deeppolar.py @@ -82,6 +82,7 @@ def setUp(self): 4.448255365635306879e-01, ] ) + self.sel_mask = np.isin(self.atype, self.dp.get_sel_type()) @classmethod def tearDownClass(cls): @@ -95,7 +96,7 @@ def test_attrs(self): self.assertEqual(self.dp.get_sel_type(), [0]) def test_1frame_atm(self): - dd = self.dp.eval(self.coords, self.box, self.atype) + dd = self.dp.eval(self.coords, self.box, self.atype)[:, self.sel_mask] # check shape of the returns nframes = 1 natoms = len(self.atype) @@ -107,7 +108,7 @@ def test_1frame_atm(self): def test_2frame_atm(self): coords2 = np.concatenate((self.coords, self.coords)) box2 = np.concatenate((self.box, self.box)) - dd = self.dp.eval(coords2, box2, self.atype) + dd = self.dp.eval(coords2, box2, self.atype)[:, self.sel_mask] # check shape of the returns nframes = 2 natoms = len(self.atype) @@ -173,6 +174,7 @@ def setUp(self): 4.382376148484938e-01, ] ) + self.sel_mask = np.isin(self.atype, self.dp.get_sel_type()) @classmethod def tearDownClass(cls): @@ -180,7 +182,7 @@ def tearDownClass(cls): cls.dp = None def test_1frame_atm(self): - dd = self.dp.eval(self.coords, None, self.atype) + dd = self.dp.eval(self.coords, None, self.atype)[:, self.sel_mask] # check shape of the returns nframes = 1 natoms = len(self.atype) @@ -190,7 +192,7 @@ def test_1frame_atm(self): np.testing.assert_almost_equal(dd.ravel(), self.expected_d, default_places) def test_1frame_atm_large_box(self): - dd = self.dp.eval(self.coords, self.box, self.atype) + dd = self.dp.eval(self.coords, self.box, self.atype)[:, self.sel_mask] # check shape of the returns nframes = 1 natoms = len(self.atype) @@ -921,6 +923,7 @@ def setUp(self): self.expected_gv = ( self.expected_v.reshape(1, self.nout, 6, 9).sum(-2).reshape(-1) ) + self.sel_mask = np.isin(self.atype, self.dp.get_sel_type()) @classmethod def tearDownClass(cls): @@ -942,7 +945,7 @@ def test_1frame_old(self): np.testing.assert_almost_equal(gt.ravel(), self.expected_gt, default_places) def test_1frame_old_atm(self): - at = self.dp.eval(self.coords, self.box, self.atype) + at = self.dp.eval(self.coords, self.box, self.atype)[:, self.sel_mask] # check shape of the returns nframes = 1 natoms = len(self.atype) @@ -954,7 +957,7 @@ def test_1frame_old_atm(self): def test_2frame_old_atm(self): coords2 = np.concatenate((self.coords, self.coords)) box2 = np.concatenate((self.box, self.box)) - at = self.dp.eval(coords2, box2, self.atype) + at = self.dp.eval(coords2, box2, self.atype)[:, self.sel_mask] # check shape of the returns nframes = 2 natoms = len(self.atype) @@ -981,6 +984,7 @@ def test_1frame_full_atm(self): gt, ff, vv, at, av = self.dp.eval_full( self.coords, self.box, self.atype, atomic=True ) + at = at[:, self.sel_mask] # check shape of the returns nframes = 1 @@ -1017,6 +1021,7 @@ def test_1frame_full_atm_shuffle(self): self.atype[i_sf], atomic=True, ) + at = at[:, self.sel_mask[i_sf]] # check shape of the returns nframes = 1 natoms = len(self.atype) @@ -1054,6 +1059,7 @@ def test_2frame_full_atm(self): coords2 = np.concatenate((self.coords, self.coords)) box2 = np.concatenate((self.box, self.box)) gt, ff, vv, at, av = self.dp.eval_full(coords2, box2, self.atype, atomic=True) + at = at[:, self.sel_mask] # check shape of the returns nframes = 2 natoms = len(self.atype) From 8d0e3ba7791d080b60ab25efb283370180600749 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sat, 2 Mar 2024 19:51:01 -0500 Subject: [PATCH 172/686] pt: ban `torch.testing.assert_allclose` (#3395) See https://github.com/pytorch/pytorch/issues/61844 Signed-off-by: Jinzhe Zeng --- pyproject.toml | 4 ++++ source/tests/pt/test_multitask.py | 2 +- source/tests/pt/test_training.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b1d110ff0a..36851b1401 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -237,6 +237,7 @@ select = [ "C4", # flake8-comprehensions "RUF", # ruff "NPY", # numpy + "TID251", # banned-api "TID253", # banned-module-level-imports ] @@ -272,6 +273,9 @@ banned-module-level-imports = [ "torch", ] +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"torch.testing.assert_allclose".msg = "Use `torch.testing.assert_close()` instead, see https://github.com/pytorch/pytorch/issues/61844." + [tool.ruff.lint.extend-per-file-ignores] # Also ignore `E402` in all `__init__.py` files. "deepmd/tf/**" = ["TID253"] diff --git a/source/tests/pt/test_multitask.py b/source/tests/pt/test_multitask.py index 3c0240dbdc..d06733b016 100644 --- a/source/tests/pt/test_multitask.py +++ b/source/tests/pt/test_multitask.py @@ -47,7 +47,7 @@ def test_multitask_train(self): if "model_2" in state_key: self.assertIn(state_key.replace("model_2", "model_1"), multi_state_dict) if "model_1.descriptor" in state_key: - torch.testing.assert_allclose( + torch.testing.assert_close( multi_state_dict[state_key], multi_state_dict[state_key.replace("model_1", "model_2")], ) diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index b9a42385dc..13e47a953b 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -39,7 +39,7 @@ def test_trainable(self): trainer_fix.run() model_dict_after_training = deepcopy(trainer_fix.model.state_dict()) for key in model_dict_before_training: - torch.testing.assert_allclose( + torch.testing.assert_close( model_dict_before_training[key], model_dict_after_training[key] ) self.tearDown() From d4ac8648f8fb4b05a2c2d6deea61a534559abeb2 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Sun, 3 Mar 2024 10:21:56 +0800 Subject: [PATCH 173/686] pt: Add support for dipole and polar training (#3380) Signed-off-by: Duo <50307526+iProzd@users.noreply.github.com> --- .../dpmodel/atomic_model/base_atomic_model.py | 6 +- deepmd/dpmodel/infer/deep_eval.py | 12 +- deepmd/dpmodel/model/base_model.py | 2 +- deepmd/dpmodel/model/make_model.py | 9 +- deepmd/dpmodel/output_def.py | 5 + deepmd/pt/infer/deep_eval.py | 14 +- deepmd/pt/loss/__init__.py | 4 + deepmd/pt/loss/tensor.py | 162 ++++++++++++++ .../model/atomic_model/base_atomic_model.py | 6 +- deepmd/pt/model/model/__init__.py | 2 +- deepmd/pt/model/model/dipole_model.py | 2 + deepmd/pt/model/model/dp_zbl_model.py | 2 + deepmd/pt/model/model/ener_model.py | 2 + deepmd/pt/model/model/make_model.py | 9 +- deepmd/pt/model/model/polar_model.py | 2 + deepmd/pt/model/task/dipole.py | 2 +- deepmd/pt/model/task/ener.py | 8 +- deepmd/pt/model/task/polarizability.py | 2 +- deepmd/pt/train/training.py | 73 ++++-- .../dipole/dipole_input_torch.json | 84 +++++++ .../atomic_system/nopbc | 0 .../atomic_system/set.000/atomic_dipole.npy | Bin 0 -> 184448 bytes .../atomic_system/set.000/box.npy | Bin 0 -> 3008 bytes .../atomic_system/set.000/coord.npy | Bin 0 -> 184448 bytes .../atomic_system/type.raw | 1 + .../atomic_system/type_map.raw | 2 + .../global_system/nopbc | 0 .../global_system/set.000/box.npy | Bin 0 -> 3008 bytes .../global_system/set.000/coord.npy | Bin 0 -> 184448 bytes .../global_system/set.000/dipole.npy | Bin 0 -> 1088 bytes .../global_system/type.raw | 1 + .../global_system/type_map.raw | 2 + .../atomic_system/nopbc | 0 .../atomic_system/set.000/atomic_dipole.npy | Bin 0 -> 184448 bytes .../atomic_system/set.000/box.npy | Bin 0 -> 3008 bytes .../atomic_system/set.000/coord.npy | Bin 0 -> 184448 bytes .../atomic_system/type.raw | 1 + .../atomic_system/type_map.raw | 2 + .../global_system/nopbc | 0 .../global_system/set.000/box.npy | Bin 0 -> 3008 bytes .../global_system/set.000/coord.npy | Bin 0 -> 184448 bytes .../global_system/set.000/dipole.npy | Bin 0 -> 1088 bytes .../global_system/type.raw | 1 + .../global_system/type_map.raw | 2 + .../water_tensor/polar/polar_input_torch.json | 90 ++++++++ .../set.000/atomic_polarizability.npy | Bin 0 -> 829568 bytes .../atomic_system/set.000/box.npy | Bin 0 -> 2288 bytes .../atomic_system/set.000/coord.npy | Bin 0 -> 138368 bytes .../atomic_system/type.raw | 1 + .../atomic_system/type_map.raw | 2 + .../global_system/set.000/box.npy | Bin 0 -> 3008 bytes .../global_system/set.000/coord.npy | Bin 0 -> 184448 bytes .../global_system/set.000/polarizability.npy | Bin 0 -> 3008 bytes .../global_system/type.raw | 1 + .../global_system/type_map.raw | 2 + .../set.000/atomic_polarizability.npy | Bin 0 -> 829568 bytes .../atomic_system/set.000/box.npy | Bin 0 -> 2288 bytes .../atomic_system/set.000/coord.npy | Bin 0 -> 138368 bytes .../atomic_system/type.raw | 1 + .../atomic_system/type_map.raw | 2 + .../global_system/set.000/box.npy | Bin 0 -> 3008 bytes .../global_system/set.000/coord.npy | Bin 0 -> 184448 bytes .../global_system/set.000/polarizability.npy | Bin 0 -> 3008 bytes .../global_system/type.raw | 1 + .../global_system/type_map.raw | 2 + source/tests/common/test_examples.py | 2 + source/tests/pt/test_training.py | 209 +++++++++++++++++- .../water_tensor/dipole/atomic_system/nopbc | 0 .../atomic_system/set.000/atomic_dipole.npy | Bin 0 -> 184448 bytes .../dipole/atomic_system/set.000/box.npy | Bin 0 -> 3008 bytes .../dipole/atomic_system/set.000/coord.npy | Bin 0 -> 184448 bytes .../dipole/atomic_system/type.raw | 1 + .../dipole/atomic_system/type_map.raw | 2 + .../water_tensor/dipole/global_system/nopbc | 0 .../dipole/global_system/set.000/box.npy | Bin 0 -> 3008 bytes .../dipole/global_system/set.000/coord.npy | Bin 0 -> 184448 bytes .../dipole/global_system/set.000/dipole.npy | Bin 0 -> 1088 bytes .../dipole/global_system/type.raw | 1 + .../dipole/global_system/type_map.raw | 2 + .../set.000/atomic_polarizability.npy | Bin 0 -> 829568 bytes .../polar/atomic_system/set.000/box.npy | Bin 0 -> 2288 bytes .../polar/atomic_system/set.000/coord.npy | Bin 0 -> 138368 bytes .../water_tensor/polar/atomic_system/type.raw | 1 + .../polar/atomic_system/type_map.raw | 2 + .../polar/global_system/set.000/box.npy | Bin 0 -> 3008 bytes .../polar/global_system/set.000/coord.npy | Bin 0 -> 184448 bytes .../global_system/set.000/polarizability.npy | Bin 0 -> 3008 bytes .../water_tensor/polar/global_system/type.raw | 1 + .../polar/global_system/type_map.raw | 2 + source/tests/pt/water_tensor/se_e2_a.json | 85 +++++++ 90 files changed, 771 insertions(+), 59 deletions(-) create mode 100644 deepmd/pt/loss/tensor.py create mode 100644 examples/water_tensor/dipole/dipole_input_torch.json create mode 100644 examples/water_tensor/dipole/training_data_reformat/atomic_system/nopbc create mode 100644 examples/water_tensor/dipole/training_data_reformat/atomic_system/set.000/atomic_dipole.npy create mode 100644 examples/water_tensor/dipole/training_data_reformat/atomic_system/set.000/box.npy create mode 100644 examples/water_tensor/dipole/training_data_reformat/atomic_system/set.000/coord.npy create mode 100644 examples/water_tensor/dipole/training_data_reformat/atomic_system/type.raw create mode 100644 examples/water_tensor/dipole/training_data_reformat/atomic_system/type_map.raw create mode 100644 examples/water_tensor/dipole/training_data_reformat/global_system/nopbc create mode 100644 examples/water_tensor/dipole/training_data_reformat/global_system/set.000/box.npy create mode 100644 examples/water_tensor/dipole/training_data_reformat/global_system/set.000/coord.npy create mode 100644 examples/water_tensor/dipole/training_data_reformat/global_system/set.000/dipole.npy create mode 100644 examples/water_tensor/dipole/training_data_reformat/global_system/type.raw create mode 100644 examples/water_tensor/dipole/training_data_reformat/global_system/type_map.raw create mode 100644 examples/water_tensor/dipole/validation_data_reformat/atomic_system/nopbc create mode 100644 examples/water_tensor/dipole/validation_data_reformat/atomic_system/set.000/atomic_dipole.npy create mode 100644 examples/water_tensor/dipole/validation_data_reformat/atomic_system/set.000/box.npy create mode 100644 examples/water_tensor/dipole/validation_data_reformat/atomic_system/set.000/coord.npy create mode 100644 examples/water_tensor/dipole/validation_data_reformat/atomic_system/type.raw create mode 100644 examples/water_tensor/dipole/validation_data_reformat/atomic_system/type_map.raw create mode 100644 examples/water_tensor/dipole/validation_data_reformat/global_system/nopbc create mode 100644 examples/water_tensor/dipole/validation_data_reformat/global_system/set.000/box.npy create mode 100644 examples/water_tensor/dipole/validation_data_reformat/global_system/set.000/coord.npy create mode 100644 examples/water_tensor/dipole/validation_data_reformat/global_system/set.000/dipole.npy create mode 100644 examples/water_tensor/dipole/validation_data_reformat/global_system/type.raw create mode 100644 examples/water_tensor/dipole/validation_data_reformat/global_system/type_map.raw create mode 100644 examples/water_tensor/polar/polar_input_torch.json create mode 100644 examples/water_tensor/polar/training_data_reformat/atomic_system/set.000/atomic_polarizability.npy create mode 100644 examples/water_tensor/polar/training_data_reformat/atomic_system/set.000/box.npy create mode 100644 examples/water_tensor/polar/training_data_reformat/atomic_system/set.000/coord.npy create mode 100644 examples/water_tensor/polar/training_data_reformat/atomic_system/type.raw create mode 100644 examples/water_tensor/polar/training_data_reformat/atomic_system/type_map.raw create mode 100644 examples/water_tensor/polar/training_data_reformat/global_system/set.000/box.npy create mode 100644 examples/water_tensor/polar/training_data_reformat/global_system/set.000/coord.npy create mode 100644 examples/water_tensor/polar/training_data_reformat/global_system/set.000/polarizability.npy create mode 100644 examples/water_tensor/polar/training_data_reformat/global_system/type.raw create mode 100644 examples/water_tensor/polar/training_data_reformat/global_system/type_map.raw create mode 100644 examples/water_tensor/polar/validation_data_reformat/atomic_system/set.000/atomic_polarizability.npy create mode 100644 examples/water_tensor/polar/validation_data_reformat/atomic_system/set.000/box.npy create mode 100644 examples/water_tensor/polar/validation_data_reformat/atomic_system/set.000/coord.npy create mode 100644 examples/water_tensor/polar/validation_data_reformat/atomic_system/type.raw create mode 100644 examples/water_tensor/polar/validation_data_reformat/atomic_system/type_map.raw create mode 100644 examples/water_tensor/polar/validation_data_reformat/global_system/set.000/box.npy create mode 100644 examples/water_tensor/polar/validation_data_reformat/global_system/set.000/coord.npy create mode 100644 examples/water_tensor/polar/validation_data_reformat/global_system/set.000/polarizability.npy create mode 100644 examples/water_tensor/polar/validation_data_reformat/global_system/type.raw create mode 100644 examples/water_tensor/polar/validation_data_reformat/global_system/type_map.raw create mode 100644 source/tests/pt/water_tensor/dipole/atomic_system/nopbc create mode 100644 source/tests/pt/water_tensor/dipole/atomic_system/set.000/atomic_dipole.npy create mode 100644 source/tests/pt/water_tensor/dipole/atomic_system/set.000/box.npy create mode 100644 source/tests/pt/water_tensor/dipole/atomic_system/set.000/coord.npy create mode 100644 source/tests/pt/water_tensor/dipole/atomic_system/type.raw create mode 100644 source/tests/pt/water_tensor/dipole/atomic_system/type_map.raw create mode 100644 source/tests/pt/water_tensor/dipole/global_system/nopbc create mode 100644 source/tests/pt/water_tensor/dipole/global_system/set.000/box.npy create mode 100644 source/tests/pt/water_tensor/dipole/global_system/set.000/coord.npy create mode 100644 source/tests/pt/water_tensor/dipole/global_system/set.000/dipole.npy create mode 100644 source/tests/pt/water_tensor/dipole/global_system/type.raw create mode 100644 source/tests/pt/water_tensor/dipole/global_system/type_map.raw create mode 100644 source/tests/pt/water_tensor/polar/atomic_system/set.000/atomic_polarizability.npy create mode 100644 source/tests/pt/water_tensor/polar/atomic_system/set.000/box.npy create mode 100644 source/tests/pt/water_tensor/polar/atomic_system/set.000/coord.npy create mode 100644 source/tests/pt/water_tensor/polar/atomic_system/type.raw create mode 100644 source/tests/pt/water_tensor/polar/atomic_system/type_map.raw create mode 100644 source/tests/pt/water_tensor/polar/global_system/set.000/box.npy create mode 100644 source/tests/pt/water_tensor/polar/global_system/set.000/coord.npy create mode 100644 source/tests/pt/water_tensor/polar/global_system/set.000/polarizability.npy create mode 100644 source/tests/pt/water_tensor/polar/global_system/type.raw create mode 100644 source/tests/pt/water_tensor/polar/global_system/type_map.raw create mode 100644 source/tests/pt/water_tensor/se_e2_a.json diff --git a/deepmd/dpmodel/atomic_model/base_atomic_model.py b/deepmd/dpmodel/atomic_model/base_atomic_model.py index b8c4902d68..990847c1de 100644 --- a/deepmd/dpmodel/atomic_model/base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/base_atomic_model.py @@ -101,7 +101,11 @@ def forward_common_atomic( if self.atom_excl is not None: atom_mask = self.atom_excl.build_type_exclude_mask(atype) for kk in ret_dict.keys(): - ret_dict[kk] = ret_dict[kk] * atom_mask[:, :, None] + out_shape = ret_dict[kk].shape + ret_dict[kk] = ( + ret_dict[kk].reshape([out_shape[0], out_shape[1], -1]) + * atom_mask[:, :, None] + ).reshape(out_shape) ret_dict["mask"] = atom_mask return ret_dict diff --git a/deepmd/dpmodel/infer/deep_eval.py b/deepmd/dpmodel/infer/deep_eval.py index 1fd36bd7e8..22267c895a 100644 --- a/deepmd/dpmodel/infer/deep_eval.py +++ b/deepmd/dpmodel/infer/deep_eval.py @@ -123,16 +123,16 @@ def get_dim_aparam(self) -> int: @property def model_type(self) -> Type["DeepEvalWrapper"]: """The the evaluator of the model type.""" - model_type = self.dp.model_output_type() - if model_type == "energy": + model_output_type = self.dp.model_output_type() + if "energy" in model_output_type: return DeepPot - elif model_type == "dos": + elif "dos" in model_output_type: return DeepDOS - elif model_type == "dipole": + elif "dipole" in model_output_type: return DeepDipole - elif model_type == "polar": + elif "polar" in model_output_type: return DeepPolar - elif model_type == "wfc": + elif "wfc" in model_output_type: return DeepWFC else: raise RuntimeError("Unknown model type") diff --git a/deepmd/dpmodel/model/base_model.py b/deepmd/dpmodel/model/base_model.py index e7cc8d9272..95c448442e 100644 --- a/deepmd/dpmodel/model/base_model.py +++ b/deepmd/dpmodel/model/base_model.py @@ -89,7 +89,7 @@ def is_aparam_nall(self) -> bool: """ @abstractmethod - def model_output_type(self) -> str: + def model_output_type(self) -> List[str]: """Get the output type for the model.""" @abstractmethod diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py index f30f6a4021..d1f671c8de 100644 --- a/deepmd/dpmodel/model/make_model.py +++ b/deepmd/dpmodel/model/make_model.py @@ -76,7 +76,7 @@ def model_output_def(self): """Get the output def for the model.""" return ModelOutputDef(self.atomic_output_def()) - def model_output_type(self) -> str: + def model_output_type(self) -> List[str]: """Get the output type for the model.""" output_def = self.model_output_def() var_defs = output_def.var_defs @@ -85,12 +85,7 @@ def model_output_type(self) -> str: for kk, vv in var_defs.items() if vv.category == OutputVariableCategory.OUT ] - if len(vars) == 1: - return vars[0] - elif len(vars) == 0: - raise ValueError("No valid output type found") - else: - raise ValueError(f"Multiple valid output types found: {vars}") + return vars def call( self, diff --git a/deepmd/dpmodel/output_def.py b/deepmd/dpmodel/output_def.py index d816ed4e84..ac41513246 100644 --- a/deepmd/dpmodel/output_def.py +++ b/deepmd/dpmodel/output_def.py @@ -193,6 +193,11 @@ def __init__( ): self.name = name self.shape = list(shape) + # jit doesn't support math.prod(self.shape) + self.output_size = 1 + len_shape = len(self.shape) + for i in range(len_shape): + self.output_size *= self.shape[i] self.atomic = atomic self.reduciable = reduciable self.r_differentiable = r_differentiable diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index f75052166b..bf6a5b0306 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -148,18 +148,18 @@ def get_dim_aparam(self) -> int: @property def model_type(self) -> "DeepEvalWrapper": """The the evaluator of the model type.""" - model_type = self.dp.model["Default"].model_output_type() - if model_type == "energy": + model_output_type = self.dp.model["Default"].model_output_type() + if "energy" in model_output_type: return DeepPot - elif model_type == "dos": + elif "dos" in model_output_type: return DeepDOS - elif model_type == "dipole": + elif "dipole" in model_output_type: return DeepDipole - elif model_type == "polar": + elif "polar" in model_output_type: return DeepPolar - elif model_type == "global_polar": + elif "global_polar" in model_output_type: return DeepGlobalPolar - elif model_type == "wfc": + elif "wfc" in model_output_type: return DeepWFC else: raise RuntimeError("Unknown model type") diff --git a/deepmd/pt/loss/__init__.py b/deepmd/pt/loss/__init__.py index d3a095ce13..d2f6ab9e52 100644 --- a/deepmd/pt/loss/__init__.py +++ b/deepmd/pt/loss/__init__.py @@ -8,9 +8,13 @@ from .loss import ( TaskLoss, ) +from .tensor import ( + TensorLoss, +) __all__ = [ "DenoiseLoss", "EnergyStdLoss", + "TensorLoss", "TaskLoss", ] diff --git a/deepmd/pt/loss/tensor.py b/deepmd/pt/loss/tensor.py new file mode 100644 index 0000000000..ee42536557 --- /dev/null +++ b/deepmd/pt/loss/tensor.py @@ -0,0 +1,162 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + List, +) + +import torch + +from deepmd.pt.loss.loss import ( + TaskLoss, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.utils.data import ( + DataRequirementItem, +) + + +class TensorLoss(TaskLoss): + def __init__( + self, + tensor_name: str, + tensor_size: int, + label_name: str, + pref_atomic: float = 0.0, + pref: float = 0.0, + inference=False, + **kwargs, + ): + r"""Construct a loss for local and global tensors. + + Parameters + ---------- + tensor_name : str + The name of the tensor in the model predictions to compute the loss. + tensor_size : int + The size (dimension) of the tensor. + label_name : str + The name of the tensor in the labels to compute the loss. + pref_atomic : float + The prefactor of the weight of atomic loss. It should be larger than or equal to 0. + pref : float + The prefactor of the weight of global loss. It should be larger than or equal to 0. + inference : bool + If true, it will output all losses found in output, ignoring the pre-factors. + **kwargs + Other keyword arguments. + """ + super().__init__() + self.tensor_name = tensor_name + self.tensor_size = tensor_size + self.label_name = label_name + self.local_weight = pref_atomic + self.global_weight = pref + self.inference = inference + + assert ( + self.local_weight >= 0.0 and self.global_weight >= 0.0 + ), "Can not assign negative weight to `pref` and `pref_atomic`" + self.has_local_weight = self.local_weight > 0.0 or inference + self.has_global_weight = self.global_weight > 0.0 or inference + assert self.has_local_weight or self.has_global_weight, AssertionError( + "Can not assian zero weight both to `pref` and `pref_atomic`" + ) + + def forward(self, model_pred, label, natoms, learning_rate=0.0, mae=False): + """Return loss on local and global tensors. + + Parameters + ---------- + model_pred : dict[str, torch.Tensor] + Model predictions. + label : dict[str, torch.Tensor] + Labels. + natoms : int + The local atom number. + + Returns + ------- + loss: torch.Tensor + Loss for model to minimize. + more_loss: dict[str, torch.Tensor] + Other losses for display. + """ + del learning_rate, mae + loss = torch.tensor(0.0, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE) + more_loss = {} + if ( + self.has_local_weight + and self.tensor_name in model_pred + and "atomic_" + self.label_name in label + ): + local_tensor_pred = model_pred[self.tensor_name].reshape( + [-1, natoms, self.tensor_size] + ) + local_tensor_label = label["atomic_" + self.label_name].reshape( + [-1, natoms, self.tensor_size] + ) + diff = (local_tensor_pred - local_tensor_label).reshape( + [-1, self.tensor_size] + ) + if "mask" in model_pred: + diff = diff[model_pred["mask"].reshape([-1]).bool()] + l2_local_loss = torch.mean(torch.square(diff)) + if not self.inference: + more_loss[f"l2_local_{self.tensor_name}_loss"] = l2_local_loss.detach() + loss += self.local_weight * l2_local_loss + rmse_local = l2_local_loss.sqrt() + more_loss[f"rmse_local_{self.tensor_name}"] = rmse_local.detach() + if ( + self.has_global_weight + and "global_" + self.tensor_name in model_pred + and self.label_name in label + ): + global_tensor_pred = model_pred["global_" + self.tensor_name].reshape( + [-1, self.tensor_size] + ) + global_tensor_label = label[self.label_name].reshape([-1, self.tensor_size]) + diff = global_tensor_pred - global_tensor_label + if "mask" in model_pred: + atom_num = model_pred["mask"].sum(-1, keepdim=True) + l2_global_loss = torch.mean( + torch.sum(torch.square(diff) * atom_num, dim=0) / atom_num.sum() + ) + atom_num = torch.mean(atom_num.float()) + else: + atom_num = natoms + l2_global_loss = torch.mean(torch.square(diff)) + if not self.inference: + more_loss[ + f"l2_global_{self.tensor_name}_loss" + ] = l2_global_loss.detach() + loss += self.global_weight * l2_global_loss + rmse_global = l2_global_loss.sqrt() / atom_num + more_loss[f"rmse_global_{self.tensor_name}"] = rmse_global.detach() + return loss, more_loss + + @property + def label_requirement(self) -> List[DataRequirementItem]: + """Return data label requirements needed for this loss calculation.""" + label_requirement = [] + if self.has_local_weight: + label_requirement.append( + DataRequirementItem( + "atomic_" + self.label_name, + ndof=self.tensor_size, + atomic=True, + must=False, + high_prec=False, + ) + ) + if self.has_global_weight: + label_requirement.append( + DataRequirementItem( + self.label_name, + ndof=self.tensor_size, + atomic=False, + must=False, + high_prec=False, + ) + ) + return label_requirement diff --git a/deepmd/pt/model/atomic_model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py index 8827e3f18b..8180c48c81 100644 --- a/deepmd/pt/model/atomic_model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -112,7 +112,11 @@ def forward_common_atomic( if self.atom_excl is not None: atom_mask = self.atom_excl(atype) for kk in ret_dict.keys(): - ret_dict[kk] = ret_dict[kk] * atom_mask[:, :, None] + out_shape = ret_dict[kk].shape + ret_dict[kk] = ( + ret_dict[kk].reshape([out_shape[0], out_shape[1], -1]) + * atom_mask[:, :, None] + ).reshape(out_shape) ret_dict["mask"] = atom_mask return ret_dict diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index 87eb391a7e..bd354af8d8 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -94,7 +94,7 @@ def get_model(model_params): fitting_net["type"] = fitting_net.get("type", "ener") fitting_net["ntypes"] = descriptor.get_ntypes() fitting_net["mixed_types"] = descriptor.mixed_types() - fitting_net["embedding_width"] = descriptor.get_dim_out() + fitting_net["embedding_width"] = descriptor.get_dim_emb() fitting_net["dim_descrpt"] = descriptor.get_dim_out() grad_force = "direct" not in fitting_net["type"] if not grad_force: diff --git a/deepmd/pt/model/model/dipole_model.py b/deepmd/pt/model/model/dipole_model.py index 6629541459..8b6f2c47c1 100644 --- a/deepmd/pt/model/model/dipole_model.py +++ b/deepmd/pt/model/model/dipole_model.py @@ -50,6 +50,8 @@ def forward( model_predict["atom_virial"] = model_ret["dipole_derv_c"].squeeze( -3 ) + if "mask" in model_ret: + model_predict["mask"] = model_ret["mask"] else: model_predict = model_ret model_predict["updated_coord"] += coord diff --git a/deepmd/pt/model/model/dp_zbl_model.py b/deepmd/pt/model/model/dp_zbl_model.py index f2af0fff52..dcf1c36e83 100644 --- a/deepmd/pt/model/model/dp_zbl_model.py +++ b/deepmd/pt/model/model/dp_zbl_model.py @@ -63,6 +63,8 @@ def forward( model_predict["atom_virial"] = model_ret["energy_derv_c"].squeeze(-3) else: model_predict["force"] = model_ret["dforce"] + if "mask" in model_ret: + model_predict["mask"] = model_ret["mask"] return model_predict @torch.jit.export diff --git a/deepmd/pt/model/model/ener_model.py b/deepmd/pt/model/model/ener_model.py index 1a5706dbbf..cd4f78a2e2 100644 --- a/deepmd/pt/model/model/ener_model.py +++ b/deepmd/pt/model/model/ener_model.py @@ -52,6 +52,8 @@ def forward( ) else: model_predict["force"] = model_ret["dforce"] + if "mask" in model_ret: + model_predict["mask"] = model_ret["mask"] else: model_predict = model_ret model_predict["updated_coord"] += coord diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 60b71400fb..f9daa916a8 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -75,7 +75,7 @@ def model_output_def(self): return ModelOutputDef(self.atomic_output_def()) @torch.jit.export - def model_output_type(self) -> str: + def model_output_type(self) -> List[str]: """Get the output type for the model.""" output_def = self.model_output_def() var_defs = output_def.var_defs @@ -86,12 +86,7 @@ def model_output_type(self) -> str: # .value is critical for JIT if vv.category == OutputVariableCategory.OUT.value: vars.append(kk) - if len(vars) == 1: - return vars[0] - elif len(vars) == 0: - raise ValueError("No valid output type found") - else: - raise ValueError(f"Multiple valid output types found: {vars}") + return vars # cannot use the name forward. torch script does not work def forward_common( diff --git a/deepmd/pt/model/model/polar_model.py b/deepmd/pt/model/model/polar_model.py index d956a0344c..bf430c6706 100644 --- a/deepmd/pt/model/model/polar_model.py +++ b/deepmd/pt/model/model/polar_model.py @@ -42,6 +42,8 @@ def forward( model_predict = {} model_predict["polar"] = model_ret["polar"] model_predict["global_polar"] = model_ret["polar_redu"] + if "mask" in model_ret: + model_predict["mask"] = model_ret["mask"] else: model_predict = model_ret model_predict["updated_coord"] += coord diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py index 7d2dd221db..21372888d6 100644 --- a/deepmd/pt/model/task/dipole.py +++ b/deepmd/pt/model/task/dipole.py @@ -157,7 +157,7 @@ def compute_output_stats( The path to the stat file. """ - raise NotImplementedError + pass def forward( self, diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 00bf049b97..8bf9cc1c90 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -295,7 +295,7 @@ class EnergyFittingNetDirect(Fitting): def __init__( self, ntypes, - embedding_width, + dim_descrpt, neuron, bias_atom_e=None, out_dim=1, @@ -315,7 +315,7 @@ def __init__( """ super().__init__() self.ntypes = ntypes - self.dim_descrpt = embedding_width + self.dim_descrpt = dim_descrpt self.use_tebd = use_tebd self.out_dim = out_dim if bias_atom_e is None: @@ -329,7 +329,7 @@ def __init__( for type_i in range(self.ntypes): one = ResidualDeep( type_i, - embedding_width, + dim_descrpt, neuron, 0.0, out_dim=out_dim, @@ -344,7 +344,7 @@ def __init__( for type_i in range(self.ntypes): bias_type = 0.0 if self.use_tebd else bias_atom_e[type_i] one = ResidualDeep( - type_i, embedding_width, neuron, bias_type, resnet_dt=resnet_dt + type_i, dim_descrpt, neuron, bias_type, resnet_dt=resnet_dt ) filter_layers.append(one) self.filter_layers = torch.nn.ModuleList(filter_layers) diff --git a/deepmd/pt/model/task/polarizability.py b/deepmd/pt/model/task/polarizability.py index 9483d1eb4a..fa4f6d7f37 100644 --- a/deepmd/pt/model/task/polarizability.py +++ b/deepmd/pt/model/task/polarizability.py @@ -184,7 +184,7 @@ def compute_output_stats( The path to the stat file. """ - raise NotImplementedError + pass def forward( self, diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index b31822d0ee..77e6b1c709 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -22,6 +22,7 @@ from deepmd.pt.loss import ( DenoiseLoss, EnergyStdLoss, + TensorLoss, ) from deepmd.pt.model.model import ( get_model, @@ -192,30 +193,30 @@ def get_dataloader_and_buffer(_data, _params): valid_numb_batch, ) - def get_single_model( - _model_params, + def single_model_stat( + _model, + _data_stat_nbatch, _training_data, _validation_data, _stat_file_path, _data_requirement, ): - model = get_model(deepcopy(_model_params)).to(DEVICE) _training_data.add_data_requirement(_data_requirement) if _validation_data is not None: _validation_data.add_data_requirement(_data_requirement) - if model.get_dim_fparam() > 0: + if _model.get_dim_fparam() > 0: fparam_requirement_items = [ DataRequirementItem( - "fparam", model.get_dim_fparam(), atomic=False, must=True + "fparam", _model.get_dim_fparam(), atomic=False, must=True ) ] _training_data.add_data_requirement(fparam_requirement_items) if _validation_data is not None: _validation_data.add_data_requirement(fparam_requirement_items) - if model.get_dim_aparam() > 0: + if _model.get_dim_aparam() > 0: aparam_requirement_items = [ DataRequirementItem( - "aparam", model.get_dim_aparam(), atomic=True, must=True + "aparam", _model.get_dim_aparam(), atomic=True, must=True ) ] _training_data.add_data_requirement(aparam_requirement_items) @@ -228,16 +229,21 @@ def get_sample(): sampled = make_stat_input( _training_data.systems, _training_data.dataloaders, - _model_params.get("data_stat_nbatch", 10), + _data_stat_nbatch, ) return sampled - model.compute_or_load_stat( + _model.compute_or_load_stat( sampled_func=get_sample, stat_file_path=_stat_file_path, ) if isinstance(_stat_file_path, DPH5Path): _stat_file_path.root.close() + + def get_single_model( + _model_params, + ): + model = get_model(deepcopy(_model_params)).to(DEVICE) return model def get_lr(lr_params): @@ -248,7 +254,7 @@ def get_lr(lr_params): lr_exp = LearningRateExp(**lr_params) return lr_exp - def get_loss(loss_params, start_lr, _ntypes): + def get_loss(loss_params, start_lr, _ntypes, _model): loss_type = loss_params.get("type", "ener") if loss_type == "ener": loss_params["starter_learning_rate"] = start_lr @@ -256,6 +262,20 @@ def get_loss(loss_params, start_lr, _ntypes): elif loss_type == "denoise": loss_params["ntypes"] = _ntypes return DenoiseLoss(**loss_params) + elif loss_type == "tensor": + model_output_type = _model.model_output_type() + if "mask" in model_output_type: + model_output_type.pop(model_output_type.index("mask")) + tensor_name = model_output_type[0] + loss_params["tensor_name"] = tensor_name + loss_params["tensor_size"] = _model.model_output_def()[ + tensor_name + ].output_size + label_name = tensor_name + if label_name == "polar": + label_name = "polarizability" + loss_params["label_name"] = label_name + return TensorLoss(**loss_params) else: raise NotImplementedError @@ -277,12 +297,26 @@ def get_loss(loss_params, start_lr, _ntypes): else: self.opt_type, self.opt_param = get_opt_param(training_params) + # Model + dp_random.seed(training_params["seed"]) + if not self.multi_task: + self.model = get_single_model( + model_params, + ) + else: + self.model = {} + for model_key in self.model_keys: + self.model[model_key] = get_single_model( + model_params["model_dict"][model_key], + ) + # Loss if not self.multi_task: self.loss = get_loss( config["loss"], config["learning_rate"]["start_lr"], len(model_params["type_map"]), + self.model, ) else: self.loss = {} @@ -293,13 +327,16 @@ def get_loss(loss_params, start_lr, _ntypes): else: lr_param = config["learning_rate"]["start_lr"] ntypes = len(model_params["model_dict"][model_key]["type_map"]) - self.loss[model_key] = get_loss(loss_param, lr_param, ntypes) + self.loss[model_key] = get_loss( + loss_param, lr_param, ntypes, self.model[model_key] + ) - # Data + Model + # Data dp_random.seed(training_params["seed"]) if not self.multi_task: - self.model = get_single_model( - model_params, + single_model_stat( + self.model, + model_params.get("data_stat_nbatch", 10), training_data, validation_data, stat_file_path, @@ -327,11 +364,11 @@ def get_loss(loss_params, start_lr, _ntypes): self.validation_dataloader, self.validation_data, self.valid_numb_batch, - self.model, - ) = {}, {}, {}, {}, {}, {} + ) = {}, {}, {}, {}, {} for model_key in self.model_keys: - self.model[model_key] = get_single_model( - model_params["model_dict"][model_key], + single_model_stat( + self.model[model_key], + model_params["model_dict"][model_key].get("data_stat_nbatch", 10), training_data[model_key], validation_data[model_key], stat_file_path[model_key], diff --git a/examples/water_tensor/dipole/dipole_input_torch.json b/examples/water_tensor/dipole/dipole_input_torch.json new file mode 100644 index 0000000000..f6903d3334 --- /dev/null +++ b/examples/water_tensor/dipole/dipole_input_torch.json @@ -0,0 +1,84 @@ +{ + "_comment1": " model parameters", + "model": { + "type_map": [ + "O", + "H" + ], + "atom_exclude_types": [ + 1 + ], + "descriptor": { + "type": "se_e2_a", + "sel": [ + 46, + 92 + ], + "rcut_smth": 3.80, + "rcut": 4.00, + "neuron": [ + 25, + 50, + 100 + ], + "resnet_dt": false, + "axis_neuron": 6, + "type_one_side": true, + "precision": "float64", + "seed": 1, + "_comment2": " that's all" + }, + "fitting_net": { + "type": "dipole", + "neuron": [ + 100, + 100, + 100 + ], + "resnet_dt": true, + "precision": "float64", + "seed": 1, + "_comment3": " that's all" + }, + "_comment4": " that's all" + }, + "learning_rate": { + "type": "exp", + "start_lr": 0.01, + "decay_steps": 5000, + "_comment5": "that's all" + }, + "loss": { + "type": "tensor", + "pref": 1.0, + "pref_atomic": 1.0, + "_comment6": " that's all" + }, + "_comment7": " traing controls", + "training": { + "training_data": { + "systems": [ + "./training_data_reformat/atomic_system", + "./training_data_reformat/global_system" + ], + "batch_size": "auto", + "_comment8": "that's all" + }, + "validation_data": { + "systems": [ + "./validation_data_reformat/atomic_system", + "./validation_data_reformat/global_system" + ], + "batch_size": 1, + "numb_btch": 3, + "_comment9": "that's all" + }, + "numb_steps": 2000, + "seed": 10, + "disp_file": "lcurve.out", + "disp_freq": 100, + "save_freq": 1000, + "_comment10": "that's all" + }, + "_comment11": "that's all" +} diff --git a/examples/water_tensor/dipole/training_data_reformat/atomic_system/nopbc b/examples/water_tensor/dipole/training_data_reformat/atomic_system/nopbc new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/water_tensor/dipole/training_data_reformat/atomic_system/set.000/atomic_dipole.npy b/examples/water_tensor/dipole/training_data_reformat/atomic_system/set.000/atomic_dipole.npy new file mode 100644 index 0000000000000000000000000000000000000000..2cabc71e2166e0e0d33989b0b4d5d744eb55fb96 GIT binary patch literal 184448 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%0DxL2r5rsQBt9`a*6X?Thn?HJ>Zd$n$mQ_!aW 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

Qiyzhb}9_aLB#D?1SVHvMD7_Vb?!oJYaWh` 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

Hg>!N%nHiHD{I8hu zNSjW7F-@E+9}?wtvuNq+-$XI*gnx5GEQa*@8r z|09!_GoofDEKfYg_#s(x9@iB%M^q>+jCY>-dQ`>lnTEW(IKhm)@#^%L z^I@L14TWxBDHgvj#!kEQBH{5BSpNJc!p9n2%7)IOr}ZK#jpMlo)I#p~IqXv1jI+C$ zMU-2McG->4jMQi6#;-_?V<*Y8b;v(DlNp(eUMNq%^adk#=?Y2@bwx_5JEB(fizDBv z&;ic?R4tT8)_tydYopLV+$)YUGiP#xCH$HlX|&@#q|e9m?V+G{_UC4?`?)^lGwO1j zOotYQ!t=efP#8)Q{uTGJVZEIwJ@(wiIHa9ty2)bjX`{&8&i&|9Z&ADEl~|v>PMoMX zCwfy&aQR8R#2|lzq&!v|^P`GH&i+E42XOYn_6!QF3)s^+pCTg4u>aOc1ev(gXDvC* zT<^sNe>G~aQ^s3ns~_b)z`1x5%1haWp*5Y#9F;KMS%9m|FtPZC-LnBaySTv~sVQx6 zX8xa1z7h2#4PnQlQOrwQ2G$YKS@zXd1gwXDiWl9}P$0Ele-xR0M*HebfKnX{=Dx$d zxf(Fk^~0U932f7K!?({AU=^l7)vkKf?9&D}6$!}>8dB+-A!cmb(X!QwWU{XjzB3ol F{{X1TbpikY literal 0 HcmV?d00001 diff --git a/examples/water_tensor/dipole/training_data_reformat/global_system/set.000/dipole.npy b/examples/water_tensor/dipole/training_data_reformat/global_system/set.000/dipole.npy new file mode 100644 index 0000000000000000000000000000000000000000..c16efad029725ca9cffa1de92abc0fe0ded3e7d8 GIT binary patch literal 1088 zcmbV|>oc4O9EDd59a|(S8_`HyDmJc7!lGi|-$^v?8+Y5V=(1}Nccmm%w{9wuMcg_G zi%5*=BpH{gMr-%&)Q2>aQbNMCok_~bAZj`&B7<)Kg`RKcne*j2_2;5Qq33lt1)NKK znLIBkm+xi8_fB@>J6Z9QvvNPlm1ZVn<;vv$abIbAo?OfGQl!~(Z9nFD+{wz--pQ)i z>i>poA}*tET5BxyR4Ef$~djYjHjBU`t{!&F2X z4lQ)6AL_YMoJtO7#W)>TRHzf}9N^j$LXxKg>ObD&V_$R}^~Lwnox@d_dS*cFLjxqa z?TV+88KzNbf$Z*b1UCfJtU)8BPr_ib{~;w-a&eUFg0+c@Y}x7qy58>$)0dB#-zMd7 z@8V*mwUtC|N6?j@j`EJJA$Rk9Yzb`f`vD8h;}T;m-}gmpk&<;336O7+4UZfFo5?wa zoe}~wEMpwCjw@QnY^`Hz5C=j*1tN8_ZNaXo((p1d{Y=erFwv0et(fk}Eb9Rl~* zJ!secg>DI>=xu)~j)YFq&3qC3Gh)F@y1^!R+u@(NBJ9e8$h6T6f1gnxdDMtqUR6=} z;uRRWyhC+eY1sRu2{C?!Z1Q{@Y|OS9%+^u0%~|-|6=Rg$(L4|SRsC6-14HaB+cO0iU7ogoB)IQvX$o`BX7E zPY1#BL=M88Z+PFd_Q1q!IgGCs(@1R$9KR1ipuCoSrSygF?hcjqj#HscI_fv{C}6IM zjA?-KQ~Y45|Awi`+>b1MFU*?;Y5d>UgR^xaLWW*yzCE3XwNO1g(r&|;4)W`}hTBJ* zX(cKG!kjoX9cW-TuU%HZ7<9zS3_~r8e#p3Q4PKb7y0qjR>UWh$@yn+e=}8ziE3q~9 zh*69rK{Camw%i69x@Cm$S_AxWZx1@BC8+NeA$z-%GCX9Ep0b6-a6An@@J2Nw!&e_# zvUQpi>u`gB4TxMxk^ltOAD06=>_kS zC1d(^DP9-6rYk0HX86!SyMwW@IET+Aui?!ySaH0>Vghb*(_*^k_6h{x{y5ooT)E R;(c#sgH8rUqPTZO_%E95vK#;a literal 0 HcmV?d00001 diff --git a/examples/water_tensor/dipole/training_data_reformat/global_system/type.raw b/examples/water_tensor/dipole/training_data_reformat/global_system/type.raw new file mode 100644 index 0000000000..6c71c85e58 --- /dev/null +++ b/examples/water_tensor/dipole/training_data_reformat/global_system/type.raw @@ -0,0 +1 @@ +0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 diff --git a/examples/water_tensor/dipole/training_data_reformat/global_system/type_map.raw b/examples/water_tensor/dipole/training_data_reformat/global_system/type_map.raw new file mode 100644 index 0000000000..e900768b1d --- /dev/null +++ b/examples/water_tensor/dipole/training_data_reformat/global_system/type_map.raw @@ -0,0 +1,2 @@ +O +H diff --git a/examples/water_tensor/dipole/validation_data_reformat/atomic_system/nopbc b/examples/water_tensor/dipole/validation_data_reformat/atomic_system/nopbc new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/water_tensor/dipole/validation_data_reformat/atomic_system/set.000/atomic_dipole.npy b/examples/water_tensor/dipole/validation_data_reformat/atomic_system/set.000/atomic_dipole.npy new file mode 100644 index 0000000000000000000000000000000000000000..3a59af8138cd39e0e57a6f070002ba56cd38af38 GIT binary patch literal 184448 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

4j@7il+6hM&UH_oJO! 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�{>_5iR#q zu*P4Bl5&DDV*4@nC~zIXe_xgJG)Y#j?Sgl>0LuW@mnhS{2a#40hSY{R6m5ER$5le^whljbN_(d%m;o z?@ButXtTfZ7rW73L3*bdz2I}@GInkQvDnnk7Boa|XkkyNQtp^SbPa zLzS416Cu0Az+ro_Cp!vvoUG6})0FyL^PrZ4%6vYur;#%pXrNR#*oRorgOR~B;QLJ+ zI66(tm#)QvEgz6mSMXi;k|-MF zOO}UB#qSzj7;;WI_>QX>z4w5acRQ4pUpc{iM{jz<&;P*HxAFUWAl=BZqxjK>vDKp| z&w|{j_DnZ=`@xQMZ~4%^@Gi7^rzfp#>q+jdJt)h=f!Uf425M+Qu1FZ69v@RF>HtL{z z^P*b0toLeV3e}+S95@7JR1UyWCAX+C5yCd^U6-<|6yI^thjpBTZi_ z)hSm5^6y!idq*v>yy{L}uD_HFE?&n>=_t{W>z+5a_z;>C2Z@eF`Zt~%OaoI=g^6hu z8s=`pKiMqgZm5ILw{)DplLE|qgfWR5a81JAfUHLLP_Xm#u>(a-Y-8{GKswFOu-jw* zaK390mF(|9DsxYueY!cRUhCuz_9j&QG$ZMwKd}Et8crBFP{ABkDs8Dlzbtbs{>zzr z=2BfeqX5_C!Pxov37%#fU?q35f)9Sj>3zmzoA?a#N4#O*6T1geCI`23!oH1Lgm4dPi^Jq1cUl zWBX%_*Eq@dASLGXn&8Q{r^3-thoUu{kUPRqlrtNx*~Ef!Eab(Q1p{f-HO^3MDVMY? z45RvJ_WfEnVC#YT(B^Y>sO&TJh@Svwo=NuMbKTdwOQ80PIjM_|iQ~W8nBV;iV|n(w zL{#D3Gih2Y<*>l4gcn2_oQC9U;7KcRQ$>Mjs}Kq zm!ori0?3lHrF*kl;k49&{TA%i{V7LJ5(IKfkKuq9&r9O^V(FlD*l4dzlR{;2_m3tO zZb(AH?VG}DsTQ-<;?dHqE<%}I(r08G_ue|h%5!0y&j#i9PQ&f^Ks-6VVMb>{&B-v$hg4q-5NQ_X)Ys0LI%jPQyv@DOM-O{bX4yeW zz)UYpFt~~d-1AT9G(eXrr?5$1gKACYpk+n@p0q1bG50yH?>{OEto*3(PY9&@ZxSC2 zefWO26UH-D#po68v_E$XoOF2iJ~>ZxeY_J6{q5+*M|Ux@W-m_iJpR|1Od)S8p&5>i zxTlsZk~bnRwfNeZyOxnbj;_Ats9=#J)Vtd;TObay;i(PVU0J zOT2e~v0Dr&3?p?>4|xm(vPj$0Vehp`QKv^b6La;f5DN{)EK?k}4eXGBQwU$NPHKNh@a zH~k4^v8=fo-~KDX^p(c+`SDL&p3Q#tXI-hV{yPG_u3^H<9uzs$fxJG;(#X~mkYDag z3Uj4t!NzRdlX9ZV-oK&voV%P^s^qv<5hvy*Ldlxvt=%t+zv&0Dc#HxKU+xE=-2JGl z;-5eC4OWNmLXdeaJo;C|p!XKE+Nh9C-Fw7NSiwAbbxP}Bi=cC=2ng&#mBD;B6HmpS z>+C#qdj_*_rNWeR0kQ1G*zenh$umO8X#jWW`5yK^9@5aAH&L3wGoDx#O5>c^v{yCo z9b-g?c^@(7U^SGEDbarB(=Q2o49k55Vn*b8*n7vrIKW!0-W3a-kIOJvUIxqAIi<(E zr0zaz#X=KfIxg!+nGbEnwW+qW`MDbn;LrHeljfvQn7MLiMZNU$y~`i?z2~m zh3DR4k4VOgoPnaL<_)5b43dm|Hc#U5-zThc+An#XxK9{<{Dv{Z%*4{xx*J2n9zdeB zp9h2!P`7700*)qP)%_^WHDzLG_7?7c#p7H_93%?IMfk}Qp~%_b&J8)@kh&!*Lp^C; z^)|6?U6t6G(U%r8SNK`0pvU1B(IWx=HKMivmn3=~N>i;otRNkAj+s~Qo zeshoK1NYnJJCOPgV=B67PNT1S z7(M5l5dC)O!%q3JXk4#=Eo+o-CW-C7kdl7MAX5ZgP;LiI+&Zp}S?BsLe5NaOMlO&tl5SAT5?_BgKG9v|2sqBJc zp0UifSnQu>LhkH0H2KEwk0?93y-khAw6tLSJ#|>-{1G*GWT~$7udpj}#pv0k=#p9| zj9;_}4c^gij4&X{?qa-|{{Z(7bf=j9<+!Ha%zc(_w63ZGdCI#naC;-}GSBz93Nt!M zfwbSMlD}~#j)#ANqCD>!elyQJGY9?73`7@=8Th>|8?jT=F{)$*79W2JC&_U5afjf~ zI7jL-^A*oM9cf>=B}ERp${ulcCGGX3@{gQ1c1o7K9?^vi2RdL#t8D%ZAxnN)I*_|= zBwqhg=e}@*csekcGTtSLR^CVSDG8@m$Lpek=f3Yv!Z;uJPW1RBLz^UDq3@A^ExtXd zdfOueJ&i%vStb-J^&J!TFdJk-H!@qjTBJG4FxRXO2A`jbT0VEqF8hVaJ*|aQ<8OF+ zKSRh9W&V6j!pf@O?5*L>&!$PZl39k-Yxx-ZHettZZ@f%diAuhI*rbNS?C35y zuP%X{uQ3*yCg5b1B)TQFu;S@O#nJx?3Z*q%9J;eI{tY1mQOv@Bt^TZ>)| zWOmYuZjf4}LBGmf=)>L3_;w~9yEyN=iL;1x+IMJmVSDd#%?eiE6w`#(fsWG_6j-m_x8=-fl3p$zqeD}*b z=8GQ?2S|?o?wyb&A^S{ zM&zYY7noAF2gl?z$zP)(Pxkv=r2N$*|Gz4EVKw`)Yi1Ct4j9wnk;iZ)$q*~~|Jzl{ z>?9>iTu-(mdpq_&ck?Ipvl~VFaPBuWc#{8`3xRh7^!W1=LJNXsiYFZbcwc%4qM{w% zvprxp=OPTae`S`r7(pM3P&^(QVUkcw@ z#;nDUYxa~Fr;hr}z1ZtyL74tQT%5iQ+lqY2Ax#tEbuOe?{)uxsx5TB<=2UU}6Ypc| zMOk-Oa!{&ARJ*oVi)VPTU6}$To1v}n3KHJo&D?nmMh|LnmAOEl7A^obx>IxFb{Lse zAe!AlXEF}p@VPrM9_LQi;!a@KlVfj!tO}PdiQx0+eeXH;~TMYH{%+)UXD4EC|NIYdf$rT0K{U%MQRisPw^wj8b zj3WL;zqodrI(g^+PRPyQ#vOQFG3&V{`p@_RpOzmtx_4(TLUjdFyXFYf?*gBel*7M| zCY_(-M{%dF;Lu_ddS&KDKN9otrMEQY8(xI%v8Qk(UV)}HeuCXsecbkuB~|sGD4AI-dc{bS@29(Pd8v*hBUhUB zqY3J_Mas_HRXwkdvUbS0pz%aAip^tj+`g6%btXo z#q5Go-GiMcrn5I~GVkP4MfLcusN3K~hA#KTWOMfUTHBGi)HX5sfiA4B2hhHa$1&c; zpHe5@6gk>tRi*MW;~y*qQD$x>3qzBO1NQmYRtFoSo|Q<)j-mtYF8(d|P&3 zIni4KOL{ZUoo5YFq@Kq2PUfq1E|uq9iYIgW9jUfnl^R^R3;uAsn6uRk3ZX~E^uS+Y z=U+qkRecsJ|D43ry`|hGxhK|5vBHcYyKvIkj2g@>@QwYa;f5}JR~n5?DQgkvW6FLT zFFHPRBWJ1|NQER+-nj*9X8Taz3{ToYkvKbw`HTuRqOg_coDq+WKdql!OXS^7Yi&5-5U)AHg@aTIPKVth)7e{xPZY_nCW)Lk7t zq`O&>lB705Gti5)pR(`kUo4!t+iS5~LW{U><9|$p7M>xxQVlAoP^Ub_09v)ro^EhY zQ;wM}`)w2{^st!h-ku7bGvKk$Py`8!(usi?S05@~b{F-t>44+E7vY0FD+ zxt<~Fqq~X9w)M<3TQ9aISKzV9EY6Q+L7e=I7e9BPu7-PSp?7dxdo=P5(%_@cIlZ(~ zlB5D#T63-$X~Dfk?jtX%?Es(1x(jNU5(!dMm$E8o!{&#iJzI=O{`KW5YdVI%BO!_)!4gFplEXiL<6206?7`3G=a}|-GIXvPQP%@9bZuHR zR6puchcVCT&7^6QV?B;l$|7iBHMWlZjb)>^i`@%;;j_kb%!}YT(fkFFQmun)nlk$o z*TQJzYj{34C9f&#m|a+S z+hBjlfx_m_gHh>JJgs*iO^sgsEKElDdovu-$rCxc5k zoz{B+%#zB4Ur_)Z>~zDzyf@;>CVysUtb=C1i9+$b7ll_WVGf%W`Cj}f4*6~5{!$N$ z+O%EVII#|AE_qOFT&0KzcVfnHBVNX)i6h+q`rfAo8PkfnPi{x2`?aE|>6|1Z`yI-k z^LI{VBW^NVp?|0*m5e)#j`Z)C#68Wbvt#)?D z8%1ze_P3ndfRvhbV%oxA7&d7H{KhGgc}2EZw7QG9;-yBj4rrj=E>W`PTPu96J_@;i zdxhcT-;&dNjBx2)1=>4L_G1 zK!ZOAiQ>!i48(F)?7>$(9GH=eQSLHScBTmP*QVpkxJIm;T8=Xh_u|m;u2f-D%75Mg z^iGkZuOBMVFscjIXtyvYuNp7b-4OeDzo4|?Hcl^9$7nB}VQ4oXeFx{w1_zNT&tsd$ z^4T!FC*2PGfrMSUyaVsg0$y&8F0jE?fctT^tBm&c1?jq;fInsYDyXA$^7LT7l-9~}FHv(tj+ z_}bHu?WZub-G!PSyV1ifMl_DkK4)t^$jPHC`QG)UZoOUU%?K;N+El6JLhb8%3*8Nd=&f}^RQ`P++AWyrE?+MK|K&=k zq)4Q7ecMmFw@+AZN1gy8}$=)WO zSMA=5*dKl*%Xqrqk0xPcngf;k7K@yf4!n<77S*rUic@i(6mPgp^f}xEjlI|rSU5xc z4iFeQ`#jbw^=4jPoA`P00=tJps58GGcU>Q|V@R;eR-OK?`HyGA!F14Fffi1ggK-BzqBl<;XScRbih)yI#Ljhx8DO^4dYNGN1n04=G}pd)o&)H=Y9A{R5HXbAh{hAGgP z%wU?eS&4@BP$tvJUKF;x8dHY-L1U^99W?7Fc{U;gTbJ93n4qP3Cy!po0ILj-nY>g_1p zScXQA?IYBf*Y?UE&;5$<1I4x#mX-iKkpU-uj=W)E>D@4+*?v!|*9qoS2;^z&0df$E$jn8+B9~v#t z$5HO6MTolDvUG`|b1QUYv9#(7>?Lz?fBR7c{(H_Wswl)qZ9}x~B=B7IPNHiA^q@yc=_%(?|Hf#uYF%GH1UbcN?c~KBJr35mL}X4bIh7Wz?~3GUO!#P z4Urex{RSfIsx|l4Tu9YywV2k-ysHjNx@q=OLKF4r+bMf~mTVAx+E?QA!Ic=f(}3hU zli6ny0jX#+lK8E|wi{`f^tmh9w5-PbmHsq}T`Zx7J9rNtKt(?XQCe|7tUtyq>aP9h zq1#?K?Q0NoIBU0LEIVmEWMOx)TGU$?;RU{nt?fM_lE2NrhPXaW2->#DBNe z#hFHNPQHP8Vuz0~6TQC;h3guS@ew81vZq;JsUH2laHr(~^Ul}Z!TBwZ#MQo@V z4+n9Ee{o-C2KeKzjx}iv@Z_9ze{78Lp~{8&h~#dD^oLFOoAnYKdvTw{YcayIZev=_ zIym*q#sWOWq!~+4P-8j|7Y;&!R(>-AP?RlMf+RAquhs?(mRp)KTiwKj6!Wc|Nc*QDA`>;~KT~@;rqv{r@3!Ih3lAD#sE%)c4xs(!7Tndg#|Nh@ z&id!U%WALCRy>AOmSY~lw99pDZp1p*9R_5y@rE4-vuP-;HLsR@|;_P4e&_kb{C&4uSu_E<-+nrjY zgDCvo2ljOJq*CUUZn^8rOspRCB82;=zCEa;&YrdfOOw2Z57}OG!OrESc%phneEO)2 z>RtDkAF@x(zhjE9ou|-ittOO~{>JRXyq6$;Hx2lR^>$h8WL|+^P4BQedjf7fTFd!D z&X3o)FmIy|cb!AA*nc4Ve1fT%=L$QwkHD&eV5(9(gljjo$z@R)+Uj;f`H(#GtWO}e z>lU!&g3Pu*z_^5;aD9JIoGEo>Heovym8!+PPj5u?hd;R6+fO|3BD_9w5nE^Z(2F72 zklJ>Z+1gIDY0Nn|tuDsZ`+ewXuk%P}Pkl}0H;i9xL>uDYqpnhkCXeI{Huo`pnzbRm zPKib_uhS`50U0-*W5V`eEchh@y?Gaz-8u&y757B^%8!`L470KB$0dDm3pd?D$Z@==d%J#=R1=lzk@Ed-bP078|HQGheF$Iw3MF1 zvep9TphUvChI_&5kHI5zD~zoxu=Hn}m<4Y!?uI&TE_f+Y-o=VXdrYa|g$ulvCW*fM zyeo)tW_CC;onwc>CyYHD?e6q<|3c`nTW-eCX~G{s1C z{PCysf(8-(Sp^YM0rViZ9-00TFdM8*l8YA+5!M^4-Ye5_{Z$m3@%YVOyy2be*F90FYwd*cdG=`()j@UD5yW#YBZv9_ z@kz|d33)BB{t_Bmc|Y2ARPy=>K1eh$O2Fi&i6j#T$rLSS}p>4N2YpCT=l{ z)%UR~OqOKWS=#@~h;IF86Qfu3hlKZd2c7h3^T#L*Y@CDL;C}qsP^>$$ z0csCzNYyruS#5!|YpW+c9mZ$b6}_lCXSf3Fwqxg;!PIfho80x*AV9GzcZ(IUY+Ecm z5s1>>nIb%QC$7zNMp2wAb~N)0c%CVZtQ#sh+I1H$F=s1pmO*)Imm@Hg7*d?(BGJF- z1~TGIX?BYxZd|#90QS~jdmO=B*Yj99-GS^L+Oq$`j*>(3@q2?UEqG%__cmtm8NrE) zma>!VEbjsvEXjIYin#mKo}3(ZAY*c&q{~=a{w*aiJEl~uyxNs6WMt!;ni@8IVFt0r zBfJ_|B<3Dr_w%HC7_ZWc9cD6gj_=t6-}3%u5IaKdcv0f9BF+|?V}zSMEnK}DOP&DQ zA9`_iGLYU(T8BuUkv{nxO!JvteML!%7HwjtZo(Lh@o9vw6nht^Y13h&7QEvAmy9#J zJpcOyTNCb2P1d1g=Lg8t;;wY06Dc^|fpaR~9lLpv|GiQiv6+dg)s{5I;W5m=ZWq5g zr{K`7V8NAI$NcAy>A|M`X>&SthR?!;m4z(!w4K{saX53EokHr{IUWEkzLZq&*gjR4aJCN>TQBnmS1~$Uq zE|`*izvA^OX>n<8Z&Ejrp_Mu=lHLu1?nd5+==|&N)b*)k`+R1$%rT{gd#A-8*%v%llQTUQq!m_& zo3{U8;^0JTidBgF?;SHBt*OoK8Ya1`(^grcS>BH1InI{-OT9>Lj|=nnjYunPFnbg1 z$w$?KQr&=&!b4J1v>9E@4Usc>q%dLLRp)JeTv@DMzWh%j0;AZgUf=?Q{S&ZDNt=2M z(u8-f2}sbkr$xKuVX`^^>iMVHY5yD#WL6>2e>=k7y+XV53PdOsaPGRAb9#O8mt9%2 zZKrY%PeM^OD%^>hg|nFqwq=R|}7Bf@*$xMApKAwtm zKK}HhzZB)X(V-2T@f}m@O*-dQ=$RgS&DT0m*Dw?M_t%-)nE$i=nG8mxO3|T}##A)0 zLtN+mXkS@X3hhwA{ok5YeZh#XKYSp5&&$A_FwR_0*G9AU5v<=-1j|Q@#qWqMnBG|c zi_~u1>vtp{c>_8S5`>}cPR#n{?9nDol)vsx^N;J2)F$Sf^(hpUC9c#{I)J{7IVi^F zvl~0gnXL7UMRqCo)K=#r#xj_>@a`~qeGs>$hf;c~9%6Rw#r-p(^gJO7zmDxd;E!8a z?(Qvd`n(1Qx;0_Pd0la)VK+`sXn@*V|MD$?GBl=Jrl?f-DA}#R-ip#slD|=FN*&qp zP&lbn{6)53wSxnx6tO=dRGK3D*|UqviUNNAU{|v@h3faBYyW-cyCm=AcwZBx7)X7& zGdk6*8zlw%&XUKS5vV$NKr z*Ecb9BVayx2Jbs-p*(L9OlAZkx%dh8Z{j)V$_bb-dOR}TBp}~9n9|4Fpz%o}PUH-v zAgBNEX-*b*5&F_TaSzX<^vFl0jQ#32Frq+#jusRlud)oQ+PYCnznd65REnx+uN4d1 zg0W$g3>il*5Ek2%F~FONdRDqfHK1pF?+?67{Q5qokr&xVZf>GoMvyPwiS9j?>0K?slEJkP3|- zJu zZt98J$o(l#S>Ae-5mhbG8(syq+vb#cdktp^FCnC=8~uAJDeIR16e`XWF*__DHxK^A z7wa|5j5~tt(MnWau@0|BpM_2+&!&}g&|{uAGQJnWr0E>oUH(ug*Idk2o z#v$B^pJ!^B=(c_os*BiNmvIf@eb&O%)ttHJ>`dHJgPvV;Fl?_g9lmh~-Geuv=ak;` zRH_C${uN-&>wYNhx=zdqN<#au8K_7v6ROseaApR(lDHW%ZxQcN-RGkFWJS`BH>Vor zCl6RCOY)DpQgrhu*d;5_+yAuamG%Sl{Bi-uhct;(oUPJ7$j{RK2gTt1=Mkn*4nx}u zqWzH_-4dHnEbA!@CG0rZ6^GS*4a#0^v7p{HOA)fASVUx>7H*buG;!l?;eK$0SaA9m zOg=0SD`Vu)WvvQ*+dE4vd#*)ylp8VCRY43+VZUkIWvqQ{A+9DFQr7Y|EF1GivR!wu zaO~G4Nt!$vUHqDa>;)mx_RhhIA0tKAUh?IUH-_WN`z|znfj&(fQY2Ixcusf3oQ7Gt ziQ{~39@9&o%m&^SAB-hPN-o2!d+t>BdooVR9A@u=+yA>gOn=>nRToEQlTAZ$uK)^v z;lpgcIhe!qm#EuBdo>dAcgbLytjaEv4QseZua2Q3|B1*EqhUSS2jd3b6HUzGRl4p1 z<=+a>{xJ$$J(#`6XM}~ZQ;@wwi`V)l&5ERi~Eib#Ust zRpe)~FC*#%Ht*!KS?v`OvJ{lJI-5Dd`pER_MGEXFX*=^$xP2Q$rZ)#lbnY!f>HKGC zH*fQ6Ph5?d)Bl)rsa;-ZI|(z&YSA=>@A7;GYO}opNx2<~CGs?~HXk!9waML3l{W6I z#inlD!!f*+sCu*OmT6{e^kK4ek}YlJaprOaD2+ zXXJWB9oC?IE4Crd{29g`*P}hl&!dk@1$IsH;Jb_h_g!<)5$jAU7rH{>!Dj4fmr#05 zh1e6g2Q#;x!2|E(%wW6<-6y9x6J3s+J-KMktU*Le9y{%;@nl6Gve&)`gCTPzzMMPl z@6*Jrf|jzL`~2v2%LU|)y)LQS6ij~liM+=)q>Q{Ua%f1yh0R7Z-!+)-@|ip{(~fdX z8!=_96P?-1tU~|Ws9o#Lvk>03pKHJz&NQ9=Xi0xAg-aG+lO_$JNH_g&lxZ1#fa_mH zI+*1yJQhi@Q-Wvxx4r44k~90TZ1{}oMxpL5)OJmeW+xD31@)rJ5?jjo77Wh<&R2x) zjm2&J30XV-?-26w1nA=b`*0u6v}^kW57QnI=0^owoM-J3S~#vS66KQ zWX0XTd~8j6j0PX(e?_Ii@XRONnJ^#Q`136*se#qL0Wf=RM2%S-m)q$^Z>D!4=^0vZ zyyQ)DYwh@+WP|AaZnU4ff)k?HB{^G%xh?Eh50#Ltkr^qtWMBjDj9fn}(M1h?Dtpg4 z?&W-7@>im*)7+@}tv4MzWkCTgZghPFcj346$8ud6Qd`IFyrKQLH}V=!hJ3=ibB4%J z=e%p;Bh)gZ#35%q+~1$#4&r3IQyYrwt!1$5sQ|;V^YM8o=dVo?Mb+9$$q()kexLJ9 ztU4c5X7=5QUC}wh|7 zZ_CzUSZ)xm9DIuP0j@}!=nJ1uE#m6&IGCP0AhK+mgu=ru++9~g=I9b}W_TJ@J%hxW z);FlPS0a`2DqJmMhTy9|NW5?kv*tg+^}D*{-*Ewhy2#Mm{*B`JU|pDTUs^$@kmq20 zpBpSiFE@=9!Ra12L^3@03M4a^1Oz=&py#9e(;D8-21RQ!|FJhcp0o=2vu{JDj=4FU zi5l!ui>X~@Y1{~D%8Y2l_5Po6b&4T9`^y}wiHg{IoSg*g)8P5l5LO4j!t7fbb29b` z=`AX>=zWNIv)hk0h-y?R?iLZJf@$u7CKzuE7NRJS0z6J5MsV+r_vPx*gPD)uM4zmi zaYex#Ap`X2$-$41ZSltUIePT0DF}rplxW8{W7?+%?0dnPa35p(tECIAlhVxTwV``5 zzM_|PBPiLOytXEYt=4y8Ftt0y8ayt&Wc(IaRw%LKGz;CF6sX5DBbaPGiq46;WUTjH z%(uMAML#Lp5myd{cssm|xrh^Qu0YDy39s32vHjXjJm6W7IClsA(%*i%`Xt;nUKao1)^^Wux*tJ7VvksCOVe|REBne!pzictH03$u}!)qA@TZaUl0 zw?>r=BI{9ncQ^97o0DaWEv@#rgVjItG4`Ffku1G|Q+VzP!8JSWaTkIM?MR$qswo2S6aUzP^VwWMK%@%Wg+XXVd&^fb>A z4`t=)-ab!#*w?|L=y7D(*N^}l%vPX{9wu~dM!gu6!~N^EcH}aj`Kc4WLPyn%W=j1P+h5Lr za_2eze)lAeew(3r?=apKI#O)fd`!Pu$37qjn!0f+%4L|{spdhKa;GDo`x2>&67oNn zgpWh}(YRRNS+ClLnzbgthQs2IInP3k1UlyK5o50$U=Bh6vmZW*Z>OX1;<+9j$_^#H z=GnNJ(S?pJ@uBIVo3YA6lWeTQXt>r{eC0F2HVs+S*`3EPTLoG*-wuaX9Y_3G<`wHX z;g+*4MUN@7D`#JY z8|-~^1Nn8a7_-C`B?-Hj>%IZi zcm8r8^B%m~tZDCF?(bJrq9MeR408U#DE=81F^AeVli6;G$N5?I0$V%OnN_|8WshIu zrP3c6D`EL_>Vukk6D?Pv~pOpZ2_UJa`A{#x}I}vJLf6F6G>U z19_b=p>5;uB4V#Tl zLQaJ)q|?WiTw0xJVQ&YzCucxmO8)H2%a|^C2a(ekcxV6DoPRxa80Rh0YX= z(Axsp!R?@z8!$ts0LNxE;EZ+*(sp*G{tE};mYgT0b!br{vmzpg`_L~NM=I^r13^7J zX=L~DHFQPx~HuwpZG49mE zz>EDcx^yPC7j0K_=N&?abfq^M^u`1r>t6Q z>R9j)QP%$Co701odcMTyG&jOAMS51~O>MkC-;%)Zfm1zcop7L8(#+7?@5KK`Cv57W zOp=qUINxrF*}phXbut7?yI5d;nj!6#<=y_z%k0#Q#6rg~e4B6)L40Nv{{o@6@ewo` zn_E)jhF_+}7+n#Ey=8f#R@Mk}m3CswOC|L1?2d20Qcx1yO^mO7jvexPZ*k^ESb zI`q>eOMh<0zh}BsJQ`v`*CUWBG@{;*zLojJY(kIqrP#FXH=L)lD?085K0TMEUnAs5 zUhzIo>b`^52m_MulZgJgX+k=!6Q))N(c+pby#IYgbly4)I+Y{|<}>t#?33bll@qy+ zFUJF^S`paUi(J0dU}#F5i0;*sg2S_zpBzA^w7(+BNfQ&3`_j6CH#mN&ShSjX(0jdK z*wrk{{8e{qyyhSzHSHJ_6i8PBv_$;RcC^*HP;q#rzgO0CyvS9j?D$<`^29xe>(nGW zHz_PwbsEyw^eHX4Q8K3JFcin#5_j11Ves$^oLjE5Tkj)&xGRzDgYhE!_8+8ud4+B- z_9EG`7aVnt!I-_K5n;nIr+z1H_dkLZr9i~?y@ol*vW2sgs;HLw1udgC@n&A4aM&nM zQa%sG0L@cnd!l|Lo%1RQ7kQUKyI>M+#5;Q}iflNJTLX3JN_S%#+#UzVz;BXcLrlh^Svi-OdD~w*1 z{nU0K%T0&Teqy$;|1Si?0yU}dj4O1_U7#4HPQw;`5xUiVaa!;`bKoG1Oa2G#_m2_v zFoymVV3qW?E*?`q9V%1P)q&X;bsaF6}VX5`km(l9kai|)=x%Ayc@tqa-20s8h~KI*pf_n~Sq zy-2!&TL)~o@6Y>b&(j#Kq)zdJTEv$RCot=1CzfWO7K5aABA@drhrj4EPtKDT9eEE+ zIW=nh$lYaT)J<3*&73zM`Y)my-iqAcZwjHex9#a=w!rS0M3T2I^deXl3;Oh>TL%^&Fh`bVEWUv(bkCFZVqP7IT^9!}CrL=drNhGtI$w_O$+s2^0_VE{!wn zHOG0UzVZc=<!V%@JZsJH$DS>`GlyfLN8 z1JtPAssnO%1{9q55A${Zz;qCE@MdT-cVRXTcU;1rg}Sur-!wQrdj_?cU8%x(4zzom zMT_-riTo8O?&4SAkNT1_i@ZRTrys|hpdDp#A64+i?mp*#YS>k`3E_^7P`Syyr0Biq zYjFjW65e5>VH}D*Z(;MERLQ>;k9q$^6gB*dWa*Cv_?5a+g}P;VBQs+=Edpu0XE0fY zWMEvL4&8b%h|ay=kK_zR=A`zfBV*UW^lBFhNW6+mU6?V|Uyskkd04-jXq`+KN-4hv z=N)`zXGT%QzTYBp^Db281~aqbq{vE2z|*M`Qtvn~Hc97W_t{YPMmSK?2%=RlE$Pz6 z9^6R@riUTAq|f{7$ISgbV(COH&xImumo43xw-3vuLecw)IZ5|egDU~!dG_K-W0*g2 z!poJa&aOiDJ*sfnWKCQgK=dLF)b{r!^Oiw~jC?O{y*c5mx;bUJ8@k3I>xl_ z#iNB6pzp?+-s~jI^R=cN^#EMD=}LV=jj6oA6pkZ%Q~o+Py0p~;(aj#TX8uBqNxv#- zJ*!JiuZLoVfp2Lz&y`Kn2Em$H+HVi)(TWqYq#5Q#QBPgzEi<}f&3e%V<^{bPr$uk- zdebcn{&&lVlD%3N$~$)%8wQ4v_YGzRKCQ%XV+n12sY9oj%R0JR1^1rsfI@vU)+@ac zXS7aZ@%1L0eR)Ovc)bSGzCXb3Asa>gSfZ}+?iAHi9{-3A8GieHXx-nHygS_f|DLkfcXLstl!>MvE|ilcM|WK=imvAa zXl9-a-8N7b?bZHdj73e)1LE+M54gRP-I?{w!2jEb*M%+k{2&|M*StVI z&o%FSyo?5Ub;`&wLYkHyPCQa(kL@E7H};_j|E5Mq7j|-AuRoH<)S&8)sW`tq0vg)q zad0Q|io91dC$$-}VL>9taTWT_y@cFgJ}ZytK){DwOh{0rqs)=N^z8wbvNdCY3ZxMYzh*sb$=j@IODW1o^KczrQ3yz8dZ3-b*x3 zm7x@^Ao}pRhxo@?!>#O5eB5=bh7ewlI}_5ynHEPeAeb?PEtj-VtRY93)m1>xzqRrOC2tD%ZVX68_hYLh%KzAJL- z!_hzTw7A#B27!w#G5s8SBF`N`eVi7>?m3N$tEmXmR-}a21?W;-j^ub9>KyD$85SAH z-;jy5=bV{;v>mT`j_3c+i=I2N!bLR-3QDnK%vSe zasG8ADwoPoS$tPH)<(#Dpd{n2yDTU0!lfoA7FkeT)jK0&-=icuxa2lsI7 zHuL<<|KJkykVYO1rp3w{;`ZJ`Nrz(^pOcq~QO8G%{##DLGJ2!vH~&i6iABkHx>uQ| zPcb0BsXc{>vl?YDwI;)=5J~C`_SN$~@)fea)0SX82N#RUs<>=}<&l2zw!JGxNog zM13zx=%0@X(-rB(=X!DG)^VK5{DP@hAB)IddHBm-n4oVqc&*|^Ufo{d0_SM;HgR_% z@IL&5fAVg^kAmIruoFU^2E0m>^jL0Bl`_t#n3N)J26ks|ga&iZ&C0IaGodt1Ys~qP zBWVeHhS#;*fb>89+eduD)g^P`X;J6z9ny-bxyLwTau!x4iddKLPce@V%o1>= zXGTY`d~*QqTlmvk?l`{mh`{HSUFff(1C8M9qQR1G^xv+YbVJDqSH`N7q*9dzNa$RY|sIGM6crp|nIseO@M`vZuH4ikS zLmiIHbhpE%t>-Y`cq86TsTc0n>@FF*4X-r)aV+65n0k(LTYf@e#0#vMuTP41IFFqA z0iAnws7b30qat76=`dT;;N4@8({TKiyN|V2oTn)o0F`}T0X<{d#64RDhs*Fun<265 z7m1{ZtGJ)1QEvNhF>>D*k0E*HWw_*Uw&lY`%yY=X zP>VS1equuzBL-temN#AQYeK`PbcO0BANs!DjlARqCWS%i23AdyU zm%W*9Lb8qPx!<@$9P-toK;ENXe&<2Gy3k&H5+B(!B2)5C zOy8VtSBB}<0=5nWCWqhX`0h4ZhzkGfr62423Yn=y$^BY!Aw{>4!>w))o9Q zAh+ZhxZ|FT!(TU`>Zmuh?;C@0od15tUa#W=(oo&B4cD^lX~_>+>TiBkT=F6I70c10 zwL0SZ4Bpdzc#lS-_kyv6>Q2(iJj);NK^9;6T~wz=yWht_FGZUCyWd8)ux0q`yV&tq#6n6W9}M$-ATOD9@OV=50!N zWYP<2`_^OOOcx}->xPHVVqv-IfH19ii81WG8(Fpsi#9$+<^8W1*K<2|l{BD;eMVv526dEi!e%>eRXTpXwUmj3~WD+waN;#ea%;tHa)=DPtvq*oif$sa+YSL z2ZWaf4H*&&#f|>C;hOYYagiin$oeTw_=eeJEkR{P_Ltc<}5qTI&=U1YZok; zZN+CLX}bKxo0Olj->LhL!iph%S`H;c9=KeDpxFP6TNrfpaH zQ?dCFVUzV2o4YgD;HaZ$8PJR=;VPW9;orRcF{q`e)0q-GoDSq3nvFiKd2vXrtvvy$ z=4xTn;|)H&kfSH*EkZ^92kduhQ+jo>_*BhZ;j*8&KXNl(tnk7G_SDpz+KGKieQ`K# zJj(nJ!MGv-kMs87OgzsiR@ez?X4&@T_mHIekgz_ZLKo#V@F-2HYp%f% zS@?J%2^#Fvd(P|;tDWf>Rk{`Bv#cb(&lkYbl=HlwvLydhy)aj7<$f^pKB}45U@Xgw zP94fgTQ4U4G{ImCGjh6VE9ADQA}n2n)azpuShvX6mX+2ylI?TO4YJd2)n{A!Z&L#H@x9CWfCD@7wjiuJocu!~a5qwcUQ8H| zgiY;g#p)WtiIzWbWE5gtkdM{UBK?uyJX@gk##iBNi>L=6*lDBZG;C{pP{&Mw_( zdAg@0koh{x9gKM9dr#;tlf%QonlvTRj-GY6VsL{2jW)8R_QwZ=agrW+bhV&&vo<8O_YxR&vfHI_1&*ydo)XXa9P|W7Kut#8>uH9F4h*y{%WVUQ3GbztNiLh^FJOy)^bZ)=k7qV5*^q+IFEma6PX`VETW!W zM_d)Ve%2Ato{<{oS{R{psN?|aGk$b5K7LNoqNT8N?hYvH&0i2u*Ek1@!n z4=um7%6~O;4O-bLkhj9A+T{HcSXIZ=XKDA`he%K5ou?JS;64GN$h?TUE3JX}(g${FrXIh>E&#XQ3S z)FgLS_^r55`q@sIDx}S6_sl6^stfIEa;C|jrl8_(J6gCCapd?))DGT=8S(L)OSGfa zC8KeqF_G_JPV}>H3T7+p#MKqXbo{9_+5J8*I;`*bt=XzbPinPD@Ip##Qr%DD0#m31Rl&4^qfCO&yQM^KC3r9 zD`Qv8P(ylD>CPDxIa)SPhdd|7A)@g+-|rsaR{!~!P^?6qLzqdBHw-b$-@{eqJ#*16 z@Lo3=+yC0&Bj49g1?_=BeFJ9$YA`-{E)LCg^SEEGu-n)!EW)MY$ZHBc2^AIL#)A#w?ab+%0 z*3#wNv(V(Tsx0X*J%{3V%JgJI3noZa;X|_wyb{rX zPhHN5t<9Y5Px^xoGC~*)wcR(7#k+8!SFb>e2Qq_c zZa*<<@*7B3snG$aW^upe1TNXB)0+)8C~7W;^Lt~;E-ervOXA&w9Zlg;b<7~*E z^HXof>QQvOHT{@=l)rBwwBJ(|(JzCrc~S_w7SD>m1ExWJ_Hf$N(+0~o2J%e(5CZpQ z(| z;d;}A?)|e9zgON9liL;Ps)Dw#_;~?4*_Hj*Ai-bBhrJDtOK_=2P3em2BiKDV54v5v z%P(xUr@U$V*&`oF$KQ3Q1?Lj+Kj-ofXGba-wF4$`;goOfgG3`mN*pyAGjp9#d{db= z8uR_}qbp)~FQ2Wq0AmzoX?gS`DEJZy=JxI%djogd0?9y^z4Uv(WB$2N+O3x$RtBp` z7KLrafhpg`hPvmHH5<3!kY|EW-PNN!a@%T*;(6JXY#l069V;HYs8ZA+bLx=JlBnL3 zqsSe`lp+0GoV=rquglcPdbJ(Br{4JbSC6{NTGKYCGeXK$m&77-+VOcHbHBW(!(fh| z^lau`58jnk0eiTEQ8jST#QjK{+beRkZvvmu_<24_R%$Y<{cG5<&o!z4xjKys36i`r zx1rsQ!OY(nD^`?uqo_nn=tkR>pIT*3+OyrTs>4%K>ClSGM@e|Pv5!Q5y$s#^G#UeZ zPf0X($kO4^{RryJMas?-;=^!ny6#+nrq}TzgZC$^^_kryqmBXWX}WpOlAceNVEQZt z`W9wN2Jzm!pVj62g%kO=sz7DD9Hr?fkb2G=G|1jYYsxn)`ttyzS}PGcNttFZZN*K4 zQB0aF68sK1}7E z&D(T&evdozPF;ci8ze=D!k1I=V@;c*ZCxKY@ygJ<2!hGZ(Cscf_;Z#V1>N zdeKjVa@Tv3Q@T6#?=4SXb<&8`y?GbIJ2oR*4VpPM5u<;9LDKL?sL)&vmwYJ-u(*Wm=_BxX54%OIK49%2 zo-2LdjF_<&c)R~BUODf?QoSm1chCd8_!xuy(-!Pd(}3hn1gcc6@G4Xb3B_@EJZ%tO zMg0;DedlA$=11bn+sk;#&d2(m3sFC_1mSc4G6OYMVNoz5lbHJrd_K30S zi9>ffaQIHU*k`W@Xi1UYMh%qQoDR*-W^64DWJd5>R5(^4ps^pF8Ng@DnSW9LWB^t6 z+J;Ts=}DcWNnyc!PS|=1QJrE)5oEgoC8H<%F)OF$yqv_dxu z_cG%5a~g*CaG_1v`qcAeAzYb)1_DOQz+mPtIi0*M&63emHA!a@6ko#yGlu9z6VB2iiGduQAO|Fy5bXb>A-j`iVmMA z8tmD%#974!OH(B+{#sO1phx$zK8x2 z%5xv0Rji;m9MCTkbIxrP3eH(L?QKfghkH@m@{8zsjpu{8eMmLsHag68Xu?l7Iy@=@ zH5&h5SXs_4f>_uYy@8~@NVH3B!q?+cq@8brAvroUZ9xOB%~Yl4{_HX{`2@Ms9aweH znDXyDKzGiB{OwyRhLxMsErTB-b*L21Z8oCa9x+08m#-)eHlVTBz6$$-U<#rJ3{qOj zz0^QD^XmuH^Sxm+%AXdNN>P(tG}hj~1l!fGMa>~rnH|W&-B|~P`5{mGKJf(hR#_v{ zg%7Ebru=<#gz*U#>UCR>Z10)i`*TD3o?t_!Vf7;H0MGcLNjclTp-#O7lCv`Oe8q3< z`gjSZ@p=^g2~)ycqFGwW$%@#HF!N7BaP_bVoNIb)8KAC zKbww6B5wBs@!;-lIIQD$^)hLsS@M4F$1z9-zGmL=AE<@elF_nOT&5QMp3MA=4~@9w z@f8j}9(1olmGY*p!#A}`SkBgC=X5x>KKYCWW^S~sk7LK(HFVw#_1jzNjRVKdA;!MG z@S#~3-t!cosY0j7_TB*eQfE@alWX`8#?HBSHOO_zf&Ja{SUC44I=J)ncxWtsxIBij z!&J#8%g0D#4ou#e-;!=UxziTsMVV8bilm;EG6&a>eq8NAvqCs~&*#|zrG7NV{0y@l zwP|dtGYvC}#B=D-nM!F=;2DO>OXi#xHE=hS^BvAQbgfGVzW#Qi1C8=@bI)Uuu`3GF zlY+>2&Q8&L?-VG!51`d|qQu+qBiPv*#5;}Ng1PPmRbjY)FHp*1=j^PS9S zbXEv`Jm2YSEs+u-ct_BkGyB{p%lD@jo6llhK%)Qm5$5!axvolgzZI56Inbb} zqxd`gciyHK8l+;WMdj?gTJ_b3o^4X14|m<^ZF+a=Qg23TKYCG;tUKSs&Wrl4y7X@a z^D&hUh!d)oboY5V+7sBu9I;9~bM62KvW#s8HBAS>YRW zv9M*z7xX-SLbP9vD!k&pKw|oY&&&zAcplSP=#k__3y+;d;(`gH|0U*_UCCfiSPyy} zWk8L4I?xisTrAE)${u4M4|nmj^p)vpku+)KY=TU)6m8OP#-hHXaaHRNIx^qD;4nYC zmN%e#S~1x|aJX?`6aK6Jn^ZWl%uUqm17_D;SLi>?~& z@Uqo~Ob=ab-tZ68%b$thUpK_EzMXtOsTJK$^ue&vQY1p(`)}0Wg2tP*Xw9GKZ=0|g zPdSUxc(hPr^)8j?Z0w^RWJGJ9Hz4ClDOU3Qcx)qc@CQDHYql}jUbqJZ=Xux?5sb}E z(p1?dK?irLU&l7X^!7qbi5blfF-_8YvlR>X`%yrB3#yjv#-N)X^uBv7u3uh^96ldN zxv)D*PSA+Iauj>h5COROqC`Ap|8xy#GB|wLpr8JU&4dgo5aM(5?C$DWj@hfA-Wzy za{Cb&Crp&Y-nHfK<4JTq>mvD3Z%s4z#^caLg`$yULj}7N(IsJ%P%>by{0PpA*)Z#2 z(_gXPLyx+rhT`n3*TSAV(J9KY=y{SklC!GtskvAZyj+d@nzt}NphU8HjU1`h{KT0| z3o-0!s_3Zg>Z_333(>N?zsou$nWt%tNiEDHKDN9lsKgH2-|JA#Vg7l{j1|dtTJ+^K zdw{~E#l>QE`l72(YkJ9`jXgSnJC*2jj5#?tUll$4)ad;nzPoMo?mcTV&mOK@QrzfO z?A-RDA18S)G&~VQzI%}ua}SeD)w=o5Dp%J00#A>USnrkT(HV#_{aSg4S#_pdly6!=iy%=f}zYw z#S>ZfS!`R11L-P2K^g{C9D>oiH+UZZ9pSHS>7!u>uClBA-B=UqpTI1X;`bO`?Lim+ zs?ry;D6CQ}hX~Lj9j{I71mqL5mLc`q%3PI>LKOP-q0do0V9Ilfuk5D!6Rij1uh&6N zfi!fRA*vQJ-*eqftY|!dWlvw?pI;8HFG|O`gd5PE^B%`nBr{LpDLNVt`hS|ldpE(} zx=G*j3x9va0}BrdXd6^CJ+2g5DFGC`lUY8N?D2Ky{_9*{N-sQx1Px7cb+M;Gv=ctn zT9n=L4}*Pu=|{dAxo-W4h}D5KD_Wa2OEa54&xKZ8k)YQ zVn-hz9s^#@fxV*{wZ$i4`RaXG(BGXruCeFuu_Xn*U&!w^TWt5&r892^W3YTLc7Hk( zw1Y6Xk#`?EGH~Ho0q)r*;thZ1yX6&NuW>vE#%5ysoeUIke?fDM1)0_b;P490;B3&N zxL(ExbnZpz3O%U!b~pU{=u6QSIWW9GQDS$_gly9?@o183QFMz1wT7O;#2yvPm8 zeTWAAOXE!TOCS1hM3otS+zE=NVotJ0W65SkkEA zM{pebR$^JFORcudHmj}|#V2^Tr1u1U+Y3cUf6m))`h=XS2$9`;9hM|NW>?20v8(BT zWRIc?{W+$L{Cg1v$E|#5yhE+n%DX3<7z=VL?nC-RRj|Gyke1vDCZom5Xr48ke)Jwp zIysqQ3eS5dd{d=vzUDOEY8vu{5^Z2dVYc5Ab{I)fL$N*$UbPOJhDO4uY#U5dz36h{ zFn)(d!Q{RVWo_64_r8hj(&|Q&i~nKBsBF=VbFx3*$WrGBSy5dROuegXp}6~^P#-U- zQU0f-_?Hi5wr0csSa{*y4K8HLtSgfhLA=uj?F>zX(G_P3>te_}`VRatvZLK0wj>U9 za_7;8GPBglFRcsh=JQX}1u2@U^%K!!2czby6b-d_gZ@zwaI1ZfW&Z8>Zj}i|_9tzr z9frI8j&O%`3;t{Eik83Jo4F7IS)2a2)zKA=U8kaUt_uI!6uJ+hF@9AqynCaG1ltXm zRDD$J`t=ZMX`G==-HW*wZ{a^@DJo-c+?Xp@n3u?1@au_uhHB!UYZGLTWCs2Auc*AC ziMh4c#EgT#p?aoVJRRVJWACIWd6>TB?4i|2&3=ov$tUv4^!c6Gi?c&Uw7qPakk$yl(Y%Nyp*NIpMcLVLQy{GCuTj} zhIY?su+>tbV*$~q9wKNk@0LzaVh8N_-ju!k8M`cx#ynrBUpvf7UA6hZW$7k|711loN8{7vTP5v@kgQ5>lRWWPO;#7z}{UpR?lRZR#GO=#Yofg~AeY~}md_jTD=#2xW7eeNMG{Sxx3azuQ}3pgFqLbs*+ z#9Aj6O1kR?)!xzwZ&Ie7!+h}guat-~l%uiC!-^hgN9*O1(eI%?4YDz!2iMNvJm(w& zKU&eqq2cJRmoC|Cs*AGLF_@baB2>?v7a8wI!AoVc7^nyC*A0Sa!#QMRUBwaJhyN(O zfv{Dl(L6$hw$)x^pHC$w+&7}X1y1C#<21hX*@sF)dukhV5XZ|`;^-hR>N@p0-hGL| zso_zOec6qfKU?wBEdq~p62-u6!||y&77kUr#k8ZzoM{!%&oLyp29mDjEXZ4G(6A@{ z>BX7B*vaSqv0nq}#HO>n*Ug2~0S9qh<{loiyD9o$lcer#I(s>fWBr5rg7ds%8FBg` z#wQp^)W3D5)#aN}J?2kg!XGQLirI<06RU(I)(57XtFP7x$J|0oY`vpHlb#Gl>T4ya zt(2o5vFytV>wwvfmx#F>Oy3^xeC^>4)c)y1#pMc=y{iQq^Zz5u375q<=@8m6kmrsO z`Y@0Q;XCXqWEf|Q^FBN;R}|cj(jt=uYV`I_f;f0nn|fW+qfh6qOEiqQBk8J3p9g&u zF;jy@&oCvrdY`kp<7>q58YS8#?M45oDTv*C5At2yojYT*Fmzl96~+azt7->6ss>U= zoG#O`axJRGf=4G&chG>d= zT2M}*JC)q!-l?@QNhW~09;(IGF8VZ%xp77ObqZiUnbg}dQT>P6OuREHRml>+$81KI z!JQb~U=FhrIuzdP7e?iDV(LJ3deqFlk>_t2zyC60go001iS(HRJ ziY=jLbbLjus57<|%lV%B?57f1Q>XgxaBYN-Zaix0PZrKv)Pe>7Rv>xM+``v0+VQ0A z3|76mhyBCku(`h*-5$d^e#a!yXwsb|{V(G65mU}ydeZ|Lem9<%g0!(Rjjhrr_j(Oz zagHWe+L;pUYDI&Y8r}J<${ytJP_{e?!!>QpFloU}wG*h^s!km`k8vX+5mpO!p!13b zl{{9UGov@6`GXwIKFZzC1u>A%SEP5G_jr5W8LvNN;rZ-16!mQp-f6e+u5&j`Y;CbH z^e7y~S=dc`3HiTG%pJ6%H(h_=;lO6Z9o3`dgFfS@N*$JFF}IiZDEEg=LF=t@EV^t! z4=o2_^wE!4{N9)nUvI^^lN!|Ve%b%b--)A;ve1-%GR zp~?qpG+uEqg`7~N#B^yoS?ojpvt?;~Xr8#19f$Pofn?=0MSNVa5*LPq(Dn40A~F6f z-ueded)b$Q(p{)Wup@0-*OU4*_Mp}6%oj22M+ZZEnBU<*>t9CWV4f36(vD-u;zj6h zV@dg|_G8!CT{!WrCw*#Q$Jiu$!nPSWHrWGl<(!{h45*FlhSshgr2EkiQ<>>A#QP-X z?RA>YmoaY7HS{Rpx#nRrnl#W9R~Gl8R1;mAJh4#>8wrxW z`Lp z@LQRj;<}U8DOakzWJ9x8b7$tN59Pg9mUu=O(CaPb*in=rxwXxPEd7e$6`@`+hIStR;8RUXm{62Zl?cKDm(IO%+^u z7b*dt-)fH*ih+;SM0%_xEw%4M0S{!6H#$V(-^tmk6IJ4I$}aIPXE6N>xG8qj+%DWT z=QE-lyYkM&1GM8cX7T&)UQ#b)1iZniA3AjZ)l9f^ZlUAOHk?oLq=9J@a7BuJ+NZtf z?9CWFUA+fetnFw{fegj(Q4)Ty{3KqS&oGtQSom_np2C0qzoW9>Br%Wb3rWU@<{Gl| zY+Eu6$2-$McW)Zob_f@z1(T-`RJv+6%FUeUoj@k8Sa4XfAelbVf{WJHdQuWzE;=(((e|Q;GCpN`3i|9fb z@=UoecE2Bo@|st0Um8dwMo)r~a}n0P386zFGhs3B3wnnIlT2PTe$IV^H|l1zYi~Q8 zhSWp5Z&w=h{Urh&Z(#dwL$dECMN7JlL%w+^UI!~un;%fqmp$o@e~{xEjbDa(k`_X&4-8~-A$APk}TJ|yd2!(XSR7+LH@Wgled;GL`!v4xr5mz-un9dKg;~tl`rV>J%kk5@o*yh6WW;TaJS5#*2+J`W_BO{n{ZFa zAKQTY1xnOEM~nTO@#wx-huRKK6PfomV^x4QqE1w!y`JyIq{vzS@37)*<(%bT#Ne&< z=w~cX?{>vv$gmS)-jugknsWsGniP@r?j=G-M{y71q`1D6nbdNZ#M3oABOcv_X3l#k zT(yT%@6!m%C4`+ zN~a)p49Mfg=MX6N)^((kMSG&Lw|;c$_7aX7x+v80Zj z>i$zs+=rjmLuPp9NE+;4B2MNN@=D4J`}yUe>}@_;OP}TsZs6Roa~hJ$v?U)VSdh=( z2pFHaTe#q+1yPR#-WNuQg9CbE#78}HV4v6u4_zFXsZYQ6%t76y0IbkZpuwqeNQu*; zuH%~6Ga)EwpFRx@dxh-HeQ5^I0;WmHlet!Z8u89qbSRelm24k_q0L1i=~}DgF+iE` z*&eiZUX_@5mOpC;{YiaCgydg}KHc5QJTR&8_<6~X+N_8iFNQ(R#Gf>uc~G{+dR&U{ zL#Gn^lW$-gibp&Vj{CNVw^t)DRm&V*&3Xw7$Fryl{V8s1Wr=3SqqN$yMmX~mFY;^Lzwlv3Q5gbtl+%Y^#p0sjXaI= z3ICW$`Ufk|T!ZX6RSLhYPMUWdaqf5yX2P1;7{Ws7C>jFK2X+hVXOZxnN+cK{whoyt5#_D{(=b1JB=N4Zb@z& z^xzp2J5ctx(p!Emt$1KfYNz>qJwY(1+KXKOb0Z(i%@{D&k0iEdaV~HM;@O|l+I|?0 z@$=ZD)R&GeNx`mW8+tr^J7$#l;K6lc?glKvSOfm7OmZbd+sO#E_C!)rCij`M&>6go z9f^Ao-*g^Eceh|^LNQVo9D_psA{^xY`+TK-Fe$NT_lN<}CMP`9?@s%4+-SH>cjU}+ zCi!6rn6rrID2;}cB$orwI}cM zx{*2iS&A3(`Lf-MX0O;SUN9f0@b4?!kNPH>ZfLM0_XQkxzmv?2Rps->cdXnli&@im zKq@pXN%&+dLQQt9sLpy6;fT?n;Vs z>>}FO$j@@lbZ%dbq|qON8(P#SJ{&ESg!WOf$Yxg9rNwKJb|#5E&AsU50rp=M9>Uk7 zoHe=M$vmzqkw4m>&W@C!X?=Xe=ZDPGPI!medwB9 z$ms-JT+Z{dyM3tVjTltl@Fve42HYi9Vn-r>{roLSCz?B5!A^8=C-=!ebfLM5oQa$A z04sIAqs(&)9`vYSKfMCIznhB3J+p8$?i(5&*P`a~K|FYFh4mBakQ;FUotK)#_8Bjs z@LxDKO_t`Kb%Thy#OHHw2k5<4g~#O`xY9WkNwS^%+}w^?c2&aFq68M3WoTPK8qP0g z&sgpU7)B>y?t+V`zSxCSlCGj$s}AeJ4WO+3O-vZ|8S=yBuxkG%;d%NuqDTG~hSs*6 z!+VY+Cd_Q>OvY1}TrAU)(2n1ItoE2vY2ZiZ8-En+y;^Sc1M=GYiq+pm>qwLfd*N z(SU|>@v5#jT~FL08j{{1Wl;!at#THV!87z;-lUXOD>)-ohM*RCa?ai_+O*@4(TDRy z4YCNbOM@-5A>+7Gtou0;t=hqop+}qXbqCJ{dkmIL@7)O*1#Rlsf8EzMtP3rT{*B`A z%&j*w!WwhtsPlX?Bc?AdP58>s)R)lN@Klry<@>OFshBXUK(hIq9JLQof!Cub5jjeV zBIZgX@l|c%y91rfu;3o`9wTZpTF(qO8;W5MdeZC!%-Ci|^%;6JJ$4YedD;1`veX%-stymSn*-`d&b?uhUxurY!xu_d8nkHk9_8HcG+tYtJS@6I9 z7;~8;vwJ!--G^ks_w`X6)OMoE++;NL=3JthFRg7UhVbDT)X%=S@4ZdLsHHQX%AY^O zpT#AkJvcgG6q1jo3cb=I{Fd_PbCMbTPWtEHXgVJSHM%shXSf9IgW+z`opP(f3XRv? zz@oRMkei_+o=3gGpRX0n=lm;iaL&QuU1f-IODi-lvf&O@8vG(JNmL9i$@Fd%=A~{e zwBXs)y0`n8kA7Gz@g0G$4r&xqw+x=mw%AwAypz7eF=g;PWHRT$ab-XBZ|0q*>pLVZ zmXp+Tp5(~Ja!l~PEt%q|PDwxiLXLdJ2rF;VdUT2Z^2v*EeowK;&psj1Jh}tb&*uu& z9rx7LQk$){DR36^D5r&qJ#n(+eo2=UHY&h&{1$QGSr=Nxxihmx z^&+5`8Z$Y(Y3;Hc$?z~88uZYO22BrT*Hs@nG0c||Z*0Oimp(k>>p@RkH$l0_FnU-M zNY|P{i~nj)r6^C_y_`3eP`J-TtB7mfLT3l|JD zY0}joN^Ea|+%!d!<1_uy6{#>ku0(P^>7ps-1e_N(A@=@0k(Qo?C`*1fACFp1tpXWd08>rcFRUw|A0v#_f2q_b{9mRB+$$x(Gsd zcDUZe@HxH3l*_i%$r<=#UliDR${yEhV|v{Ey>Mj)pb--WTriN=obtB zGHE(i!g=FsfANI-<6b%K(E8DW{x=KxPNGZp`L7Vo=lI3flVRISof=iX;W_u3%a_QI z^6567ndPE|yP}t0wIjMr7V8Vn;O%KX|2(f2U;isb!h$_`{N$miU&H60_ADf{K4GtX z2Qz?lNVmBO8Y(TA`Bjr%xjw?|L@S?@LABlW}wB9VDseV*k@r+*?ouW9})) z#hyWc;#CCRNyfPP6l{4>hkHw_C1wr}ajoxHf1?rA67AxTxN*w3DADv);qU4j_*>du z@F3NOxrb+P=}}iwdB;8f`Nxp`O`YyGnA6eJUAQ~mf}XDZhuk^6$l)_H8Y5&#I-@W1 zpY*A^MV8(1?(~ZD@OgcuU~nZGYih^)&rCljc81SJTS!#lswp=`l`1p1rnyKm+I!Qo zcMep-KHdq;luykT$bYO5AtmeDZmFvueGERVOd@$woRi+uLe7{t2qY~D5KL4&n7ouG#(aoFI z7g^D}gY43B>_vV-Jw(VpL%QBSA6*V!6t-0+)O?p`L4FQm&q^!G`<{U}YI69?=bw{t z&6t^L0B^ZW-s`ks`o1W!Sbq#z7ui2Xzlk1 z$?7nkBkYWl{M@I-*~|dS*A10;Imn^hcNo=;mlONL3dFuH%+WBb#^rH(WIVhtYHn2{ z=?tF*1ID54>1zyhSE2m+6-aHHja;{IcH}xx&$onohLL#S=0Oo&Lt)9>fVk0Cv|@}3 zMUP}&4tJaekJ08mT&m>s)*$jOXk)i^f|xeckJ$)LG(RwiZl)wbe!Bx{Zug=B`~BQy z>B-%k5Sn-`606U-Qvdtrw0;}AoRl2NQl=YixZKA3Uq{*?qfN3_iZtQ92aYSe1(wN? zV`H-@c=DC+hf=h())@nqe!^Ss5`Qf`hy0e!{O)mpvF*4kr*-J8!kaZawV?QAF zZVI~yim)bFnnKEM;U6=ahkbYAouM-B-jt{0`I;zePZxAUhQ4U9S0K|HG3~Dqo7I!r zX6)zwRuQWCtU4)`GqCOrnD{1`4oTQ2#`D&uRSr}d`4V3}~;+(WE|hcFbJmd$?Pq zShVCL*5B<%DxM{hneJckJiwE79cKn&rg}(ggf}{Npp~zB` z2CQEtdJKrdmLC0u`pR!O?9GgNPd=NMqGpGQVZt)Skce* zS~_72T7Ut87C)6X4H&mPah?Bh5dvwf zYlkGIftdh-{@mxDjCGAE{=dV#=-bV$Sd|-ASi>I54{_^IQ#VlfMf%a7;`w;ZnfSZg ztwhu3NI10)$GFnD!Z2_Y>Sy_*?aJuFx68la^glh$llP`UyPo4pvN841?L%{KNzuAZ zDxB2?^Hfe@-6n1JlV*#o%JYaACP#gZ!^Ne`*U)RH4*4~y;Nm?~T4Yg=50$cH9l%bI zC7<9q>JyG^HKCW|%g`rLk!SrE#4P64e^u6mt^W&=c)^V#=A9M`Y@LK>lQlW%Do%qml9_z9U=|nF+cU(V?Z6Xy859Dc!UJK8CqEOb@hMYIj6w55FxQVi~ z$Xt$Ie00K%Bj+L2D*}%bWjK?63vUd!;^Ars7~MLDNy*1?X7OuS-}#B$-+GjKumfZI z{)EXbZ8|#pEtcCb<9d}ft>mug;d|rI#h?tWQd;DvHy-CceS~3uExM{31qJpUfZw-m zne#F4@+;W96=a{<1JNe0;iBSABXyZ^`K1h50VgoUU>ZL5z6uqebEufS0kbPoaFh8l za{o4?^;Ica1NqFe`Z0!@hKW4O|3tmc7i{~jB>a2qEj-bA2kVdDm1OKUr%&^ukUWw3 z#($h?uW=;2lx0cztRl4gHq1+;0=?^tj8S}VwDt1@?PoY?K!lT2S&(o9|Us-O3xR_QSD zXPFB1OMQlt;`d_BU>!2FuR-$i?UD+2eX><}4BL-Z7_cl7p-msL=7b9Z-=wl5^AE;e zD-{_JH}fv14nYGW#h;rCB;b9<_XsI=m{s_e#JbRwTPubC(E0*JJyS}|xi4v+Zi8VO zy{V%y+xPrUZIo8>-X+FHOked@l+SS^d*<&2Cm2%W%|R<HnwQ&%nw;q4vMMOHWOD zIw2g|MXRv+);Mf9WJf25^hK5e=ix3o($nI(C?6Au&H+aB@t-_JCEXP7Zu4ABQHe~m zbw%C{P@ds8=;F5MYUV@N^c<;GX1TbJM_tx2!Hb3;}vf_WZu)hkOzm)0DE;USe_6ma+sZrYI z>98=qgUAa8WVbsTc_uq?{_H7nQY(*LaL2K1wS+v1S27dd3$zfPM1`!HjN0G^4nat`vlH=+>sd>N9t~ zW+tGN=cP+m`_nXjS1o#b5dWP}#milLB@L?S$rnr-%^8MZU0Q7L7*?Tyr1?*SWL4TR z>D>^@e;P#p84EI%n}kXA%#;5SNF%pyWbX2RxUiM<_Y2kTb9{VF!e!)?8dCgktPnTb2Vc#h&GH>Y= ztN*)%gVVX6Abm&Z>zCu*NgZlk5Xy3cu2bB|G2azhUN7@#q0G zhIHX)f4&GEYeAaj2B=#4LyV8KV&7Sj`1O!kDRXp5+f@@bIeOwv@Cz99+5_RrUJ_|6V7c$$j``AKqXTGhl+) zY1YVl!4GqeBYpX4>~`ovT0G;{+Yt-*H&S#pwHY@*TtoULepb}A!5Ka{vpW;-?`=n4 zPb1FY-GarzFkIbg3xDNI1iw#bx8g^{8#lv{zvr2WGNi(c({~jbbbIkfq~vh7m0hAY z4HfC?tj(zW`4FD-G-yxS90ZSRU`Agn=M47XwdGrWSNT!g&3X9z;0-P%GE?-VJ4%&5 zFl)<$+D~y;rRD+O`7T15pB+uvb$q6|gcgS*c-?Xn(>5HytrOXtZ}<$kv%aFg(L?;P z4WMrBi-c72M||ATliiNP3eWc|!=K^)ba<>C_y3Zxw7({8sC1(Rdv{>?N+r4l41FUGO2 zi#a~sr=G=dY{LDWwsd10yOsN~ySu+1$?i|#UVC@W$L(Pri!#1nFs1i;QHU!wz{@V4 zl#{;z`UU^Qp`GW^b#FRanc1?b%U z()0~oY4Rd33V$6)14d|(;y&JadHd4lUJi7C_g|8Fe;PJwqL5F}rP@E2v9jrb*y?OT za$z@Nu5K)mRGE?V%8St9JbykS;J9gqu)jf0#i5Be> z_DQ^-JJZC@f1*a=fn>FrC;vTrgy(u`o+I-)O)*ZQU1Nkgj`mF6*v5hkz)05it=F&|uKO$aIki?ymAQdtC<$TGqv9c7lt6Ho- z6jLy0za{4vgXvoH5p-w9=!RR~+*?15)b-BHl_Cl_vWvU&-Dy^>DHW=!(HuULmp!(n zba@r}aMpotj!>rxi886=hBCkO0hHwA=wuHk-c7uQdlBzpC>(ab?&ClgZ8}|k9Ul{S z;E8RcxF}tUg2a<}KjX4kweB_4?}uaF=AXiDkvXy+IiuNS1SYH5aFqJ^^3SwSp$u1J;+S=#E|#w zw|g=X+Pr_uyjzFkvJM!WXGfp=Nl{L}2^gnlM@pBzVBxD?*!JF>Tr<_kS3yIv`|~eE zGebGRbb+{cvJOx7?iI<>N=1PMb!dCBUg)sL;bP%0;m^;IQvRTyMtzJaz#S z^F__l$2i^CiMZqU#r-vB5w=~LR4vWmrc{7QC)w%q=Y&{r<~R<;ONo~iie$na{s}2( z3J2X&r+DU(%sV$+d^^LgftE(dE-FAIGv}Vzv|;J6$7u9=D5AW+urIV21(H6J)Y?|= zfSndKrw>Wq4v-<6bxKH->MLA2T4DdP=PR?W_-2I(Zm4!v}pA8nchvK|GnX z1cC87lr&?!Xj~SASxX(*;TI(_E53sxCHL@ISc~ZK4anK{6d9@A#P2HZ34bqzN$Zuu zuY;UPxB5J)Tc%3P!yW1Nor72xdakfh(~dKY(Zaj;WZ0{5SJovIN2YdTjs$y) z0>EGI&CnjJN<31=#cgWjt=))6#mt&)GNue$=ImCLOH@^uQ6!|A@-t&@~f41XFWWD754gI1c5Bs6b(Ug4Tjc9I~9#l9B6Y6A5TQ@xwb=}qJ zP=*P$t*}C2dX>ltm#07M648}Yg0CIl4LNr-_i?D`E>S1-S+3OVG9B$jNxmQIiN4*A zLD{2U614f!#=`k9R&|ja8#0tR>r2shz9H5uN)Qib#6fSVH|F)r5~HS0f-tj3rsZzQ zwJ*&$VQS8vA|DEwRl}?g&P(kk-Y-g1w!98GJs0%fgR@w;Q-z)%h!HZzIoSUAH*Q63 z5-;ap!`ui>lDpF&6r(lBuJRikRh4P;VCKR!wV;@}U}|sl$SmR!w(@La?I4Lbxq@ec zvn_GhjD)w)k~Ll?E-- zr-2%eh1blk)I8pvV!CoFZNDF zb+ZH1*?-Z#F%JqWD^T`VmPQURpltOo$c^vdGn6*H;5&cxVGVjPj{8R3!LW^Ajb*AlF<6wNO7=yFC;W*MG*9Za%bgVKQnX z%aMQKER3_G(4+TLM2|{C=-wsR8&!fLrxc{sAA$V78f3gWD5546;-Nf|^y^c??S3^X zqI;8(`AkXNlgkK*CmL*KN}-mAAvm|R+N>M@`LVns>q2s(E8h`!BIKGeP3@w}`F0Pg z@Rg_Aw>0RdR{)iNQJ@8>GIZ{@JB3w!N3Nv>tb5GEA~W9mm{*9=3Ct3H?#(+RRpft+ zMNN|r9orqivpE~O^x2bk^y8WkD>IW_NzH2&UitacM+0-5ELs9% z>mKyrusaUaOu(O=!L;DSV0eGBrGI=jGGyjY7SAE4xKHCQzXRt{+$k%25)KsFaJOYQ z-i)Z==k!Q+fQQ4#{2HSBZo=p<$=HZoC~NlQ{iG#n7xc%zVpnQ+Go+Pnju^g|-R-ko zD7(}J>vnUe>-z&pzvA~nhY9r>(!lS3FM7Z^e%YFAsPS27q>>(UQcc-M(UV@?3zXzU z>oAw(f0^gqg;V=?qiu~|^tm#(Fs4_CxWrlfuNN+0Z1q-gyTypCaTGn5h87y}?t84{ z0(Q(aV``6sS(p)fhlvaF3L zxVgcL6n-8M?GDfVk2ac9d##LbiqM1{_ty$$KlUFKqKAds2hhAj=Oy<-Pl=w_nRjjT z9;F@pe@^KK_pQ|!#qL>~K2uRY=oz%FHOc?dO6ba^VSr>Zvtz7i>($LT{%It(JhY=u zhog8B7J_wrNA!5bo+|gJqBUiZ|2!jA8h?6%C}=(|88eOVMp9X#Ks%t&@3%d5T-_td znwx>=<7~+<)UGK0%5gmN^&r2D8!G=@1q&RjRe%yWm^M!`gmpwkc-6OH-ydoN_ z@{p97ge{!IJlvxcrCGD_Rdi*pkr8e?3RrN?^yxL8`#3Lw8?%oV-U!0%37*ItYK7U> zm3Sci6Wy1lK-nrE-MnAJfO~ISLrS1s(2C`?SK$%yhZ*4B2pr{s-Lg`2WUVo_xHpS& zVa!B|SK}erOr#zt<#}-cU0$;p{r_FS%i;j?J~sw2ot5|?Ka>pjM_{*F8Qe8oXv@MU zs9aylcVSOZ)LW?}~<`9}$s!rAYwM57bGv<|aVx>hcTF=*tSvQK{^1O<( z*zICx_Aihw$VIUke-EbqhMHJKX!#El&l@!g@Kg zpr>2)nQxPUUrfzNbEOJN`>vphv#CzQ^eO8lcS|}aQqNp<_`V*1HSSYM_U}xw?}-r( z?f0S7S@!6R*F~T;_uU)sVqLl|?Tq0w#qa63%4eL5d@lXu{s0;L4sR-RqIs*BtMfDy z4qiO72s(=sIZ-gPrxqEHNmc0xC9c56hsKt<QRh@4T^bnD77#V ze$Vf~e|jaFBIe6&97ob;^`Bs8UMRbz%kCf}o>LZN%8yjo(CXY|G!&$W3!COZ!pzkR zo5JChV2g$bLmD?|E(X}_#d6LY4{2MAemix@u<|t;thdPCc`;AY{ShL3wC+^0BmGLt z2aFwXOe{JTFFVgpn|Vikkslr;=6&Q2aorYNE*HY;^Jv;K$r;MgdgS*?i}ZyQ24#aO zBFvD6@9GjE)SnJ6*Q44DTcn<=5j%+=OQ%UPZ(~0eJxzj#e_mqg$Z<579@6{Od_ZFOEC;FiIv3Ovz2W#~u z7?HaXN@qRL6j>nLDn8?ZnLh8;-N}$=#=*P`d(QmmAO|J-b+0lRRmvzwmAlq?y3{|- zSp@dv<72E6ts7$_3~(1GcwY2Ur%FsPGNB8_ZIHI9aZk*EdRMf<_1g#Zs94bXv*q~d zz9!FruZnlWjVR$>udp+CC*%#rl&*bHbUVxwtIlcA3Hx@jEI2_tOKZiKP4U=avP_Jx zdCi@TAPoJmi0_VlQ0z^^(9|OCrsfEX+hfUnwP~4 zcQ;nUT6z|}Yh0MalMQ2^A}9p6BFq01BH8JAyHSB6!`^a!Lqd8tn~|!++1CUs%F9=! z)D7Y2Nvp=EIef0@+Kh!AuTdQL9%n|xppU9?-j7`ZDL0QTO5lm=n&<3lm#XF^i5A2D$i?>G{f zCsOK6T5nb8<2`m9SWh6ucIHQRw8H(vSc=j9heP6qr%8dRK44ERDe6c);)#1{Bj|n& ze{Ylz{ojrJE!=~hPIk1QnEk6#CyIU{<@~HYZDt;nDQAQWa>lSXeJ2W|Cez3K@rc%5 z1HBekYE$z?eRTjbH+s|Jie*5O4QU_W3rBLoVa_}*H{F0|IUcC&<2g>>9A^Iv#cQir zIN7~|jB}61&0E>4UW2Edi}1(4l5xE0PkhakAlb`;D%u^`>Fh*)-V(BnQe_^38+Y_a zQOS05^o6<*I6oWI$A0UXL#a}=0s&c$RIg!3C2tckvB!nt=Q8{1n}iC-jU?-*h9W6+ zAf;X#MjpYp@9cE5rkK&A=(Mx8Y{&Q%Sx$g4JukS8ZiOJR_kbl$T#QgZkLUEq)BE^-DL$6l!WAT4<_ryiKF%V<;sD>nd3Zx?*(bl zD6^ysmz>FvGr)g^xzKqJL67z0FlQ>ym=2lJAIJ2_W0WE7<{WnhvtqVBv8FgNXtOR>jbqb`j=- zn?Aa6mA%8!y2lu?U;9&8-*<7uvL zT2Sxy#*>SU2=f55P1vO~yNr9a+@~?!h@?ZzQ~B>y%FBzxtL~qX@xY04=l_OLKbOK^ zk+bw_*O1bDnR|@GsF7#=r?*;_Bx?xJK4>qJXz?J);j}-Jd`&5nC5-+H~ gA3-`j8Wd`{8kWpZ@*bi>3lD2TDb1QHKWfnb0Cw=SfdBvi literal 0 HcmV?d00001 diff --git a/examples/water_tensor/dipole/validation_data_reformat/atomic_system/type.raw b/examples/water_tensor/dipole/validation_data_reformat/atomic_system/type.raw new file mode 100644 index 0000000000..6c71c85e58 --- /dev/null +++ b/examples/water_tensor/dipole/validation_data_reformat/atomic_system/type.raw @@ -0,0 +1 @@ +0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 diff --git a/examples/water_tensor/dipole/validation_data_reformat/atomic_system/type_map.raw b/examples/water_tensor/dipole/validation_data_reformat/atomic_system/type_map.raw new file mode 100644 index 0000000000..e900768b1d --- /dev/null +++ b/examples/water_tensor/dipole/validation_data_reformat/atomic_system/type_map.raw @@ -0,0 +1,2 @@ +O +H diff --git a/examples/water_tensor/dipole/validation_data_reformat/global_system/nopbc b/examples/water_tensor/dipole/validation_data_reformat/global_system/nopbc new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/water_tensor/dipole/validation_data_reformat/global_system/set.000/box.npy b/examples/water_tensor/dipole/validation_data_reformat/global_system/set.000/box.npy new file mode 100644 index 0000000000000000000000000000000000000000..382a14b7b6e7739ef3a0b7a50d77649d01f6b1d4 GIT binary patch literal 3008 zcmbW&UuaHo9Dwl`k;qzZ+@2*~wwJ$=vbob&6P!953d?`>U>>PekS zcZBhgC|w(*>+;!jCP?SIq7zZB*xnU&gn!RBkj9=Zc9xj*jk+ldV~Kx zmW?gkxjpsIVg8n4W1jlefH~%+KiQaD%52QTvuw=ozOpeleq>`l^OlV{`-Y8q;yD}h z!9-)8e8xHE{o`!R<4@U`AN^)yzIU2?!`xD4WBz`j5&OqHvZx7-`ElniH0H4q8*})Q zjrs0pHs+Z%%~+4QewdB9f0T`R;~}ocytz0nRCon68CPNTES;%zL9tr=1r}f zV}5mHs+B;W6l;h q$2{H1##}72F@K2Jn5%l&n5)mSG51|$V{W^|#{6lBjX6y0AM+n#45vB( literal 0 HcmV?d00001 diff --git a/examples/water_tensor/dipole/validation_data_reformat/global_system/set.000/coord.npy b/examples/water_tensor/dipole/validation_data_reformat/global_system/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..779b44bad5699b21080f7cb20b3e333c5c057afb GIT binary patch literal 184448 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 E2jc}X^#A|> literal 0 HcmV?d00001 diff --git a/examples/water_tensor/dipole/validation_data_reformat/global_system/set.000/dipole.npy b/examples/water_tensor/dipole/validation_data_reformat/global_system/set.000/dipole.npy new file mode 100644 index 0000000000000000000000000000000000000000..09c433b9311d01c3bfae3473f09be7c00362f262 GIT binary patch literal 1088 zcmbWr{X5ln9LMovX%?EhkZg4fI|-v!lAG`6>t;IIsG$tGTTY#h<+P(zLqw&Bv)tR< z7hy^}>2jCzexa!)tJYOo#U*m3HP_TE-Ds`-3w!?deB}AB_gfdDAxID;iee=3n-n4! zp~!9Xa*>@-v{|m$p@@!)lq+H+|G7`}SMicJ9xsjFE_w5dogM9jAKTaolZF2`?Ruv^ zPWIFTw?~aIURTXLr0c;){7PR|YO0TpW^G2#zxae`=lC>i<9;S)1Cly=X8 zYyOYy)Y`oWey)!j&HXI;up4GVCTULQ9u_)ghqj%1nAQnkCR1WOacCsjH65NC35B*& zLdQo>v*vw%aA=R<^8QI;YA+i^JS<>~Qg!*u=bg}!)2){6Xkvo$mDnlk;xxg(&u#Vfp9kNJkuu8nbn5&-rw+ zzi9#aosP}`X&bp19L3WlU49G7>8zCx1cm}WGWRg`PAx^SxhC)EwV!Mr#-ZnR8%w*} z!P%=0;MQIZJ~>Q52YMtZyi(3QJ}p<*zPk;ofFrDXpq6{2pMl$L=FHZ{j4MhCgzF)L zPQPO%%>RA@CjU~gkhMLWcp?zNdd;jNVlF)?{RAn$6mpGGI+(pY91XE0Zd%XV>4#^b zxE7E{{?aBgOOWGk$b7O**2dh`?szphLRYhQlSX$iW>pQ**Y4gJv-QLNt=~~xuqPfQ zJ*KMeJ|h1Q@%lv*#khT;zGR<{4Fl%1uRf119Qzg~nO7-(F-P)c`M6SkmHzJ4AjSQ3 zOp7m3aaf^xNp1!#&KhHIs*fA@O+oPnWB5llkvdR`d#-wT5K~VRykmj_G8BF+jYEHyARFHYU~ z@*^0^3~+j(h$Tq3qM{><9F}TQYgQtZ8R0Z=F`h|FbFtm@1H7|e%vLw9z?rC37=EE- h&SN&XJTC~QVP$O1QCCDJ>tnp@56(sGhf2i^{R@?y3daBd literal 0 HcmV?d00001 diff --git a/examples/water_tensor/dipole/validation_data_reformat/global_system/type.raw b/examples/water_tensor/dipole/validation_data_reformat/global_system/type.raw new file mode 100644 index 0000000000..6c71c85e58 --- /dev/null +++ b/examples/water_tensor/dipole/validation_data_reformat/global_system/type.raw @@ -0,0 +1 @@ +0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 diff --git a/examples/water_tensor/dipole/validation_data_reformat/global_system/type_map.raw b/examples/water_tensor/dipole/validation_data_reformat/global_system/type_map.raw new file mode 100644 index 0000000000..e900768b1d --- /dev/null +++ b/examples/water_tensor/dipole/validation_data_reformat/global_system/type_map.raw @@ -0,0 +1,2 @@ +O +H diff --git a/examples/water_tensor/polar/polar_input_torch.json b/examples/water_tensor/polar/polar_input_torch.json new file mode 100644 index 0000000000..b0329ef609 --- /dev/null +++ b/examples/water_tensor/polar/polar_input_torch.json @@ -0,0 +1,90 @@ +{ + "_comment1": " model parameters", + "model": { + "type_map": [ + "O", + "H" + ], + "atom_exclude_types": [ + 1 + ], + "data_stat_nbatch": 10, + "descriptor": { + "type": "se_e2_a", + "sel": [ + 46, + 92 + ], + "rcut_smth": 5.80, + "rcut": 6.00, + "neuron": [ + 25, + 50, + 100 + ], + "resnet_dt": false, + "axis_neuron": 16, + "type_one_side": true, + "precision": "float64", + "seed": 1, + "_comment2": " that's all" + }, + "fitting_net": { + "type": "polar", + "fit_diag": false, + "neuron": [ + 100, + 100, + 100 + ], + "resnet_dt": true, + "precision": "float64", + "seed": 1, + "_comment3": " that's all" + }, + "_comment4": " that's all" + }, + + "learning_rate": { + "type": "exp", + "decay_steps": 5000, + "start_lr": 0.01, + "stop_lr": 3.51e-7, + "_comment5": "that's all" + }, + "loss": { + "type": "tensor", + "pref_atomic": 1.0, + "pref": 1.0, + "_comment6": "that's all" + }, + + "_comment7": " traing controls", + "training": { + "training_data": { + "systems": [ + "./training_data_reformat/atomic_system", + "./training_data_reformat/global_system" + ], + "batch_size": "auto", + "_comment8": "that's all" + }, + "validation_data": { + "systems": [ + "./validation_data_reformat/atomic_system", + "./validation_data_reformat/global_system" + ], + "batch_size": 1, + "numb_btch": 3, + "_comment9": "that's all" + }, + "numb_steps": 2000, + "seed": 10, + "disp_file": "lcurve.out", + "disp_freq": 100, + "save_freq": 1000, + "_comment10": "that's all" + }, + + "_comment11": "that's all" +} diff --git a/examples/water_tensor/polar/training_data_reformat/atomic_system/set.000/atomic_polarizability.npy b/examples/water_tensor/polar/training_data_reformat/atomic_system/set.000/atomic_polarizability.npy new file mode 100644 index 0000000000000000000000000000000000000000..2aa2cdd4f24b43064889b2de8c0860da82fd3a19 GIT binary patch literal 829568 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

)q{C_R-gUTfz5wn0Klh@A9*L=2P&a`S6DF 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 literal 0 HcmV?d00001 diff --git a/examples/water_tensor/polar/training_data_reformat/atomic_system/set.000/coord.npy b/examples/water_tensor/polar/training_data_reformat/atomic_system/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..baa2c0a7c38c17d134c5157850d0e7bd5017692d GIT binary patch literal 138368 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

#S38mj9nBJs&s|FKPR_& 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 literal 0 HcmV?d00001 diff --git a/examples/water_tensor/polar/training_data_reformat/atomic_system/type.raw b/examples/water_tensor/polar/training_data_reformat/atomic_system/type.raw new file mode 100644 index 0000000000..6c71c85e58 --- /dev/null +++ b/examples/water_tensor/polar/training_data_reformat/atomic_system/type.raw @@ -0,0 +1 @@ +0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 diff --git a/examples/water_tensor/polar/training_data_reformat/atomic_system/type_map.raw b/examples/water_tensor/polar/training_data_reformat/atomic_system/type_map.raw new file mode 100644 index 0000000000..e900768b1d --- /dev/null +++ b/examples/water_tensor/polar/training_data_reformat/atomic_system/type_map.raw @@ -0,0 +1,2 @@ +O +H diff --git a/examples/water_tensor/polar/training_data_reformat/global_system/set.000/box.npy b/examples/water_tensor/polar/training_data_reformat/global_system/set.000/box.npy new file mode 100644 index 0000000000000000000000000000000000000000..652530cfe8557963039781971f3e2559c9ffca0e GIT binary patch literal 3008 zcmbW&O-NK>6oBC|gTiPCY7<0Ug1QkwGRc zDuQYg1^p}<+P0C3AP{WPqD@ed2vaClh(rYG&UbiM-Q9ENaPB$ZcMfx-?ePAC9W}B3 z*g%+ziYE%;wjkV*ZweDZnC~r|D`d}RdJDN|{(U-orWnn=FZN{nqB+-Z*_;TrHY9>l z@V`%8>fWwoY{6rW-`b5B^WtJFV$7>LQ;0FweA|l{^U#4bV$2Pr?8{uNiZMrhtjBzN z6MHwW-@XSmo4Y#LmwDq8#+WZOvL5qv$U8O9R@l3FbRX+654~nR=1Wt&FZ1#e^O$Wm z7mRmo{`Zk~<}n|6!&x;S9cInuXYZNEJl@7LG}o4x$9!lVW6aH6{FeDtKkwZ<@|nGx zpMPOJ=E)i6F+Zx>Q*-A;68mL-ca!y)@BiYSnk#YcjyX5Oddy4nJZJNrS)PwMvxI$_ z2Wxrn<`rf3Zoc@JyJJ57o9APWA7jntHDio1|M|ce^UHeHY#y6t9&`H(-l=)nSLQLN zx_KAosq@TZo~YUjb4wlbm``5ijG0^R@(j)8PyCkoM-OM-+*slKm>;KkhUS$&*_XNW zku{sYx3YKhhH=h@dANx+o8Mkz@8(6#%wyg;z+E<%SF?BXgF$}Fd^yA3&H2;JV@_86 Zzne2x*q8a?E8ds+`cvjHubpIH=6_axjGX`g literal 0 HcmV?d00001 diff --git a/examples/water_tensor/polar/training_data_reformat/global_system/set.000/coord.npy b/examples/water_tensor/polar/training_data_reformat/global_system/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..4f6c37e77a3ecd7729cce35d5982e87f7777cf45 GIT binary patch literal 184448 zcmbSS_ghZ?|8J+g)82az?Ydu&XH&}FgpA01TcII)6cSRB5fzb;jP{VMN)kzmhLvQr zwD3Kjf8pCt-S>5MU8mPM=lOih6O(36oHCc6Zwp_f@v@LjOE((3>KMB(A7^Z#W4!#o zjhiB`mULV4JHgUGG&~b2bG_}y#uJix<5nr;c0q?$a z;^Z1d+NPR^>h6A6NotaHO&b>22~prGN%FoJh-1d?WMU?Qr&B#4z0HlLN%){Yzye2) zj3>R}_v|Q?Y5ng?1XhG#%ppZu8<&HK5l1xBSbBTE4eHBhA^FvRh%#$}-M{DT%A1wI z^FdTtePVv|Jg_mX3Sn78EKOA-}j9o z$L20>HMO8)K#WRvAI9%61KP;O(aiGa5QsCTD+`UtJhvGUwMtZJVNdMgBdnI30;dg@ zw7yE5$~=OgTIo*P=gCsgr)aEEGNk`91gPp<81_HT$H%Y2bf9HE&c8{4h4(1NS$o1d z{3-NBMQC04Vf=7E!GtR`sYoXS|K)YE2kB}w>+~_q>)*$$lPu_3uL;G7#j_&{5+vrQ zNmswU-*R6){s3WCq>PFxD*NDh4BmVN=cv6y$)#lFh z_3u+&lT0$ibfqZZ%D0knHnCV&CQakN4DlW+XCQ3jUpRCaky^3>#R;m@l1FOfqNGY^ zF6xs;jvak?u1JqU^+`3`n4(o95w(AehfKRE74JHWfmiE`K1bS;anWXshIz6pUrG@Z z6N6p9bJ5sw8U3Nb7%D2pM$sIkzDUNw{?q9ECqwRoSFp}TfO1R(>GHNLnC}!NiE%14 zr|m3oL7Z9=|DYge6;EFw8zFa_QSd&9C&ZtL9v2Z(xwW6oSbq(I39f8%_&b(rbQrS@ zuCe9Cs#vh-AWn!|vj?3oSYSscmL{8F+L>**BFdxx{Ffo_(;67P_alER16)W;#{2C= zzqhE+A$xBYs3MK%EBX{=k<2Twv4KXmD*bsX%G}Gcm}AosyvxpH>(}CL zKOeRSoW$@FORQORjR^+Dvj{^w3OD$GbunY$+hs+=rah1n6lJSt7*hVUPtfp@#&X>< zc<)cZ33Ut1X{g8VJ(uxHxR2GB)ZvNXPDHl6#!6>RXuljw$!}glsA?WWC8VgP^b@Yj zxZsnrAvw#hMQ*)3sUNo@*YhXgW~D%efev*3=32~c*Q5uN^k`H6Z=8S3-6vN|suFHP z@_KtZqijS6%DZqQPn+%^ccd-{Ta2H+04m!;u%yfew_YEBSMh$hFAs+9)}`30z;N~G zJ1nyX9lfdmE%g!PnvN&)N!s|b;u&g=`%=9nkS=}@9)G^z_rPN~-A;$h^iG(U8 z8EDw(ioI@@v~lPn-f54cpqU;tb7eSkj4kMeq%(EYCSy3(m0B+ZP+QkQEZp4yqcTY< zejm^KH}(^D`u;;ovNmtpy3d#rsz~+?p=?)I0u$j06S$mr#}np%M%elQRGMc}NLXps=1kwANHZca|EJ zpY6xl!W{G#DA8o$H|X2nfEqVL{D@E>6N?M1Hjsyjp~}Sj?amwgPZv)F^eFvEB^!~H z@%Zy2(EX3LIW0|`!S)$Sv(m@5dv@8TgHviRb}CgMJn57e_hYaPrDNYw#eqpwQ z3aOe7;QmQ|nrJCa!|iR@xAiaL+;nMGq%et2_ar60O0@lWih!&s^x|{@ChF#6ZM-vy zZ@rGI?IoD{br-hX(SywC5)8c!g~38&+{|x6_|_08R(U~2<|?`$xl@JJe<+Mr!}+=1 z)U+!RN&iV<>O4=9F7$?1#{`6X>|~)~;?zESDmr)EVN3d@snJ9Y@#&Y?*!yy{{#i6m z_?l3WloWY)?&kKEKIM(7)9aRSOv-npo-jF5$le8yr$Q{dshBBF{fHu73d`3}gkniM z4rzt3zW2IpsJR2Tb~Q7lX^m`Qv^&ppegiX|Ylad3E{`0$IJVTgnQib`!TvjQ38N0` z9!{zms7uen;fdmq~>u(1nSMh@ea*=&^lnLr^&HPLB03t0*R~Z?I`kCiu>hDk(&7&3F-Er*XmeNSoTcyMY90?_mYpYY`^j*@0O;gX#?=wmk5s(cRKu4o((#0 z<*vh%3R2dy_rsS^v(kfRJh{dSl)f;9U+)pFu13K%vKUkK4~>pG#P?qcTRH6~Vzgu^ zCC`y63bv!WOpX$2EXiLX8ZuL~=~k#6F-IT3Uy39&MJRmpbe_%CaddarFb3SAaxY&kN=G)S#(I%{zmV~9k zGGrNAkB|p<5t5`p{{r75+BXrO&i}=o*k%;!k0GC9nHX3%idtbYIuM?XYZlLNphSw+ z*~cSs(1xVMyl83b1xVUB(&Pm^YHcjVZdqgMJ2{CKl&9fg-3i2=aG=+7#At2dO{j?3 z)2$4C64-nUj&Ho_b+0tH8`j{${2atJjwSyAD+~#i;f}U4@f}%((1a7{t5TuwYvX`{ zo9wc%B_$4C#jT~E*|VQU)P@sKx~Ic7UUnq47BhNfn8SAL(fx|OJ=&C(8~29v&SnD7qk=sHB+fXVJZF=m?KQugZ#r}$^P9GX7ekTH5sc@ z{$kCNIFA-KHd&I~)x_EK9uv0Wzf6cG+QEH-7;OKP;K$sF$U3PH;gdHZtJlx?FA2dV z$`kdIOK(u+|<7?Rk!o3U!`vr6z>Ef53g~L0%b2 zVMpp?X!;lNEN@Q4kw1qacH|SD^nAopzfA6&dm(cEEq<9Fgz>&sI6L!`=KZmBaDqIE zF6?A^n|z$(H;GqpW&dStvNE8U;2fNCID&p*B`*Du|ANR`3l0#XYovZ240xnLuXG3j`E$rmIcX( zihKw=t=-V5ser)rCamYhqjz&KIu_ZGmVF8;ANph6FpsW`y@+uo)9~_-DLv`qFqoY+ zweGd0;>SjiwzHs@!H%SQeJPgRaHT&doT+J(IA&fi-{hq4nVv?yaloT;!{W`0VYis}v*r#y{ideUZ8Q+0qH7BZ(kF+Wz(XhN6%j-yV$ zJ8U4+p3WB7(2FZ6Z0!;?nmQWE#*dajV#z3bU7pN>HQm2HM zmXD?5U-XHeenNi#T$XnB5Ecex;e}HU8`_qEqUwu?+G$a|OfLyJ`?J75KL&zbVrWRN z!t$cExId~71G9UuPTPd&nGe_*hqq{IR^#Q~3c|n7icsS5v7v$(+)YkU%YP1(% z$qHW_v?*g#M3l+UV*xIt3gFTd9a=DbGkSt@*{de*Of?MgwAGa;OpwH@Ijn2%L#y2X z;(~lG6c&4vj-EKZ5sroTD|^=X!JR@3-yvz^ZFZUY(q@wZs9iq6UhK4{y{`2z`}3IB zy-W~4?Y$^0ez72oj#wcL$B5zW}|ZfrUz~z&ZCa4;$CQt5_G>*nB#!2aWBc17PY>@ zj{UD8FkY8V-mApAz#fbg*wgaT&-keR3%}>@8$A=EuY6 zd47P|b*e(>NfKihM5r-E9InsAu|h_OraW{;n0qYyIZvJv9*EQ8^&cVI;znm)$&k{y zQ5?>0 zp+-r~6>c9s7ADo(pLo@Ox=^*R8=ZI0f*0ydhYvsCIj^{c2N?l$y)lKkbsWK}bSLs~ z1n*hfAk*B`kI7m_6m>3xy)^j)t#9gdr)4Z+RfXwb$NzS^8-1=%#FQ%<)L7_3K`&3? zsgV`=j&Y*gq>VT@s!D5~%FsNEe4g26TMD?PO;Q&Nc%I)l3}P-p#+Kvk{js978;-Q}?+r{; zGN5PciS9XPAxy!M3UWN?gm)#{k0xQT-iGw~6luLu8sZ8q$ltOIB)9vC2s((sfop(SYX6NkRRmWo$M# zXM;konDo)FFJV-3&C%eyt=;GoB&@X+0 zh^TR-Q=83U$O^1jY(%c{HQ4eZ8;ew2Nh0q(o8g)OVMigl^?3_B=6w$C#S-*k=rmJ9 z3{E`w1t&2MOGhZt+;|1ro$WyFGsaRvq#hl6Wk7i!bjjwAEVZuCrwsv_D4hP>BVd&& z6>aA5jr%-SA*M+a))zrCL9uk#o2R&39Swoy4^Z^_7SjJrg|lrn!Vf%v#)9Lh_?d-> znaXt6A{#$dhM~Prgm&z?0{Z|#n*T$KJXs+;szqtBvMfEgGoFq9mw`q*Sy~x)ljYw% z44aJt^wcPnX|?BIgKi4Due*@dEZdFBurfBfr=VEr(?Q(7G|c*&3Yhuxop_k;i)a-B z+w4tlS-a50=a02Mfs~Q90f+aSg72X-mG2dy+a%2V-;}ZRVKLg9HkL_Um0-ReGIZ8W zkhfum7-V*(qU_{!I34a`i*t){=+Gui@ch7%deRa7a4goX6hMTtE*?(Nr6(Z+IC;ky zhhyY9&9D!aw}Ff>cL^bX2^#Zz<)vm zzA1ZR|0_LO@$dt>9Q|=|svWgoYry=P{}AwZEUC4HBhF5pRK#4UEhYgi2?{hTj!1Rs zUX0tONd`&g^tI+Eg3Gk%v8Vz0X}m@r_r7*Xm{H5nAMCLs}%{CxNhHuUJwh+Hz%);vcWr-3x6E=AIZTNwHB360B+ zngj#^>()mubmr9)TW6WdP~LrsO#E`;~Na{m)JTXZ0G+#xJT%S6Bb zBgj=OLCP@?{F`7&@lP(o&N%{x3v6ju)d^^R@x}73=G42S46O}DBp72u^h*QYV~uIj z5>87qp8@$jcGR-aj&^_k%XDvKBSuJw3{)quZ3`>#Pe+`-t*&6bx+VDURX;{zT|K-N zvzf#84EQyH-fR&^rNALvwkPUnsbbNiWe^*3rgd7%bo7c9_Eh+x_N_9t%ojtRrV>`G z>C^0!f%vb%4`vd|w13$Meq=pC?{hgS+4~lP91m}*(k6vJqp-2K3I7Y#?6->+xi$MS z2Qf_~DQVK$*7LkMpI>r=JCmTY8k=&&h9)0(rm*9endD6anpp!5pB!iY zn_cMbn5k^*iDHZq5W&j4D0Z zBM#uh$pVZ7&STM)nYg?=4{up{Nl9P?`cCFSX=XG+J>{Y6S_Zz##ps*n1}Eq1Fdv+Z z;NzlLzO5e0TNo*9Uj*?&YfhW@qxIsGA=D9uqGm4=UJ!%L1{N5OzR#3iN>Xj_bZ(jb zWUIYZDJ*Okx~A2#fEERcA!P)=aiNxJJuneChBHBar1kJ0UKzwde4ht3o_!6gS4ZLM z{U7hPxev`hHHvDh1H1=|9O>5-5jwg)jv03Plf1eBh3pY!FI2{$;N*MWZQE9lrVU0| zTX~$RR^DW7-V$(|^^MoEI3F_~j$tnzUB-`Pr}1#|KGrUB6L)@ALiLJuNypTDZ1sPQ z?Ho?~v{fJK0zJ?@uS4%!e=+~9PZ7Jtn!9FWlrO2mcTaPArSlSFK7Pi*P&3ZAXvVMW zPY}xKy}_0h7(OpR3l1#94jWIpeQgLrPLWV@ol1eWDg8_u&85z4gOYAhZkX+U#`(=?Lh$Zb~#x_Zq>?uQz=+VGLAwE?wH6QUi%oqz#1 zbAEk}L%&ybVOxG6UMZPS+5kVbd^HB&W;d$#9YW@`6-s4`&;f^0K{!D1h^WBxRJ@nl2d%Egr#1QY`&! zCP7>EBx#%J2@H=>){mIlN=vGfv!K04jOg3b7~an*ZghZq z21W!kip2h!Q#$80G>7jkq1W%>w(%N_u8C30;1k5lB_Q;IAk8xW0j7BcPOi%2=Jys# z_l2o|oxs$+e;}YThBj!PhVSiqTo#fg<&|3ydeMOMjht!1lmc|8t5Y6)Y1W(qq@>!@ zrekjOL9PVqpORpG$b#N(RG{ns9fOv#2f030qA6FeB3s{tR>+Hzy~=X<_2$A|L7vLG zUC}Rl3hNfjQcRaC_#GZYaAYjGmhD5k%Ub5M%ZTJmkHEb6H&24o-*Y(r+Pt@v$!Hi- z+I?-}eUZhyBYdSBcM4J5*B5M&)GoAr|I9s0I`maX zfgaea5}%v_&EBfX;Z1!iZC9k1l4EJ&dwCLRG^J&m(z*ZHk?s$>kX_+bG)Q<*v4ak^ zR2E_2A9oE)uVI^C44&S9iRVkQajy0hmR!rjdCt4K+cXcCd3A6!R;KNW6*%zX3(Sgz z=|g-TR2;rxWS<@tmEVDt)F2+FsgnQePi*|)MVOHUv42BMVewT|MX6KDhq2ga8wYKF z0W@#h#S={52eV3bxZF9(8UzmFV(?cs>Q(D@Iye>0mY$fuEfmqG9BIm*b!eLIiIRnu z)V6Q|7XDg{1;#!c#*il-JIrif)UfS|x^(28CNsIy#e(={DS2Wz^O>p4d<|nEF6)a$ zCg0dM`#9tr6u@B1OU8dA6YAq+lhox5Y@d7|CU@#!t-2P$^W8A>>ocsJaS}V1 zPlNZipYS*pjJAbt7%9Am`DJCew{{GdX8hvr!bW5(IQ^PO?+swq=~&Q6lv*#>*qM^(CbV$g0?{{+>H{& zIB!hQ230o4u#}0A*~v&2`XwEed9q}dRn9!5(ou5#FNTZScy8I6L_g;%lZ^BnsI}^2af}W*E!+d|av4ol2yi%#Hk;cQBaS~BYN~DMTA63t|LkNHtQWwp|ghyc%L0bpdYA6B`!%N z=V$(8sV5C-+;nR;Qff(&TR-uh-qxno+qs$Ru#uUcl%qQ*ohf$lHkRvPL=P$znD4r5 zTu?ATcA7GCQptq&o5w6_)s*6Nx(CAJNGBzAXg{*4~1t{9%MlNn>?$D`6+Y`A-re7|6JVoIjz+by~&*?$sgc zm=!b0vqGtbI>HXfvoW2UP@-spcSF{^_8XE|_Z~G1bO{xf%+I4` z*IvTh<{(p1;zhqr>K35Fk{-`}*xAv)RRIv(=8Q?Un@AEto{tQx3AN z4NuvUUPlsb=ls}ru55nNRUG?y2Zh10ETA_VQ|EHJtE*DU!-yRCCcj4ebQLO@BmiaZ z%r)oBQO3Yy_GU^e28NC4La_|a*_EI?-F-XnbEC?mF9L~E+3IrH|8C;rufgYRI#y_%aZ6)Ip;^U%Gaab zPKkspFXLE2BmU!4punh4n789ALgS>!Ch!bvp1zY;P4;kTkz;D+Ynj@`iAZnbgIdE> z7CB^u{0)cL2v^k!b^?vZg}xFUgpul?R@n6 zV-|)wy4@GAJHo=}-@yE1`6b(Z7x1M1+(yDeiPB8|HrCznft_#XqrN^)^EDQQ_B2_l zTP4KhU|zG@vH=9id`JFOJ9?HEi||)U^j?YEJ!5Bbet|5FSlLpU*eUoQGoYdsYUF#@ zg%0akvflFsB&g)W`On$B?%T4osn3au^ez-{I_5?B8Jgs`(UFX8vepRunsw^ej!r(Al#gr zQJW=5Ki|znY`_;34D{mEa4HtPS0{6SZTietj<#TV+N5bou@g(-B4J8C{d)BBeKkb# zlaRQ^j=nl;(rv{YT)%5f{z)n%yX7qG#Xae{w=P}g{GWo>fR?doLnbV}Gh#HErVa%!(;!zZ8!|A_ zp^l&C^if=gT6tC^I$52z&N8DfwrQBH>q(ctI8a4cDgtLrrqmgFG~b{EOWrc#>Al38 z`7;pl@ETql^};TjyEQkmP^MP`A4^j%)02Y-m1^X*Cl|lJe8b##QXC)Uw7c#>D75KQ z_spwU%<-nccz%kCnCkIzRx{pT6{6ecEm&OP17z5KMC2Zu;wr^PgkSFB<^P=p^^#SH z+NWJ?vSSr?)f`7r=T&B0wjOh|R%7S670BqHjMvA_Y1_iXD6F@Dk*Y13`2Gil9?s{# zV^8zuC{V4UAJrC4!@USSIuqnY#c^wJpA z%tkta>9s~dF(;lm+^%95lK}Nc#uTN%M+r6@F9@=ubCm*QdOHOA@#f^OBuSBbH6Rh& zg^RQgmtJ_|4)r5iAO)qS0+=buPw|bbaQI_4ywlWCBcVdQnxAp|m>1%|h>`ldKFqS@ zG7R~86kj|Ct42)8Wt}(aAKMMl5>2`l;6)2cz-$ zt`S9e+HkYJ6?c?OX}rBF8BOK*@s(vb6|)A``$J&5#Sv@O8E(&9i+qQpa9*(lo+?ey zs2@kS`IcbOr5CVIbRdWNIGmdF8&TfQPBp(pOK_GK=VlzkQQ>F?M%Q7_We zOoH7<0rVExa~^OJF6?!oQi1VQzB>s=Klo6&38U%ONmw<&dEj-^NU$ao!P=$hiIpWI zEh5k020T=kru?gcw)`pZHnj~<;{Ei z8lJlLl)+P^e;8sz1Lx5*SBbWF7PH`Ud$7zyo$$^9SB6qKKS7kzIUiN=;vUU@&ryD-az z^0$UDS9u<}HhNQz&0{v)<4voB-D%39U99YzHN|EPvhKwV7<_k@`SI%5`SCp{JhqA5 zd?bxI>Ma=AB8tgMDl~b&FZCzNlfzL7%3C>+JayGMj3!4i-~H+PA3?I-CQ6<+gy@g2 zG$~d}(}fGY2)HXiH`gdoypkM6)k<^O2_7BFR3(WW!>DVvp^u+cX-4E2dN{?8ayXuT z{Hg#+R;_}7c?-M!r4ZrQgJHZz6(9E2A=Gdgq#nw_Ks*f#+xGBo)Foh#!*RAlNQmc_ zcL<+dgoOT zeNQEux}t!6=&#}OqD9PgvpR$}`O@5pN0|MpsqlR^g(O}KGV#6kXmN0%as7AjNzQ}* zo_Pcjq0^|8ain~e2Fyyn!oAA@^fjpz#g9H=<11;(*gwF!^95;fx*Vn3e`D%09f%Sx2|=m!c4Kce%Z9JSpLr5kwyv@Ui%^3sSxbw?2p zwgOX^Poe#nWy$XG9?(ZG3ZAiB`O>5v;z;H?e87SB z-pgQrbjHvF!(Rvwc@0&Lml{ub2UELN*zGZ=G}})|f0U0`cJ3q@uF2jxA4ia(1PyCX zDET^^2&ESa)Kwe6?s{Y*NI;NM@0rsNer5WgqequZooKayKJ_;klJ;gTYW%K2ueRya z#~w{;Xg!Z8iCmAV9>x^B1van{_do$1Ep*5+aFcs^>DYGq%FINrx`@WIPA5L-6`Y4ZNmJ;4+=`PQL$#EFzP zOo2hvR9~j)WPN0s#JP-p}FpGnW0TX zCHd}QIsHsLM`O}b3xv;;L5}hkc*uTax}rmD2ggmD`!%8RTmjW2 zjs1I^X<+&guA~#Z&e+lYw`1tY-FB9-+MJ@@1?cM1378x40*U&E@Z8e|*QLAha>rUs zd$9^*E8k&G{W(1TT8l_|eVo3pOFPxyIUgByi(WCP`gzu}wwRUe> ze04M827jR=*@tGT_(EpfQEZgtBO{q=4yzr2q3l=Kc7K8lhdrME?dS5*96lb@rQaO? z`+JTbqGL>H;%7bT6dz`>94?FIiPL}5NoayV7cK=9Dyz95nJ6!3#JI%TdVMT@1Ax zXHVcKcaLku)sd=*Ey_XKIZHBF9$>YTe9*Z>i2`b7K`VA5gmUD`#QPUyQyZWlqelrDgAgdJ!LY6@r99$vpmp^) zS@?uWELn!~(sZ8Ie3tJC&u8IN$+*R0gZn7pNDvYwlcbi2%xwR2pus==N%OXaYE zL&oG+Oq5u4oITy{!s%J_86B%ZWwkIfUvroFEG-ea356xYLy#i|IkiqhsvsE@9N}ZNZ^OflR?y z0B>$D;YHqB!d9x&0)N|AIH&x4VfM8*d=H%$X9)tFgc{ z5&i*zbp2@o=2`qil7=GrOPk{K!Y?>;*^mwvnjuL~lzJRxX^M&|X0GJ=K2Hs3fb$>E zaaj2pH_uOBe2bJ<&#-8rJ{jHphE1Gjb;oW8+Dtr&XVZwt7Ag8Ye>Fb0+vBF7JYDP3q(;|a^gbqXT4O}}v-oL3 zq#dQbmZrdte;9V=k;dpqIRDvrHS$8)Y1b}1zo)q1KV*Zv3x${jG-P=oebedhR%Ee6FjXiIqk zI${Ly!EPzif?Ba5{vA7&;fc9B`N-u(2b*%m2f_=haCmtZJAaFZni_* z7Cs6Wks|?48&fbE{QT<#? z+Wl|~EX)l#-9VoX^Bicj^R@ePdmHlCb0@8K!P50XTxTrEk*>Kql*SYd@+JnTkb9E_ zZT665Ygcf6g)`<hA6p3v&KK)@ z(1$_KdK~rrh5o_>7^&6a!a`wMxNAGt|LDZ}MFTh!egz&XT6BJk4eb%lN9b2+T4Ur& z+l5PE^-zmmel(=5X${ciErXzu4LyG^Luxhq(X45~b(I9^F7?bg=w1@?YIzvPN!6~Tm8dJ*CccWPG=5345{wsbp&q6XWGYfNdIjT zDpo9I5vmRp^1+h)o*ZHpQ)Ma4T8q9dmt!luWvEWqQu}1niCpl~0R+RXiwB$bvGj1Y+jG zK#K1&r=Ez@aC7vc7xhJWA{>UPf|ucGeFIz19Yp7%Sp4?ShSU@%tjxNC-#?V7IrS2@ zza2n~f(V^Ik%WZKVJ>T?OXn^n<7%-0wKol8T&V$DI>O}-1V2J!kvflm^cLECM)6`( zHhZFR4Hk+v>}0+M)+d}o==>Y(T(=VrM8{(7Sy|R}-2ewJ-NxpJnZQw~SC`y^8G z2djN1v)NvpZ=2YIW%GscZeA_&&L4qApBDmsn;?~yf(wD4*{}Yu_^@jyjJLeT*DZSJ zaaE^l+dJ@H#T`dSP3V2f8*EuW0ZU_KY5cU+m?^4E?Z+KR!#Ek1T;FGKx(n?~T!uM` z+BBSJO!iVENRP6kz4Er4PyG==sZJC!YD7<sBMq;H zA2=Nuh$B0ASorz{GR_}GT(u5jPF~0N;G0PL=Lh{&C5SM7h-$-N?45HK=^INqj9>s; z?{;?RmnG%cp2gRvOWB~5D`kbH!20qB_P*AP3K!<%$y{er&HTf2$=rv-pF#dB&#_he zPvg6xEk(9u0DeLk{SvbUsS{73dZ zsRG7apYnS`BK!L6Hr6gNr6yNCoY!nak(CXdR*|4l{%QWneS zbHamphSWFQ&6>~KVF{NFQ*xGKE=kXL?xv|8eyQfvy(g9x9d&0}Vs;euLbCYDtk1l0 zp}KUsyoaZ;su-3@Pnem_R2H$e0^Q2WY@UfD@5`Jw*eCLh4JrP?+sqo?rGMPDU+u;D zjx?rMElauhgJok3S=+r4@N=CAk!9*M|Jx99Z*`z-vLT(kK86lDe?S0FmL^>8#-9VS zZ1nykoc#M7&2nd$__1n?pL!cXN8?IzqaIc=f)nw~qrFJw+?^33`Eju7N*O;2mjiGohn>qYfmo{BhB}r|rf9k+x!Ip{A z*4evYIm3iqxqp(G3V+1vBF4_P`tvkje}MS!n@sLXKAS%B6KlegnT(7a+%=DRc)!E$D+KUujUE0>6eueEbptmH-H3nSI5fVeaG5k;I=${Z(wAMprK8p)V}1@g zcHiM_B~Q}(EU{4g7yjAl()vz!1Sbm7*Rk@{yGaF4)3~{wt4aHNxZJeuHH_!_fr}j8 zA+zEj*ezKyKk*3qTZfUz5~&&?KBQeN8!0kd^DvSd7+vb9D|rVajb zed_58reJNmBJ$ce-Bwf@HM5uF?rTo(`D;Kk{&JmaZf7S6D$ul$X1GbY(6KxV`tk77j)(&Rd(X~%^iR=E_) ztQFG@-?81C9rW|tKWGYlW*sU%xVMAjOEa%BF-`|<{v<|)qXS6($;jTbA2T`s=eCVK zDY|l9{4fzN$9f(r!JrQnn(R@^A^aS2A@57+>|$~uW>p$4uev3i3;shZ#r?_;0}xprF6%m5|!CdZr> zm77xlyx4JmIqIL_LMo>k5O(Y&Oow`4Bvp@wxL9bU)q$TXxxUdge0bW&_2BB^BlHWW z!xK2HQHNKvg{f@JcI4=Bx$oTrn6cvuPS|Ksc7zQHWagp&lNK#&F{H}}UZeY$G|ek? zrOj80akhUcVr^_lRBD<%I`rilU39PTPW?|6RdPo05o zsplwY6eDq&6)5dE4_gU7S`(23wceRb=DQ(fMc#nKox5z^N*yY#NW{pTMJ)02IP&ka zB*EH4?4_S94Mu3uno=1yZLtg;@V2KVm*%m)TLN@Kr5_6x*Tep=1HGN}9z%1R@o1+h zi4^r9xa1!8?s6l6`cO6@Dg_^^q$$#AWATh@N3pb1hN^Qdn98lIcwH?>rouYZ7Oq6k zkEl?-nK_-GHCts{MLKtryN8@FsB+STJnACw{v@bH#e(=8C&Bk_API3;Xx-$~ zxc$b9JX?w&AQOfqP5(#HdHCi0wqdwEq`hdTUD|s+_j#$zvS(JvmhDHf$tW|Vl&q9Q zLuSZ`WECP&RFqX#W~IE>`v>5op6C0$@AEp&#{V24aW5!< z{Fiq4I35?D?wo>$m#dK7ZG>a%lMwQ~tJwOh2O4jmg_k%fO7y)^#~qEltO)!)_8&j@ zeQ1HvBseIfU>cu!hS#q`e*8lGz7#?UTf0&5qT#|gTp2S8tm)JmS#dtt3ajrM(TP$2 zBuzKZirVuD(A?&Q2g@Fb<7>8a=cGYQo_bRl=jLKpP+v@T{UfX-=fxCX2a+lLfL@Qa zV932jNlgdh^M{IR46Gf7J6e!Y$nvveCeek)Kjx`I7X!L?Z=rp*Fk!ua9XFQDQhggX}HcojADb?-pvoyo*)j0^a%fp<&z9`*v1v&{OxZ64z4?+*X z`1@6CRk!4>$7_)-=R_^a2XG^LmT*1hNB>%r@oMoKVOL;JSEm+Yt%T>| z4N4fDDm=HOV9P8QTGQ|>u&?3)ocVAIvjRE)iHQuHedj40HfqxH?JLB|^-q}~{D%X7 zrWOTUYvlKKF|HQ7QQ^S7B3$YWGYPiz*Fy@?8|t7lz=aB!!8BUwf=?zJVfDkRdo<~n^)KQ4Y9VfL$78fn2eSwdkvCF>yz5_M!L}OC{dLGLp&f(kufn;*5dUTw z(A0$|M9p&#q;=z-%Bp7ZD!~~qRvFW{rCo)6>OINjiu^$7vktVycB=?I8zQE1_HB19 z%{M-|R&xKd8I9BXCb^w+1yf(&7408~3Y*KtxV+3*Ts9RFvlY+LWy2RyFsK7H`|eBf z47*Z1XCF49C&cUw4Kn%C36ETB?oGBs#`g)X^f#n!Us^HdLj#ugu_UQ)N;F;d4Jx;4 zP)yk;T=vuu_sefXtL7ojEJ_vGP502hxB&N&T$H$_6cy>$;C?!mnV~A7*^m!hABSa= zYQ^)H`}iKS7=!W@5gJ#9h(^#OnuyKnau^cmPf{mC5hve^=OMi)sB{Mk&dH)8YN*J| z?@CK{j>3!j_2Tk1ZJM{*60>Hm6a7Z((ctrIpb=_ALnD>ww`&SM?lmXH@!d%6U?!q8 zTsSLJqWN)a(XMMR9`X$Q+`9(c)bJ27v%)03jy0gA$3@XJtWYeiZo%Vu$)f$9CIUk@ z2d0ndA$}e*hwQbPV*WB&G%j~RyTOhC)AzY>G4Us7UGC5tq~P=4-V~I55O<~>Lj8DW zI%t}P-wvPQYpq2On|tD|>32MPWKQF56YTi>(>+Uz@~ibAow)-|+l^`JR_=ih;vU@w z1G>4l7P$jA!y#9lQmt;IbVwUU2duz4BR{&{ph8MxH=!iXpEADwz}zEqu_n7ONxgMK z=Xn==8zoCuZ3iP^oIc*o)g+}J>hMyS2G>Yt{r_1~q0CF@I|NYj1Z^r^Rl|-QAF9de zN%G2-*skS7L)T_u-hdx?$FrjHx99M1%L^n4E5q_UgUB6b#DekkNQavvCv+JJqpAK~f`JzVs*Lz>=ge7Vz~zgHmH7Ckz$+=HI<^`^3? z-AI4GExk`4KmmECROsqTj(_%JKxw#G{KHvHdzS~(jmi?`2P?#{NvkkS%}kW9TO)?9 zZWQm5T9NjN$;mBEp3n|vxk=WJ+ z2i^x#p^hDWH>wbux-e7ZXEQig(MrY(KacJ9JlCEwbvcw9})CbN17DY8spx{ zBN*V~NL$X!!TQ2A?Dyh+`T{xJDcR1?^B!c}BNZ7>OGSRLE!{EQ2l7}c@-sbX-HEdp z{_4sdjW*#oNrompPRFvj1)RgU(XVam zab!vXiul=FqLYq|4hIm}FObB7LJ@j>GiFxGGxPXUGP2))XjM=kH@U~+#k%c?Ui<|M z{55I)eG7^-H=;$(?5jF%OIO)Zu)|%K=9s%sxU&TrhV~@W1DAo_9z^T@p9Nlq*DpUx zKgQhu#tKwzbEl_P_n@D$6w~Ay@TBxICR7E(Ij9kZPQ~c?Z8M@4+{3qNMih1MI&K^O zhR@orv~Oe)qH@~cn`%x`xASmNQ-R)jx>BvX4;~E2g7ZrcQoX$jPnREu>tB27sB%L8 z$=h+dUl-`)IEX&aH{fn<7x<2;mUIm}id~QFP@r-_Ebd&({V!8AyRSpdb3M_^VhC;> z+=vT%Hi_b%Cb0XHfb-YCT**A7Mww1QwB}i+7`ae|>aPV;=-HDJ&(Zp{!pEQD#A7k; z_Z8f)7=_snBgDsRcd)o@4D`SLp8+bsj`=3oJ#ea+_hc;EqiyJC_AeMqhoXY#sqLZz z?J|q;c9aX%KbEIc{G3XD(~3QEm!Z@!5R)pU$v5*f&JOBV8r-+%D4#Gmh= zRy1%Zd$xu~VSJ4vDHOJ2=ZuN4>@cK5DeEzIc{dV|eM#^4L8J`tPLKI#_NQYV3Yk~m z=FyKrw%voSsS#y#u^@FnKIf0^K{qFw)AK7uSm0wrZ;u&L7pHN|7l&eMm+d%F*%SZ$ zn~VF$4zRmt5ynQZ!nh^NP*w5>8`9kAOr8on^}nIxxT{J>G3YyA9S2~*i7{PEBxJLWMx>g6fyS%oG1gh=w17 z>5n1wvB-zomuy7vhb&kuu0+4f31XjSF+R36!d!8OIDRG>-}&E-?^-A6nR`NFviSyT zTrW%3n>vdHS@j4o$q!6ky++*eIfa-xFC=|4bSXNg3wH^9FbHOprKf`gNI`L&KIz3J zir4;teX}tIT$81FrbQUJ-;5H&KVspda-JbL&_-n$b~$7tQm#alC411Tb>5WxW3TYq z?M-uE22+HTAzJ3SQS3P{syOIRTVI?K-}_oputFc||2a~W_p~Rqq5vwtrV6=frZn#4 zIw9%t5Z4!mVZ|3K@$~mCWL)Tmbv;LkRfV5mx2!Lc7yW`qy{rh2e2cPp74rXhE$~=T z9VV>#h`1GHlBsHZZ|6B~?jC2_obwKuZ%nBDhz04FJVE~?d$J$pOVb@{@!C%owwE%w zFMJ)>O-ztBpU*-MO7T%PTTFVGgF2&gSS>gk54k2T9xvguKrEVPt750qIlO+i9u@OP zi7vU1@X*JZ)})R>X>x+-BCIJSbv~+fUrYX2d634QPIX0aCDfYzLTrEj##O*1X*ljGt#QLvjYgsNnD|``Y6-JU{+PR|0$ra(l ze@QOOCj}ah(ZGty4z`K<^&4SeZ4bM&{XKH#K* z0&Q5U0sW)$^azTyIOeyoNUMeGbZvS!@|Q>$e-OU$#x%O&1Wu$CLE)Ajee=&j-$4g3 zmV4N<-rdF;c8g>W;4`C4FkP!vqyBOE7;+_u_MUA;h|hjz^27M|p^Eo4wPMW573x^8P9wjXV1hBI>&Q~E`(~T?;xmx!Wv`0K zT4vZgTtb!G&5#qVPXTf_(EHJNC`EUpeV0!{CBzzklFcYjwHnU#lQ5=u0Xjcbu%qIi zh|h4u*;Q4TrqLm0p4A)LzS{G7|Djj2LI_^!8quA4x>_%^f+0cV& zRVa&_jREUisXkMSzHVHL<+q6@ab~(OgL93Ejud-FiTt$JLUYu8_Ms@!x~!SF{`v$v z7`srp<3Oy@e1&ppH7a=-kAGI%#EMJ{%5F}?>&|h)-O!DGbN6QG>NjG_8)MpW!+~Vw zHw#lob-LWggbG8)h+}<}sk6?Lj;N-I$<=cF^ZX8*argMV?MEqcA7I|E0;>Ca(%yB< z)z;<0+$E6OL$3$6i@iv(Ri&VP6R$4#$xfHD|MwSVippmlLz}b|jXdv2egoAhjST3+ zXcKB3twfKdm`C2`MgOK~keZw=X{%e&qHi%Mdb&gMkUNR$lOwQy_gbMo*@o&>51?7d zh|rfMP>+kle1)@^H75sSZD*m~G#~YerLdDfh)$L>b?RMRc#Yi(~e`ylNJ=Fsu!hg%EHN7U9tZ6 zGm$fG8F%lpBnHFYiU zXd%Hhox75|ij)R*r-l{IBFx2r8DMKNoN+`_*)bYZYt8AU!d^+r+uP#)j#)J}!t;wqTAcCOHJ~R(Hnc9ii6Ghg>lIC<{C{@R!&hIBi9l45?0Vnav z@DDrZ&fwy(SRC_z4T+=@Po}bqXeiGY-X@8n|J>+&1^>>sZxLGIHuRCX1zoRrk;$2G zuU<*$l79;8dzjF&_#DWVbKv;Zge>b0An#`>+%A8Cq7IpXW+d3eE6>R{!$(u%BN=2_?qPK}b)S$5x z#{x>QGg*;dcWR9CjUjb&3|eY5=;en_ zeEazbI}P+Hh0lLhZf9^eM2Gs;*5g@pHTFN~0q<}fYIw>}xQi}aLYd?18z<~d`atTD z7PaR842XXcOr7$kl7wAO)ILr^-{-oDAYTjGc&H!EUgRrYO!FlR*CnD)!!5kXY7{pg zZxjc2mg4b^A>tBEk`z67ith_$am!tv#$8X5{K=6empklys)`pMTa@Tcsti3CuwGKw z;~lo={KEMTV`{k40aLyoZ&;;G?&&S~;;O*iVCKNa%Fy2v?Ex`+im_+#W5kS)y!vdz zIT$HihhYf`n}lm{bGnA?_lqz;#uC=a*Kx~#1WYwO&^zKK_o4^EIz9jfN3&oU8Axlb z{?92JBKldszO+sBAA4WTh}T`7z^1-})1Nb<2eyLz4=wT3$~7AK(Djc1-`W$0Si1(Y6t z0ll|!E&6f@*cze_9UhCwB*Avc=fTxL9mG`cd*>-BNL~`*U$})?G=j3EeS0 zn0>FivoPs|2THgHnB28@5%)xL;oWYFweNkQTX7z<267Gm z0`mkj@8P|p26|2nrkB06Y4;Ro8dTMvT+>ZSLE4j!>G)AWfj)IvY(=AH?8Qw7TcLe5 zOk{B1FD$<%4ts&I7&tXyT_M;>jRp z#+G`}dqpqixcB161?~!}x>H2yA)M)Jz@0+}+OTU4UIi-9HD*wLFJ2?GfXDC$6#<*W+@Icywteke?c;rcNSBmcSDL_A-WqjJ^L)Du~IXjQX zkA9q6N=(Gxludj;R-$g^HR5yeerWIi0$V#RI&Nn{QPZvHwz2^!thJ=WAGIm_xi)Qx zWT&x$1G)Znrm~6xl=N_?KEG{f2H!&b#(PnGoj<#K3h>yE+4Pi)%#$s^kE;iB=7V|a1A9~QKbW4m3*n|HfxKm{v3H#fpP`dKDXne9xEY;ZnukiaKzRgd>>K}zv zeY@zE|69B~9gX$BG}!~b2!A8xM2d7D?&YmT`KE0`%AhA~M{UBZmo`NYNQ08veCY3x zL~)RNHGnHLv#p1=TbgZ~& zfL`zCiCxMOn7P$~ZfCdRgj!#GJ8nWRzsXQW(iXhh?MzR-Wyxq+hluViP5wg)F>tv% zu*9ocREs3&RzJ_7?ORZAvIGoN+OL({XO?X zjuc~YH#-WSszn!e_+s+q_r@Wzc1SGt9@fq5ExlnzmsTaH9p`2jyw}D|MJ=e}lih z7F9-?(D#Nm3<eJMeczK1!8Oz~=B;=v}WvaQ;m+N+m#7+8sgP9cb^#O^}mu2cG*;Qp9%LTs{l+ z@pg1EY6o6E2%)x2S9-||Tjvvy!U;c`o_YxDUJs?8+_SuRd^5uRlHm~b1xdOa#lVQ8 zSX1>J0ea<<4Ht8n;r{{exz|OrO{2v5FS9f;N+LJtlqhsL4uke{6946+MWrLp*nh5* z*zeb+{mC8Tu}1*raL#l8yaiN!@)JEzMniPU;naD z4>6}-t3iF_<>4J~igefXrj_|EsNuQL<|#h3Xm>MK2XsJj*dSVdwgx?gG2IHZC6n3~#Brv1 z`kotU%xJ_<9|Ib!W=!4reEg`#DKVlt1JCFZ?tUqgTs)Bvo8Nh8-drtSEh)ye^a5Cn z+l;@R|HQGdD@gCQ7}fdxR)4L3q%#3Y{19- z+al+-B~5-2h4A8Ml7ERZkd$iE?Uo|3R!$Akr@PaHLLCg5E{|Z&$lLc;i7r0`dsvj{ z$*P?&Ed&Jyb|WjSLQ20dDo9YE<;=;IM7b zARZcapk>-9p?~4MIB+gT;{587xb(#kpM29K1NZKf#7UXs`!WZS)4Lp}okFO8RvKpQ zxy7FaPf{74j@G-EA%A8NtxL^8;i7sx&sL)LTm$rTs>0GTJ_qo7aKs+=Z@YA%zc>Gh zlKNvM?FT-ZHCLLxzf64Rx*j#ANzLs+I8Ldd?_UvQED3}&~mm+=pgD~|Q zN~bPp(Zu2me45{jBKfR+r{s;;(5XTbiu{o5@J-xS?@nj3W#Mtr65}5#QXKcsVk3Uz zIrD`9W7X(WH1l7_o#_*I&-70<;jSAyahKJha*Q+;IVvy%$Krv4I*d^1LY9ADV@0wO zNuSlBUeqdl&VLTvbakV+c>RPZJUmq#*lZ}i%zq?ev=;|PFX|LK2fEVN**wP!8jU7f zA6i)V1Cl>$@$QQiIdQISImrzTv7_-i{5kH<9)#;9#t4nCf`)}RT_d(rv{7}K>v-?pl^@mD8n$2j;ePh=frNb@YQbI{Z=CxH*t`N%ASM5<{^^T z!7D|?pJVu^^XH0#&H<6Q=Z&zA|Hz!Q9jRC)3*RSyk-_)R?mG?fHL)F1^PK6&AaCl) z^VY6QG|5TMoh}zlL*O`NI=9o4p6q0QN)691E4YvJX>4GRa&HQHCr!nRL%0LrMW#G6 z`(ybl@UXc9y*uDSr*kYR@zW|vU?*sfvIVuhS`xTujSHoyyU?5WVIp(dJDg*0y=^cv z{hlxJfjevq%=BpTvkGire}11O9dQ5L01qQs8sM6W4cBETkr|V%`6n^+?;BJwgEBKd z8b{9bq_um4DZKA>?j{AWqo*&8D`noZmle4<4<{>jsttP2e8CcT+Eu1b;|C>U&n5|t zH|D-4&zs`Q9qF~I0y`rXKxcOmN)&a;QH*C+GaZ|jsnSKCJ~*v?3kA9+w7_XAX!1qz zCYjmKAvt{a>=4Do?z>fEQ=%o!9(oMDdbMNIKrN~%J%;2dHF!~?PVILMS?PHa z3s2~f#%WI|d{00|4tv(>y5R4W6G+d#CJf@sBt27Bqi4$&as1mKiTV9hTqYURtWOgn zYXv)LyihrU-{nIEX+)32zU?+xaL9w22adtDgg)>v4kH=Tr}%-r#TmIr!g#ALwLhqn z_SrfLTT*8U{!{PJbH#Tf5N9bI4 z^f79}@vmv{)2xTmZf(S}%PeWnPt?X6VLJ0)PHD|hf6@zzweB?6Wf}%u(xK`a4{G(_ zh#Ab_9grg0J31a??(scQ*MV$aHR00_~es|Kd*^P}k+mU<2j%KRtVumam>W^G_&YFjU z&71J^WCijZ;^F7715c~(aGn*74NpyBd%O(uN{*nU!v}rK`To4}Dvp}(#4?ZlaI9wE zRNE>%J9L9{rKhlQJA}pT0uNou&Og_USRdLSk2Kxr*^m{;YTAb6pSJX|Y&-PA+(`dC zd&lYqqjyCwnzJmB;&05vEguKE_s5Iuu5LtJpS=j`+JUhVBu0 zUgm$m`0A~aGbU?9@tZ6ZZwsLVQ(uVM!Mia0tf11eOrbZZ1X4dFq%uL1?u8A9=Obr) z`(euGy?Ge@#uq0#wWvu-7gJVR;7^hXol2?w^icVB5uR<%2TxX`ulCnO0!D{x; z^xbq!Ozv+%M=u4@uszbKzGy^=gZ$|3-`mWtIa2P6{~}3Wx?fDIF2Vd{J@nTtmYBb-!teV%ad*^e(J}ozVy@j3BePmi zZT3hK{6~>u4@gs}hRoH8r({Xb`~y->4Hp*9f3S2LvuPK2K9~I+DnWYmD8q>K&b-Cp z%iZb86Hgks_%$qk-xa+#rQq_YLdbk;5Ys1K!jPJaJYVttTu9AD^qOf zeF5u?XJcMUhnTcE9bPgk`L35Owmp4BXP%aJXoV!W4F%_Kxlf zxt)F(H6c-$J=I~a1@Adg{Vj^`8q%^a<8WYVk%;)AOb&w#uz#DrB!eBtYY!hs+ukNg zMZOjl_S?qinTbMkk}Us!&S2=*(V`DCwXSh>2z;_fgf#S}lj?sEv23>pD08K^o8RGO zgQ76m+9_fb$^!q4h?aDj+5^GAH}lskO$;fr#>$UtuWa@o30 zcS?(~_QMrOA2}p?+|0+EZm+O?s16;T-W}K4+ELg|lO{gy6jGYi2xp#Z#2*vnbbX4> zl~%Mc=>gX3e}`0-4UIU;T+Zm{IC-r*9r#^{s~X=RV;GMek->EEx+1wA-GwDGK@^=< z2m2{;2u~YAX96AwpRg-pl`S(vE|0|bgdT|9)Rp;AC2V51^)%=1Wc5Rr&R_V9!S?P{ z`jO96qrbz@-;P@O&NnGjmLA1A(TvtRNZ4}^TC;UYt*QdKhnsPpT{)SXj`Mf76sP$a ze<^pLME*=LjZ3hEpPs9Mp&cgq+dlQ(Y>kLMDEMZ3?lU#S5e%s6F(Obowa+7 zU0V|%4h*5Q+-*O)X9Wg7U4#4G4kK{ce#yaIQ!!`41sr+0L`<9Uf41@@F6Q`4lAmiy zDyRp2-lal;?!NQlI*BAHQHoIyUF@q9uNNgd{K#Lr;3r4v6zrdly;FYSXJ`X5f-B)SYcHmJ>csnBFAxxY464WKA;n$HNw(aL?X5!> z*}K#H{3`rHcs^8PM3>Xk@qk?gE7a{s_FO4em99gCgP<2<^y$-r9XRl;FNO8*K{tl% zW%eq7eM()avvMA;{<{YqHSTk7o5km^T(l(q!r38#?C^ZWGb+xZ4rb$t?Fmse--VVd z-^JHC$tgE9ZQY>LT=;;zChf-@v#!50#&L(F!Lcv0!u(R?TM)almfLp|ayR z5_}m(DSeAVJreMuvmSev@P3|wy7Y9PDW&p0tg89ewA<61!f)GB=sj(k@ywL8m@!D( zd<>2Cf}P%0RAPAzK7WYJx_MKC@lkvl#-5a&ck%q}DnuSBg8Z!GXpESG&TZ##l070d zlb54q(h(Rcbf*x_R5U#MfpzcL19OaLLA&MY?CCDFZSFSM_W2FT#}BZ*5KP#pXYT)xnj(afp`=>g4U$@V5{~F+|2i<532`ppL+r3 zJ(~=VwZmYv!iMj7QCJ(g7Cl#bP!x06VO91pzidyN19a$y$pB${&I9v$8ItxSS24K$ zz1aHDn3;Sx(Xe(ZD%$$Nu%%cO&iX4J$ob&1`4#bEQM@=78iUHeapL{vHc_qfMs)lM zlF0vm_nziJu`h0UQRPiOd*9-DL_tm9&4=>zzP}CTXurbZd9he`*9Kia{KLJ&EBVei z5L%oyZsgwg!iO!ea8u&*v=#m9{}1otY@rkFN%MHV@YBf!A08S|9sij}+!y@+x$f|? zM5w3glIs3GBp|nYO z;E9X3XQF=fXsjPB!PHVuoakzegD2-=?~3I-yBG&4yI1(w>_?j;`y+qQLrgdz=vVL> zSZDpkZ=N@d4GzX#?BnxL8&0geh~X{gF*5io2Cpr^$y2*9w!Q&{xfSU5%#L=pYty!U zsyv^tBD;Ocd?#S1QVPEt!_Dbi&zE9H1J4?U>_pz3G(Iz%k!MN*=Q`ZM9O+2CJks$= zW-sg;nYGcj$7GiZ@$c6wT<=bJYiY|o@CD|YJyA2gLX?j^jVW_d(6l&&{XjP{s`(Ut zO^m|bq5O>8e+&(4^`JT57H0wiNL%F?vikEbu#f)Ky675)M#^ENLof2Q+QTkUNBUSE zLO*pwgzx zX@V++PkSlsGfiRf;3vv9+f(?OUKlm87L6n9$#Aj@9u1MA`U(@WwveW!(|WU?ZZQrG z>OybQXW=_D{13c2hq%}c8T-c~C_ zj{Pcxlo?r>76|{wqoO0sp32j&iqomx`3%SNP+nztuD^uZou>q7s=8927o0a-_%2b~ zYfbGr!4!15kH~iOrrnwPIJo#BqAi|@t}>RKZ9YV2a8K?}%Rx7y2@|H>6zS|bnz*SK z_3qt`UhwnCY{Ed=C~HpA_KLL2hG-~14;v>oAuCRi>~oca^%U|hx^Alp|m6EJ+2tl;`cK4oci#tJ*Bfyub7OG zDhcv-C1Li{2(0hbEk^|3$x(UKT)W; zIkHEr8=Z3g4?m7OV);@N(rWj{%B2=C+iF42Ja%DJL@z3uu0)aV*W;IkAI;I}O7or% zWA1A(MXpdH^_7d5Y5q@=+Pz+g)uk}di4m)|pBHyO)*w2gPGWq=0+xT-ojbfpjJ~Ie z{mDd@7nBjv!v+rNp46}*UW|T2kX7!>zQN-d_&Huub@v#)tvZT;!}((4)MH?21>$?| zEBeY@4_bd>W~&AbI_ZH?+Q9aBi!F`Z+0f?eY9`Y7=4~KN3oT!Q|DfN~+(Q(I;F&u-7(Rt9Q|o7XsVT+>^;88BAl%5Q>dT#n>ucZt~D(Jm&P;r`SS zZPExd#W{H`T99B&I94dKCw8TTx$;!;(ULju`DiLOA@j>t6tv431q%!*zut=O7%f8W z7Dw7Pqz4uA{uJH9UbI$2m!w8{Qp9dwGMU8gn~@=O+|Q53%I8SdL>bdlIe%hyL}+a1 zetC_AyQ!}v&zXfC#vVSeo{yotcQ>k~s*Q3cm# zc4+NNWbXbOZpL?DT5}#|?yScsC*B1*HwI}j_Viz~J2mYu$KNm`lK#(|9f$RZ8tF#X zx2?&WnJV>`*_b`mm0sM@qBBP8kljo~F8VZcVLayW|LN38B^uGb2J+VTv0y*X;8x9o zhg>$^_;sP}fdlbAtOny}s?nk1c`3%4E0^l+6YE%!Psj*OJ2t-e1HGL4z1PG4GF_YQJCkD2-D zN$;2a#9GQ_<~V>Z>*WQec<)7mw<_H;7<)C2=S35CXj13=`YVkWIX9D$BB?@0@*l5G zHDLynvc`laY*M1aa0{BY)QfUrG-!g94OvL6sQd60XkWNnGOOK%5z6KVRue0t3I`I7MA=b`^af%5L{6pG%*p}JPl#nRt`Q@${DWC>wSHDoU@hZihOaW_72( z74__EFhkxUTgrUVfC}y2*xf^iJ=w9a_R%J5cBkLe+lZsQ?@8vHHC=sn2yz`-)H$5H z_t~9jaJA$eOZ>Tf_!mF+v%mYNDXo%j#He%=+GgucA0~Q1x;h-M11DpZ=R`OcMdJAf zH>jj~qVIT~?W|jkA04&0v^O(Qiuu`o#*g5?)1@eW&Jfsa?!>R!hCl~rBY0hoM>jri z`QN&VQ*tNa(Ec0uxC?(VbpyH_dX3qAD)F8jB)cSf6q}SRR6O12P^db2uHGy{oZ0QX z-;zp-V}&Hjg6>V{9dos(uza^M9XoiEJDiEoE;6Aar8L+szJvBFUm*Wz4bJS*#gmqI zXbn$AZMp|SV}9^1uIb!848p*bx6!2^@7Ri;1T))A6#HI7hRRR`-Y!GQ=T}&n8-q*d z1gd$qHTwHTq$!yrQLmT>VAD5jr{<7$7>_u4v0&%e;2uXT}Md@03$}Ag)!JegJ z_B75pUd2K8O@mN7q)bchTJrnRn!Zg^qOlg6@$0iWb?9kQ@BI;IndMHtO)Au~c_(}N zr$`)CZiu1YRnT3iEXED95+mcEVSJ&onEq7}{(afQvoA@EiTWd^?$_sTPpRnq?8ZKw z(~{ytR-*KrJdU2s7AEzXC|WHgc{cq3ifbFF=cVq*{WoT_w zcaoC|gnCg|`clA|;A=J9ll_CSO;)r)!wSL7L@wH8NtuatXnUA~7t^`3_TLjE?k>T& zx8{7OeFH0J?xS`afa)m0J2zG7sksk!@KNh~zh)Sz41k7zC_TJjf+hDO;P+dZxma&R z$%bM03*J>6P$G(S7NUND3RQ;KacBN9++;&&PiA-O-TDO8alL57M;CHo#%^r}@Bd18 zi+Q?IWNF;V&a_(Csj`RffeL?qc{kabdYm05MPb)!MZmhmz_yk%Vuwc|!`5wA-fiPu zzppllPX`r*;fD2M)lf%vFTKEOH$q9P3z-LggVV78*x_nTA9KGVSHT#kJeYZ}evjmq z!Dx7H1MT@$IPC1tnZ_DSsr!bynO3;b7DRznvXnGVg=)PAkhQrw^*Jp|vdg?le!UVo zoZxP%VFGHpS4n(VtP_*AWnzD#RFRV0T@gBeI(oXFk({?n5`W*^7L#Yb;~umvnf2Df zVBYcJqhU|4kBt_wv%ll1vKjqO^QH8myKrfl2K~rzCFdJUP|2OFcb*=!Dn1_LwwutW zrD_y)s~2gH2%z7FhID3oFimF$`0ZC2+V(G$c7I_WhVLhlKdtHFnL5e#YfcmiXR4ca zSlEsbv^&X~j8^QGJm~u#r4KW4K|zK#A9#%EnY%E-YwQWSM!GtBts9C*W2k~`rX47JQ@tm`=O&Hf4$EBIOP zpNBZ>b_=o{n|YrUcb_cOsN9va_;(?^qg|i7eyNb=bNx~FA8kFd2<5UjaOAiFvt<9l zzWxO7V*h_{kI?_?9Vo|}kdpl_46Mi$IyH8*KrIFTcdn|$_)!fzCem*=if(nxVli`^ zIo_Oo$ZgEB*;4lrcg62ha@5=2m|mOcBWvtCtg63*1@gD>M%9IyhG(J2iYqX3wWYj8 zm9SIF#Hz<`r1$r|P&AB1;$|5dq4!PLneD>z;P-g5c&ga#y9vqM*-89lLSIf8kg29Q z&HQdk%RH=UIy>J-W}8u;9i|k~=tN=5_&M5~fz(gT75I6O>#_`39R)p%;<;Vy6{rk# zqk#)gz@u^{EJr-$os`)aSsIU+!-Z%vK8OPDQtp3sA5xXP_p*Bd3@`Eyl@@h!@3_Fe zhc{42FsHi{v$1_tCtN&L$e3M&X;wMt{Qn&Kn;X2$_TcznS=v%oA_`XJp!obbp>sh& zypLUr@Qj(l(^NsMV}GFQ-j8C0?g`PrbB&gzu6W9OyH+a-nma23(`~&m@ryTQbRC1- zi4o|vtRD?e)}nO=LwO(1dvV3M2L=9#lq@>@OZ-UEB;&`@;=7DQ+<&|s_p}A~Fg}TU zRr`@OTN+d2TE(95Oz5kP$NM9Su=uHr6AyX6;|(cNP`EhtUUk|q`%_ow=zVYcE%097gfzW$lZ;%2-YnV6)v{)4S2Jz3*BI^Z_h`)W$XuJ50{1`6~@}4y~&PedFv6NITm5R128gjA~xxD#h;Ri z_&Ie8M&y@cM4mrA@6iL3I`82AH3@0lS-=b`=cd-6OeIxJ@!pJO!=)&trJX%$8+a$r z7v8<6Os+$YqW463$}w%n#S{b5@t38DoJ=wLt~KeLRio#7c8EA51A02>JDQ$1N)A4E zp{2Tekk^<9H$L+@oKC}y_vdhmS-_PA{8_NyibbhUk@;>rhUKfGXCv>Rmp_5lqamED ze&8O&atzD0g!1rn=oZ8L#IOX!@XXuM`3^o!+KhJ*rAW1Y2+6V}3^^9Td+qJ0b8-v} zIBPi*U`=Lj2e9R_1mn41YHqdy=Tn`j`(!VwOAEjdvjCdc;6#aLV{qJ)-LsD+G;hs3 ztYLSC%B^4U4;v&r*ya8pRfZh&kBikc7jWTOBaE#_NLD2F6jSCNMQknSy8gXH=j|J) zaPgr(wHL&y_+99J(T5#-?4(H#VOKpNu8{Y2FN|V-NrJa^s-(a^@g>sM?2|L3x(Bk9 z`|<%)J2;!=oMo@^WsLPQqVbPE;`<5Sts9;y+M?{Kk-Li-YIVYhXE9G4`qJk>8TLDH zRvqd~mt$SX)5%awa55w01=5*&@j`ZxE!lSZP{)96fn$g1(vFg2qQ~`8JlNYRI$p*| z^2_c+db1tu-o*-ojvV}2byN(KG&2vfM=~j2hIC6i(5CUdXoBfa%nfgZ_L^a0+pv!~ z)l-*la_%+J`zyi*=B39YTV{y|&MC~Doq$}ltf>5V3~SU5 z;(7E_@$K0eL>|sVl=M0{T3iv4?7XxaI~OU>{DkG?8vGeN5VLJtg*l&hr!gtvU`&}lQ?%E! zzb{jhpK;6qXUS3A!NbT=?MLA=yOQMoM!ekApS!=mp=EIrH+zMMEm=XdVL?60qIj2e zbQp!tZ^Mh6JYn+Cn@%OZ!T4#Ol5U@+5LOvPIjgpc)z@?|!H=EWiEc%2PdOk%eGs+i zKZV>)6;bY=jy*FfasS1YA~nrieB6BrA9L4>!`0ac4t|Zm;cDc$+!WK&JCQ4|M*qq? zM2}&Wxaev`DL(xCD7w%46fJ1^g6nwHR*QhO#`I_P3nY)IKqTjbMiX+e-n|nY&70t| zgf&LxEqp#;*6->-x=^A@bHcXpyDf-{T&3_N_>KrZphmwx0`52Nh(%{~*njv=Osud( zn|BvVW8cl!qaUEju9PKxb;zgnA0}=!Cj<6f-824(%lB-_I^rSR{yoCrxtvW$WMavk zY7DE;CL8TWY~Ij>SJoPoH|mJ+SvFcSK;jMyM+;%EA0xU=3`5vK861u8DQu3}0O_$} z=vU@BE)*l|@&gfjqneirWaH)D5t3r5S{SPp;+Yn+eR90FYKcE-CLO}%vfDT(C!uT2 zQ?aG%HEeS4!`+|ZLhfAvyMa|{|LZ2nzj9By=xRW1P8t%0(E+qyiCz8t{%Jp>6PRYP zRoGabg=eMuRhJp{VzX&3!Ut6qbsFrDm|MIRe|fi$=1_T>u}2jS++&;bUXH4GU(H@r-8qfsvLP;}L!W)Dwtx-}A4A869u2=2l3*bNm=ekQ9L(AHC&Tg17Gn)jBp zRhRed8r+ktG}Wa1!9H|R^7=}#e-Le7XH2=SUKDzM5c#ra{=2CM{Z8md1DD%Sx5I&S zm)_%l6rFcG)@>WcZIO|^Ws|IoQn;_Q-pz3VALp^}xNp{2dhGFnEvq#?AV zkP6YD@}BRX{quZ0-1qNyUFUfm-*1!UPP9J!fmOQCvF|8*24}F>{_H#a>YWJtnhIFC zKSq3G6BO6SU~}thSSZWTCC-Cby3}J0=QIDs?!(b5M$}x%Ufsm&xLBh_|Mxv-eB$?1 zw;mMoio3(ijeT_|1*4-~X^fN!{fIutoi97W2|ez2 zJY)hpR%%hrorvlY%@Lhf!6?ix;{XU3VgA=$z$#EKTm7GhMNBSP~) zUm7d%qS4V+%(YRc7^xwYwy|71PM4*V2VE$?GgYkAQ>8JzE3j>L0iLar(3iN+$ZFDt8)cDHQho$BlAtVE*5o@k}%Jy9a?1)anb!Kc6q;nQ}}ktf=S7|$5tQ>Q!94N za-T5Ym@*Qa>Ed@&s#JC$|3owX8Mqs^%7Dzr+taImnMkT&M(izjTC?^5l*a~=2JbBF z!U{2w?-82LtghL zP$wCfG6r=T^V!4TmA~*`IKD27LypH`F|2I?>;{LS$GO?4{4;|8HoOPA5zd~l%_yJh zMlW8+Lao9XbLyN)`;iv;y9}cK>qcSMCT&_VqCYKi)AO>mLol^3esN z--MsSAl?kQm2ZXixkS;4jri0%QE1D*6rX~DA5U!QLPRH?=4r!jnj0x`FJ_JBG-TBD zAme0Z`jBVD%$(0yczFv3l{AYf^)j^i%p5!!=ZkR;Kj9XbhOw`{!2hEmB0KfyWb{Ym zsu4EKQzCcmHasxn97-1p3K=sVU-NrV>27uvFWrpf=_b^Fl`okt6u{)3RQKA0f}=ZN zpU{hbe(<2@GRpL8K6CIEuwVG-2hOPX;9a99ee6CQ<9p45xy(Y;hR%eCswXtU1a{pG z$8wE0p3_c4^QQ{PsCiJDN+@&W%Hgfun=J1q;Pt?tIQxcs4^2~G+dmG$E2L?{*=+1g zID#dqzwqeu0R;Blik-<#aILtH2{}BY;u+(Qi$8?UW+S@d-jy!y4;QDeTT*x+cfFq0 ziqfDSRJ=X{>joXi>O(d(b$JRThl;tg(}U~=?8aE@M7XF`AuDb$cI(#**O}~h91wz& zk9%XzuUaIIRzo-0u24U*3wM@Y!J5v&$Qrc?84iyz?8Qt>Gd_#D`r9z77>~xom&F&; zzI3R8`|He}4QlB{M|S2R^6E!%;8B0lS(Jw0)qSa?ZV;`rNyRzOfxMIUp>1aI5t1zEuPQ)$Qa3UhL^M+NDc{xAXz`HYber-prjHEy|6sR+?P6iFq8Xbao#>3l zUSTw}1&!=lIC?vn8Hc=h}u+$!-9S3kRwSNt5|HvExj9O*y? zTi#2O)(+r~mmrIx>&#m1OG=Tego3?<^j&>uvf5VhrOAmd%<-jBqf3SUR6mmK|63d_ ze}=gH454zq3$(&2Ih)Acc=I=6t>t(0^9U23je3+kbs*`T(xk#1W#+s2Q@flAjbgV~ zcc&qwr6x67^_Hghl-V;ka4~ zO_M5^WpP-T2Khkc;~RKJJ{N8ai-eEbV=P>Bjo;mdFdsaS=C!RC*^(g0PZ~*1vVTPG z6(`uYdQv;{Y>YW?DrH=Xob02xSv{1Zqw=BEl+Jul52`YMf|RO{c+sv#mmd8P*LKU& z{zu*DYe=2=?No!{mB##8)q-8{L(IEiLU&5#NN2}KG;YwO2bUG;*y-!2@nN^_krsp> zk>UPzI8>sEvPN=Nx6d4Q%a5cD+tsPyegvQUd}&(m87TDFg4{8(v~bL9Jl?nf>J6IY z5@C&{#<5U_6tx&?(a+M)u$<^i7G7FZRPh@hxfk<$j2s=f{2HS&{3v|b2TZ-hj0;Uw zn&SOdl8zC);~ zcfN4m-JLEqKEv7@4j9J#&S#UpqwKFX2DDky@q4epEAKou^p#ih^i{)~)cyRU))^;xU*>LMO)*k1b?d@&w zYQNxf)!0Nb-{A=F6{HHfX7m=LJl4ZE7={0o3%u7XHbLHqR$Pj8r1W4bm}PdMXP+FY zpil~3i(8P_YDRyfJ*lg56xOEelj{;U>JhyY9an8={$N+STQw0=FKf`UI0f1}BS>hP z2^#xOho-A<5^tG5ae9w5C10r)5$@i!ZlMQll2}nP=j5ydJm_Y%8+p9$MfMp!yf-uF zT^a8VbJ@M7nu|f~h5g<38i8HULiblaHZmvsRc9H>uFBE*kxekD?Lv7vhY{>0PyJmL zDQPb6k+WM-1v~11j*2kU53>d`xc_-Qt>`cCWi-goh6*4RD zPNSVJixY#m%Rj0&&ApW&GPia@bK-M|@H@zm@*uy;2RO+bq(8-W^l^3tZY$?9dz#3> zK@GP~MDe^!kJ_B?hznBNVJp*>uEaKoPnR~aXTge2TRPH+p33xKnkG%UZ9`Ra_?*sj zvXDDH=|H0nedTA`{$b|SLg|=c=|?&}nM3kA7g6foG?H`Ddd)|;>l;LI_0O3_v;+Rm z7jbx7K4La6#ra8B`14W>rJ#K%dy|2jKIW8qrVuq{Kd_i*j;(>YaO?9At2*r{P4+y} zqUFi-suInsdL%wPJcT}?ZD{d}5i8ytN3Y>3)U5VP94pv?#v^CNddqB4G9e!2Ga7_T zn?9N|c4BP(O|eIFwYWTsxr-VSX7Y?-R)inna4-h08qT?7-VOaX5Aq`h;cd4-GTN$4 zvX)-LiFfFy`J6E6jh1t{rI{T&sq4_aHj z>Uq-vkCx`3P|cUD#Y|k>T7`=K{i&|9C$2r?Ufre!v?^9&YUDOJ4CFnp@*h}qq~KE! zGjUt)G4EWD{(aRUt?3WNstH$1)Ij^euEu<6yK&2d@48!spEvL=SVt`(72i>(0c%L+(dG@U9~oXgRpCrQJwyk8&4GgHH~xGO&0v||pg4ULc;jhhSlK>d*()jj3Dk9|4DpVB7Z z{l8FKRu7MXmh_CdS=-L#%S}#vG7F`tg_}N<$tUwPto(Zes58Q+K0W&Kp&_M+Xs!*W>2Pf)1 zR*Fogvj3|0?nFpM)W8+ z2l?Fl7+92qWm9zE@$DMsQ6!f1?iBA_UqB%u5-(dUaB;*Dto0#kYM+d}2OcP;;nWi1 z$83j5JfroaC#}pn^X`iSIDijBI74|vno=EhA%87F+9Vrrg*3SL!;+g;kk)*F2(bbu3Pe0nO%Q=z5vIU1v zY!T(}_>5|PK(f>ICTG`w2qT#q=ppRs`8u8< zYONCiBV5QZ%Y_~`S<|0isXp_Y1*MHMXU5?wp8!K=3gpgrx>NF_kI+Ci!|GlnfaZt{MlK8zg z-Z~ER^x27i%$Q!En}zgMc`)bhxZ4pY#F^Ydmor8*DRwGGHfG{qoHEVc5RC?%siJh+GdhFNvaLv&!%W&yqBg7UBCL6W>2| zp>_m{dfPhq1-4=NBLtF@>++Dsho z+EqM@>P~y(N+joGWSDswh1KT+nAcb{K_L$hj#rosTe&n*Dp9+QbKEeSSX~ zlcwf(EcxdHZ*_HA)6s&`JrZPnV^>p-F*3%AZY8|M!=qzyih1&_d~Z^bx5wiVx+EXRzA$FI*#2~;Bzg8< zF`sqbJP9?<-GJs@hUB-#k{w}<@XXVv|72X~_p3kbeEYw1!iGv_mEr1k6VfYpqJa7A z1nuID_Vv5DM`HuK0in3;bAvOAZ}{%%PitNELX*0(aWCn>*y(5 zw#>$vCC@SW$_nf};01*{4^jAJH=N&&!M}dZuvVOdCf-x7jJSjcvkKVP%KNgN`%yCX z4z_e%%}mMrs2^R9Yc&bTpB{ndMP~G3VhXaxOoF+VBQ02)fk&LNc9*xMK0%vNR^~)? zi4q$9W)M=69mqJwo6nA05N_Z}&+fX>zV!kcwn-S*ojcKO3&q>012_=ahA)RVN#-=1 zLNIq|4s6R1zH^t0n9egu=g;1|r~AarcX!!+=}j|_7K?EknX$+7+tga_Ii3x|?PMo# zx`H;`v4{82@1k)}cb?;KM8`uDjQgTbGoNu!Wls^VY}2O`oGDF;e1wayt!dFSWio8w z%+a_rBG28JR&RSE`OBWVpX{D6di1B@Zvda&(|eGwtE2e(vlorlmKF&c%*gJJ7e!2( zCK~TM&~0sZYDkI@Klpwx7iuZ)DV&4aFBiV|NhEzU9$;++A+p$AIOZJ3A(KRZq;OrSXCj6%(Tetk`CsprAubb(V>X4Ec9HmRD9w4 z$5o!m%s<;lIELxcM8u;1jR~Sn&X?MJULbCBx;P{~j2=}rVw&wbkrvN96809VtgDjf zH$E4;QrOFp79pk=FrUU%LWcV01^bgaMZx|6GH1v0utPE;N#ha*`RBuO3Gd>&WMk}u zr%*ncQ!xI>Z4_T%eqD$$W$7zpPs2<0IXRM&zBYoRS~#<C9x;{;v1nhPX=|&XSX&}htc_E;%bvFjo-ukJIN<#4054IFZHNI_8-=S zTalc*9MzV6fEV+7&nwoTa4eq`7jm3@fjV!oIE#v9QDvgXHI8Y3|N zX!1#kMxR$?Kll*geoK`GZ<3{?6><1IrrrAwpN~Skr=j0WIZ0sYPUg2Af}{C=1*EcD z?B@c|V(ItnAhx1EoqI&zz*gMcX~@HFvOYOy||7%XOFMHA)t!!rM z8<6wdL3DHXS>E?J(Vwz@oDD3*n!r>j-L|I(uKeuY$9`V!4{hdW-?c^iu;z^?*)yNA zlx87n=6R?*(V(o}{UCQR6$T2bH1joZ>_`dLJuxIDqbQ8mjT2c??0J6>39D_YVt}SA z`xXwuFX$3`r0i&qwi)f*ERS9u(iB{VsOC za;K^xsTe+)ohyYs=;nlM?rZ0w??@lgy?IKgMRI4C@2L0GzDUZO*THq40wuZa5jk)E z@0V`CsA;CuuB<_OcrGkI(~unbJo|g6I!#frpu3w4=*%ul3VGIp3_3Fr)6avv?D-ya z`6#}=@gVsIQ!+EiMdnCP(v3NX%u%uEF{~WTcTPbmXAj0Kzl>I^V{jWW1xLp)qvN0s zN#DGQsP_%npJ-0*%-DZiCr#-)6lwN5?k#rk47|*c@~BZswP=9F8v@mc{2ug>heV1j7|9+`Foj#Z-rgM=7_#qGcbLBf4ni6f#1Bd zF^m}wt0{iS+hR+v>q24nr9b4`1(mPYpwmAS#MZU9#pg6*n%SW)N``(FBVAO;qG705 zYu-;}+=<1cc|Mrd@l%{Ty`BHoits!3M67vz09OwOK=rl>w3A#>T3|?*p47vDGa}z2 zEXdWq6S)TlqvNI;P8sNVg zL!wRSd#MGvY=4Wvq(u?cF7&hDHu9M#WfSZ~y7jFX-;X&!W*&6)MF{px4B{n!5U60U(4^V&g7tV@IOi5{koY8Bf7i-h36J2S{fGja%gdruX8`8YY z`C=!t`!+NGtH4FctM z^PoEt4|soepaMltIy`b2j8`XMQluhXFjy!|ho404SQR?);4R;%7w~K2k+H`t_JWX-EiaHl7s>@QMFAkScVW&qUC$wSR@%w0MKPBvj>(N!c zTM|){C^l`irO!&fBHWLizMJ)FVfU*A(J2meqUn?5!^iGaWM)qZgEtAyJasxVs5dpv zPY~J7wq*QWNoevJPT{#B(hnF5B%eUXvTV_Alw<_0-%(U@P-k>A+ymJn%cI0qhxSd@!Y^rG$^400v_JhM8b|jM_WxC z4d>m$k%BVxe_~9y^%j?mzhQID7JS~k)MJtxE_dH1yk}^T%fULK^rut2m@7|>ay$cuwl3K_%nbk=)&!_td^%h6nuPJF*o#67Y)%vX@3OKt`1kyWQf3%jz*>#67$@kBBw@sTh~ zxhIxQG8Szw98p}DCRXJpi=_EG#llWYI_+KrU+<9+McwI*ZYAHB&2f{@1$&Fj@Rr}7 za^b-+P`!it{{m5-I}0Y7<&cxrMe8p;#Od5Y$*X~Qd)Jquei@O;5))cb$=o_mW7_{y zn@oQXqf6~pShMQNBBZ%zMBx=~g7 zaxC=Lp)k$?OFtl*e8!!&r)yAwRsj3xoyoMSC++w%fMT2+sbfwbx?pNd4pH62FdaX- zpkYUu{bman?$-@TG@#G1R|K9r1Qa%l(Ip2`TI013bfbUp4Wke|=_mnYq5l`4B`UNTQrmhWl@QgP$XIT105XDx>>qH)VdQTolE+Bp~9Ub|Ri zrh8I<=8@Ii;Cb_I1qw9jPEX|d+*$IId5o@fKjpbN*IR{lHQb%Wtux*-EC$+hgv(xati zM&vNth`Jr_M&HVq8TnL&>dN)W-o%!c{Ij6llXyP1^&)}`eaK_74ILQx1l!MglItB$ znr?m>p#!vmR zU-q3i=DiJ0wO!Eq=a4vjgV{7n(>SkOF3vCLho{lbl>es&V=wl^JvsK9KmCR=5mOLy ziWyk0zv0Sk>RtO=IIonCdR006oc9mujR(;3`J3oHz8ROczCf2Dov6t=EH-C3QkxTd zT281iyR;X5(ENpIVaG-52s1j+xf|qi;D_Q_|D(I6Qg+^ zeRBm8Ps!5c3(~xUVLzMmKa34kB~5=lo|klB?%!0Q*3FV)ytoT=^q`2kpiTq-{=|f; zVu>#2&E!9C!*`#P_{p82!(aE})ATsJI$}&aGScziu_BCiV>Uw0D1-%?;H&mq&atmX zjOIwFb@>7B5#6D8$sJRr3ehWfKWFWk_dQ`B{D^ngE2FSdgLel8c}P9JmGf7#*yCnT z^-a?e6Ccf4HglTaa|^uY4(nZSu$Xd?Qjx3J_~Lt?;$K4Sh^dLWGMa zj&-QAt5b%4y)8!MQ=SiJGmEBg9*TID|8w(aEaE)W#tID_VuyGzXK?$Sdo9A;`;yN# z&SNy#VfHr8!5K2^(?mvcw`i_dp=3pdRb`SZH@}IcnLX(J$_O#Bs#apY--7)2e3e+s ze8i_37Z_d{D<-LwqoA@~gy^{ysF#-EsnamT@;)W_eRpwJTb|jFGPEygbHS+q8CuJK z3;q5{JYQ}_Huu^tpS7o&8{e=ds0-!3Fr{tz6_6ikK+0Dg==ZKlxXxT4W?bEa*sbSr zvPmLNXJ5ung|ql;dcwQ=nFNfR&V7fc$?&dL#(r z*Pvn0hxSTwp3dv1SeoEUb(%ZjwMYZ++#PAj);ai?kRh@?zYG6DBigsBQ5^jBLZ~Nj z4)?AB`(F$Y&V9+&C_Tvfy3(9YGPJw?5H3~srsu(YM`9k-?rcGm7s*nWQ`hjS_goRu zcfMqZR~1%PR*SBA`wAS4`OXo$MaaFk6gOmkAkNBMTpYm6AN~ZMT6Re?w`M4m+(y!y z;rn=ZKLnjhJ~Z!UDLMi}eWZ@3(7-i6-a96}dWS`1G`-;)R<3=U|Hk9K_fF|eJ z6?vZg4VQ0sqXz3zaZ3FWl%>q+#8)MxLfkra=eyB67$zh;%Kg zDY^^yt_sxEZ#LGikML1((IkzMX#93)&YR+`Kpu*dvH$cY@zixFWc<~!iZcj>dkuJ| zAwljg6-xA60KWi5q^9dp^K%7SVf+Ju%x{TS_=$nLKVZNIJ8H~Tp=IULw0XKU{ZM`i zqn90=y;Gx6thfa2j<$T=TypY24}p zlUJ%j(|)X&Xm>@_O?Du&+{aLy(i7P|`MGiM6aKURAvUx+^ZVjG@_a&3d?OSGve21(#% zHM)D~2l{VGfzMuVifhmmzdTp+&(WW*?hO}j+K=MFTpt>B=ajh04i&BDH&|_CNK+ql zLzjD<+_5qvsYQFm+@L0GHC3m*mwVH#tLyRQx&|dZVCI_E2B-&i=X{+j`Sl*aB4t%l zHc_C}%Yr2??1{|f+*{nfSh3yGi3*tOzfSkLBx9f>b>n}ZxMj&%98by`=uJB(SkeE_ ztHBlybZwdo=}+uMyZ^kx?4VSH%Bz#(oTr$1I~OxkwWu@T4bS^hP`Cdda<)|=&ysUb z30e3N_6HB;l_+}kX`FI+i0A@U3Uk|ulwX_;Rr00Kw+Er((wjz1lF;jyh1lH9mRggC z)1(`RQ2Zp3S>PV@Vw@5!ymk)yN^W$Xcf&Hv641NCk6wINqn^VT;NZmb*v@moAwQh( zTlP9~y6ch@vw-F#C8G77K7C!dox`%+8IX6RlwT)sA@q}Y*knT=kO-YiW@5=Po>x@c z(=!x`thiQmyKYA{)$haxnP;fVGA4~| z(I(w;xPFSpP>*|fzW*93-_Av<+Fe-0KY=;Vw#?^T#gCP`w7BjH^Cf;@zm^sC(8)p6 z8!7r2#{HM;xw!sao>oM#i~fG6@a=IJlg=y9t=uj=hd&LS4_dT$v_76h#USNLfzTbk zU3?qA7r$Feg+_@oOmC(mT(U(}=1dSlC6_@F128#n0(xBZCHvC3*v`y5{|6)atj%1a zt39z?&4-;y-N@~Apm1rvC@O7@X#F&kg6%%lLN85)JPehEdyAczyC@5aDXyTQN^tvI zfN$f6;cT)c?vL7or-$s}ukjf7nWUq-wu(E)Hn_|#4yW&Lk!loz4-bvV z=f95_sKgl69v)Cy2ktDy*KfYXDc&vIs39>96B#@xAW9y=9)*M^K&R3oD1U4I=dkP;t^-c zu9S$!cy-H2qEuu8dB1P)UKt~~PO3a(E``aEJc;YMVTk`5$G(@3C~2uh=%6hAJ%5MX z{z^3WjDhWzxA63oCX<`Y0voDHij(Wbl^e#ib_4eSrPPrAP@iItXw#YxoKfA?gL^Z_ zaXji6mO5F{)~V-_>=BPPT~oRpbsQgka*!zX0V~JEf@b!?!FCy%|1_Grh6A9=okRJm z3_LU%g|Z%(P~TF31gS)Ge`&?%b!q zyhx5@%l*aMj|UKQ;46O5HkC+uoyM$Id8*GiB?c{y6xrXi;KyEP|MOdf(p&E4SJ~60 zskOrWWeR&4JSfOhpJG~u;$=@qEbPPV)Eook?)%O&25bKPjAh@M8pbr}QXcm;^*f4C z6st=Xv90(v^A*e=SW&!>>+zIY^h?lz*lCvo|Q?rswnYCOBX z7cJuCdNNOIkht~Ij*k9wr}{xT;s?7OGOoH%+3~$%;dDcq@Mol<$5h)@F7<`FR_DrFnRVs#JF4+BNcSX+Gi3n4#?nWpc#z|UWS&Ci=zA$ z|4xmqarbRMqUExrd_N8CZrsc9{)dB_7n%9)N9GpF%$?i@zh`dZCG%TmTEE8rCE4Pb z7|3^oAJAY%VA+4p6xr`7?EjWZzRZwClbbJf%>3vR`b`Iq<_D9?#00T!oe>tF@}~OT zPw?F|Lb#Y6gl+T%Z1;I0=1DkvTh6--@9jPc8W%aE{1RiCmouSR2iN`I0r$D5^O=3= zYCmvdMOV66D37r`3%Zori>ADNhK*6&F(1s?ze`033uxi&tsBiM{DfMgX0%P*!sm}! zl5c&OxxO|Azh-~-$;|l#liD~mJdPHw^#+i;lPUyfw_fg5M(>Bpn5`~H0mYtJ?!8TX zo1jUF%!Zs6UW;zkUX=b!owWA-LEIG=^0sJ4_0;#cGN=z7xljgq?y-E)R-w#WO{lrl z2rF-Ok~e&cJ#V|xu~Xa|DoYn$vAU9?k8i}iqCjzC6vWb36UDaBjbg!4Gx2#$PmJmA zLeHXKplozsEP853&Bl#T{HBb7Z+ek_$``EBn}t{XW?=LFM>ulb1Jf2qAZuGKzY~<1 z2kLN>ej;FnM&T>SQu0?Nid}zFa*m%%6$jhcWwR6UVLoK( z1zm`@@y4OTtk;#FW+!l%u`JV1#JDI5fIzCyKijOYGWbT;+Tj|l8=&9`OONRbj<}NMnhk;QA*czx$(jGfF z8_aiKSqE}?m5c?={e`l;7yHysz~WYwI8kax#>~HUUhtGVF~1SD%ZQ!q_k=9(D6e#} z;f#qQ)?9fDmHHgWWU-e(?WFO;Fs zx#L9UqxpE8#rbFb8=_2VG1O+(!f&n_O`oGkzlRx7344Qk=W5WM|J3N~dF}@f)1W3@ z8_vdCQrN3hWF7aU{Xg94XigedjPj=Oe~d`>W-e|F^`eHC_mDeiJA5xcfYOBm41PTw zmA^_6viTa6j%8v-bs-k;+2*Cxb@*=o3+c7pd5?V_S$z~J%C0NjKXL}=kNrj7TLaQk z)x`NH$M8*0g=Uz&5RcAhV0E-19W34tgW@OGk_F*i;n$r-bOY?cz?wZpg_U z65|p!35OXMV3aYKJ>a9!Q8d!KPu3JTUYvlIZ<|D4zc47hb>=ysZsF0n+7x0wScG+y ziRP{96x`BJ+-orr-QSq-Gi!$TRfn(Q-MYh2k?}x?nJliq$%V%8Dfn4$hBoaSOlAMW zrU_b**<;VUpY9aDp&6^sjYYqgHk7dsE1 zugn82l?>gZfEv3}_Pfsd8A4W<3EZ;PfbW_y8)U0HALzpL%L(7Nkca-5yPJI`?%~Rl;{}ZQ_ab154)|n z!}FPUtnM#PU~lMkDE0n;8|qOoDj@hvUchfXpZgV8z{TVb?p<5RY{0R+pX5G7Ngg(+ z#-VfW74FGbqta~;4EF9u%eg$*e^>_d<gb#%#$~?75hJ)fe~xxbY|QT4M4eB1ei{H;zH`XJ76EJ`>NjCgWe9K4kcAuvqPU6^8YEK5#Um z0QV6v8sv;JLw$NO)&M3SWH6%Mo(B9k41aq$;y|%3Ik7waw*Eal>~BuH7j&VjinBPw zOwkvve-PDLgI9?gh0rpk0jmv#(~7O4CYT-X8=^(i))F!7t_gYdFU&u)sVB{g94v01 zwgYv7lVk>7!j_lij)DPB}II#e+{<|b4n&Xh>uYraC*@$8B-h^^+X^ z*}jLJMFVN;=g%5zNs&m*Qr7GSF7Qn{WEqMo0D6(5q?bj4jJpNl-enc z;m2O0CC8Pb*O%j-$ulgiu_Vv@BJ4~4hHsv|Xk%dmf{I(<+Z>O)Tvv(CSY?vR+KOcg zpM4e;e?#$_BpCkOExva*#=i6MqJ@1X%6vzx>=Pt*ujBbL@7`V&$>GT}Me?`pN*mj2 zuVv7azFn z5NQe5vDUIx;yc(|cvouUci}%QS5{-Uls!6AlxW&gBT^leCq8k{ajCK@Rj>>6n8HGg z-_5)IzI+efJ`b^@tf`~WmR8R8K_#D6Qu~@wtZQFNU)`SuhxDXV8~c;xK`)B=q(vz< zo)p{Imof$iO7_gtr;Y@B?sm2vl)`#!TO0@~2a@+7() z>CXIj7wW~c!-EGkX{?eLO)|KK5vCS&@rFHj_o@)%7|PCT_8@lEqRi~&oEa4qpr=dA z{8r-Ee4a`DW5>7Ne{hkz2AzqrR6L(^65aP>f4mZfy&jH;8xL^1QGvGRY(f8=y&^r+ zjOMVP+$AqYWc2GrVUPBqgS}jX+5K41U`M~5Hj0HeRLK3EKCLTt6Jov+*=_4ZtL&mg z4Z8?5rtvfX=M&`dd+_>{dbG!ICiOG3RepWPa*Ina`{qgS|Be^gNqaHugc_+-sfzXN zCH3IE>G~ukiD5`2?94Ujfr1%bi_xb&%rYuB*P@BT%_-xy5!D^GrxL!m*F7+!r~^Ie zR_IzxJWq7{l?$E!nT}lvf|h%;zwP-vR8Jm3Q+;k=QE5EyW==wj`B4$u7T`>uGZ;7a z5_iDUamg0C>PW$d@6KxE<(4%1*xZ{@M=s795ywIy`v9_{{7QM zo&~=nPq*Rlq3&Wk_nPdp+F)?+hd3DNLU+^^X`Iw=q0sXQ-pVH7-gi%2$*#nu^?Ptj z<%(F??*n_SBA8MC5TCM4@S$d>FyUF&W+CNdPXB%99q`EG2)t!TxoyW`cwrY-C>zlN zn?&qgR*EjQ@8G;}1MdDcg8CQ!{@$L1v`s;%Re6jQ|J|H*?1>ROFCn_B5R-b1L}KS{ zOyBpMon%XK^K}N=Uf#t|lYXe+Tt>Q|1N~c?%>0-)q6hEWMQsjRwuXr1dEBj-xDyMF zy=m-3qIY&{;5oZL&2tH)=vADH&T%7$JTICtXBR@lo}#(8DmhGvlni@W1CKE>bS=oS zAoN=mq?sYLcs@ehqY*`&+9qtm{kg+pObdLsNOsgXqlbkZMeZpP{ldRVhLSJ!;hay1)%Ak) zj|Y*)6mvTA#avV`3Z%{vjeulFD^o(e&jzcdK>_wFRmaIrb}Voz$lFVa%HIl%clas?=lo zcSI$$ayP+}F1E?jAB!9x$B8A(i+KRwDWeNB?q(zNTOMR%dW#<$A7d$d6Qk3Zw^kqp z|D9*xxrMpRmu&IFposTP%kb)?0j3zG;`VSax_)mee*0TNwT~b5dKizYJX=^_6U?a^ zj+RN&@m?xaSQ{x&M)N4d+eV2O#a(H`ZGEH$Ul5B{YEj`cJ{LFG&{1Z3SL?(xTi$~n zZDZ$YTnx$t#Gui8kYGp3_RmFXRYD$nm^e*M+Wk zImvgtgD8y)AVX%F%o^T`=STIqBi#$f7AcYW9}Th_Y=xssT9K4!OQF_w_%t{U?SAIe zWgK&tdt||f@3NlKFWAqy8P})iv750N^M3I=BWwfbKA1p+x5a^i839yITD-4m>|YLhbmoFky|(uQ!`cQPylB^E1pBkvme!RE-8mI zbzeXCreD3;S5^0s`yS3zc#2u%XByD3R-62WzsKXPa+D=h$@cdZ*!63L!%rh-$PL5x zHZPhrUjcru_S}ydLM3~vgtxOF9;))3@Pokkp1O44b`fXSXF@4Xi&CGSMTl2#?6lJ- z!}hl@^P7n6cfxT->J>Xr%Z2e2A83@`L9_Z_5w~d;vp#-7a<)Mj8+b96SA`tg8#w>u zMKgD_=#qsbU=Hr0u9}5N8QGULbFVtQha+-PM?YR zA4g{yR^`@pVY<7!Q%bt`8j~)0I#*rH;fA{HW2f{3jk*oY{ih>3xLA|{A{ z@y+-9eXesUviI|>HRl-jJx|ez&dztFu#s+%cGsg7Q<(Wa%bz)Sf<|j<(Ghklk4f`k zS6CMcThC5n7d{*5zLXr8XF^eGUi3`bO5*xgkB;O^DBVUwC_HkcCCp=ZqILs@D^f8k zPL{kHZ{zWzR3rp7VAZY@xT}+kjsFy=spnI?v0$$7{=?WcpdCZ{{(<3;Q&9f<9DmhS z$obGFTo-n=koyeTP>dB%Q@Hd7od36qJxEs1-@;Jm5ro+GcU-korVPD`)>>DUge>ZGG zWRIO<(F1cb5t+EC^g)EIF`=R{JJ8Q9LF8w%!<4y0(QaErn!76X)-b1w$@P-2Rm`Ed z?@8wKcZi)i|6rNliMKB+c@N>w-p)^0!gqtS7S3ex`6pHu7GR}aAni@LCTXwRjdcFq ztN1-Gjn!C>nuqGNOzM&39^cRN4ocB@o>jQb zy@yL)L>^;aGWV=M+DNhn=HeXBWJ9#qh#w)G#k>(Jq}MzbftNR;?Dz?>$iV`Mr`F-* zI%l!m?uHneo&&>2T~N4WF{*3Xai=f=eHYGx&Ghcne~B}+p7Oi04BSC7qW+h|h1(7d zco*B!^KI)T-4?qsyWW`8J8nqstjiTvvo|9!eJq|U-4kn$?Lyf}C*(C32&3n@SZ_NW zJJqC7p1)OO2YJz}mkqc#Q~~q1^Y?1YSG2$XEisg_r}cOT7-&E@z6=kzGxT`0D}1@L zIV?PveY-!!mXp=&3|R`-+&V~Em?PyV^9qL6;?TaKIQ4-&0)CCCdD9i!7V~E@W;%O* zb*Oe2JH0r&9Dh=Wp35A z?hEI;`ukCjb)GnoHw>#BCP32cft1-BF;jO9vckuqCSW3b9SMbp>zVljnlG=1?)-WC z+TNW)>#R`59qx3wk36?Kdi+*eQ?|1ViUcODhoU+I0Km0wm$7Z79@q65fyny51 zttj^o^M7`&7Za82>B>xF+H@*kqPEtSYHmx@1K&XQV_VbLr`w?U`82l7F(UW4RLrTr zkAa-YUw0@EJ1R2~Y{JjHvkQ67qK`w3?VNEO4_Oh;_uV>7YuJqwkGt~y_yP{nRm`IS z?2_R32X7X?J9y$q%Vnq*Kf;wC3*o+15sG21)ZoLj#MTfj?c+i<+(Gu>v-FpV%nX>7 z$&N*D+K>=RdQFS)DA|Ry*!SDKX*Wtg29fm0C`ylAjrO`)1T0mf`nQ`UuY()Wl&D9! zr)CL@*W3dxk)rmxv69$TDPq#So4gB|IMEo~SpRC3W!t>>+IKJA1Znztw%yua5h3nE&-&TZ`-R3N*KFQK!#6{O(I=hUn z_xr`}sbWN(vcu$6y7a8u0iog+goOD9B&i)B8MxMlcNDfXw@ZoW_*5R!k$gBL^F_qsD2C+XNUBbG<33S z>ZCgSTszTG(@XNlQJyl5e__*_M-rb!A7MA=Gn#S-i+Md%Y0en-p3X9&OwI(EJIPSb zotk98@57NPe-RyJMfS-G6gE1H_9a}ypcTCTUEYrz`MlG2_*t0V_UAc69xfg)MT|-U z&u6TmlUj4~4&!4WRdHIFjMb!(2TdUtm?aLR>QK^;MKB%gL`s}lGj8NR z^P3ITOPSJY<)v`op6#U(+BEEFBBr|hll=GXq9}X+2C?x6MW4^wSo7vJVl(E6{(goc zLiH_r8`q0LYLCV6(Xq^wzbl5NI-ZS1%$`ixu*#aI>63k$%C20!G7s8KodpL5>d$5eI( zAH3^HedoW1w4FOiZ#E$Py`xKE=-OGGwx(6TkL8Wd5=o?HZ*;VbdRD%7wR3 z*Oww+<&R>dAxZkml#4A!JB8f*B%$cITXdgyNGw$yEJ?`Kf&Dg5y6Jlhf#Zn#?~Wv; zUWe?PDp zgJ{_`=)xq&3*zaFT9GFI0l!{2QbnOMHje(wvkxztcVe43zV8>hd0CLzDCS>QZ^ig? z8nmU0C#fXv#uT0{9`o>|G2WBWGfSP)%{17rK=kDxb2Un($!;UNbwB!3$|h6#QyxJj zQht>1&x36B?P%ne{t`(HXMtX_^Y70FvAQUf@3oHfJFfcT-X~2sL(D!7XvZ=O<{fbN zyS|$mPF#x8kS_H6 z<4nn9onv_Y!;Ct&3>0Qnr_kds-`yvElT=`&H49IdIVBc8g%7HI39MdLf;Wqw5ab6?kHr7-%H)7 z#wZ0*RWronq7YK10+jr{DawC1@?F!BrmfJ%$SN5+9_mcLv(F3rB2_XNW=e1Bw2^b- zCwKO%ps02g^v*7W`BvlvOfJg#QoR;OS->+=VI%aBhob! z;Lna*sV^BX$iU`pp>%nPHT7>R#TwZl@}0@~#dV2jtE^_$w#vYPFMe7O{NNaNPme}*Uup6F8|vl z217p~7ZRJ_c>lLWl$&HB&*7Hn74=JE7QF@wk`?ii^Hk$Y%{N zyxWW7{kep!?~FkE0Y}8T`q9--Q5ZdV2xp;tk==1sQuDebxjsW3>z1gI>60rGqn+tu z#Wm*t{C6y5bD)qn^4>iYZY}4nb?fs+)wWi?>yJz@DMqS=KIBiGpaZdWJ4Jf zWVl}zh@;Q=oP0@^;&%CzOmU|eQM4B^DS7o(w%hy^gt#aHS6=3<s#g$wn-DL8?v$dm%ALj@x+&5|Z(;wFKUI%ZrVq~D(b*UVBWF_@ z)Di@PDd3rEG4I>|U!9>=K2Bpf;>Jc~a?S?5Sbo+|r<+Zr2>Okc)&~L;VxFi1vJYO0*58Strklo;+iJ z83+n_^g8t6H9OjVJwsR(GygE5L#X^bBPm>T3#LEypg25HY|y@iTUWP;#OIB0J+e)5 zggdGyWxpbEOn}%I-+*a1|6(7cE)9AmMcX3{=`r)4y_fyuu7x38oMcXsZ(m~ga!Y#3 zU5*KAZ_sOSi74{kg`C!WgakB63TAD`zHZqFzELW6IXiN3mR%TMMc`)}T6-x1EsW3lS0+nxk24UT(3@(qH0dOF zrMk}VLpe!5vD-Kgx8F*{#jYDA4aQGlnj0n3zt~?|W&H^!W-Jm3HxijO`3(VmVkOt! z{1Csl+JvUrY!!Z?)>vYoT-sT1rX(v(108e6iL}F4Fx2OZL}e1YYhG8v_}{zIk0r|N;d8k++sg(!#>i2Q6!VP3q%muH4SFTmkY(#7 z{yaUzBWASBm|cLiyK6WrXhEq{p29uuAA6)1Lxr83(Y6}I_Ek*H1Z{Qy%KfZmxPLs7 zYL#Sh<3P1|tfoP?RfCu{Q6|dNm=UtNlUb1tm~xo+5eHN#e|Zz!*1J-@$4@v!yvNBQ z?(~cuty25{VU?T%*%vd5^9!HY87?Gn7n}Ro(R%Das3aOv z|6COEo%O0`Uy?S=7pIbV_Ww4L%5E#N(~H0RO_hjijDg0YE;LsA6ABVz5Ixt0Hfvtx z?x!A#!jhTseGwm1zezlH??cqRt9URjN4#Az8Wp`SqTyE$G2Yx$%>AfMr%p;y$f7kR z{=T~OCQy}vU2G-wbJXca_YYVxXfHy!8@DrkyO=V*7=8Ku_O|-F(DmDh7kzyxT((%a zF1Nz$Ia1`xJN)Jz!1Wqc8Zy(3E-ro`)_j$tm<|*ArQ}MN5AQ^O8O|*lccFJDW+MHu zG3m59P#yPGU+r?GlnCC3vFAK=nThBU)P>?k6Kx)`s`P}t3GLeGNh4>+NuK<@CXtJB zqdYmzlP^yaFQ0OL?~x~6+_8`pj9n|kD($eTe#KgBE?NTegWEY@zs4-2+@uH{GiV)O; zGj3NyC~MT#w)ILvz&dAhnhugvKlqmn{)n&&%^;56d37C0}thhWN$^Pz5Nxz0d+_> z2&7N!)g0yXikbZ`%<_DPd*Uo!ybPw35>xT8CIx$ARcY{*p``_9Qc+&4OWQ~E5%IIK zvG=kJJ;-w*2M+_9p=Coq`B~g;lnp&ev?t{r7Buj_8TDUmOCLE`;$Cq8OUGOaE$(E_ z-;ixMkuXIJ?P5t8&r7i^#I5A$#EY1BW-;P>on-sP{1f}B; zXUz^PW@3U#GvY5;P|At}*lj3F$yw4gdEH+8c5Yz@WIM9L62xa^p6x$=4%JWoqNmjv zBp#NciJ31&fMqdy&I=}`8y+~(i!;9?ds2sb7tU(VfZt7jO8eX&4tWc4IIb7w?hC{= zg8(Y#`&-h(uCTZo&Y1~9?71ZvpcYJejwZA*q(yRXa4=RkS<+{Z;`5UgJH)qmb|LDH zkW@xZhut2|?|*j1^g>N6Ojv|{XB)*nji2IM(q8=0v4KyN4nCexht+QG?Tv0nij!b& zjTg;7s6^3+8-&JP7n;Q9Pi6fm%;Q|3f$m1UjrV~6$WQ1NIfK0{)3~?w9J;>yQQUMF zdoFOVk~xOyC!V9eF%pmMJ!t%|`GxL20oBWIB+6m{v)Luu?TbfTB+o7#Em zpBVelp2qG{rUl8VVq8ZT&M72g@%jC@G}?u($>hR$?FMEmSdp3GPS{R3jT7H0nb$l5 zHKyutY`l)oy*I*ir5k28y@HiPHx%!7!Pbmo+*@6Y3YpQ+G}(jpEf-;TbrsqtlrgvI z77X`J!_2NN+@*7;E@!sl>~RGwZR6+6h$84FtQL-X{?sy){m+`9$(O=O)*uUmox0J? zlYMDoVJZ4-`05dwdH&ND)_Kv9 zcdOsF7Gc;9H;VBIg~?_98MZpnP>n`0aNBWIJ6hA=u`1N!<%z4-Bau<6Lq?xl#H-?N zXdR zv5k@?Sx)E`!I^%afY5%?PsLMtCn`QVNXQ@dqr>W9Lf_e&R`>CtH5DhtkqQS2%OY}^ znIgUn45X&o;iC7_D!Ap!3d_eQMbV5h+@7+p^zzQ`V*Z+Dw9YXQ9u{(RzpbmtYvZnH zuXa=o$``s-`ZSq&274oeM7BvMCf<~#a_-9Le{I39n=17EmNL0Ck>0u+_qGJya zW}uDKj>3LLC-iSBBVKOLb94T>8 zoFU?Cz47C}$>Phur^5XGU(tD@cj>;$0;uOjP>k7e_;YS_KR-kEPP_&GWvSQ_)|+!m z7x076xg%Wk$STbV{dmTxkz`LxHaVl4!cA zt$xaLqdSl>VSdTLCdAHcz!c+^FmVc_42?$^bYKj|ZtY7=7XPrIED0+^h}2*8g!Ib+ z$n;R6ywK@*=HUfRGtTku(n7YyNSt4-PBpFO)I+s{v#LaGygT^eCQUg9yy)!%4SLu5 z56XP@?0E4O`+Bj<=(!B%_3vZ0`D;vdm8C&9>-oH-NFTRrG8eF!8F&)XX}>A--`o&k z;XNs;TatM9@v&HPB!aBY>Z3BylYCr1LM^2y)HjEa%g=xKXEP07R=AMmzTeoX>44O# z;mj$&gHm@p$bL3Or1~{xl2~DW(ITiPH( z8$M$=P|KxlBIVCF%x|?1=na_Yp)B`&mNmJ7*Vv8U+G>m@^c zTqtCkFI`BBD4lmbko2C}(z0XQLp4gDWBurStf^C?`rr>}VrIjIEY4YR|6OnBE)4!E zO{3@;$_B~PwISOPdy1bE5431l*g|-&XhXw$88Rq21ewk3PM@B+XL3i`g;r)O(0tCKZy!{Q zlyDuYGjQg8M+M5aGB11{Q2IO#tFCL(k2kBap?sEb=J?5Iq$fx82@cL4pWa%8Wd41{9|TeZmkU6+j>SEHeU*vZEp}%`b+YlW(l4o$WhP- zE0VjROA8E5sNVrS8Y^W&w|=sp?Vkgs{kEc*AWJghJnsJDOmyQc?s49?z0Jyj?nDXI zuD7Q1@+D|d_MuA_$FRJ59d6HffNpn=V)dd}l*!-0o&S#WyK4vJ_0J$-x&iguaRtFM z8qrP7g5KvHM?HUz<9Y98aQOoKqCeuyQzPoNbCx1uEM&Z&XEjlahPEk!OnaOIx-a>m)86S^AR~<$_ z2hr;VZ_%8kOXcZZ>DJhP$ezodBYu_)&AE$>WJ(FVV_lLw2-A1;#)8hBxHm)#mzGb$ z@VY&agbc*a?jcABUI0(Or-+t1E}4JB9^vk77*_LGa&wplw)c2|XKt!uz~5n*FOvwr z?@gGl@ByQiFN2J1Ene|VV8-S{cv18d^LD+)`A8j#n5NFZ?*eh;uO%tg>yVS{Mj>g@ zq^`0u6kG)173xmL78%IDc?2b29GGog!pyW(Tvl+P_B9zuJ9!z|vzY0g{~u&@1b2}5 zPP6Vme7PymU%ndK4(>qL15xP6K8?nOWpLZE8dV3%QMT-8g*^O*A2hpl#f7qmTp}~Pc6yIeL z%K1gnZJiH&U*w5$nG~LpC{UW~eBm3Bi}Cq?&@1haMDbb%7M#!`iTr6%Gitan`E(Q~ zRtp-q=CF8^SdRMR{uYLMbnHaI{vg|Z@wx@ z9_!Iw>sh4^TV9B7z3iC?HB20N=SGWnzmhnux1_%N1L%kKCUNMY1LaM3r4q9QQC+D` zUzdX z?}S&I0ab@7VQY&&-cB*1xZ-G(B#e}(ChL-Kb{@R4M~J9j%G6_4DZKVf5@##eN3NfW zLZ-Du$WO_K<>p$bUff(-k$)ON2QFgHTJGviEJn=rN5~s( zL|uQ%Af%%SiwA45N4#9bs6K;GbEG#6bgr0PimX}(&St-W>7du#pR{I{_iL<^J%c1~ zGYX!42S+Dt4ItDaJ<++%juZET1c~7*Md(InPGGESxHVu@bzMjlJ9l+ia6@5DRk~7cyEGZ@O z9y1TWqt#f4-Y}2uV)<{B4pXIVy{qwJf*c*nGNZIz@^I4^^!$VdnjicYW%}W?!l*a; zHr^L^U3*c%qc)+}Y)<{$Z*!jB7!UOfnJ3x^Jw7|OZ?Wba*(3ZN6N4k#%iv(bS?KU9 zqB=5xy%r64aB8ykZ0*y->vSp}W z0-uM5Y+~17hGg8TKH}Yn5%_&5H55~JiwO_+!({Iy$)TugV(6-H7=3QWURNuc^ne+$ zfpR2&*qIJP9bd0}K~9D-?|cJj`>btvFoM}V2Rx~|dkS_;bs>XtKjw!`LjepZl%1=$ zU-*lNvOxM*&CD)zIQO z+GOljID|m{tYwTy!g2Ns78=`AfmtTn&z~0yk6F;s^7Z%^x?5x$`H;#pAF6o!PwXjI zp~)E%n)&jx(3}4Y)iDlKB|eFm!^)I2wg#T}?xKp%OUl=8VODB2x=&=rWcyuyhMz!U zVIZvu_$u7ZHo%Fw9Hg;H7_Z_^L>CPzJ#t%|NzGt()MqUCDFqH%v)wG1RKzaI6J!JYe|=V*@q0GwSR5t)wI>P>=Q#R`^}lN#cbw63B7xM8ujxw zA!c_0@*@kdqGC97^N(Wj$MbN?-iy`zp4}5^L}ec{k=p(fhk15(lRL{TJa60bPLU!* zm>H+phV{s!09c7_dd(eJHYxg{pA007-&M5Pcf+3T4kqbR410!# z06%PJkIcZ185r4WO8T38Df?71dW7(qvPn>DU=o7++tEYSE_BPV9uM+tsiWGSRQ`F)wC5`jlfT~ILl z8XWouQd^u4POo_jpP*2(8DNLn9h_VF8%(P|&4b#i?Ra;qjr-0Ik!PC>r^$gfCOuE19fBdT6uO#f+K6>?g3^z5oReR-#b<;=8PvYvUR{enc7>+amO+KRnH zbMVsImUIrS$K8Y5n8)r&-G1*yIM4PUzJ7sq7ng9C*#b>7|KLySFgP3y#PjGE7?YNU zTYkQ9UQ>eDINmWmi(%&5Jv_>Jh?2mmh*Hi*htG8!9qx_ka_)%D=b7!Po!B>dHlAK} zrLa%yFk-qrR(x@%-6dzSHo}9m2O-p)D8Zc<%oJcRLdvS;ShSIMz>=QKUh2lPpd;uO zr$h)xXlRQwV%f{9bX|)yl^+XLwVwE1Z$n{{AShoMfQ9x3WRfILh0QhS zec6~g0^2Zud=vBA?Z}LoEIGICz+lR6A){}^yxt8W!|t;%YjUJTX=P&aovt{w#*9KP zs*1e#?7DxYCl&>I(K%*XH$Ki6-tFAwoZ`>aQ4jI+unko#|0}ukwFGJ5zeKuEckz4B zO+?KaEpB}%FWn@4gL$~$M8!uH+B9jKSWquZV|yu4pQZWY?f4IPvHLH&UQ0Npov9pmTwp!|U)*sN&q%v#5J9mRVHJIG3Ek9P9=7{Yn}`UfhH6*dQ`p9mgIG zQ`B06(j3>}MW!~^ca&`0ioV?O-u>w@9vdCRb#Fm`+GS|UsRUGdcSuf5?JLG^d5G_+eMRv1 zF{RgT)v*_2wHUlPLqxkZ;(T4WcwP8j+?Medf1ia*w#~D|mi7C^wKsXf(A^wSC8s2D zif8dO*j+N+=O{cfn0;=zRy^^~gEr5DhRyv|`ckU|8qv=Xnxspu+Fg+l*NIM{Lw}cB z;(7l^*wf#Zwp{Uphr&s$xb8?7x-{@Rum&MTHr!8ahB`aNYR8&V+4cv_#eN6bh}n3~ z9khAbkMUv5eC*HcO|rUDWVB>CF83z-J;)t)J${Js^E&ay-4gE1ruSvPX`P2)9>GP? z`-%z`ndwo>a^~*M_opujvb68vBW(2drl&bZbfoGN8ZDWNSXY34lYihtDW8v+>ll<; zi*RXKQrmt8)w_5HwLyc*lQj^!G+1)}&dWGF7lIv_-@p zAGGa1hn&m)F#1HajhXpgvR!Ej^K{m?=u>f~Eq$r&LF09-srwU8+EJW}!?J_8TQpcW zoy|e#qMVSZu^WZ%=s8G#Z!Tsq8=}#zLn!rX!<+%QGD&b{@NF)5< zlepLJNPjLVQ0LZ2(L=DON!pLzJh7z#Qg4KmI-1mwTEqO*R8)H@Q&{ymVPcZM^Rc9Ms;(hInU+Mbx;Mg)m_MVo&|ocJj6Uj zW7=P~m-Dlyh4fEnTBCIV;lb_VS+6eCxMVZ#)~ylu8Usi%PlwWW2Ejw0&(js^B$d)3 z8t+Mw=X_>&-4B7bQ5_WbWa7;0eBRr7k!fWPo($N+F6ba?FkFq#+Y4ZIy(?{)!wf0; zWE3%P;CxGicu=zgOLu(4-oihU*D2dE&QhLYwY8}CUFI@6nv$x6A*IyXQ+293jp?FA zGmd%FHSVeRXm_FKMi^%!Y=3&p9qhDtm-L1ub0;f*qzUTDT)B1J|B}ux`OYlU9g3lx zso5fi4cDV?O56`y-WY1J%#3o5`_ZL)7X|e{fmI(z;p8nfk?fO?(KgncUzZjOa?j!T zod2-7>a0j@o`imP?5XJ(?h2H{KQfQIqjzTwtai3pL=Qj?%(#FmSzmc7O7HzjY z&?8fg+7fd)ugTxlogJ8zR)dFwTOnKQfE;${?4R)q9qHZSb5Ngj?tDbovjZ_j%AH20 zFT?fShBSuH<(t^Y@G#nvCcO$Hk7>IxX_Fouc+C8f{T0|a!juk=FsBIn&wTDSr)=&~ zIdYG=?S?b;{%k^9e|E=)cSD#dz6S9V`=f^Wps{J}M_gcuxfkc5uJ;~XK6#xv1^hWu z&_MO>YrN+Tpz$2p3tsVpx%z&zZAhCi5Aj5gy&Y(Htw?T;CU|?b1Hr=$=;?oBp(j3K zd5J3J%b3xZo1IwNdP5k$=eg&5&f)cL5ndj~UD)Axod8mZef8CC(ff;bUUyshBBjVMI zOK5-G43iDV#Ev1EnA86rj;%Z;>AAQ;a`^CBd|IX|wuw|B^C$z`i{^{+vIS!6;JXOL zrqF+!5mVbKg|y@0urITutEY6JsbYZG@tX8`QHt32$PeG0O=x(3d8(Rs9y?Z=QRcZH zP|mF3IlCjtdB{-Mvn>3(bzY2f^5tEAAT?ZlA+|m1$}{!u%&%94?o}UJKD;a0eDWgY z_2-4|4okXpMvz?KI?;5@jw*%(lA7EXk?=~FPHjvS2RXAn&9M)rSFaMfu@yYqSHZHb zMq=b+&iE^i#rgezV5Sft+56}t($^`FT&3-$W!)PPWYC0*dv=C)`*aV_yPMOy)tnW* z#ku=YU1%RWhb5l%m^s&ohVsvIZSXVf+T0}iM`iNo<`iD08$$ZtIgI)53}hYGic_iC zxVq;U##e2FFWN-t<_mZ^ZYgH;FA)7?D&dfyi0fGj+~YZf*`}U!Z&-i4uG}i-hdA<# zX9ZHyt_ou}S9&pv)xcTF66F`=;?!;}n*HXns2coB4COgQuPQn8wUxm$X2I2LT_7H$ z5%n=tp!m8}#DqoBMD{GmM;~KmQ!r`qT&U4L4Nl$I1D@U`wD*7-9Q*xI{D|)(lIAtS zV?8@6%(`D%!JV6=sa=KAke9;P`=R7{OqbI2&FToB7$ElgpBA-4O>x0`DxdGJq2)vb zJsi)>qqwtBXTD0nv~2FTo`Uq$7<%~U7~&SZ!g~`%a(SwOgxfM?VWL1i%B3;tYaNa^ z>XVD7J~l;VKt9@n0@Vxfk^9;s|LD>YyCS&tNk+^&bJ8iUz(F~7I_B+U22cb=ER>^= z;vJZ>sXx86(4myed%%SNvg@Xe!26%X*jZ}y*jm8k2;VD@8_`m&k7DN!S2*+cc0?Te ze$)S=JllsJ_0lGn6>7XYbs=lcFhtq6!QrbnZA!?2t2Fa3*_Uej@ifM4dWu60Qgqn- z6uym@Bb(oHBsW_Pzo%J;I2=7M5c65&&Y2_**{l$a9a8K`kc1BTZ36c}HnjipaoBbD z#9tF_x_{&(*42i>`=~Wd*>elazAK=vekyi;J`c@r-Jq(n8rNFxVMdBRV*0yc?v3j> zS=b99_8^TG1Nt(@hj}iXf94&7IORlFm-V8Vmi&G5^dxtgy-?jdOpMd(Dh7lX;disM zM0VnGp?zgJT&Ea`+1e|`+NtjZwzuQOCr?UDnWi@Tur~E$_vgji6ENq8IyEeFr*h*hINaTg+FG@#caKAoVnt7CFR>!qnqZMq?aJ>r zWq$vz6&F<8c<#Zj(L2s`%~TbBQ<*!g$hr6b?umxqzSJ+&jZ)4N?%KY_``xFY%3P+A zUF&f3^gcYVSER4Q?w}v%i64&S%wWSS$g4}y;BCxs@M*+aQ#tn6Cox;U6Ze<}ZS~+d z%qIqs{T3;4g8Q-Y1AMvvcv9>!FU8%&9&}`)su*~?8XFP|@pLh$ElHI^Qu0vIBbp8` zFrZ$KDquI3vjTPOds8yUqVx>B4RE6mwQ{i3yo`7acgl`cz-3O(P90)Ha@y&*=};m5 z*E1N^XBVzG#fw-+AF4Tjia8~1;)|>c>GG_2(pX;k?LNF%bqkiaGb!mHAW|sEki%S9mBb+<#syzqAe|ujR>D_MhbU z&4tLI z;F+D$zQu={R|U~Wy$j5I4x;z5b~JqJeb~LfZ;-u?|<5&$w|$ z33CVuxz2CVu*!^lR+r-@e@||CsZsTx5~!7RK<=0|J;*6Saijv(5Amcrj{vN-&4B^W z!lwLL2D>9Um|M_=`qaAOKbI}AOjO33PcA~UItfN9%4lC&F9|DU-@<5TjF@#=EZnvV z7Sk>9J}?1qwi$^0R~9(4bQ_-U=~;R$t`Brg)?n4`4I*rt27Pl5C&M2{MJRKfbQVU? zh3&T`pVu;YapLRFTUe_%94ZICh|iq&Rw=b$E(W`>qlaVf z7Dw{F`x8!S^P&CKgQQ~=X!+0{2n^%F8JGhp}6jXKOa*y%P3W4`>4(Ugd*Jgbj?8Av18 z71^)Yn9N;0$<8nl<_(6_KcgqJ!7Aa#GolD5TiTn~g#5Y8qdvns6SqQtx&?UGPZ!8sQV3{q}bYuU;a9xUsG^dVIc~TF! zFFJ=?k;gIxsxHZDo28=JoiW)2G8Q6rBf%o9K!-W=V%8=&9 zT>M#cO>{r#L%R$8DNla4=z77Q`+%HVu{32a9rIyq_)MP{Oo6)!#K<5!deza5ral=Z z1~_-28CyeWdYCHiUbdtI6$xU3dNn*FqLFuds94wb1^r68;fJlWu*klS!T!c@to(`6 za%qtn`wnSUs`NL}AoP3L3oKaHg!DBfl5x2=Fz=%^XG1*b^6K}v{?~%ORM=8??x_aB zg>JGl@kdWSM_-Y}2a{~{;^zV$Ti|#=1$1uRgzcomV$Fo(d=_UP#GqY>V;*c{(sd{u zN|;o``m*)lWQ8cuwxlrY3=#4!f@FM^X^B%hO2+jkvolIm zx#bv+Rz^_ydL#O^emhEX|A}*R#z<=ZK8B@6he$r!muJNv6s(r?1(3A?5H?n0Sz1a*1FJFL#*Sd|n`H5=Lil{6IO zM$^!->bS)`<~5tus8$|`{Qh1HXJ*|+tC!+$xD7UQ-g=dxHZ5Movv_yzoXW{iLI}@2 zo_kV9kv4a-G-;t6d*(Es;9Ra0&5)O+CCn+Rv6H58XFgzG!E@#isnJp9-R7s(No*d3 zQryV~u`J#|_~!H=&0Ps%V@H7KbB(C$S6l4aZbsb?)FTXvK#DPGMYqGU_dBuWg)JSK z--KoRy>R#9Se%Zl#&jiJWNsdXaG6GAb*~fK7CFOuX9dRAX<)!RTQTi_tiB$9F-)aL z=_p@$`qW2>%6wmiwu_fAp6~~I2XDiYQ6oa@D@RBkyh_LUsUs!zgLaFKyUVd`fzc)N zds>o7n!m)okY)rr*^_;tIc$bWk@j!SBDGu<-u#RYXymToLVr^2NXPvLru2TdALX9e z$M-T9_AmOg=Wi(%%`_sKtU%MhR!OeaGM_||-N5BJVv4UTozBpw@V9Pac79iy6&6G? zXKYFM`cVA0ZnTBDi4(Y6=EFO%xb>dwZS$m}S9Lu1;2Ej8H0AEU4u|W9P+-Z9wdiVQ zi8H5kGS7>D)#CJ3C0d`h7aJaZ!>awV^p%)3$SnQ-HR@z_Bmqi^j&$3{lcbKFL~ri& zJ&g&Z3jx*n>76(#@K{2ckrQ!2$XSy{)gSu#?!8$sC;{6SnkCla#IWDAH{1;V& z7b53p8GI}aX;8QEIGb)c{u}hJc#7G# z_cDu3ljA(9QwV0G0DW*j^dvcGaLaIFTGFa9F_jxoc)-OME$+lCL$ z>`ZFcqLJ<`yibb7F`i+`TT9c13NIAQD#L>Iqxh>j97BFR#KsPuTW_>y&iDh2G1!dg zJKW=56@!(WTNPK|;L-9JY&zydr6G@TZPRj`(9$8j+Y8|vYfh>ZM9UZN#Iw!%v?52) z+?06eJ+h$6MJ_a9&@aU7Goi99ds^}N8~ezdDebfq1-B@YriTT+yk<_Cd%e&yb2JL^ zAGW;gjlCA@;kI-%?#=51t@#ot_m9HO@bjGOF%t$JhPWjC6wk)E3u_Bm^seUp&d~;m zt^OeFS;_ORi!BIV`Vw9;nK;)^nw)aqk4bbq8dHLac{B+qOp-DDH`-adzXEitA{PmnSa$2s5!q88W&M?D&tA^`-}?u9 zonLTy;RT1a%GCKphOQemiugZ`NXk&9o3XBBa`z9Po%AVM%Z2Whzs8uAD$K|Wrpxu* z9SaW^Dy0cHdFU+uU3yy57PkSnp0gt@b*fm;`|$#oV<=RdkFMXD!7%s=&Y8ubb-z0% zea?k(V=S!FlyJrMA@oxu^!VgZbaeB@NnZCeJRh5WIy1TNUmuD!`HP+GBdd9#icimEM0(3c zNuMrm_=N~0a|>BK%sj%+bp)iNY^Cmh7-TNm*m=S!($`$?d zCvfFrJ?_jkqk)%9uwJnhg{@|^P42O<@u`F*`-!@)GDiA{3S>1q)3&~K@SOGmTl+9S z_tZ1!uDXTwKg`IK=kUAU{DW=AWLTARZgZd}vq)EBL^Y^m-B0d%PDN63G%Z%pMzj$l)}1R9o#%I7J9j}HCgu(_E$BHS7V-sv zX9uYx>oD0~gPHm+G(+|~db`dJ~s1^cDZyuWY2tFI1d z|54!Gi*ps{YZd9xPB}3|li6H1Wy!B{Xko=hMKV3vosPLBF@LXK;&pY6XmVMImfKZ6 zPkqyb&iO1HEYU2Or@sRb!Yq3_sn(bF zafi`7kv%06XBwip2BY67uz!)WGY8}G^sEt;TdT4+%a8VG`_T2jGL*J<2#sJy$>6R% zX!7y^W;eLdFLP&FHl2H7y=~tYRzK_z{63jo_n__*kxr0-Rq@AY4(EM z6TSLvWM@nb^bjjv-`a>7Q&mXsY)^^|G9V2tQ|esFZp@RGWPVDAISCeIyUmn_&v2kW z*Ud@O?l|^H`cea*`|JCq!DoUmZQ5>12@|j5S%MR-)6PNOwFnr;KE|3sdok_BaM)kE ziM6kCQb`<)+i7GjcdgJ z@^5-4jmi057Yb5x;`f^!Wk|Zx`_mp+A)b(Czz6GOnR0)e;s!p4&0s8_-jW4=lp;mS3l=Z^CUv>D_TSCTs zU+>>^4+EwO`ZjnH&mf~=lh=g?amK^eKNFtQr08Ch6z$m?fy27&#E6!olo{+7b(W^< zlaGrqRSWWA@B6Z|^TmJl+VtmDBhD4r2)$4z+P7m5K1C*@W4t{b;C%lKul4NrwI*ff z!#I+67LLpv$zQ(%N%=~6rd*CbhthG|4;ZUZ!+zWiSla5r_bYbc@mXZ?%On&JcnH}w zW$2@M7@PPtG4o$3;*;YsWy(b8Gsk3w$!g?;%}4GzbL#L(#yh_OyeD!amjj#d*V~Z> zpKzmn>wTd5sV|i-x25DQzWAEqK&#pP;?iRYwxy;+{#_GXjs=Qc(!9H~`GAh%NbleF z1$brMj+o;)Vk_V0q;%3?G0B^*O*k)BWN*Pnp0$YJjpB;JHLTt4PAY%dV~`dA-7A5R zzMw+h@hV8^VvY7G`egBGGM-HH!^_JC+{Kn5WJ7s{4QtVe~XpFcZml<=P>c{Msa`LMTyF^QVb2$WmxZ5 zvfpfUz>h(x1;l?8+iU?N~K1<$(zrk zXYK5|^@G;pQ)Q_HDZ3!51qio^mJmGv9@)h8^B$vvX8w z5V9JkVqVoM@km{tTspnEKek7#S*l9?cdFq_@;ebbPM^M=n1a|jy#I>q`acE$mfRUi z`&3#X)Gt6|RUj!9s?ro}MYX%1XxY~8vuW&G*f*t!5BCGa(2C!18gxM%GBYSF?qo*N z3O&hxI_)CNpdZ!oZbx*nL7J|F#-A}O7L=zF3gX-tIJ8FAh~Eestce0d)$o z_-Z|bUUX{}Gs>Qbd?!`vRO-UKFMC`#r$EPhb&S#thy9*hPkP{a}LptlGSS>{Ar*@=*~Wt?cqwWcv$I+$l)ETZ)tsifV3 zM!&YAbt_FI85{c2Zr+Peczaw@YU4y#vYg1U=c$6!b8qlZK8N3L3iLPk9t=Yd<5NHv z`th5ejW5oj>kS>!*j0`3qd42@lLQLvghhlj#oDJ}!o4c|w@jJzZ!d(IX)mHno>Uld z9*vFqbgM9seng%|-+D(X%k}2&P9d`A?!|;sTe`*^vgZfV;jzP(KJgxN%=$yvRO3m% zv-PReb{2E*b8+OEI(arqFz{CiT55XGd;a{4liLSMSD^tN(HIo6T{IY3(8+h)w~W;h z+wMBkhvHP`ndXQub~coDpJ&kbv|Pent2Q{FHlV*dzA<0qI7T%nko|9Yq|ph; zX_#@Ay$5F2C*V}2G_HEQl?>k!i@WT3iHleyf*THCwsO8G7#EpusLy*)>wXBbnTcih z+-T9`kqGJH2jyBT3Yt6(2GtTc-UKtn)M=SUg1Eo-rr6x1NXu#mi=(%_gz?HA6mHu? zOtJbd24-!=m_{G&^8FRVa*v{4?QqUJ8N)Px2Q2kux#w_KUa`YF3oF0eC zhs@~uk#-zu9)MX=s&uWa0dLdv5tsA<5C3JLZ1y0?xPC{z@->un<2>%m_ptrB4Q8|M zAXLf}Ll5$~zbAX^#swqAt~Uh)zeK8@K9ct6Q{u`A7_vZU!!H_J-(Fq;_x8UfziCZHDYfb)|*XA@ILGf_tq= zJX2u*d--zqO|bK5@&arKoW*X}rI=RDzOsT75~nMc%pzd!UWvbGTR8O_-`!@c3Zu-m6#G@GCQIGoS*O2|HMKV^1|9`%v=Gk<6I~@i);MQ*1!r z65Z&usXGde*;ALX9u&z8_@l-!9vL$U=(oIaedlJ|87@&Q|3!kdgnT)-8Ek%4{n6q^kYKjLOaHP z?}Y3$ZPDo?Pwo%j!Z&n}nA)!vM$1(xwIB0S?>9owBF-A1%>sV$9`b%DI)TF)^C$Z_0ix_l9i{1~6hvDfJ zl6u~o+&y*)$DV78=_7ndcY7tiJc}1PXL{2P`!`5@7$H)(1k#nabqJ~{k<4HDT@2`~W?VW}XCiM~g2FG?BIWddC|Sk)otSzd5)y?+ zrZT;%aKYj;yTtQT8sxt7i?B-jCo;y!k*l=|Jqvh;fv4=~SDpqfv1O-dl_|YT??MMk zKf<8inL0LK#JBJDknN&Ji){J1&S#BpBiPv;ehgPTpD+_#onnr8qb8rI`Helc{NQuT zGauTe$BxF6aiK;L&4v_oY$HP zsT*(c`1xz`@<;$)EtDb|ziP4QS3h_w+{4R1+2Y?OL8^v|B-_mWn%Df?zw-y5H+^Jg zm<#2dRibM-@??G@1&j_O^Zh2GQYQ{mcX-qFPbtFU6T7?@2#VEA5UU?MW8t(e)aR8N zy^ZXKdF+P$T+^K_&_O|Nd-XWaHj<@il{~Q|&`u=2Acb+?W%Ucy*z0fR4wb3L0t@iZ2ql@ri zj#l5k_B7GTQ>0&0qeET#(s!F@ddi(thm@n_&2~8cdWNBG#Yl=ii3cx#qvTNq zb{}rQ?lHTuds{gsos=c3nDtQm_7PUk|KL(v7Ub%TC}F4*$=$tzAkN}SID>52SiUQnukO$e*CqZ|EYXl`}{J6f(_)>eVt8kgCM#mhDsjI$@sC4Et`$;#tVHqNHoMgyp{BPvyJ%Rl-H=1_;J?br( zQUAe)(y0lz%`cBW;s?@4HHotJ|4)lK7od##9iiMm3A26T&ZKiy7 z=W|K-_j;uDQG-;VOBvosbMa`lp&mD8booL z_VlV_9|kM}b)RlW-*fsvUZW@W=C)}fX%V%HD>DU{;0%7c<+Y`&@OuI#p z(hH>YFH@18nv5k+`PU2d$Zz;CF~~#_iG!?2cD}ZF8e=Ge7v$DU-130D({>6kc?Xg?4*VW z@vkSf>owwFaaRmfFGHTg4s>4^h|zzmc?Yu(ruZyOZ+~T%a3p7hDzSZiFDy}E4rtm3 z{8`fvi&wFiWASqYRt(4FRo%&>GYlQw*(1BujUJ1=u<5EvOJ;F)b@?1Df8LY4rEKWA zQX677^r5@j&eUY}5p|{>^lX_04PXBO_t!g+MD(R@-WHtqTLqOZYZ04g2*a4e%tKp& z_rep$kIcsRg8~y3-sA0EK}jx#$XfmY`z?sZ?iz;A^E%*A=Ffd$D|8-+!-Y|w@LuaY zR#qJ6^VMriDn5?B?>1r7-N%r=a);SP<~%p&{p$GBBJ2{g82k4kTe;aHZoCz3HD)h$ ztDP9_WI{2!nS=M0`+-V^)WJV)pH$5L9`3qbVh-@^1W1i|hhy^>VXC_-9`A0zgr_5! z?bQz|mnyL7P9j!mnz1MI6qfnsqE>YP9__l0%}Z`!LB}M>IUa(`qXOiW*xY*$hTe3h z&a^V$W1%Vd>~ag?#QWPp`+fgKJwm&*I*BFQgn?frntA^JUHL$PiDHfT>{N`q>-vyu zP!|l*c#l;Dy=k!CQL(k`0^etCXwY9pp1=1-_L^wyI;>4mts$H--vj8Y(W#~;v0(fP z1U1W()6RChbts3QpCSdldX28XUSU;)CXG7XiU&sdIM!~6`F#vH(|l5l4D`mjnMSns zSEuAC=bciV%qdr|QN*b{k*sZ=?Q6E1_kYe?!~rFH(I?)Se8*@MILz!S`m-x#&c_zX z&W3A94l5OQC4)slP7xMl$O*&2`jWWn7kuvL%x>LkPdIF;EwsQqaRyhp#q+QpXStJ{tC~ z94cK7h-mo-s4Qp4&BJ8{*%mj^N9_jIL~UZ`c%=v)os06zigOp1hgp1DXK zzE>;=u7ykaDDh^dwZ#2d4Ya*7MQWdG;=`qSG~^{Si=aF6(@*#+w>t}~!Dd($vq8k# zb_&@JPjvJvzGl#J8t3%9|GzI?^Xwo-amFZm)nWKfNkjdr-ZaAWFji^TqlEqMsjZHP zDUzj!)@o$@Mjwx4z9U82g#NDTgXW@mcC8yzTkRuU7dcqlqE8NP)i`k>78_dBDex8d z7|ylAaeO!~UhyK8CS^*k-^A{XzI0|)sIuTDI{yb}^r- zGed!-OMl~?ekb=AyHG`KIduQZ)0xrikeWVQbWYgl)3k-X(mBh;T3Rk5PTm($Cqsq9 zm>fyRPzi#VgOPOp8T-O?aHZ6QKD7QoosdQ3d|Udy?+wf&`(yLi1$YI}+S!SlH(3Y^5_{g)jP0Vf?GUqvV z^8PB#F9u0z9Wd39r_bLw+poj@rVBnaI-ea}>hAPW2%6mM98U3b`th*=2&6~yg%E;Pu_ivK^)#h3xj z=)(88RjFshFLt5krE*`+K?Zq_ub`2Vj|4vV52<#g0}l7uO>qom%nXQZJIgs6jc}!o8aZ{y9JNTGLeZLwH=6r-(fuo3sTaV06MOts(lfWFyPCone$g-esAsXCU zGNU2dx@1wOMYnlg`CQ7BhCMide?cBJ>JIaPRIXr5Ezf*}ExA{wAhKUS`u*z(M(C`= z%)WQ9PNo7aM|j^@l8*_&SJCt~6qkvFg$wUHUy9q`C_AR=EJ#-y7W?3=tHwnXg0Hs*{i znx+MtO=&2~Ixph7O%_u(&&@ycJ}U?=`byMa_C34yhO<}9l>Sw<@cc&^zfB&ZNiGX> zi+OHR{T!9!V_@XV%wN|^j2nL*%XgRKijFy!mRL~q*Jl_pYdkLV*?jPVr?}weipi&3 z$!_p4+%9I8PM!mOO_+rzJq+pVG8ihR3*B{Qq&yX54^;Q*EZfRmc&+X8iU4tNH=5KoBVEoIk?5Au%XWRy) z94yD8Prs3Gtxhi2G^u=lz6ji}L!A=#L^Rh+_B8h(w+V)v|NAafvaRTX$`N=hPRHyF zL+Y-(i5XJqNY^o;!)=-Hn|~84=`)^X?ZV(&?nwM7OT&8ZfyVsaa7wJkXR~ySyFCS) ze_iDc*=;;mipK@ZJIFis5slJ&pn4`9eQF+JO2{epG31=RGRpuO^FOg#d#vr=^M ziutn)u8#;Lh_;$nwt1Q{6{I)A|d-X8(uO{{WB1N|*-Q=u@DfOzC zq5*a1F=Lz-wV8j1u&zSGoERZ{&xpF#8H)0KdxY0>Tbhu6RJ?p%BksI4p!Hr)@}mYj z)73{pTu`v4ecWMQTCzjz{GvyLnRQgOG*M{CxRAp3CF1aiB8;(V5Vp6E3j0$R&_{W= zXeo`qHfQI3^z*6^n|AU&r72f9-sZWq{U-#e6p9skN;Gd$9lqw=6up*yLfHB4RL&d$ zpQ1WkNz3EJ71zx$;4+jTS6nH@sa3EzS# z?>3`>eS#5jaeU8Zr}y7{tk}5_`r|#2AD@c>d?$Ob?7gTgd<=Qs>pb1Vo|_5!P;Hh_ z*WU^Ftum?OXroS*LyeAjZ;Dn^FO}(uRt>I z3t#PT!L^+U(C*?-6I<)>%>O8Kp9r#=p+FCVV-R%vx1`wChfd!82zT#;V#$F3_MbMR z?%pQu(L0i-{c9Yz?J6c*=>p}USrYF&&4LU=U6`$%Ev}zED=If@BmDYViOsINxHo*b zc-iv=R&uU9ylR2(?8{eh{ncsMOgt=Vzh%RA-*XJ&GtVnCTX?lr;@riaq;^jYcA^RM zIA3*Sh!#FfdWZoF?AaCY6tjB&M8_XTYCHN0S3MtNjzLcv!9BC+zm1p>u?z=49+R9d z`~|O%TbMDBEk+h8kcVz0CRi_&v|cc0?{S{6TP8;ydW!7H(?qZ9%(dI)jULe_#gOh= zRJ2x+J7*uD66(rqu|{MBzhfSi3w@fULOE|bpwGVBZ8F6ukNbudf zH+uZ36rXucQp^0_L5aN>IL`i24_R>k22Y29T!s$B&>24k)efTmcm4`b8GV{xz&q@> zE|9S|Bq{Fn*Pe62YTnPuMV7LkQyFn%SKx_WEoXol#U6|QFtt{i6t7C-Q>GsR=e@;t zKYipR9g}or59dc_&3gwGOD61SLDtXDu#NmxFv(AWOg71|2W3D0d+19?_C|>Eic9Qw z@Fb_%e43oF8Hp`kq*Y!aTvKczf3y=#k2PueI=-9ES0cH1BU(`UO?XWl)<^p7TAgl||Zh2>|+Q*kYo0BCox#w=b4zzW3PQi(y z9%K{kO>{ynOSgFPjDh`2c}MG z#g_UT7^Jxw!-l>`n#Dctg)*avc#iL=e{@IYl6*{==<&pyG^Po8wtM($LMo^Fj&Tjwq8GsgPapC+w})q zV*W^O&%c5Z7R|Uh=9E~yJ_}DLRbx_-yW~avS!}%BLO8IMNrzj_*Y#?@dCJr0~_fLhR+d!qsF~ zXlVWvXZ~FmcI?3UdhrK*@?%A)v?<*<)6Ds3bv|D(Ymm>zhkx)MTIV*5W^BcvTp*?Q zBa|FDfU)PE2%Wt(xE#L`#Y?J@HPasXuErEo^Z^wMLpV!qO}SQ882WSw;`uq)@N^FI zgLFt$&4cc3_zy3c&lIu9mPBSEl%MI)^T+J%jQ)+Kg}rGZe?PC6zeLDrOZIqrlB?Gj z&h2=S-$X0AblDw$rYy&sxMeWne5_h-BA*GEM_oS#g|;gYQR!x zG`-N0(BC9w9M65hvtn=3c*(rD6EWDi?Hi8t3!uv2U=6lR5OKjTs6*HF%z_Bb}oJh~b+9hX! z!T>~_*@JHqg<$@%H@QabMBJ*EqGl-b zM|u7ryV;AbE|8U&dq%@VE^DbcxVr$YaTe5A zu0Sc?rg-=x8fKF;X!gFZ!ukAcT87rf$Z}nY>%N#tzHcCkS`t82~$E%4A+) z<{VY(UdCCoH2$-mR*CvRJ$l_WU8KIQ5<9M1l6?4c5glTNPn-kkw`ZJ~m~caK^{b8K zVqa(Kl|58w8|)X6)y|aHTw1WTpiuJ6(2|~X28jE~cd&X-fe1Q~C{#8)!q<^z;=l6V z`KNk5#|@Wk?!?Q`d|w%{>tr{&K1YTWXI~IL-@DU{>JHc>bV_y)Xhi7D4=C+rNH12& zkfTR44lmZFcgo*TQq8`mf1Gh_lc$4Gv6A%%<7aAtgle*EQn~V5d6f4GGP^R9|E=Y3B5#G${U#-6px-YEB z=({4_ew56<7gxGmCP!AB{Zu<@LGpDvboFKmuIal9Z>xFYfL%ShuGlEn^qG^-yEUZC z>=jB%XT()qDbftTBerRG=UF@`Wvm`z;#`?6B?jS>ZV2EGj+Lp@e#fEno@iZQ)E?iBZrUbw7@`t znK!%fmRSQgdf$YnZzgW-G^Eu}tMPa|`xx!jNTa9_cjy0tyLuEZ9~Km8ro_CkSom4` z(lkfTU+OQy-^8JmH=7XP5P*vHa@1q`Wc=mM+BgeM3hAMaFQGHA#7Buz4NS>$>T3+% z?@jRm>hvt{4RbLhB)hd2*^U2zQ`;R#?-=`cmbPLCJ9`b*=HjPoJNAdm(-nTcd5l$J zzOWkQmi#X_4t_6Hh-G*F6JEcEF$3bNP#jnyf&=}i8|MJ5txTz^rV7*j=fLQrCEb4a z3s&m>uu?Xl0EauAFA0EDRSeA5+(F{$h2oOsSY$nZ2)hqsM5E(Yq!nGkrD;!v510Al zKB!PoSXXLW<5;k$L6y2LRwDaSdGU3zEDg|Y#QGb%*#YTKxfvUUOH4Y#6`42vvPMj5 zTg`nCK6hC7h%G;?VST$5$tp%X5AcIEzb8LyT9dllZ=rjG8E>DpXexKMuA6VcA4O(7 zx?9ryhT(8Lq)%}?SDfn^kHRN*bab#OeYohu^Z5@Fy)D+^ZK&-Y5VzD20q7He`sqvL4j8BGjxD-i$EJV%6 zY)Jp=%CpaM6x+l=md^;r3%+97gWveIb{{%YIVZiS3BNycRz>+YhTiE$nIaC`v`y&2 zTp#M!lb>On|1mh)hb**SLgBj(?Of2GUh&y#)tDVv`?wF?OwprR_p)%~6}uwV8ByDr zy;vD)Pj5dcQ{$Zt?1jIDzI&vZvB@kP=eO8DNP+Y)3O>)z!}PZlUB_+AtnsH*&L>ZQ zdm1f&1ITNzprBzTP*e}5?T;MUmBe4uj6Y)S4{aLOZb0{KD}+p=GCgCq$A?jWME`5b zGjw9?}c z_qPM6aHA>pGro=^T?f#{z7CWcmIT|eLuuKIQjD1~9TSe^F#|FKr^Of?^|^vc>?o^P z7K6fsbliKYO+{CZLZ5qhE7!6gW$gj9Br((0LY~(DO^4shCWw*JlzHvA@YgPY!F4Gb zHBkz?6pynD;tRawwlR0-DxY(G>C*{;xua&G*Sfwme%M5)<|m-qe<012>WR+<5qR}& zCi?vwjMX{%y9 z_}Y=P{y<7Mp9CwN%}Bp*#_qOiF{Ac4($4w9YHqohYOw?VML!T%?W#rKzJPW$EHUhCQE=yr`F8J;l_5nhhex-J=#)EvBXwV>&qhnP*oU1$?q@{MPf;+aEO zl=BGXizAtpqzl;}{8^AT0eyU3Ff8gG^tASHUz=;PaToEV{R&hEufqwCGJFbTF5t>2 z{FXfemGC?8Qrm$btC!-$aQ4sa-ihkKFfobmchUyQXm;s_sNUSy9-Il|sR7iUnC2V( z`Y^l>dsFQ$xxz~MGw^>jkcuA;6mp+AZ+EZ~;p{G*Y1^OQcdv0aL6s8nN0Z#rDhy_( zzDz+sifL^TTm4UAQ-60_yXce{U!KjgeD04F_e5uQAx>C1Qe01E>T2l3yv`-4@1;+F zR3_o#?P#p!oQ!{rG)%e;#d{OxfXDMQtK=zOX=u~ydrwizZ0&yv7X10vhC8G0;!?mB z@v3JJ3Yz;(5)fpFo6I`)*gajMusu)Q>SasEDjkLXY+rJy=p`oJ=tGV-{VDM50Wqb> zm*?!R)HQJjpZ#s7no8H*#Xd=tc~Z3lN^I@|2am zT;zP|M#1l;XtwQ1@$7jcO6{eno9THmtyPOoI7w0Tu%48@vK5<;%Ft0hzgMX?p<<^D zS>5bOD%;qlSYa*Rj?RO@sK@9#|53j8fU~&q@;WBc0a4dng1gDQd&`-LA%(qgc1$Tc zs>Z@c#~%-7*YY#QA7w{?%5Thid&WM7{*kzEnfWH0WwmVfUAq{YZ``>{OtsE_&?t5j1>Z zkw|r|67uZ((0rUOve#%}^&?;QyOaBS@(d_N$MqMC60)Tv_!`B@S1&{|SZqf84feuI5WnPkVT zPW0i-{QXOog=_4Tsl%}yrqR3k{y9SUzv(Mx@dY&}M_K6Fo)b2l9kll^l)O&ZDE>XH z7k!7yl53F``9{Qx)1f?n>#jm6U3*}hw+fAwHlkoRGiFaL!cfkjhwkFMZpsqecXgnm zXT8Wf%MJ53^Vw|`cT?8+@ENopou0z}DR*xg`Q3~D?bV|5FWjlAA7_sHC`b-j7}3r6 zzTCGyE_pG*k?*gr< zXkA@7KEHP2?T+=}psB=I}QjO@ppM11>E5p~*>?iX*y z=?mLMReT?kW@b>g728GAw(hik2s1l2rV8Wwf9N>hle88%iffgs?00E^UkL9yqdcf> z<3}j`tVZ)28#24|3vJ7?FQ3t~y%n0q*Wq9VQfXF@MHzp*c31J?bEk^Ilcakn@X zIX`Yng>C5n{pFHQ6NWJ+#UvNx}$E3H;a5;N8uh5~zV z?4K7EcvWWLo0PS<@b8__uUUnqZ_34Xg>LZPwisR`nk5^JK8Z6P2e3Q92qXPt5&fng zpHoL6hqmLh{9xLysE4iW$hn>D!~9P@TAtEV7&IGVvpx5ncfa=SwCan-v3eBLSTCu6 zR4T57CbAc58m|5<6$2+SBec#82Wqwo9p-Wk?YE9SBM-!%h+1*U(2c%CRB^xIfOu(S zNwe0x!}bFz_@nDa2?Z_CH&H>;qZ00v??*|pA0{Sqme+!rElyS9%*|K0YrYMiV|a%& zNeb3{2KVh>kC`LPxc6d3ACzn1mFtO3Bh|?BMKpGoXwt`*j`VL$I!rjLe&&ul^{L&8 zN6gk~)3KyEYotgcwioYld()BG&DdJdgAVZ7_NvlPysR|g=NU7dr8xtBdjdu{&cTmm ztN30r2N_!jVO+dDnxAY#)}eW*pYs|W|NMM6cQL|@&E+s?ek3tX7>qAtm}NLpN+LRS z@xX2~!qzuoMdcL?9laZ^F5l5T^BHtvQ}IpjFN!KMxsPl{#@v7Iw96DN106}-T$RiN zd_>|oGa?@|QZfw{xr%0-mE6yY-BSqEHzb`2nOM`DgfdeD3VV70`@C=CL_j@WNLDkC zRsk0We!w}!{rGs=9>1Bj=4UzxryscCm0v0RFXo~6@i<&R_Z+h?S0i^M=k>I*VL0Rl z_b??GHJ;!w--$veY{W|CXe?)zw$JWJNENwa{t;%j_DIFN$*wf+6Fa{Zmmp)LGmV|- zM#JtV^E1JZcFgr9m4C}{qt^}Y_e+yeWv*nv)fqodPp`|t!Y3y`rc#$V^Vy9#L zdmZvx>%p$Lo%q*9l{8+kQ)xm4ly_^;+gYD+VQmf6D)ea2YAH(N%=e;S#@z2=CZ78U z?wlF3@0UHg&trtrIe|6rHRxJ&f^T?&vTuHClJC2Jz3J=F=LONuT4MN5=FFz^d?(UR zxW90tQ6-Z^wZbhNFL);AAMq6L9~ASP+D)w6>LSq`_zWNZ$wB9oG_`--D{TPWpv9yVYV|t|I+IE1VJxsc32^b|f^j_uZb3k86gtt`hyo;hxE>pLkZ^ zj}F}~Vn6DAJX=4K`tuyO&#D3_M0n7mgXiI(SOkxEt1!KvF}uqP+0!=^E4*y+ZzcCY zJHxTVw;$GjdV;Q>eCUy6HZ)rGvG_07Swel`EFT1=%s@I5wFN;+W_bH7Q+(%)cEs{2 z2-x^o#4l2yQ4@4=(&($umeHaarptL}WJM_?PorMPAb+F>y(m?pNX2b9=hlmc+*PEz zB@+_ zEuAU)oMb0(_6pH?nX@rR`(xyHOImXHDJDGE#-3smddtq6r;;`iA8toKvfsje^-#?5 zU4V{+hwMXkz^&s>SgLm);-)$}KSbdN|J$&oE5>Alh7XeBZ0j$WTyP`Fb~!rdz+JGp zf%J*lgmd`*S;u!b-*o=WT&N+5G(U{?OmA{=O%x^;oWryV+ctA^CDXR(+tvk|*raG&N-AKI9;8SvMj&E0Ltdh2Eo8B;AG>Y3(ArGXCQ>gYh{ zF1XTU&j0MqH=qMi5}qN~GP^SwZx1QarGu{#S{RSo)>iCa_Y4mexCb&$g|xf8#MlGf z$oO44j&*jTm#sW0Yn();>3BlpmM^1NLE^dp(d=^DKVUAW!Bq zN=S9%InLGX^6 zgB_d=)}S(V2|wG45%yDux(95;ajj!wdWH>6;mgbP3$?;H-JBZkZHBg%flv){C6^XU z@;dumtexM?nQ2E#dfg_5y?TlIF8XvgtXS-qZ^hjO*O2H^fcN)Z=@zH6ZmOi?z#R#V zvCU#8?I}$EXTy2<0-wvwP|taSr}#iMBSENban4> zOfYT5VI}rZ=w`veRf=M7bN{!qOvHUUj44JM)K=FG`cF1MW??HbqsztqZz)(1e?o-q zFA~w=NeFryA(E>&9}$y=zE-IsY*?_6>zj$%Lw=~6JOo?n1mEjc;PJ~q^z1c=?22Y# z#B4L>18{$Bt}^-FRTIaLR)}}$x)h$2lAnI63l6?kq3ur(N&Gk`;orucs4IQowoeh~ z7N_$5{fm$Z?E!7(vaS2tAL=)Cc<#aMmVWxQ-0d^URYqav_FnW$u@ioK24Y;0A{AWw zfRX)_G0~d$^xv}}f37=B+MdI2@)mrH_2(IQCG!9-An;{5jwW*c=bZ&LkA8+@#lFz4 zaHFt_r&vF90;F~u(~8m|n9y65wk>m@vAfs8nwcxzxu>*o$#lHv(t~z&u_5iLRVX`X zL>1F*scbcS?!qjnX|*-o*>D%~Kds2t#*Rk3o{G;!Yw)o$1~=j&u;=`A?h}uJgVJiO zF4_;t%1tO-bO)Z9lOl$pG#vT5h zr%~tp4KM9~@jY_`4u?I0WLG0J0@bO(C42U~h!kvwOK)Cc>RJIXT4l#;j1YK+vq_9hP#k) z4`$=^J%ax-q-f9`Pmy`|0s?+EVBJeQ-?ZeDXzk0arSOYl%fmGBzj@NZ5^w4`ph=9- z+KESwUd)F|5+lx?MZpk9k}}dIjc7kCxn;|4J6n2M?#DY;YdorE{?7^Y-M567YrNs0&n$uQrRsT`mB8(3r@v~$Jh7{+Qm|Y7&eMqfAp!jciC05 znJMDWd28-Q>=LoE&Xn!uFUoxR=h0pivTD0fYo;WK`GrQAD?i2QHC=~CHe8P~#A8@lM z6~P0T6Pw+g>Jx)N)D&E6<`39w}$B=$X(8R9Y=#5znzMc6gaY^@~S6e?~ zNXkwYYXs8BD}UkWy;-C*IMKFoukk%tMi?-s+3?>{NoY=p5`BgM0lCizv@D)f6m zF)2hWc4n4S-gt2t$Jw=3!mq&H45xh=;8co8XI&_1iaft;=F64=uPI8n{>;t>%Lt;uJ?;w`* z*WN)3gZjyl{7glf_31r^ySwsErx6>{-=e&SD}@Rb(pcSsEo95@j|;dmM3&wKcA+;F zHz6~)4q=PfsgZV`^B^kBwpXR~Q3wBzqw@^txqaJsJK8&G4=U}wf7fvmlCt;A-ut$9 z3S~=)WMwxrkfMQ-ky&O!D5InjlE!oXpZE9urgD9+>pYL+^AXn{X^ZUdjl!&~Nc^W8 zz`H1cvlF)qJD1C%W$JVwSd6%Me(~xza|*dTd|13BHB)>__~H|GpdHhS5sy zGCf54&M=<&?!!0ENR^lQKyyS85=w6HygL|cboC@XEUjog`x6W&iULDF3tH=`OW*7V z2={^3v>{T1o||sOUGE5SzO4&zE`9E&CC$*~bjnp}y4NagZ50 zR^-}lO0mZRsC5WuGs1LfM#%u`#_y|`k9N!{;T>F-JF{i_(27z6QZA{MjF}orsV! zC^~b47`M0>CT?-;b?-|fkIT_nr`_Cp9!y;;)#%r<3_M9=C)pS2|B2-=%$Hph+9niW z;{&}vM^Vha)VDFKkW+LM5oX<}fxF=Cg_R;S)q&)WU1i@eRLMnHc1L6p4}Czlb%U!$Wv? z+-X7s$LUe;{GK!?L!BOd?na(=*35^uBehuOZTn;}2Sw0WOMeQJy@9T0dy{931r@$7 zX6CLh_1#l}qk5as^!O1P#@)i{fn(vh^(mA-KgPZ(yJ1pwnSEg9q|ZKrvqj8tFY3j; zf=kRfQ>GbHwMjet4(EHj(A+a}6c&5 zUg##6isAouVdNPt*jio}?TULb%Wjv@Y10xb$DM)qbqNBd#$&C5allmj=`h)N2>0~| z77S+J`miPY5%JYW?6cCRmnQzy)%~`J9brbf^IRxeN)|(&m@*gEkN1H?#jp~d*KMAH zt{(DuFS`#{Hrt}uFJFX>x`4NzmNTnf8`jrluyF0ZLqqnLeRHQ5rqbm4 zQ4!_b!#pgJq05F*LbI#~&1#Rp>nvUJ)+O?r${dF?hU~}nq}XP@pL;0NwW1(e994+b zNj)hv)s(y1AJ8q#j{FapQFH)1T*kPN!Y(~ZE$j=w&S3PLunY6!w6Xfj67*tcMEi{) zXs;ZEwOc2_?#Uf!zH_7X7hj2C9v^Uw9n1r?^2MT#YcSs9OM6e5!tTugo+Gwl<)9WU zY;l0MlN8A^u@wrm%z1~{%`97 zEXnNIF7#Uc2$p6PA*s-oMh`iI=w)Z|YNa*R7o5Pyeie9ry9U{Hb73yg#boztc+cC7 z!7>4OCffnc;Ul=`*c1D1K16Bvi&zr56}2g^5VW%t177aJ^b;4+UzK@tTed>$)m)rm zuJqE@aop-=Z|-028(d|Uc*a1!r+Cw}$t&@@(v3ESJJEjs8Qg7hr3fuSmvvX6l>Lzb zdmJh6+;|M$&bhSXpRiLqR>V&#MEloH^z7UvLfP5*Vbv4t{B>HQTD(~#zb?h%cY^Lk zzYw#T6R}EJLe58%MOt|U`$*i#AX}4e&y#}YQf(vzo6yRW+!eAnL=AN5;`jqXgMJB5 zIbGVZjU583N>H`Kibj@8QS|tGIP^`O{y6@_`3WC6!|_zi9_Yif`992#cVJGxJM)5w zmfw-(UQ=%xAU}le8wHVZm8Qs2bf6iZ2GIe{Yr^WFJLR#Dv*qBt{H zA9O8v9HL0FC%O0g%7NOH>M*TDYo_+BBxJ|Mb9d zaWd`(t{q85>31cuG(HBp9m3%%N#;D!i6kFb%+8-rg#KP{(f3A}3 z+VCE;S8d~+NJDutM*A2|u}Nc} zn55nxZOaV(-+h0I4IcezGrt2LKP`gQK~E~W!HkV(+!=QrMpuH4;BV4*>^h)CeH|6? zG=yEHUAj`b;U{t7$OnA)??!j}{1TVO@*VM?G5sBwhTyil(9hJRkYQ&aKR21#c!tz! z^8lwm%kjN>4oVO9qrdzf`H;UAhqw17i}p4c+AqT1yTd7ZtT7TdNx`>_zmd;8F*m7| zzkM3iuG=coN3&b@Wjp?ERHYf;n|QwJK~^W#=pN5Jl_uEHrg|xgN&bw)W>?Y~atql@ znxVoyuZew`5&DPSV@XOR8lEC>q&!(pRHeoy4dy8HrgI}(#gUt)Sftg59)I`3)Wny< zTO)*go;?)GmF#EZPTQr=fe5|ZjqewqFh_bUHjQPbk^Wtn{MAL6%pAC?-{XE^p;&!l zD^7+n`$Kjv^I~i;VFu5@9zGVTF505qL5>Pf{zQxE6aTDcDLTxIdY?u0lAb$jF>rG= z`uC4Pz@;wY$YE7Uw&!`MZ2Kr_a+oFx{g)s>ZI9&HW<#-q9Y(<(jhGV0&i2t&Vs1zq zq|%h>ZMqwbr%I93G!xGIyHI4f|p~y8A*f4ZIf(+zHBdGv|r%s{pvpmmhUSjjZZD>1Ei~Q1;sJqvdrq7ON zHqUG9G1sE~pO?WwyMaBA^2{Af!r-m8w3->-^Vm79YG6zUNBNV|)@$swaHS`aUSw_l z5O*%E<98-^SS>Utvg>Z{RbW zD{^>rLadRBht%yZR97f3EFNvclPC?c?9R-;g=a8>9kBCH@UG2Cnx?1f(@_;OI%fO} zL4LXv&2L_xdS&`sV?kj-J*g-u9MQ?WNv_j@rdiL0>+3;ep~f>;l@r`+?MJ)!rQ!YY zL+B{Gj)8nP*z#@_I^Ji(^k^Eql#>xt%q$B<9WrWRhVok(3N|yL`H~ZGR8?UQq8!zH zOU2b#S<3a0rG)J~M;@1pVI`kn6nj-NE$a%l70YpE{)TW&J%@lH9+GWE72@;eZJ6*+ zUG&|QEXvQFMDCk~V(K78?ujkOBFFA1*t`lhO+-$o#-sO;&2W$GPbcQ6;JVxa^kwec z)HWTmxG<3WIWp`|G$-F((vrD6KffoZMZ+I@iJwN1V&T6ns9s}^wXRao$~cC&QV)Jt za3<-e}H>B|$P2nXE@&m$*{l72Zuu z)TXf$EvWC_FBqMtL5rNcXx7ngSon7i=S7#|s)-Z6d+o)S%8jUVbAzVGI5;N?9MgFO zw*{advMTt>?A6T3LFCh43vv+!oT=$e0r7p2@gN2s-t8DY_cp3d$3kNbb5Iat7uwe zO?zJ@VQaus=7kv3*elEhGEIh6`C~k&UW~NOs>tZ^mAy!#p%-C;+QawpurdZEnign{ z%D~)+8(8#g6l64GA=HY|^Mf?-2Q?S-{qx*3=Ew$cSAgI6 zGKyHS-idOelhN+a`>r|6=!hMO!TSXb>SUym#RPoOcBXOCZlths8~!WI#Af#nWPR(+ z?72c%X~>ZNjG>gftO%L^eMQr@0D89ERwO0eM>>CpJ1zE$Gv8lh!WKKao^(B+-0%wT zuboMKjS5}tY6m;5g&32g&+N+y?DCm`8&;a6cJikPxzQgMlQe1AHyK)7S%UGLTMNIa zOyMtXVNWkDTH5gq3p!rFM7KmFvuD=BP+8=SP{Eo+T}qrcFaN^&=Yp82IiioM&{h|8 zDD1DK=%F*2z4fEaqQ0WF!-DSr>_g8cZW9%PyS{xS;?JGCuo<9^>(^X`e(rrF+U*x> zwyerCuzi8fOegH*4o0DQZyKqsK>p@kcrPDJKka{VCRK@UT^L9o_GbOY+2p`;=5pwVd($lC>6jyBf}t)Fsua5r zdeRB2e7wjO0o>>7k4JMph~Po$%)RcLxXSD$L+XTWTlC%xFu zd8%={xKrPU9=XX<{M-$kKk=vPp1*N^_IXGqZ4!@u?-%uXFVQ6+RG_gAGh=6^@LnuFl+snss2b9U8Eqkm{|Y)c{XM=uTflwc zU@{0(pw>P^a67Uu$>ey!`M5W<&$Z#$CmrZF&cgKrQZ(__SX2huWADnYq-1Ey{huoQ zyKE7VyFi0(&#gr8#@qR|M|x3T-_MZyDqV0#_anS||3lLj8S4435+858#jMK;R5$J& zioKZi_Cb~Zy)=s{w?J|Mm&EkD5n`)sFzt^$FK&hZ6ywJ9Czl?&c>dgl-Y0%RJ%-_7 z1N$Y23#asI#+2dchdT-g=0M9hT@`WPyOP+qj4>_&96{WDixbA43{P ztR_fI3}ngokv!R6SuH;P(4;SirRkI~ET~%EmA>V0r~C451f+%s^l5b#y<;c9fAZk~ zo3fq4S}_F%148n9nC}xcW8RAc*FV4^zXxsVYX{X~Es(6{pZm%k;_ZrWIDN^Gu8nk{ z1nC&8iBhF=zV>9XG#*KfMs)Uz4SkDSfD?U{Xe+ZU)~5N0!Tc6I%w95?y&J?N$6gej z-@$CnTyf#B1Eu|PpfTJLSig6vB=+pCuqt_#sSi&tA+PaH~%PX45UfD{~JW_QlygH7>wq=Q^6%!>f?P9+e=@dRflsg za~HEWi~BkuUNqz1Wwe}jroYTQ9C14j(ly+}b?-;kj+t1}FCI@U*e&o&n?Cp*z$~6; z#}{^|z>qW8RBcO*cjd_2X%^o7$V3n3I}Nt%4|%=AXkM&JzcYKIWkELPayPX?Argk= z5u#&853*i!1}Vu$MW8z~TU6I!eS)Qk;aHqCy$w^lR>**sTTQzC# z%o;HxQi`;hf4DiR6iWA9Y4*%(crBBP6@C0^?c7+b3%rVBYIZbQAxoHsFM{b*8TO{~ zZq+gpQvGYNTJE#tPrydh*2|IQOJj0)qe&YT4CwV13wk`PJI%4RrE5Ld;qlsl%5+u8 zW~436DCcgEw+9u@XV$q#0h-8@!ep3{qML?A4c_FXnu=7#Xk06LitXwraOK(<=6Kx4 zQ2s{5eu%;PRoQT4*5$rEw-7#}4h1mbeGEG;mHFJe{KXB>Wo!rj;5C^ZF z;$1H@AH4Lj`M@sh&rv4jBXW4Q_6+xoI)vT(Cz54N+p(ED3JJwiMaJ5AjJtYA{4LM+ z@7?<}cAEv^{?S>?wDF*t)nPp62*%GPeaK+fa17~`@VC{L4(e!fKWdMdmryFC0#zyb zXrKr@YAIH6=cs+OoXA@9U39cZ@vdEhHG6&vjh9JS`fn7LCUrwh!ft$CAO}U|GEuj{ z4Y|~vj7`5{Psu=pjP6R;O&gH>Xe4(2w4|%ATDkM1552e#K;~J_LAmfdvkD%{QK&i` z$Zx+|d|!MW3N!EEz&0yHZtp>NvR`B2T=w{}OMY8jInU<@W7!LPD(F868+K`tbi5-? zIlUJ2-H5!-FfVcZIxHT@UhUI<)PLD~v?ypZ|G=604{YJNnF&<{dQfcl5^P?~Y#~$b z@Og|u@t&dlPKd#CnMIgx83v7u3*gpfj)acIn4EABG0gMW;ZiIaxz!S@6`S#Ct_gc) zwXwJGF_x_uBu3PX#=&0^nB-dvJyC-pkCSk(TQknY*C6rOa?B1c#{-i;7`9Z0Tojb4 z%Zdx)(RWSaZZIi@{+2wJ>rQif>yyRcXkl^7j_{71-U*y#SYk;j(=Nh%{|(+jbEYlz zD!RVigPa9r@C{go^yPr*_$D~rn1;_gywJY<0Z#G$$}oIBta$b^^I{=}^^AlLcM9&B zyvESDC`dTp_+jQPBp+CX)tS+l#XI@s#}n{;J5ag$ag_I&jftG^ypn$rsiltW zHuhqVpD%ObY^m!8?&JJk%M7ppI(ONde%7#Se%@Z(PE({-so5gFX9{M-s8LwjMIn3k zAo8jjG2W|FVs~MIXgYnCUDkq{_0Nd+X18Fb;z2J13x!_q{aC2rN6Uk|QGb(Zcv##6 zl`x8 z$Kwrc>gDTAr$!J=cCpP?6&O6%o7T$JRq9Xk# z4$V9#E?wYE{em{wo_{Ry;h9gH-3Mgm?-BCbt5CULm9DAt_u+E`f?nuRSh52t$-T#v zDrI^ZrBAmNtB^G2l(^s-gW1e8bSSzmx$S-kbzjc_|DFlGQ|U+vy8_is+fZtqEOuQk z;Vf(z9R2?a4V@g^Oj-mV^RJTg&JVEst&Pa2a>NQ(E$ok)C3H;|U}mTTz8@Rt+gfje z6*v8OUiC-p=W}gZk0B^O^hab=Xi$lRF838&akaycZ2FDB-?1wsxl6e7#QewFT_eQ& z7n(FRV=u;UY?5dnlBXF)*Klc_o){n%z`cr>ocD_r_ci*HU;oe8eISN8z|Nd^c*kAO zV#znz3ej|`H|NtMMA;xYydY<{r3j*ylP@ocpKpJ#_Ty>4RcFn{cbV(oRRA$31z zPyX9oC~KQJ85miS%R27xq;l?Xcn`AqcoJ{IJ;Vg(VUlm-)21gp= zX-hBk?P%3xeTe_u=-nzlgE;%rZ?Zcz$2!oCDqqf|a@S;X8U|@7(9nBj=)ds@_ho*< z#<&VTdy*ig>(GqhweaZs2mki*oWFN1WcSKYh3-U{c}Uald$P1^@G+bY@FV5i_mWHI z)sT4=ObPqVg?sZ?yxQ(TPgji*?uqx{?VO3`u7cduddY`FA6`Xr4aqL6B zi>2s#%uwWxzKp#Od(y!oUG!a1h`n8Uk!pto^JEV3-~IoapwXOsssZj{aGu6tAyH5mQXiz&nNX^I9~hyjARWu0r*{PIUAN#>R&=7?^ww zE%8TT9^*xQG%um)&^DAb2hsuUQ~a$y!~6p$@(xK6Lx!x!V|!J)KYxcLH)IN~vis%a z;1~`!Rz^&X?J-53$*8GZv`c#~I#Xe@z&QUlEU>U0w*olmwhK zWWL21BU;d}2;b&^fwB&39YLhh7b~ne!7dm3u)k3H&o5bhM8}VY^MC=Z+r`CpV zQ0?9y-#;0X|B%1v`hEj~tX*l|2q}^*S3$$~@8}zxi=O}7F=$#n297XOEQ?Xc^uN?DxGyL3Me4^q`B z#>0`qmhVNEIbCR8c@UNO+=kZ=V_F($Np^e}=$OG-u!p)7821KqW7t2sqZd`%h4OtO z5b@*oV)&y`_^Z7FmKQhSU7IbAP9BXH<4$7;_ouFNPi~+6CsEp1gI3d~S>5%$s;V9VLqYjRZnMpcZecBOaU_T#|mQ;5D}$M^ix_K}4$SBZOzZo+5O zBe*<{5o_`X1+0|4j({<%Bx|nMNS+<2Kx-=nT=Ly4ifS)n-NK8Kn|(P0(7}$bHbC#C zK25lvF1$LeajDvfY+i8g{be_-;OwdGf9$R@y@f7s47h{bg2NMEVc;uE8Y0z|?ryvc z3!^TmOR^)msScE_`cEW&@u7R`-Dzx_fc)299WpSi+r> z#@p`n?h1cPMqCmh6Az)mvjFcyuW|m7*(-(V(5;sj%S!j6;btZ@KORNHt#@L|P+sAQ7bZEV94QU96GEHryVJ9;Y@7zMVv3)G+G45cBdm}doxc%Q@BjFr*R|qAmrl? zk@RVZNKdff98`|T+jC#cT5m#US2v0sF~LIfKT|q#MMXGsKRj4Qg?8;qL|bA%TK=Lt z)$8p?fBQkyHeQbG)}DeFyRmw89V`lrz9OopHhhhi3ggL&^sbu=+8R#<#8h*}QpT0@ zaT*x^@Uq1JPEp>gL%lG<$xo=P*d>nZ^+9QCj6~vo3Fp`PQ=CpX@*^)}_fco^S{sMZ z)hW=t5=5_*_A`g(4TgAV(AyDpqN2|$)IB$)Q$?~k-ued#$GcM7p4Xz~Y7&BZ7pA-} z3m=zc!^m5U^Q@(q_C1Qv8t!VN7#sR4(u^f*QC$^4fsZwL9vcVlAdn`z&WdjBMWQ^P z;mff%0Ks7~=JJG3dIu(S(e&%Fl za35=S=%ahoA}qgq1$NAI^Gxx>@QbDJe;Wcv=_rg|Q_QX*GfZkGa+sn+_3ND34bzX3 z*k@VupC`$k@StiZ?oyiC(@CRPR1}4Zl-Pb^n%!bV$NiJoz1t?nG4I@D-f)TZoG5W8 z`>!Ys{0ko+H@Y=Ng1WWx)R^W?sS#;niN6AU({(28yKXclIRcyFH0gglns$eISXrS= zlRccd{}qeFOPTG;j;>w(BP4n3gK4s|pf{f%O1`{hS0Cr1egCW$mzmwCddq{nGka3! znBVM+^Pq>kKf7+~iIwJ{09hmM5xo|29hJ~OpUG|mP12b7l)E`cF#WLtO)xFRmzl}1 znQlxEyH;b?h<2QJIf;RJ^=RoXMJr!##FbWQdOA^-p1)5+d5|aH)1FDLnm#}VGqJs2 zWQy@o#Yk&lKguE(QNX>PdeNa8@-F z#Va&%nmGsanlvao-W!kW{-ETQBE3|R!$IGAOf1x=>wf;w>so;>Ja>{Z$;3xhSNhl| z9KXynFs#FgBK|vt4825LwG5!J(o3RO@lLEVm8aK(UP`>@ZO6m;U1@E2sc2M>gVM=b zylm## i2ILVOY8dRyM){{>BG^3%+lIg$2k~U{6(URl#G+(y>SK&gJ`}CmfUKI%H zaG|DjHyX!p^m85(8fJeJN6sw4%)8at=bO*}?M_(tE`?h$XECJvz&zv?GBsIU6eIp3eVX+i`R*bF!LmW6Rq%p2<4Wmm~?|BF>(!kpetFTiTcUQ}UdKt3D4;uiB*{wkW%HMPO;l^uZ#!=fN_%>|*l zn-RA)9!^Cgv1I=;Ead!8Ui>|DS;@S-!%8Tsy@T72UAZS2j1;>X_#bztz+qp-guTxA z%X8IDqr1?j3Imw_`GsYib#)!ho`N3@xEi8Ft6m$?AcRuQ|k-J|J{S{rPq-5BaOMe?qt}Mjo~MD!uWbUO5ZUjDaRXb zmG6<}9|x)8+4!(Qiq>rygOnu%%ZNhWkEEhxY66~RJ;k2D`#3e?1TIeCnF6k%+$0LR ziybhxn;qrz-_L$?f{^l~am%BSZ4(B=YZjD|xeM{`fn>+L)w4Hta;8Dhv1CE#q!W1; zGo0#syHK4%G>*odhT^3+$o+Fz5YLNO1~=fn)^qWoIs>OoA7h~VD~ZzgyArj~YY2I! zE)OjXLZ>%POug55<)BrSu~mvr8f~vIj9dgXHIF!)~kr9jw|W z8r2@)*NIRVD!GauX*F=@F$XbAJ;d97caZ9=gI@uE;qN(85|Hwhc^?W?s&7@`J>whx zy#9vyzA+Nd2lw%JpDD>k*;4kzFR=Y!O4cUE)TQJpbL4wcPZLi%!*dB$uO^W@`Yev~ zzq3;Yn}vlpyFd$)q4c{`WL_^u#L`RLsgA?*57i=U)eU~%b8p+hkQr5H;XivlsvB>L z51ccUZ{_odc?>2UdxcCm=N4vizBl8Bn8&kug?xd5<)2b5Rz0iT2i=LICKqU<}366p2b4bGmy_WUCt8k#Oo=8=z&=Wb9K)$KkT>Y ztd*0z;|ylbU|B31*H>s?XhPgtJ@konDTuFqg5($v@#6J6@$c(KN#*1RBFb736?4By zRy_`sSZ&b2g%7hu?4mn(`@0{N^*DtY|MIXd#hX6Xr6c@PI(wamFdsJw^KX1aO@t!% z#8vPkrwT6dyx%Z0#XkcX`qeH+Xi-A&g3}n)&Y9EhX$UFK!t$T$3~IlGw?_`+%WPvh zZ*d<>DTdJIB;29my&96kQAogEg#C~AiWeHn8C?>XC9zgiRWr#_2a^Hu0dvl_$* zPyA_9BK>kZgeFxn`+)hqftt+gl%`v%?$mXZ3_V`_6R8Sr^r2LZ25?sBuBj_6kElZ3 zM|s+MQH9*Py~5Qmij-E&PQ{kH*!xJ8jaCdFTabdXzw$5 zl5LqRe&q*{mZvl=@NDJ&hcC~g^=X*bKv5a*OC>KnC{4?XYU|mBzJc8|Up$zN=)u{X z0J6+9qk!l@nq>5ynf#X#!hWlVhpO?|;~D0p@GMxr5;Ds!;B0yWETkHdZ`zfvS)W3; zx4$9jElpO|TR`zVtL>#kDPdWN8*EK-qd;pzlHp!p&ga4q@=raB^RL|KZnYq1sX|y! zIEXN3Cn_GGMxTq$<9L`KJ?_w=%sVIWVXGtUPwm77hi#bBtB761dNhGEM#^EFKg(68 zFFFfxSm7DuV@xTy#{o=#5H1pP&B#Q54=z03C`tr6N$lF5zBkGR<%O$Cnp-S$Enb!Yt1VqAXtf(T8U;Z%b`+HImSAwoO2{o6gH=uaaHGZxrcT>&_`n*x z-cW#*KLhAC_uF(rt|Drupo_id;wiHhw&*h(S2*I>^v!&?{fd<}FY&&FyQ0g!U{2Uw z7rVKD*dA z=IM;E6)6ZAUWEK(laO5-gT*}y;aB3%j-zb6hPbk*Ia>ymyQbXzaNJtT^H&(_k|q$hC`kT z5z^Ix*50rXqS>7sBmBr%5+x?`OlWBq?**>^C&GUBq&#s*BB{NJXOHx;%-gGA=IW=& z5AKFu;~q;+rx&4XRjOEaOrAN5sp898X?ouI3%mN&iOR=Hyzh{s-+|A>wP&BOzPlQY z({iG>Z>31%y&8RG=1J6U1=^abL&te8RF?J)Kh3s@Y<3H-H2#P+2F2plpi~SvQi7J7 zBP0iy59)0E2BqisahF>c-5=!O;lxdtaoY`J8c)J~`g-=|sKZF9456Qi43AI5@x^Xf zJ1CeO)(RA+Pr|yUK-zj_CpwqgAoX94xYk>d&K&v=?~infOWC^IHC+Z*i}^zDM_2A< z3}8l>D}{b(M!ysKi#7*$mMJtDalnw@vo)Cyao=ZZ_e zm|1yum}I)-Bt9(7#>5|51ulKAF*hz97lM|Ei_FL9JYJ5q7qscibp<4iZ^p1A+|Tv; z$=&)gSf6I5Oi35K34Vadi+a$*D$bKHe}4nyC53Z5-4l_`m zEhw?(3!3#;b2cE9WOv9>KZ{6o;~h~?=|Rl+k;aJ&QdGFc2R8No7^bU8HGZ=oYxztJ z&Qzt5C)gQu;1h1xICAGmiP_#CapQwEbxN%0tJHUP%3F~$_mA2OW$4WlC0dXc!euA-iCBGjW zcwYRFcSKz{XE;>+&mXX)zYL9t8X$5{zDJX2gI|}k0mapw@SLYi@>8N<`pj5T-PDJq zlOoY4ZdAdRt-aZ2%^CE2PyPFq4Wh)zPLch%4P|rfsrpJU_$MjzIqXO;H$M{ZZggUn zy&2D~z3371C@*GkcWQ<^Jy<>qE6$kHrDT8Fmva~iO`NZhlcnZers7p^H@d@aliowd zi?+4yB>kOpXQdfpp}sFIgbyvX?n(dSzZsUg(Vr4e8f4=|?)!*VL|M`^cAB|rR-#cV z8H<**!E@|w?4Hfe=!b7H_Qo@K3@t%ylsu_Ec?7ja&d-lI3h%k~s5z%XD=U)Wn)(8n zAGlZeZ3|x2+EVBVPcqte9kmO+$t1y#PvBxb~={}T=ew8?D~$Ar5LuDwAD%b;`yNm?-jwoR-9qZ``254Ct!Qz3297F)i$w{!C^^0dnZ2&!M^h5! zm9NBq!){=u`V|BpK8p>9PeI{;5#5Q+#;`+wA+OB2rn#KcsaB$!wd}thl7Z&6ZU1wM zlFW>=;>WKuDE!)$%4RFV!Qvq7Iq&T{c7u2todoSGSz_+L7%|XkD-QhahFtqMBHWwz zH_l(gsct&L?EGP<_zlC2g=3)FQ_%9{dDwd14@0+v(E8FvaQsMc^>(F}EEQ_gc`Vs} z^_Q4jsYBO$9nGJ2MIGxix=@IFp=9OODDkE62#n(GIeYt(y*_85rrRh6Wou&j>=joUGpb9P79O2{{Z*_Q0Bmpfulm%6Z1#NCp*RaleK7-hPVYD<+41~iO& zy~VQ5^i;);4E7I&j^zZL^I3ol&)Mj^c{FF~+;QbZApZF8;7t1(gipJJW7@r`PlqxKqvGGjO_T$G$Zu zN*G;)t6DbnUDclCZ#uD4(~G9wbEeVh<~)0`qIJ(KNqfq4_@|viP|r5#{B#foW;u{E zse}Kh+l*I-6dG- z($4E{$eI)kNeVOLIvYi8XHQI-&;A{qff#wZ59*w?C~kizKFF6rTUwn0Mt;KejE`9G zw>#bE{#!;tAqv05i^7{mWNs2Gnv^C8$pY^1f4d_V^;1MzST}l-7A^+HIFQ}vSz<@I z7p^MfL-VLmusyKMwSl?sZE-zElOAYq!pSm4)K4~}dS=4TDk~6cK4{YVfPS#(!+Vi_%rP}# zZtPAUGC5O)7qc!vX&=b>hdgBl#UMeUNV5K)LO|NFMnttQ7dIn=h2xfb4DY&DOrPGP zpxE>u%5~gD`>aMWTzH7q{ud-I-A$o$FiYr9%@Y*J>;&foNmxcII)VmBCLTPBCvt~y zp<znU^94nEV=_QY-|s?Cb#$q2j-fc6XU{VwX?oXo zQ|x-;KfT|&rWEMMcO|`~ zyO2a2z->J_3U04Q`Lc7^u}+U_R#jkMxjab?i(#)qJ5KTUJkRhj2HfXtCbK<@I7~Sp z&5CAR@up9%x$sOjq+7%Lk^bIXIQDfX<)?o1_DdemDGze~#hwPv)TXt}8G2CaPuZ%LV>92T((_8BJmCWQnai&Hj^&jgz8~aJ3xO$*I`Z_Yh<`qj+%I zS*Vzeho1ZsG~4UZP>WmW{QCu=jqFDhz;+|(ol z&QNAgOuGm=bOASt4Qb$4J=~VxjliKYXulsJp&WKpZ7_oQ*I03B?{=gm^51<#wZC4- zap;cj&HE2#`z3gi@3%QvkkT8!589K2J3O;4havh{U+R~rMSfAyBIo7-*IN)A{ zJw=mY^!PT$sBkZ5_*OjMxf6bCO(|<(JTm+5hVflzN?#F!rf)0ogU>4KE*X&i=0HwQ zd}toCM9SC+e`>NfxpDv9G}n)On3cRb))AA3??Yoh1vVZ?-{qtR8M)B$4NWH1WJw_c;{4GbC(RmnMrAPj2n6tCK4AWm)PcBzGpO~<2h{ScaJipr;G3@Yi@oV5GsB_Qxx4sokiDqW>yKbb}U`NistKeLq zNhRaE(X6WP2XP?EoPZzO3EeqGRb~4}Rl+cbY z#>JS;s4iDPW&qE-SIonfqZ7o@!ZQ3k;3%H>IpP*`1Ae9Ji$Bs!F~Z0Oljo^Q>*Lld(=qA>wOQ@i|9RF&!2~&9|=Id6%??agQFsDB`t^R^Kn?gzI z+h-zu57E4-PGWX%RrC%UM1yO$iX#RkVvB+o?bmvRpWy+bYU2ecH(Ww0Wm4FA`J8h1_lZ^C_Hq*BEU2OHk2Yr?i75BoZ-$tkW1 zv3t@nzuA~Z-zdTIkeBG;XHKZA;qHAsqE2mNmO1DG^Wn9B>_W{+zyIUtECZ^}zAj8i zH%NDPDJ5~w+KK^Uw_^9$-HH-|fQ4d#0iq~if<-DeA_gLgC}IE#7GfacyZ`UU`7jK_ zz4v#|*?X<$p|h(#;+Ic69On(9nJI6@*}(@yU7{N8o7N~Ed{IEbY9)&MY=tQygM|EE z9oko7eNnt2iB`P4Q&! z@Ll!{6P(NK=)Vf)MQv8$*S#C{ZG3^0Ys=uoU5z`-TafBX2)N;fH~hPe_jE+;>?kyP ze1?a%JSGqCMN!+kkijnIZff{aQJOscWVga-K94J|)uEoJl&IJGMELK$E$MF^Bet5c zFY-t4pl>ZZga$vyes6x8pSZq2tgo*W=Sv#-d0|hB6I7rp{SS}JyHOXJByoD|Z?v_T z67e42E^>yo4v7f9>eWZ1VgfU~x~ zG<$?GP5v587izpo?XNpInX)TBX_&-rp&Q?8-FW7-S&SMap&t$ow0g_h!kO70vD^Fv z_Kjv<)xfv7V0IFtNsSz}D>1J7A$W}7K62?7>|XZ^<0hwI-$f-Li#r|( z2=?ktY1)RAV7?pE@&o9rBKOJ~7Q<1I*`>2gsQ<17_|P>2=YFY^_lDlcd{~BbcMCel zXEY1NOfi%*B24SYIo%~9g!dQr15Ux;^1gTq2eR&r%`l z0wem)&%%*vKM-JWA61>DShmTPvYi@mZfqtJ`Te3Ua}goSb8%z61GTStBc54CqqAEY`ACuJ9gBVCHXBaffn-$c8)!Zyu?V@@;Rq|`3cBB-G+V3 z&T?ly4LT8np%DB6R;^~Vq;EbR|9TJKP<93IUO?wbGtND=r?kwo==J&^bgu9W{mU;g zCxvr-YU2Vc<^F}`z5WmhW7%OIMyCr@D1L2;BG7~Gbj`avwrJ_mTNPQFKi3Eo-ZjBWh-tFBov8 z0^{caQ+|AdJnvD1rZ%zr;+61K;y!!YSDgJ-D&i06lhfih9N1wCzZ~wE&YFhvq(L4| z-c4bGC8lig^;tKG2GNn?Hr zPa{ljBuTn(7UY&rCw|-fE{d8eO-Js2#xNBxVf64F{{GdYO&gp@>(~$M3;8bAUeyfx5uPl%-Lk~^R{f$2g|&qvUTEOsjp1V1yHfPf`5>`fdKjBy%5cT- zb&+9f4hn~4!)1S8aqrq`?9*sL*MsU5ZEKA2lip#>5KFpWYk{v_<>>7rHF{bogGu5M zQm0zcp`XR5Z+e0uw{+=A*%`#Ye25=E&FE;ybCxCiO0T*+7)4Y>17^(F@{CKIxT`7O=DHn;&i}iRn+b+D7Y;h=Al}cljDarmLY-3z# zy7~|14K-kqT1rb)dg4Cf!h?M|3wcT%UnrJsm9kQ5~v1R(G=_V z*fK_iUkl#fFZ}@LXEX6AejK(uYe0PcK-ko6M#RnU7%|%tXGXEpNK%dWNiJZDNMQ70 zSt?Rcp|W8^i(Sts(@{H3@=DhfDgV^zoC9ZJ^pns-t5I^-bAvGPJPyNp6m9=;Sq%TT z05Wwa{r4U@BC0Q66}8Ga&g`*+6RAubb%px+9+Z1+~3Tg=KJXtKl-p{Cyor$ zq`T})k&EXZK09NlurK}I>KzDvV?r(+YP3V6C%tG4px}2hyxST;H)OqOS&k8f>FC+}a4|0ht)vMVfTX>acg< zact)~`>mN(=sROK#%BNI`N(I)8UKNn(P4CJm!bVvx{%Aib6D!Y{7imbR&R+x-3*{=fOyB$Ov*U}8V!MNZ#|rF$~P??^{# zbGd~4-q}K#-LTJQ?ZBJs6GdICKV|biH6_*<%ieV&?7RUf*lR%PQ6r9bccv-TrQ+}g zCFV}v!{IO2G5fnaU66f-FcWqmOz2LjRj1HC;wGNPIMC?&2H~C?38@MhT5z#QbQ(lr z_`iBgS+P!h>>ULe1y$NzVNMy`v2M1uq$O7Nlr_kTY%*P`beAc;=kFffCIh;cX+s9x zGobgwj|R^7rrKeLQKKA0Pp_Mk&)EVfT6nOFBnKl4*5Tx&$C#aZ3EszcvD^A0R;%nm zcfBDvz`yJ3?q(F8a}I7x-XfBDv`$W^{{NnQ{*WexyJWz^{u6iIRoJPji2WaUceq)F zoD9u5Pre0>`8=gJLdV3!a+@P{^>6g)jtVMV@=9kVjxNf z3vsu^mOGv8iP3+e6Dm+!|^3% zRQ#Cx4v&agefp$f*ajC359BX)qOdYKDm-R{Sa<&H?8t|0kT)I~zsBT0e2#Pg_lG`0 z_rybdn$*A<{>S0~-!D!q|Ayg)r^I?sL&`S#g>p-MJYpVdQU6)ks-(&8wQdw}GY0dR z{Sa;eQn63KUOmp-IXY0$@5jthHKX@hHk82ZmWO4Iw6}?K0DC{6Qek)^$#)9f3SU5CdLewrqdfa6EZ&nsXO06;W{&8pBPiyOKDo1c|^3$ zv?cG$>>|vYDIDzesDN5IPiii%YI@Md-^nnWw2!?JuB3b6Br=^M*lEhyoY(tsdq^%0 zz2WT3*sZX=tBMN$htNB66r;uwQXbT!MzRs7HuZoc_W~|E=fh{|F4&DMM}WyA>`qBV zlI9_7;w-fFibQ5OkAuQQN17$K5>XL-aLUP#MoYzF$;Nr;8D~qAD>>tl=SrvddDEgY ze{?$gQ|ucj8v0EF)1Tb)JJ5r)d=_A@btZKu*N5QLyFlp8GHW_l$wo5dbTXZn%qbIL3{G!vr&9WKAx1A(&Rs%u)d=P!5+z? zGs}q%ef1|TX=P-lT9Qsk4@w`KETr7LsX?kQsi`wVHPcx546vem%sqPg-*NH!AM@tr z{i(gmN^)1nfV1ld#XW~I4CwYtJoifzqh?=3W65^$@nO5EU!Gn%o+?ueuy zU51{Vk|sHI)#8aAzfgJo9ad+DiJ8XDn4Q&?E}A$~Ejuw{Mp)48Ja_uKy%y_#=};tR z;&af5;Y+fF!QA~QVgFyn%u42H72tXAb2!=mhUDkj-B3Dl6mL$h=RD9gank<|cFkOd zcp5A=4}OJgqXFpcR4qa$UxM~7ADZ~t3m;AFn04$;bCchQPnJ&nUD<=ats98qsS_|{ z#u6bl(vWIi1|sVtclSdzs6|T+^8MZmuLb&)S3eHNf{A?gcA*#F(~x93kWROBAd$Zd zK7{t6Qs!9~PL0RW;$ZPN_Eq2(QHK$8;>6NzF5>QiHlB&)ib_qz;;UO9;ZLrqMCQ;R z(L%ZQ?+CC2ERJ%G$6KEU7ZYNFT8!;oKc4rR|KiU+~movM3; z#C};t*H#>d&#%{5`9hth$yvgDnG_j5Riugbs+d*y8g2ZHuAHa^tgXP@3QLM_DZy!_ zN=#5OBZaHA><@YbKRbO2U?0Hm>`uIhh{G>i{@hk-(C?jFk=X~NJ>e^w2XDr^Z=qy= zPjb`y@&H$WiroKqo6BSd4}7F=j_D1|I!qj^+4uV zZi|I>%vLKLNIBh#MO36IlH(-goMDcTt@>10cNI5oOn_XaKIv>ZjU5l2&|?qx0Vj*r&oCtH6KCh6(5&CWe~)sJK0BDQ8q|0O)Xd%^=8$@; z(L&CtO}#LHskEdRlY;0(yO`h)cC=MUR+v;$qwc;pw*VjS1v~d-=$V} zSd+&zJYW!)`i~e>rLJ61W>+ms$_+~DP5}e zryscogb_QUiatna->_foD0ZgkIqXTj_YlX}|ASt+Crz8A&CJjUo&$g#bNXbqHVzjqyU^sBiWL7S0?VTA zp=N~w$sC>y17;_s{!*g0!vpcu>ow|q)yT;>fxVa8g|Cq{tykF%R<&B%g>tsUh!Kl5NVCa@=JeYu_JqjN4`xB9UmPt$&nwc<-mMsfGMuE$o1TG$_fz@%Uj%73@4~n)YSc4jY;o}5D1?pHrr(;XMcGnE(Y^T} zo*!`KJP7}H42)><6*H>puSiLzR`e*Fg6(`ec0q8#867>qqu^njc1wXf=9WE=#UqagttI7m<5Wp4s9FVmkXQ=32h(VAH`&`1h~J z0xfl1+{R39x%aSL)(>$`I^-wQfJi+Xq|9@mP3*1HZK#(N!-^WbK>`heyn&7W|Qo4LsUWf&2Z{$+&-% zD7!vbTq(YTZikr#yU12Fe7nS1XiG}^H&4_i@cd(d9kuekT6McI-j&9oDO8tEP8ooE zekr&;P>VjSG=tH9;gD+MPUz=Ow6s3N2D z9UZ6Df~41$;$iV4TyvUQ{5vWS`?#~XYkG*dr+5{5bBm$hx&;5mTSMj6Rjlbh6jwrm z;P8(#kQf1!uGrNXONNStxd-_zA7RY_~#4un`) z(&`p<`tCaeqXTD2cBK^xQ_E^ZM%apX=e0z@yDB`Hr74EYP{#L{A7QzCub6xGkMP>l zRm3FT6{8w`;98R>xjxZR6!JV}lJsGrWOE#5S{9P~2M*$O=0Pl7*&eIW>#}c>s{fevxq-Csx)kTcbrrifI;Q@WZmFEwG|KX&)O^S{2EV+dRC5nv#{a; z!;B~`_AzP~%M{I<_6|iiq(~!5hEyiKgyk0*>a$3jjP})G{o^+XpD9hAXPd+dcJW#b zeImXLc_Z}7CA4H?cYHokB5tk@Vv$y`{OOxy*3nl%JG`2!`R@xb&ICSLp{_USK|1a6k2+pdcAXz5Jh zSfoWhsnX;edah`EqYBNCXp)c49vH0>l5#IyQLP$*u}j>Fa#!pZVQ(^#dNe}vd)9g3 zG3B?opWJ{JGkYq3w^Qt8_R-!_bK2i*hZN2`96#?w;Tk+2?y(bUb2Z4cs}0$For%^O zbsD_UgN{1v#gk=bq{FW-o@w_u~DPu~;E2y)8u(ZbMy^s6#xt-PM@_KCst!d%HK=BwBA;#kVpDbw!gB9G zTEUxAos%KA{~8SHc;@54{4DQNP;q3x2>WZ@y_R7M_pc?(ibTEHI;>+yk4w0g7-$=V z=g$<$>4YUUT;G zJOD}P#R=v!)EA(6r!V#1l+9iJIJn+t_Zp>QV9F#6&3b?X?CCkCoP^#<`EZTbqdbKY zyjb`i%O;r7;@tCa;LMl$CMELdSAbNVHolAN)7g3N#Ke6^prfeI`BNt}z1xZkZv`qp zRVp@(K8!Mt@8VBRanOu`(Xe^EPpp^HDSW`6^{>;FV6lIr*z|1^>~03*2*O@diEe%^v-`EWTqHVZRIwJqO3GLEj4NOjYy&L zZ-Dq!8_Vb0e&~GqS@?Kt#x@@nY%6UR^20MQv+q#UTeS*ZKN~!9Ga=o8j~JQS1HYMX z^7ElIeLEkD<`m9vP5rMsMt3C8+0ndhtGKN5V1$Xtdr(-H}Hu8q^e&*V0Xcrk%-%D0h-Dd-= zK5H|dAb|AR-@%=IQNiQgS-sbe!OYW~xS6@#i^}jS*NE9c&h&cX2&^{hk7HiDvBbp! zPfpK8r2HYgmrTRYGwkhnITz>3okQYj$j#t z@Sc5J{AWz}`vA9L=@|F=3uLsp`=hl9!xJAP(BcQaZ`GpDZR|>ZdR7>JGp1bbI~`=k z%u-2L8k#LdZx-|xJHy?1rgellxM>*G#g;Tn(xGf!$j^Isx^(3dmZj~%{1=aKH(?HQ z3WMO~{s0eNCPH@IG;FB(ib}Qr;Gf(BCKoQimuIjo(_%3rsR$t_>yg-zfLTdLpnU2E zs;B&i<4DB$684hMPDZl)Y&>G$n9w|hFCStdpYK9<{5B)+oC_(R;;d-z9vE)wOwBPp z=zI5Nm>=&)TbsOS+DdEoer&_!bMoYrzf@d&xF4J6e@A=yZ;8*!Baq?_@ynA>#LgN2 ziEqcb-#r(!>i#_uJzy*DpA4d!*mSX$nHc?&1L!vQ-@pGEj(2MvaF;W3i-%g^MIUu6 zA7nved{&}-v;nf&mDW@xM_mpVb1&7JGF=s@X74HJ7#k7Y{D$%wW%$!2mwS_j6gas` zA{+czWIkmlOaGB#%uzX*+%}{DlifuFYTW5cLX~81mKo`OcA?8zal$=9i<)=#pbsw= zGVhcB9b*@!`YIkScVpkiH^?d;5K(f{^kl*p ztb9}-qn$XD1GtPz` zE-4mOhP#<#bsniVRK&!&$#@%b8qS0GO!E0GOg^7R_JyrTtV|bYxl0-xHVfx%I4b$) z2DZ=Igr^>V#LlbCX3KRHVVRy7XK&0spnH-&=jWrkzz#q6-4BXbuZ%s3-rT`$6^l88 zIp=98y9|9$yHuAn`Ygrsx9wtFt~R|+*1*akLnZyrbfwF7*+|Bin5=sVpQHKO!T{iBQ? z5ULJp6kKe9hq|kUYOyYPup?#oI|YPrrgw{%1|{0QgL1SB*)>bk*nZ!+d*6+!*%=*s zsskHK9mr(j4Iro$wb`0fx8y3;WpIZ+l^rjfeH>EJj37;Y+8b+xthgXLbNjJ~{i((I z0Z>h;9UeGH;Y)ZZB|bPSB+}~iasETRJ>rB9GREwbeuHrn`=arQDs4IZ6vvYvi*wo& zFyi!0{JDEWd`w&lDfZ;-oVc8sIX0Nka1S2t4@A%zMeagM(fawV*e^A>Xys%XatQ81 zzgui24=#Q~e$_KHZPi|bGrH6HHe-s(=FYdX z2N~$P(m{TP2i_;zscOQVB!AARR3Z5K4xDL_<$l){9FgCT`yFx={j&;z9^0@e^#c-P ztDv$(k(_t$!6*5*FpO5A(TnF{GM~|V+bhr+E$%OQ+fm2hoS(C=W_Q@#Leb2Q>ywvq5S02_lmv5LXz>{i{~4DV{f_@8LW#GABHH96+b)1F1yYwjcznrs|hM6 znU}-toNcx*kR1CE**w#pCd2;4wJBKoNS@;BqD4sBaa``xiUY$WVq4ZO{4vs^TAuS4 zNlO#G)aN{e2N`*)kwS(gwH~q{ORay9O4Fdw&(?I-hxf&aMEjX(*~VPu-RlO^xmh;k z=$;0n_F!_ndK!O@>}F0Y`wNC;F?;7QrmOFT?B3IOwt5W?d$H@>n)iiEE+RbaAIe@S zP~`6%^y8WC#ka=HJIaRo0Tq&+Cr^tvhKY5>7f^Sl1?A;w5}&cBp}}YOwRShe&Y0u) z`*n}xo7EFBeP}Ea%C1QQ4DX8?#Q2ZF!0y%PGx;F0#@8dTPzC+Dzp8qr z5sJ%t!)uQg-TnQJnQz?0O}D2rrzi1jR+qDLe1F$rhi#!1rRdpEWAYX#?$@9XGrg#5 zY$xuN>a%018(nUdAwzc~GUHC)tCjU=$nQ$2Z@Q80P%UJS9*&R0|ATq43LMk6BI40h zgq?JRs+9z`cYL89&hF*~5()|Hg2=oUeC*wi{k>nshU6P~@S5m*i5Jp_Z^fASf9T0~ zcp24fNUf8mO@7(P;QUvQc%Dm~yA6{eRMnAdV@3viChqoQ znb^%80^j=WxTbswrs*am&$+q?`+H~_ZcR(q@8`SqLF`XszD2@Pn0GbB^+EU1hrLky zE(gGGTr;e7M&rL9J+ON18ASM9Md$u;@Seh5ZHF54m+6mN+xEcxc`45!y`eYA6XEYY z>5cw=W-%{BDDO*l?qK(EhbgAN@+8CFxp>8X_(AF2sNl{RJT_;aq?ITAJ-iJmRuU>_ z2K^z+VcaP@gY}1{sK(@iWHt9b6Wm)6(3lsrzU~5cDR-fR!G}dwfxV#C>)71cgND3F z7Z#aMF#n!Ab@bg|s5$61&YoxX#nS(|Bm(ildKPrXsMC?>s%XvXiM1Q_>14=obRRMj zONQyt7k^pyDBeP|xi;NPXoS_&M{pcvMru(~G;Ygz=7--9lR6D(o~y1Hm27|#=uyz6 zl%Nj2Pna@w6q%B?5uoXy|gzJ62@;zN<;OGI3-8BJC85NpD3 z@y}Di`pN;4!(VD)Te$Soe|P{8I!N= zMw}Q}D_R!o(y={;=)KU7rrrIEy3=te(GBL$N(-VN?uA!bFZ#Mtj>dgnh?K5BCE*qs zyjOVzo6p&zbml{GOY;+YACeQsbI(ew44)(5#VE0bUFAD7CW(TRgC#x563t!nM91yR zLj9LHvS*)>gk0v%=L89vMx?{2rT`aJ*tM5Y$Xvgp=<}){sh6L|c=o#89HL2)^VE69 zSA&$n7No`e!@U!vX^p7{X~$^de&ty-^s%JGi%pQd@BnkUHyB_010Rm$Ae{4uX^B4?rx1{ip`0~tqDF!=3g&O6D{ zTng~4kjRBEkE%5t^%YvWH7JI!g>>5rTfV>isdXXtiAhNdzX zd|Oow!u{l_m!~?#h1X)B<9mF4&z?N955lxPK;p6Kni!>!CZy|P#s1tlvCrY0SgSZO zsI*EKmmhmlT>Tx~`ofulFAm&&e$KotdCt6aBbOA;`0if`JJb1mc5A`YpAtMRipK7L z%=n8{LZ!VwOdH-J@1+;Ec=V*h+{aYepu%}?UrJQr_v5jy)T-Wx_C{)v)K5jaF`e_z zL9Zk?uf>Qp%QCTIaYtd1$5mzw&crXZz=BG(tzvWSV`06siJ!#|?8k~12AnIt&e`U1 z_FeENyd9^cJ;?ZfJ$1Dm$Wzha-jF9vjN6AbMrO3?peJn&_z%}NvKM%lI`yb03TX_a zZk$btF&;nzGySMM!${p4(B! zh?Kw!&zhl-a026x{)Vi56(*+~z_l>;Jr=*mL7!8QeZj6c=PwAj{0r~Sr=WJvXMAsv zrt_TBEOVD9&1X{NRGAKyn~vmre6d8UG6%NR-6^zdv*fbVEmSTrrvvCGcJw)qHN`v= zljis8UllTXydC%E_oiXY8FlhKfaKBunq(|RZ$p+KrJ@iS>r5zjaSzPkEbxKF+$-{5 zz&Qcl;f&QJ?~){Z?2#p6pSsdsaTepOip7*FJ9>D3CzeP}7xK3PsCuD2olDV#;U#8^ z*gDa`zE?!nDpg9zG9nXsHT3`QCp+Hn<8Ey!e7QGMozTD@j4b3jdyS|yLb*Ny%T}%aX6y&23sP`nMr6!4Sy`D_iA&xm}$kc zI_6ULG9?=?Tl%&|pLQkL(dyizu=4dK|B~*s$^1AL7fVQyowSqp6vFur--(%1?x+yO z&Y1^Dj6aTkb9TY|(FG*gALZF;I6nG5MW4-P6z`aitcMLyn`B2+xkjaA3h3KSN?9dc13PL7RKe?7SlVVMda+wC>blo z|F7r7+fC~+HR6IOa2qRmkdX#IEjMI62*EiiLF?p3U_pTudz`$;f7~F1{20o9?@;<| zs6t^bMUvUW)nL0{g$~$0kc_iGEF3=R(aQH9f}+p3qjgy<{Fe&2=C+9|JPWE^|5ME9 zUMtp5XFuYFao9Xi3QZGLVfEFDu7}9btrQ=g=X52zmz*K^(G6eA+~@%Fkk{H9A-uW? zt4ChIj6y=surIiMHWhp7yW{4$7P!hk!0$s%xU=H5c-LY_`hj2Y*yEb0zN<&Co7-^J z+!0gQnfq+{3{2qMLeB~>+N!+~(FbJMM-tM@rfJ0G-;VH1D4cy`0Ki z{UNrrug#rovM%DdoE?3L;`6Pa4|l!1;ooHwhTRRvoQ?gk@XP?-A*rJH$t?8o+6+0X zN9=j$oHaW!vYBbTrcgqG`Xf+!;UiL+{~vSO3e#*>VczF<9Nhf}>pYI2ZD}WCAOGTc z)>_E-=bj?p1@6r>rc^H(n$o#aq*vS1-$~4!oYh_IGuI>c7M|%3yc5)1;zm78ld-rx z6(f1p{oN=XIn3PLf5DL~@26qk!6@i32Z<84BT7pe@kd*+@b_kBSli-m_&qH5;`~^J z4Gi;hxo=pEfMe{`obeDxA3eZh`2+k+$VBe@dzkWl12SI^gJUCi!^ba&?_Ym*NcdBk z)fUX)_wO6tg}42T#s?)g8lK8b>N$fjKvTl^BNv)yF#_{UT-bZ#N9Ok;VYeX*5dq)P zI>b@Hsu*1^bfGno$3@AYLRe0Ji;iNoAg}&jB5Y6^rgC<0`|nB-JaG>@(){S&rOhJZ z`xSic?@5FDDbXbMwM5S8iR34Sl&aGmrPF+2lftg)d+M;R_Q9q`6W+ylA={*zm=WSg z_096MXX$xNy>3GHIzGc~eHGkpXNs&E=IGz^qXUanaIM{%Ecs{;o?bR8F&{yh;AlJm?a4oL%Ws(-ARtN)`74J2>OHT#O7Y!{TX6 z#K+?ElEjp9JSsIoA~m6-VV5NEs1)6CX4gkqFX65D8e`=Dpv~N**denMZ_EtXLFhzp zPPXBQr3H=i@uu|muVFe!pYo;5DSrMN#7(*+l1?UbUpEJ}&!mO5*acIaOnCOXA&v*+ zBBbvb7)wR)=b}uUzxV)NZOgDfTn+kTt{`jdV9tD3iNdPa{CDIzIRD)a@ec4ux*x4l zP{yCK{&3h5L|#9qU>q#5T{}|9ZDIyTurFdg+QqxcHgq0+pmZ}sc-+#UU7@ZBi6HvY z%2|VoBXHOjLarK`r0%&B7&DMO_1od$mID=5d4{*dN@PaW;KG#{F=}jr$X?%q?gkUZ z@4AknN3WW&_t-4S&<(9(jq|FYmDbgT%|%K$lNcqkZmbf|eNE6|6Ij@JLeUjKEW66LA15i;rgFWI3HP!vk&}ec!oN& zcN1ZgNi^YPGh&}EgYNe}RGux1E#n@GVW*hebcB5yMjjY5L4{Ud^TqSqrJ_faF6G&& zP>6Iha|T@~F-MC+F36BQlrjxt*egU&rzUX&QITOU%FAg?63!g0)>QOZ?yHQOg#2(s`GPDOJH#(v&2knWOC_ z)syN+HVCaLad6KKpoY$q;#FrT9(#VrlyVbFTQdwZ_~$qTJ5l5hUCiZM^AzF?Fchy z$LMcG=pA(&!#U?3I_x2CXl=&soLcVVzQovDvdla@0`pCOaF1P|NveGAh^b>|pfatA z;X8d#O?vj+g|2@%gC}cM>4c3vDXCRpK(;bD4Gp00C(gic*JjSb`jY2pJ$f?s5IlW6 z=!bzSg?-wI$wvHK>19II-{N3(;|88&FpqXa3>@B7pmBvdU2$2B{XXnbd;14dmK3u; zvq|EykXhMT+*|QV6F$qG>5($;h2(Wa@mC4|J=ldNcTcG0JY0^Q|q zeqe2?*qEz9(cvHQR{jlV6F~O{e?o=kXZ*)b*2^k&(AsvBXFj08*Y}H0d-ia)unW2S zWQbGFN7;$;5$X$~CBwp#u*pM_uCL_)%7d=d?CwZz_0F`Tw>h2uXu)$hJ(_vcly-BM z$#jGR4L3iC-?6Ph$=uCr*t-+P3eA!oPaK)sd>Q$+*9+^-ZozTQ6dYW09_@7}VA~Xi zRqP`U-*g{_Jg2_%Y(G*;bV%AV3x9&XLGG9w4Xru^+it(`d^qn!t{j0`qAdL~`3mcj zks`~W7_YsUWxih`Hl^ml$D$p62P=er!x^k=*ckM1xh^_gHe!I1X7RBJkAS!uum_L(%CyuCi_FumhXjJ zDMQNZ_P)^K!gL7!J=;fq3p6xs7gi_xv0LziSWu|Od7TKnY)lvSFN|>5yC3`pC}M?9 znmBYz4}VVi(D?B0?4}LyE$!xXD5o5eLng9kS(g^|;XZrmC|D^u)6I@?un#h# z`6a&O{WuyY##mFUGjm*ya2L*6kD{jaBuBn?1>M_*r^_qN zk!H?2h%uiE@%Y>g_?HbsySfYle^eklGZe=@6BN_9x3F>(R{Zx%BwkC0-P>HuaqW(_ zsYTGpEkUluK)ej)`PPnGn0-TnTTfmJ`>}51_-Y@9J(Y;h+jtg~n1;E%6mVsQ56$In zb>&4re%}kKD?ALJ0=`Qa1k%b!N1^>OhPsC^c9IV{nN+q(I+{G_N>L^#f%Uu*(b&f-yckC6b6exH= zf7m=(ihpu?w5NU&vVu0^a<~df^J2hs#TabQ(xCvUR_u6t8*?~2=xhHM2l`z`UU66Q z;l9VC#}D9pBS)O#UEX8PANyYYC(`9DXsvm4;Z@%g;^iV2(i)m5Vy6bsmgP;7A%#A) z?ssqID<+GHHthCP_M$l-r-&b)oM^+~e9=DtJ}lNoiIuB&h^6hfvA@AoGRGlL4C4H% zpKg{|zm8}B>D`5QHx+un;unhZi$z%%9h(39JI4L&E6Vvi9Hps9G5npd(m|Tuw5rmQ zCPnI>{{@xI4tjJ~o3<6GvF9jKn9a?Dtpc+EANWhue;?!S(`l%6Rtx#>1t^R-flJ%w z;~wt=58f}p)$CcYmG^}D`==OL6^5FD9(Zb#g%r8#60fMW5DSZilhF{7F={^?lR2IS^N<_sh?4dP}tWU%X9`&zhNIm_GlZtvE@uh^illXR~59mon6$4 zDQL}fFP8nu?Am2JQ2%F!P&l8%Ge*wKc(U*Op*zoIUZH0lyQ(yT*u5!5(+_o}`MLqf zm~#rzE9~gF-g~$$u3(Nd&-zwy?l>k5r|%e1gE8;pe7@rS{Y^;L?M+FQAMt|ElzT3Z zpa~fY)Y-|qpG|#8I(jsEn%s_p z=xoWs*;;ha;R{R#pOGx?ZcQIHF=Kl8gCgP?PxORNERbbCR#GLphu8D+SC&FHy~M>* zdGa`_L1*JXiP%083K)4ud^>Yj$oCB)&sEVPX=|Mr^C_4eRr>6k^rTRO59}WbLHzJQ zD$DxEOwKvz^3RD-_!sH#?UA@Q9NUlF$0}(X^gg@-i;wc2{?J#EX=Z{;x;Id7X$6z< z(dkDQ>8q<>|dD``i#H4C2L#c7=D-`Y(__L5{mUN!XM52rs5u()(57cfPPlsT4u*)Xq|49Td$b>;@|fF_6^;KMFA^#9 z>}X9z8U}4I5L0Tb=<=rod>^kT{+H+J7+_9=cI)7ovNSD|v7#(%O%!>&!aH?M>U=7L zniEpw-EtCf4tEey*PVEQ9^2tI}UY4PL?FC}v z!!=0sd51sG+a#0cu0@S%7aIE7f^tvkkaeRW-Rsn$b9si8Ue2?YW;=RT$n3eZ=9Hc3 zNH%+q;^KTC8p+O6`$2rZahK3E?u

Qiyzhb}9_aLB#D?1SVHvMD7_Vb?!oJYaWh` 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

Hg>!N%nHiHD{I8hu zNSjW7F-@E+9}?wtvuNq+-$XI*gnx5GEQa*@8r z|09!_GoofDEKfYg_#s(x9@iB%M^q>+jCY>-dQ`>lnTEW(IKhm)@#^%L z^I@L14TWxBDHgvj#!kEQBH{5BSpNJc!p9n2%7)IOr}ZK#jpMlo)I#p~IqXv1jI+C$ zMU-2McG->4jMQi6#;-_?V<*Y8b;v(DlNp(eUMNq%^adk#=?Y2@bwx_5JEB(fizDBv z&;ic?R4tT8)_tydYopLV+$)YUGiP#xCH$HlX|&@#q|e9m?V+G{_UC4?`?)^lGwO1j zOotYQ!t=efP#8)Q{uTGJVZEIwJ@(wiIHa9ty2)bjX`{&8&i&|9Z&ADEl~|v>PMoMX zCwfy&aQR8R#2|lzq&!v|^P`GH&i+E42XOYn_6!QF3)s^+pCTg4u>aOc1ev(gXDvC* zT<^sNe>G~aQ^s3ns~_b)z`1x5%1haWp*5Y#9F;KMS%9m|FtPZC-LnBaySTv~sVQx6 zX8xa1z7h2#4PnQlQOrwQ2G$YKS@zXd1gwXDiWl9}P$0Ele-xR0M*HebfKnX{=Dx$d zxf(Fk^~0U932f7K!?({AU=^l7)vkKf?9&D}6$!}>8dB+-A!cmb(X!QwWU{XjzB3ol F{{X1TbpikY literal 0 HcmV?d00001 diff --git a/examples/water_tensor/polar/training_data_reformat/global_system/set.000/polarizability.npy b/examples/water_tensor/polar/training_data_reformat/global_system/set.000/polarizability.npy new file mode 100644 index 0000000000000000000000000000000000000000..893767e5654da9e82f8ac33bd782e2b18c55e8c1 GIT binary patch literal 3008 zcmbVOdsI|)9v=`ngoH#Y;sY@s=phagA&WS_@7?bZDS_cL;us<||twq-0$W zk*G*YfJiR6NFz%$bAMBy0*dBqiHJBvhAaVV@NN4N!T=S9Im}0Veeui`ns%yw6xzLH1t{M zyJg1sqX9zajh~^sb(zp$b5g{_8nEnCJKw2Nf_S!rH$OFj7bZ4{&1+RaXNvEMMsyCe z)3ZBl;M{4}RW;7M6*??cOoO_4W$f7Z2Ih6rjD=fFXxdl;Zgu56WAFgn-)qIK$3El> z>nk}X7jW-L%9oEF+{S%; zAJRDzo_RD#`M5l(w>ry+J!U^1{)v;25b+|9-eX1YmNBZmTZ}3yhx%O)#Ru_Pj4JYg zYEi`<-Kq#3#PgqzlVc#u-BcIjj z#hu`t|5q`3rGd^mL^J4uTT%{m#!8g4gftTU z5{8C;%`0E}H#GTO=Doi?a?~!=&HD+8N27$UMK-LQYDMdpP8#v-5uRRbLcPOlZH? z#9Z^j2p=^}?9*c5tZsFWISE4C>Q*N%wqwSi5#NW)tU9X~*7P^CHT}ala4wSHer_1T zl2-9f4--~r+3>rLV&1+-RB!P-%KMv5n3k5t(u+scoj3l*V7K`Xv09z=Kdi=3e`;NA zxM^UC7}*yt6ptPhz4I+tbGmhP->Y(gV#`}L%M^hDGR6Ev7lD8;&z3t2X*E{d;yF`8 z_p2-B_-MQmY#7-m;ei>hf>%O2_0k>igY392rGh6vsNi|WtN5lWnZmSj z5(5ioflO8pKHZ*>cVCMaU;cvk?^hBIDtKegV{GV^kg&WuLb*nr8ri3A3^fo3U*pTS z9#hA7q;S}6NBI#WVSb3wJN$0kuPl765yL;p6T7P7xuX}(!+K0=9>;>=_2P_+*TkKw zT(Rc#1orRG<(@$eBK4bJd*vZ+*sH@BkMC(t9mM4_7WBdxdIot@E<2nHstKAavjK(UiC9aqQp%sJ%K2#0wZon?z%+3BB_LmKw={G=_QL7!uFbrHbqD z!A_O>ev<`wY$&*GUs`Fo1=IhEfh1 zd1#IvgL^-N;NB>TTLGn0j5vRe33r$H3y!n0<#PmOn~v_AIN=U1Of>M!I|fWzr6bH= zg54!2SyJr?Bb_A^RJoBtkJ(iSY13oS3lh!ULsLAef|}DFnvvTQ zD%%=~CzpZhXX61nEO)VB-f`mHE; z?*hln($Wl=EkD9kl(T$;ylU}SIT56#f zR=gz*5GEz^uivmWWaqxhk5PQ9>U>g z+?;~{5HsA!%tD3`K!ABK1>*^f-C+Nq|Y_r_Kul+(MXy!S0K+K&6z2ruh$V* zp5mk>tYXe|?)9Syor@efuh*n_)dD7O))Z%$$rrCuA1=b+B=!EqNt6jXy!~8{dVXT+ z6V5e1HI9RmYay_33%~gCpTYAh9ce0Q+0gH-ea{)7cbU&AJ)Qjz;%p^2uHsocj@&lW ze0GWs{d4f6I?GPJoFL|;KO^3fHi~X_25fIMlP0x^K@C%dq~>EGz~2 z5eZ9Xw}P*WpWxor0xG2e8|Is7-i5o}3)*mCG75u8A9C`B3& zfh^G@T4rq8!_YKM(M+4K?k3qJyI3q1i|=}|SVdOx)ce6A*~RXj5e7nF7=|DSiK8gC zrdjtH)?L4~FUTNY@`bxU=uVOB);&Du?7jBdYp-+mFaNhcz30ChsQr5F?|k(af9co$ z$FG0&Kl;j7|C6u%nXmrHSHAkSU;X-lumAk7{13nS^?vH2Bm%s6o-~F4{_P4A5sQNz-Re$=gJ=#gPuHJV1WT)EQ zjlVK;rTU+L_mBVBcip*D^_Ke&N2)*7`=?zE@8A8CW99EJJiYp#Pfu36yZDpeeWU!% z-h$id*7aR?e&F>MwBsD=T+LPgMsIsAm(jJq`1+`}ykCX&vF;D5Cm;XOKl#7?JNeGx z@*n*0>l@OozOT0tYZdzu-@kY4K5Y2>Rhz%QyOQ~$`**y$@_RR4uRPy5>>axwXmS1y z6V>nPtJQU{4wcvZ*ZzW?5z|EGU@s?vVuTh~AR+q++|Q*FJDa{r>eQhlShF`rkjWBTRS zSEq7*`iZHH*Ej28^|jtV-y*;5E3bq6>qOB_w_nP$E!MMATD$4$7xos#lLzH5|HAkF z)t{K^5H~(77ccDobF>*{^WKiaGochl{^m}fsO6!LW0y+z-D?+fD*^4xOiozlPfkaL-DU4Qt0 zdH>J-qhH?P`<^8)9gOwqymhMHaom_L|K{r~{QBQ1$`6mo3;E*4H(#HoAN#Hj=kG9a ze2n~ixA+|1+P!w)I~q7cd0@N}eyr7#!QR?Wy^eyPA8b~Ni%*@ms(-r&@K-8rPp$rc z?=I`F&Vt|X1TIk?$cHy{Pui^OQG9RXCFQ3E_;<1hH{NyrD#~NuNf+mJ&UQFo$08r= zUL7VMw<~Z)8Qg`re|RxImCqXK*7f|^?mxa?drPm({Gl!O<3Oc*)e3ws-1H)TRvgKf zm$fd}%h&jOUnr`d&sMrWT|Srj^~oX}zgc#x<)8b<`Qp6dLbIYij(t*{KT+*@UG0D0 z-|rAl7KxjuD&u3-mHGBd@u61dJ{95mIq=b*>aX_#dE{+y z?Wq4efa&=y(8j`3jN zVbx{F$jkZgMz4(e{#so;+Bo;Dx_h_YZ7VX;(@jvRb0{>AdPv^rMf8;f*`!oNn89W*| zDe{%RcLzS;8TfIo`mcKh+IA7II*<;}i@)wxyKMAd_@1SzN8|qHGI4OL`fL}*|3r&8 zu~oqNMRmX?JWsy+^@F?g^&jh2;sHO8x=}cOyZo(QKx|a{bF1XJlkoS;RgcF1nSAw^ zc#0-`*+Qjrxmr2@YP~IR#&G%79x=QM&X_6A6WQuY#ko!;{JP|Nv2y(TzjkTr-}oZ% z1vsmMuY%vnmzPxsx9MjcAr3sH9vgudZNo#|uT+PwM_v$rpKo2lhh6iWoUaY@QN+bI zb=YY19n@!8;9lm#hvKJeb=8Nf)E^V@YBww0r#9<8NPL`%Pw{Zgba-BTQX}|5;k`}z zUJI4+i0;*;qCUzxIO;0@UcPv!{n)9LU!NA$kzUUd_`U^ivm8A3`PbFv!qdgFzT5WO zn{QpOo35`LmTm7Bd3P%MlfqH{{hQ#Yt8dkp`EXRP%wOox?;kJ1M;q>o!(X(5m;cUg zd-D%A;Maz!Q}XeL!cn!l`b1N~Ux?DU)hB7dvn_*b zFIB1=H!JZR&8kP^|L&UCMSVS4?TS?{|6acPWqy#n6nIyC=XK(V`Kpsqm(ho)^qFtJ z6#rVmi;{n-kEe(ioxmTeUlyv@BB|HJ)je=jK76P?`73bJjQe$t3-E32swd0&C!ZAW zlj@Tf+{1kFP&{BG@{@l40RDOb{-Q%XxoG~hXkQPp&-r+^U+W#H9rydKzpK<|y;Yfi zH{Yv#ceCi{QR=dM=TLf^Huq^5K5*W=XIbsZZrbe2wW4_P5dL~7>a%S1vh+c%;9`fZc<>gvkeV@mC@1@W)h)3*J zikqLf??PSnDfv3zIh3BwI@gDBZxkO#D)BkBy7f?sE2qH2iX&?YLpnX{X~vAB zit4Y~;623?u9MdupzD266hHE&K2SRRpQD$c#eF$ zsP0ia;)Ho9t?Mc{#Cn0V6*_v?WArWa@&M+aqgEBqW)BVSg(3xNZ-yE zH_Xo~4Lv*Sf^Cm)`7oLApusii=@P|u9e=qrP zulhi>y5itH<|WsHXH$N*etr<&ih1J9dHDK#d0F!*O}^(Sb^e3UF=)NDI(!DW@K*33 zS?A3)H`8(7j=s-U@B-2;H^9G3^krX!zEpK~zWb#(&@^vT#K&(@C*J_a*Xrmf;qB*( z;^-p0TfTK^&ewX-ZQ}fmN_Fry`omh?xKVj)kb3&lN;p4XUKSqjMx9K*yx2cPy2MuK zn3?xrUmv4W$=6TOx|-I}xbMaMw7Pq{0jKt{X(CiXdcn} zscYsp=&yfJR1f5f^X4}cKUTq0PpONCf#oA0|X=JPaH+MxcrA9bhJHG}>kAHSiz zyaPVmANW__`+QNI(u}^Ibsom|($D5wm+LEimf$x$AM1Xle*a_Y>8Yar*A32PzI9oL z=X@nTa2sCj1HQNY566iwOZIoDtWV5#zciQDK_B|4XkK!e{?;(O%`5n-_X9WL6G$DC zFE1P4Nw>Dn_ns$zjia||1TP@|=6=+jij(>BvVE4+Ctvj3D13~4&1!Y~vC+2#=UZ={ zk3V$Z!F?d>y{xaXAEkY56fZsmC(cIyIothG|IYhyjyd|i|DkLvyTt#=n0r&-Xo&lm zuTD`Os@3f?N4;N!J7$UJO?19<+_$I2`Y`d>`S?S9Kl{L(a{U3HdlfvXDD4{?dHZEAZnO=kHqd zqs15Gi-+o$n7>%|ddfF?La3FPL-gE~&f)vPXA3Xp;{oh@K%5Uf^FLv+&*rZ;bi1|^S(|q|$^`z&&R@~R~c`Q1Y-I!<7Ih+Sa zJwdmeZ(Z7#THXAi`;x+i_us-BH#~0>`Hi}(o~{0}&!+oz_!_`hFNg1e;>#L*{rSLI z>Z|9gQW=D%a^}2U)aRoV21sgB0o*S%Xi~v&#g%3 zHVHqPFCMy|qI+ha{4w~AoAfc=*E>u9`Fat5upInHwsKoH4@(~23H)Kd zRO#jy;eGS*8>*wMBN`3f#&cfcHS7a(iT>oW`+V@C`POAWO7$)6AGIIecMRUCjW5wt z;>byK0}J$t^2I~Vi8k;xd`O%hb{`$Rb>A<=bF#!0-s8xZztkUTIbIOwJ`BGA`y$Yf zK=*qM9_nG*IR9o};acP^?c)i2Lz+>SINl4NEEM_2=Iei1kLJ8&9HBgYiuvnWonNP( z9ts>O{w?1*bls@>WX*jO_fznvu>SYBeTj?i*#+WDKHRH$A-{ugHvG!O%ZxJ5@QVJ| zGV^&)gAcGiI2+#ha<2hdtGf@W@44Z3M|dxwTk-q&6t95m@1&jovTwKI-5R>dnM%IB zi{Qg1b;?rc927qmMIR)5C(Cmh?w?4H@tnA@LjE#;f2v4V^ul}q_b(sba9^DJRD_>K z%oD=DH-q2x`*W(3$G}th*5!A6e1D`zz5`CY&AWjOzk7zh`E7K6`87yO(wT%k#_l|5100p=0|PKa^TscgRw-tEMb8X;%)|oFaYaQM2DffE;;+Z~=`7ir8YF`%cSzd)V&9^S?SJaK< z-y1PUDSXw6Ic)W#$F1kU&oUqGRo`dF{Vw}&M4VSYvsJkdtNzME`1wcV@qGJbzDYd$ zdyWg_<#Q3|-8VA7?Y^viWU|E#$8mfpi~PNw#vF=qt?HKj#K$%B1oV;e@c`0y?HI4S zehlBhH+>DZ<9%4+ly8EI^UZ(xokq`%3eTPn-HCk&-(KAUv6Gzw8U*zL#})em~FpLifvs=bsesul{_#{+I5F^YTM<0Q;Gj zKg7E}_H&pvzYWi^R0)^lJBPYYq3iO!ig`ToQl3M62%f)69rh%x?n~#dRoQPt@%%7x z@k-IUyOrvZg?JA~KM%qKKIpfF4p^5d9EO9UZ$_d_kTpCx_fv&Hj9dbcwlzagHzzaO4=u)fm#1-u75?@f5Op`toEpH5Hn8BP3A zSC|LAXg-&Hdlm7~aZz#dD!xAX_DedOw%=(JWrX{4CVYwg&W-bdc{kzXeD$*5@z(b} zWgP?kpgZiJ^T4&>HSjA!CzUU5*yn}!=*_cP*HzTdYAY{8G6I^|N4PS)>`+=%x`mCsJ1cgr^?rTyyr z*gDQ>-t8X!{_xKxo*coiXAS*EK0I&#UHj(4_eNh_^Riv%>F~w&zABI9n|DzE#C6Ju z;{*G$gbuqIdN=pi(fi^{lrJ9I@89u2_3qMpQ-3OGk+8BkYtO8s&^akHK&Z%>F?bpKGr-Zh3@N+edsIuOlMn{c(YgN7Uno- zw}^uu;>%O3^ZsGflZx|mY4exns~gr|GcPpn{xSWn7QD?^^o#7fl+}O3eqW9Q(go~u z-w2+n3r-y6oX!$YPIF%K@x7|QYIS&b`VXJ^do%yrHjfkf4b@!*-Cn-@rSnv)D?iUi zyl}jCU&8TExc_kIip2Nkpii_qu z_XVG*{*ddlTlkt!gr9)?Z}ag0;{QCyJ8WMY_G<%Q@4n6)z98^p)^{s}d+p0}ud+^* z_bG~af{xz_H;#%qOwSEuJBPxD{XWs(=n28*>1*8x-#M-KIDFz0d@C04(aM*<^e(h@ z$@_wL*F5u)SeN4A<#-oHbUyuEUq(g5yUx!bMb@s|*`SO?EDQkru)I0||X6{i3zH}{kVa3G-;z>Rpz zDeFPKFZepXs-#buv41rF6pQ6g_lUTB{S^Bapx?00!F(@#O`pg1eu9hB`Z|82M=Ukm zN1^|9EAE@(MXOTW+zOqI?%xtPCExv${%$AgVef&5%t%mX|?C)B2IzDk{PFLa82hc#RN(*0^# zr$+x|By z_g?0d?RP%tdl>pR?RUO&Xxu8j$r$Hw20VWz^o95VChHc~Ikz zB3$8j1>g6&h?^fU?~pGZsz2v>@)PcFnRkK*@H;*Wju&xGjhnOGFYQyyyoEohC(q)~ z-i&vP;CtZ}SKxi~?U(Avcweu0{)zeuJxABNu;MvH>d>OOxO{O#IB)>po(1mHgnc%P zbg~`$Z-n2u^?cI9<%=8g#TuxahcZ7czVs>m$XZ=``j^CuwW5D-ne&>jPI26jzW;jk z6WlklpCx=^#aw{%GWa(i->W*S1z$E!zTGUY!?fORCEvXDc)!>>VD&5W?U%lv=gE&$ z@)z2m|8T76=T_(wg%h89zj!|--~CeDYtT;_GLC}ReUCic!7qA=dUK<=Z=Tg>Np)|F zdHE&!=kw@5pW#o`LI?Fc{ECDNpH_+|`S>XDhoRr4k8&J+)G&JG7JZcxxL)zZzJdAl zY_8*_uU-sZSM}F8xGLTa3_i*_De3s~?U&xQt<{Y)JO^f9;xG3EmOJ3W>t0vUz1)Mp zW4<`A`MhrB_w(!%;5iQCB=?=g=Py$K&sC~l^5H10tBH@sIC<*?IDRVPyl~S8)Mvhb z=PL04`SO?hk>){FZ%u#;hw=HU)$JRg^-UMgJE*^uPv;>2>w!9b1>;)o)BE@>_xTOx z=+W6s!ei&l%i`U4qVCt6=rikJ>7V#r>4VnSgEvlbFZ0!3I#+%tYk_;VNZmL^KdA4E zU|p~M=#|Iv#SN{?bB2%6M?JP4k$Yx-ag4gN*Pf#qO6yZ$9$WZ#E_9;e0~hEY``z~Y zf#=mXo8%L+Uqzngq<*7mQIPmlBz=hW9iHCd2TwA{V62IGZ9E^81 z?DuNl5p?DS|ETEei5JedU#`EjjxqWpe(%xmq%^IMMOXBKdgea&E+6i-j#c&HFz+^u zd;f^*P51etJ~95#znAYEYJRzezIqis+qC_!f*(Y$?S3Emc$<5dZ$3r-s5|i1Bkota zUxY8Jbwt~)r(;gVJ~7$ymw3Wv=;5V%cmNKZj(4!^ZzlfwB=1bEQ?KRY0qm2cKIEeH z*6@NW;EG1@*XRPQlj0uc;{oh%tvGO`;EM&Hwhy147X62do{I%X9^v=%;lrtoUw=(e z)auq_sD9q&p52PPY~9pP^#I9B*-e37xI2|=y-py_Xj+0e!%r)=K~-hHXnm5(>+Q{J)ers(e@U))gr z-=glALBDa6@4HAI>k{Xe=x1J}uHG!_+vdAp@@eRR2M@ZQq)#*q|J1fF1>Er-`E8Iq zoi83*hXk_e*?xt*-fi7jeJj$Mcf@Q8V-n_V;z4 zIqQ1?o*NZjU*bIW-`!Lnsp&lJdYM0Wyz)7AoXqm6{(pmh);jlQ48McrN^!7_-|`ZE zmDgD3WAZ^hJTE;>6Mkdb^M=Ol@T0Z5>lo#&aq!3@{x|vVm*RA-Ze6Rs{~7vVCwUM2 zOLgmpbiY21x-u&-YCi|X#TD20^vhS+7wZA$3hOg}#d}5h&Y|wP{iBx9Az$M@P2eNt z_YyC}c{MMUb)HD~tM3!7KIC%L`sM4aa1qtw^){K**m@KNZ#)HiR@kGSFA5B;0!(R}-5-+bzP`umH~kF?H+ za~HZp)nPOEAm;0*n72WP7yUc-mbpU1IR1(b3ng04j#^T4%Po|a{fkK zSJEfHTf~=kz#n(0zpl_nvfd^ij*`zrJMf_2Arc-Q;+>TK+_wBo%87wNh4)s^B0 z2jDL}2Q}jQvPcI}tD~0!N1ZD6_f_4N?;Pq~-cIHB4gCH6&XU$0cm#i?$Ax+gT#^qT zN+;C!qtrfph;QgRdAOgq_4^OGXHU}hQ-lYbk%v7O!2B2Sq2qTD;+;0nal3xY65stj z?7#M=ztAfC@gDKK>Gzn?RZiR2oH^Ni_|Wfd*niS}bNUa(IzD|KbwV zdC~aSdeTbk%GXa(zphr-xw`1Q#d`Me?y`LmZd*k7kd5>Ts4ett>0gEA5;5#q8K;(znA4x{ePdM1;0ZN zPJa^T(E488N6(#)Mm?QnJ^miPU)KHWJkC;&Y!&Mhg||KjhfL9@{Wg9h`S79ilCFoJ z(TBZiol5j0RnJVr*IzC6u~2>H{r2D1yZ=?};oF@00Y*bLeVnb?ddo$3H;dyjrx*e07Tc-fMN`=NraJp}!V?@Am{|Socx# z+Bkkh`Ocwz^~q13V?eLG#QR_7tEQ+gxA}e7q50OOcsEc7_mXcfq1!#fXM_9oFnD}r z-Uae^zPxOoTKDOg!>h>4%r7^YUt5W|VqREyIbXf(IVtz|HBb4x^8QI@^aXYF5Ioy9 z`S<`nxcT}iiu3NLOh$aSPk?x*7X9?%dXxUE`O$pmP`)Q2C^;-B5n}0V> z(!LJccLUwj-OBjV`=900dQkZG;yEGV!~>x_Q9t%s;0*g?nODuS9)Az}r3fc3fiISb z=L^(fJKVQJ%*{_ZAMkU&zJqxizgq^+8@NJsS1bAu=nt*$V7>Y9yx*_%JP~yj>-xCp z*SqeAv7bY{o3K#CQ{{_?>QC0{%<+aFiv6)Xrzl?cT4fy-yf?aleCx8#&VIxAvwb5O#I*8iTdJ^-AR56?@F)`;(EzEkIJ zlKxlMyc@iz^6GKFU~9e zHJJNak9QAMZ>}<*{fc!Rw$35?Nm=Kl9PjAsu}|-Zuk<$$?pDtBsW@Rh8@TW!zFGO| zWyR@s=%SRj&Jq`A;(c?~F>B_fm_yv8pOTNa5#DR^J#RWcNB>+rN5}Dm_a5#S<1HnE ze0W~`X4iO@e!_L*cKkTHp;xiaLp;F^;!M8%Qk~KXTnlb5_D%Wz-UEDnKTG*bkAXK9 zi+P_c@!j7;eY+i>L(kRleW!S5uVWk)`aR)}qs(*Xi}UgcvG4Fc_|Z+DL+a#)@d*0S zHGcm{5ucS0&#Rx}dA&2?8v>pOS9GFYw%$R!+y;HWd_0@-R`_2R_alY>&IFGwz0e#! zShtJ%eJk*y`SO>2iHY0j@7Ar`EAq_>y_@xxF~6cZC*Qi_M;2k zhJ8~N@AJiZ`y~0D7oR`rUZ;!WyyE9V#6|OQewW(uHOr^)p?S|sF+Z=ka2!6ZRf$h~ z1|M+Gb2)*tvaHA7!}E65{j07V3w}`dt`YC7dd^axAHg%`yI=Oxv)`QKht@Ypo!qew zhk9iYox@ddN51^!c?ae}>3=;6zE}No>x^zO4>pGm_H2=FUA}cm-`n;a1NEZc3pl`> zO|7mtu~>K~jCCy+*JnQ0Sw2<&yUriHN&)Z`}&rtN2m(!)J-(O?cnM3O$&0m|jmk%lY&7(EH|J zps!gd@(XxE+_)ce*P7pW&+9_RHUs~buTBx3ZGhK@V;?-fXJ0wTefT)fJJ6TT#{-D3 zb>1Dczg)~gsb1;VXXZ6@--hsk%*SIZPIRmT;GEro51p;-&*3;ITrv`WPJP;ZdK=|q z&wtG`m+~_B3*p39)L|b}_ivals5F0`56?@#)@6Rt{bWU_r!bjTZ7eJkvZ@=_C+rc-% zAG%Jdgy*ASI1Da|d=KNv-ZarTtXQcL=ZKTbFf{K6mtgqVKQ1d5iP65ObyOCrRg= zuTGJ!q!II?_D@yaf2x?*(LKD1&SnujGQ&Hn`Qp6&V8wfm7xQf92mQ_uaS}X#3;pW^ z`8!`c^gG`0GBMw)KIU}zY)ap7i@I`z&tuik_8J%Y&Y|*8t5RHCb=-)3v2MlrjJX`w zU#u%%+>k!c@7nHnyeQT&d{2)6Y0#IQG`|C$pTTE3UtMYc4E+C`x0ru=$-GUi?)en+ zqpo8#XOxeRQoXwqb+G$is!#T@PUHPG?u>K5Z7XQL8EMwybA^2Z(p}c;BX0 zSDbuSsK2Z`Dt>P}&x-U-?APUZ=r{;p%{p7IqlxR&^#7i6AM@3f=5wv9)cW>&KPttI z4tmQ6tYf0k-wggE+kROuY8{*E%ww*Xi~J3{e(%P*FL?aN@bmfVFV9IC@9F1FbgqY( zS8nk09s1{=qO%+->WAgS8}eE2Mt{ifnF;Sc3O-l$`V}D3o zIDv0St#026M9PJ zsgp&1J&%gtZ?I3L<0buCc&U8rQeJJ-U$|`j6LD@Dys_i>4iCB~_+axs+2XwUdDqL< zso{4UbMnq_&I79d*W*2bZ0oYWjqp(kA7#}irHChJnCB)xuex7Zvho|!0ey#e*`9+l z&RNe(AJct_ew-9l(8Z&t{= zmkRT-;EGz^IN19nUCtzZhkQ87aoqL2=WEErtg}|vJ$z!{1M1C5@>o7R?{^-B+h4?- zkZ{vBJhpvRC-HGyjd>Q$bLZpP?B^=J>1oWH%THv3`na#FG~edAfsv0^zB2apd^~{r z2I9-Co46e`wrmxrNG76@|W&^%-uvEO#8AJ{GjmFA^6^LaPO5$yiGnl zul|3n4!;z0Yv`DZ^&Rvni}^S6iJGs^7Y~is&7+BjI*1pa>u@+=#j65->v#X9r(}9;{LL5#m6<*S;c&neIB%L z3+Siz^Oz zZ!=#M7caU0#oS)LxFLM>op_(u{Dpm9ln46n1}aXzU-?}N#lw6!O8VA@^%|AVGJU<4mZ_+2a7J1omMDtVm?$_6P|5%T0Kjo{Q zcV$k#U0J{9^@v}5%0A}fZ9K0B4@!LB0tXHj@up4t;NqXM<@xiNQ_Pl^{SKY>ZO!*8 zbW*O*grDrszDWH)UhGerZC%!5%O}9^Uro5r1)l0gyi}Z@Lx(}g^D^Q; zw}Yn^zh+(7aOpiW_={Wo{(I!{d_1=1$2;al$-9T(4QHbdq`t`=`sMpVS8CoT+c~t4 z8TZHWocZh<Q>l?B{SyqpMs-)!p=ztnI&32u4`|9PD`jUDQhxk~Zz6aPMVD&PGQ zkI|r>e^Jou#XVA8IZzj$aW-(Fc{cTN^YI(@6;Zu7VqI705Vfv$;3n}j=5uN_oy&aq z(E54L`GRYUb%ySjXb$rpJivkA=fBY_H1pxZFZYZKy7n86PwQOmJK}ze`BCai-iOS0 zzqGD_x_FJ5N_~jW1NW*P`vN`ALHe>Q*6A@Hn{U6QFKkn9?5FQAPM={IokJVEzZ^Ou z>Fk#xUS^B)>fbdg)nOAw{L~O};^tdE(F>t#qi&|o&WATNf87nd?sslf2TxVTBlMeD z?=1D@Ip*H-txLFd0DsXEILdX4^eS!g@PptFbsx{uPsw+`w60F*xm0(268cH$NSpMF z=HLrHjeI75oP2p%{ktad?l^VggP13GezQ*7?>?AE^gBP<;)eQM9oDr`Joljdyb3>W zeq)mU~#7DP1M;|&-_6t3j{T$BIKRE@j^D+4>U;a}4)yGlji2_H7 z*KV?JPthe_L=RA?SMtR}y$jRi{5{3jW|{Y|p2DZK?aKjQSey@-o61*Lim&P5uQVQU z&p1iAFnoQ?12_+$E6Rrt)t~Hy&V>10?$IE8R5yI2#fNN%KE=FhmiX@Pq5kkdU2$S4 z`pm+;x5!_w*w3r(|HZn!pZ@P<`BeYd@63$`p0K`B{I-1$hFqTz2S@O8&!^L~4~2NM zYw^C9-wpJ9GIqM<*Q{CBSj&hYcbK3e9;%L77r9N^4 zAB}DMQQ`;id8Pc;3cVD4b$q~{SK>wU;SJ9jdhSa)l!f4ERb$`!&Bk^zf?w8-A*Seln_zs6I**bJ|v9XUn zx9aQV!yB$sbe}e2e%bS1=I41QFno)Yx0Wi;oo2aL{vPU6c3q!@{?)!b_G@t6qPRIs zy*$f3%Xhz|?`@jD=I?(ibiDFK@O$)!i}rOUaF*u^{P&dQ(|AJn>M`}=LUBJue0&Rh zIO})ldH-OxXdU_XOZ}7v`S+G}Hssw?=waLDKZ^U5!jJbu@0Mks{5|Z~rSo;r{2_Jm z(cnE@mk|ex>YEcq{%!fl zpV@Vz{H11n&)%9l%C|1hY4EOXrFrHRd>me&mvaB>n&Sn1ud|i%c0T=v?te4-B;1>* z`|UgDc}wFc>!TD$@~z8p+`a~iAN!cwn}w%oSqBmQO6D!W!TI8b`FY{_3HbQ&h!0*j z`i|H~^Hu7X<=ZdEJL6s9q@(D&j-qq5&cW}39Wj4Oe4IsxBD*QHt3Pz(E`{>gs&TLHQ)Qnr zoxk^r^d9~9va-ITDty~Qr@QLDxb>5)qs99NGt337!0&uSU6~K}N+0F-4wr+Ea$GP! zD4zFO%wx;zP)Mtn2pMP9br{s%=p7T}RvF!J5i*c~_r2`(^L!O)VpQE16cMio{bcusU zA|Go#2P*Sy;_*Kxt}nTN&KyfVJ;2}W6^7;q2LtbdtMF0kQn$<)=W`AVcs^hLa-6r{ z)i-(paD{yQsEE(5)m0ZyFi&cKfWy24nQvX%mkx1lgZ+Bydlc{Vz?0g?yz={<;_347 zqQZH#y6(?P`JLv?-~aTdm)NgcyazT=*S=2p{R;EKp6jS*iSPa%s!O^te`j9CdQip5 zru8V5c|^|>sm{q)e@SoS_cr#Tr<^5D9LG1L8SlB_7i&MJB7QMnoL3*n@$Mk{!cp?? zNqDG+@i6t-NaQn}+kE)Yx-aWD{0<`hN#3Vu*dKs8=DOnteXx9Z!@4iw+{f@RcU?cC zQ)ya{LYy3NeBqtMe0f=Tq7icdn&Y@>eMLB#(R ze}hiU^Qm5&H@`ui&bMD)m-;H7SZ7WhvVzZHGkiIyH~n5hIsYrWN{D><%lZuSpw`i# z?_$4Rk*_aU&ldiUI>-6)mvB>?djEj+PeuKry$i^Ey{;zsEtn z|F~$qe*dsl*%d427Ldey7|Z~pZ_2IW8%ii@M)Ld=CkO}s-AgK$tNpcURFP) zYh6R=zQDhMC-C|5{Fm`Dd`Q0hWt{_f&v+MHG0R+AleyQ$3LYSI0rD5f7dLc|THLEy z_m7Kp55ghN1GDH;#zHrmwV&errTDhbc-{Sl=%+{*Fhbs%;=PI~_>X*eL-XeSzJv9X z@QKXPH+jcrs8SzgFM0bQ?`7xXZQgvJXg=mgU014K)O1}5E}nz`7^NSWFCN;Tm${m# zue@Kvj{|kZ&F9fyHlHs&Y`*={_wTq5>A50wg^S=Rzn4ABd7J?E4&sZDZ@=tU={XzT zc>rh36#LVQ{~a_R7xNDC1Ivf!g=+^qH(ET0$ebm9dM$K%A4DFoFRc1!`OcyG!#noF zj(Dd!@(AbC`i?Q{eu?ud_(?#6T62_J##t5f5zI;D%gf>ot&f`Eoy|MwzRu9!>ayOm#EmI_e~A6dhkJz& z!v}`^binf{Mg7cf)LrO%(R)3nZp*h{sslTf@I^sazRb_}(XW~BosM}o^(B^LKeN=6 z{vN`619ioE z{vP&#w=SmjfzdvA?&cu%<2X2JKf087*FD}1G_F^i&zG0gCvOElVn68b-A3oyj(DiP z(yaSQY4N<~IJ&Msz#IFSkD3VoYpwT``(M;w$J6Rk6bHi3%lRw#HoxznbuF0pfL|+| z*L?h;d|bJb=4C4bdz|oz#Ol2m#gTC zKD3_&aU|cmtiO`#XSM!X@x$xC3{Q}c2XGz2+(yI=&s(~WgugUA z_IT7Y>i^}7hnfd!q3gQGJ{>9CpCTTvR;Pag4xT^u*h62h#ra&Y-(C@q<^FoUb$O0n@nDSg z3{qDv73J+(-EmTXZom7>)SdbIDfZ2m-syT_&Vjo5D146J>o{yZGx_-jd{sU^O7XkK z`$N~jzb7N!OCJzCfa8kwzsynPt5fVBir(w3e*XdbV9n@f%I9F7_%XvgY(CybK7dX5 zjE{-$lkCd`aiSHxz5ANtd#BL(<~xU)%kD&<^&6UN0}t+@-`WDV-(bFUF7BDsVxS*%}{{wQDE@LZ_#JfCbmq5VSChn=CGY?xn! z7rhyAME`!i`s*iqgl{W+0Tu6G65pS}gEsx1H2l*#eacVa5%cjj&dZ7$dx_(V#vAA! z8qtTK|K<1S;S=-KmF5rq{uh4c@G_&!|9`1&-vi@A>CVUDgY%t3?Nck_h4k_-;pGeY zcPHkfG~cjoy&k%NeDg%QUmf}i!{)E)Q%**l*M2=^{qy!!37<~Kp)8*Y2m{PX_3^s- zcl)@TPjp^W9=lSMr}NAJe{uAKB7lZE$vE6rE>UBUC@ZR-SvsN3?z4fRnP=4;5u zANgnc<1Ol}an9?3;<{S=cfL4p|0m(yr}*P7GIz2GUhjs!LptAS@Z$!&Z@zU2SG24H zu#ViiYkZ`4@U47G|L6(lb1m&$w(e7#{CbPIr%&9kWKN)IofrP}pTYxgF~6Meeksq| z@AZA~(XHs8OW(U=Ua^?Z7B9RB&yz16Di7O-{VG0Qk9kMhad9W+=9woAzD@5V&G0$(n2OLH>+QkpyM(A?v%c=Q@d~sfJ!1wDk_2QcQCDfCBpEKRZ&G2)VZaZH* z6pr$}UocJ7W=pAdXoOo0{v(I zz4+F?`%m$iPaN_w1p=g@o-bJy?{gVA4+&Y>CaAu1oZ z|GJF+FkfC)|Da16Xl z(_>zH8lEcOIn+A_)@O_r@oZP!AE!^$Mwk84b41*;wY2?Z`|2x?&70??udo3x+H(Jd zb9kXRkBJ}1hc}#;)n|SdJhke_HT){V7YqH`Nce(_|H#+ZQ=QWHozq;$GwR2C;415} zmx+_}{Qe8@NWOIm=QZFhri%EcJM7ySa~rj~`m@LR^+DHZ@TU3Vyz=f2^~dSJ6X05K z{445~^F_M84f^IcOICjX&-rQ|ev$V@mzc|&;T`%K?`cnP-&T$L;SKV|4f|hP_vQC| z%)i?YjCTXA17IF|ow_q$fB9E?1Y#H7=2695B|6Y?;(nL-K8wH7Ow22a|Hy|oG)LdG zkDhfL^ds-1t2f^}gKy78e2CV0Upn78)O%&t+t|1F5c<$Dc-@BQsL0p*TsQN6QNDAi zIjLswH1>V9zk_{?RnM%rU(Wg5f=A3(S8A?$pze8p^~3f@e?flA9ef>@LtpN9-GnFe zokPVv`=N{*w{zY`EBonrUQg>7XRh*g<^J@$uaMvT2I@cd-^12^9mWr9ig@mKe&)gZ zr%Y>zQ>=b?t!~}C_l-Fm{G&YowI|N0 z=REY856|n~bfaEWJ$9eEYJ|GjzWbwrORP5%kDYH_@~P-}ZnD7J*sqs9Si`sqzTWQ^ zO%?gxYDd`67OwlzGEF8pb`84 zdLn!s*6}0FmzOnn^UD25^bC)R;p8ZlXQX4fMSYpC{?a{ZM}HFjhV>n#4^gYj-)Ecpr4-&vpkJ1+PVpRu=Qq&JTc;Ab zF5xZfHO@J{6y?8s@z6RV>DJabZ!4jbGM-ocvRyQPw9NeP7Jb=#>ymDvSt(wxf;(OY zUQ)Hz0@t5`=U4-GO~WVV;{nvy+aVqt3_gl_i}+z)^ay>T+vMky=zH_6OFBaP8r%bK z945{mgNJvY=qmH(L#|Ws+slU!9p~TNp9`@r-Mfb4NbDEywior6^5tdM%kGDRYdM$u z=yP?fp9dF?yAKO5oG;Exzu~#-1?MMtjU{xGUw{)wLa(BI9!bm3+h5f6vHL@w|1w{t zxVS*veBV00wC{{)JuUh|qwq|J!R>de&gTbTLtVc^cZBnKoc;TN{#U;HrSs%|3>jd-6( z@o+Te0;B`mv0gjyzI9a6A?3@xbgr_VR6PEo=K-R>=ll64JO1u}k`O57 zZi2VaJ(|Sl(C;A37WbiDp9$yZ;{kM^!hZvtF=E^sK1-ZW;{0{+?)%KW<-_xSzY;yA zeWc81#2l1(w~f%h3RkYsx6HRL@z?F>v!Y9J-iv;IYO7C@+{GD%I(&=^ZGq@LcS8?MS^EWXkg?{f0=kFwWEnl6YeA@&+ z*)Qq|da0x6pK5iVPwne|*1HgS``t(OH@|`SGM}?;bWgL){eM!dyTq@D{TgDw=D>&f z;=JyU{mKU;9%x@qF+bYoo%2o4d4PM@sGsx2L-`K3L$~br%g{^l-kg1I7vO!ji}3Pv zB_1)~etEtG{}Xic_BYrceksOT?4#pl?;5;~ z`bp2wL*0sb=0DUEDCRqddjGZKzB2RjA5%|m(${O^-?K&C`9Z`F=@;{zL&pujhob#D zkMI0Cc(HBWGV>}^!TTzo=ZlAW$HDn)p1Jrnd>kHCde^7nI@A4gc*JYuv3!0h(wVpE z(~bBO-^#O<_tSHV+MnTKUO+x|`RXslgW$1Q&vN*2YR;opm+#)E^u6x$e##d7O}=%R z|C9gRmhX?>jYdD&<~;WGHsX60!H4B@7n z_SIKEFkjpdx^JOBTnS!Ex~Qc}`K?wrzQE56U!q58-|_MLMD}+P?mY;OScP}$>qX&v z%_FAO+i1Qr=IEKv8>0TY!~A^P{C%8Ht^YXue7^ci_3i+^`je5jtiO@YxBo7ae*K~I z9QvYsc*8!o_Hhu;`=E%=T`Q_PYjxqR&8VY=gP-C{nlFE8&hQI(v>WgMqws4>_%(E* z{t|C>nK}A<)L;4bOY>hH&pFUHKkaqmFW+WvZ>$1OxsD-!=gVK3Q}f*DdFD2rfPb$O z|6c{Zu#dF#MH}=}@|{EBdB5*aJU?$gB6yp&@nf;ix!?0u-IlMe6b~44HN=Ufz#VVC zLpO$QZ!!3J@rn7)q4c2r{N;IH&pQZLd2aM>_-}ZwO*}!q{SsdH_gdh+l+~Db&^&q* zd^kd!e^!*oKCJ9#^d{T-?tij>l;<*-w~79O@>ByHxDoS}(%o;v-{*@PT9H&{fl`9`mnXS=iL0xp7?`V^0(v8yN?(C2C7pA z>cYG4hyIKH1M_%&zjOPzgX^Q;;{RTjPw{^(c*2i@->|Mh^>N$3PyE|QopPWkF6Qey zsQ&8XTEBM#j{~n;t6K-=U)O$)GIx{j9LgWX{KZVA{^KX`PKV&%oA#qekGRdg4i)<~ zXE}fV9{OI*=qu>m$F+#-;xl3%ow@T8{RziQ|M#+des2%Zzp{^<_bKj~>XHt;)C76p zMDUBw>si+0@1b@19q?iB{Dk=n`pkXbIq_~o=+K|V{H6bUSw8))5^CQTd!Ptk4b;uYGv{DlfO+$Lyr|Z*gZ^-ie6(!6 z2J^`S_AL(GhWzGMnP<+YOVPYZle%%6I%S-B%B@Q0v{v^VpW}z|hj{&b_e<}jbUhbp zy{PrD%%3|CSRXscJzGPMldlh}`}GPv=sbCACGwc?ul4l{QK$W-o)BlD%BmysoQBVt zcgdNscrxF9d9F=!*$?S^U8hc-=bY8*o@*11pJ5(g13gi` zyewT;hx%|9ym6cJbuRStitjh0|KPe)`iOiyfcUkTyJ5c`pg%dwzI1}Gr(U*S4E?@* z`{lf>x_HuY1N^r)`sRx78=mKgIwq^$Ms;PUGH)&3=5hFn$Va5#Z{htcaMUe)&ho_# z^(mwO#ri(rbA)%Ln&{3S2Hq7v^{}!pNtXJ=-$V6oJN#cgX9@oqbED?pR6mbJ9@AVw zzPKU(s1EO!Z-pPR>fq1NC%0U8MPJN%VAZAh@S$+J=M1j~pQb#0rKqposI>lP_DcyK zUv+0bJTH9VxtjUt55ea~T`4>@&K&S8f9?W(=X~{-`XMoIPW^uZox=!zN;~dHfUm~r zn_oj;nr~g2*XX+6?|op7e2Jf%#vka{?(*|6K9Jw>e5u3HJO8v_B|a6!^F;KG@QJp6 z+wr0}@x=KKJ}%#WsjjrI$Z`63+r-7isK3P9EWxWi$A9xV?+oUvD-{o4`R^a!wHco? z<{MmB4#OARao>fxfqZe^e#@GdS}f`(KeGP;^<~TR8=*VY{!PH!5Z=F>Sy z{}le$uJgead%+vs!28VChaNyWk9=`M`_KyCb@4M-122ikZsG&5Vx6M%7`_kr@|Sg6 z;$ilgAEiE-rZ3y1uQycCdj+odyD@KF+jst>^pOMjeJzl8FOiql!`A>E1oz5%fMMP% z%E#O2`*thEzX{{l!|D%>pmXhpUk`H}zK6lou>kA0)*4*E!6d%{DBZ_yI(22%9 zRJ?eOPhdVArTE@-y$HXyKX@Cz>u!Awxt%Ehr5Dvb{evHC<#=JT2 zk&cny7D5N{&KL657tsA_a!>m2JmRNEocDWE`>YcUeXq~)TmRX2{-fq2JwJKit^72F zPhhQ%zAf;T`g&vB!+iPc=XykTt!_RSKbW{r)|28>fzIeo%*dP*OmJiLQPBV5V>VEK=LhetFQ9s>!S`P)@+Vs09<7$2>IFQ%&%8w(T){v3^Qh0> z`p>@e9~J+a_DzlYOZDOkzC7*VYkaTxeehMj{3SfJ6TaU5i?$BzkMx9KZSco2_G^lI zY>>J+-?~(vxUV+?FFg!jbGGRHjZWYZ^)=5%{IJi3|30#O%AcnHex>>&^U=qX4y-|4 z_Ly_}jCyPd-9SFvt9rTNxi9;$dCrS>Ra@>;f;SdI4`HA8Ec@i|;eMt1cw6+9U!t@A zr2Jryh-=1viC23W^AB0ylluPNAY!d-&A9A3*<*uTJ^x?H_vm z6+6*?bR4&zm+DTxqdHnNA29{qI*+a@AK$C@&FwEb8oF%1UoM?d!@98WhY?<0u8gyM zuCsiq-{rh}>Fqu;bIkVP+*8cc>ztmdzT8W6`Ocwz=kz_tqOLUWCS7P39mgy@!JhCP z$r=x}u5RE7&m&5|aSj~ZjQLot_qgASEm`-=aol_-_v0}BhDGaZp#xhEyv2I~^egk7 zL-}mh>ga3SuQZ>_d-NU7-(h(9348-;hozUww=U0d80Tu=My&JYJ#70+?Khuo-5%@A zH~(cECEoWz)RVqvI*&0IfbYR#Wwb?!u76Hl{A-+2lB zd_J9?@O=0vxxS>1oU4Qf?Wgz1`Y`m&3;g+f`=vU+NxVB-DUKh+ui_qf*S_HUiI3LN zpQ3-8kGGLNvRQ$5@e}!wy73TvPSfwjqUU{&x^uZw-!EVNrS}>7?{TR9o#Pyy1jjeQ zhnI`yhaNd@q>YDvy%#XLo|oj^#^unjN!Q#UuD{Pb^F8Z4D(U?4;d%RmT956y8vCMX zey{6#+IiW0W6A25qJBsNUgiWm^&aBGIsE54zIX0t);#y2zEQq7FWeh(qjy|Gbx^FK2b}IZ{#J}IE zJQpgyZ6fwlIC%=c`h5E(9qbNu<7N>aTtN4=8SmW)2NrZG57CKERQ9>ZQvdsVNN?D3 zKBB+>!n!u<&A#u5ecQBO!{~tX^}p;N$~j~oj`&@A`YN?LJ`e8ug)gUgx_swQdJX&b z+%*2Hs56QC*8iRlyruePZ&AIL4k((q|nH9}3McH;v=rImV*zVEv-wP?k@{ zi@rab&f`M#?X>O&_1QJ>mVLh`i2M0?8~0P}Yrs8=`IJ39qP`Qj*L?}S&oooCpZV%# z&t2Q+R(NpM_Y7amg^7aXT^(!B3{vXV6dP!f){we+(W*2sQGb!uZ7@4rR$h4@(t*MH*OaD zfvNtSVgK^sL(db@PvKpxPryw_H`F(2(f^(U|LqChNpZ5x zzRU*?tbWQ;F<+eL1nQf(Un$&i8hz_S=P_``PVk_{Tg;Wh1Lw=j#qd>g#@n0n<3e{LAvR6e}%qrD5V!#*7izRB~; z^0Daaf9aQp?nb|#FMo;eY-1<88#(T5t0K`*qfNtZ2@@ zi@xCq_-fRA2Xh?x`YG<~xvwDnx5#=H(Gl&0uPSrQ)G^jO36dKVgu2Gzt!E zxjuvEeL&tGLQkKsPo(qJpuga`>yhBwbf3D`Z&*i9+&=>kkZ)aje{F#IugC05zn`K$ zbNH#aj&VJW|4lv~z1RLH-0<@FZ!5z=4cVWlka}1-e?<7&{sG`y?nM(U%W${cpNy&{D$sdzH_L#a{K9> z#=mC4-=BBQzHodm@W9S*Y5NYsv#r2U;?E}N^RLBRfcWei_8|gioWMsg-+tLANp;qu z-_?ux2CcI}-}$Qje4?&)UeAVmJ(umdN&LRJXJf3RTFIzuPJoNlZJ|0`&Km5Kbzw7HhgU)HwK40jsrwVl%=P}=Y3IBQiWH0k5 z+tIhw++H6SYkxQ0-y?tLiyPJ{@E&^+&+$>Qj$VG{O`kvOo1@RJ`IdbAhH!7K?*5N; zPv|DW8}^S{a@}RWK;~!jokQiX270Iw;=v03>^Je7_q@Yi*D;<;hZo7WU-CVu)rAjF zqHCBd=5wuUcAa9MGtWn;&ysIl*7YlYT_bPpVa{Q)SZ5?2_!#`!J;(X9?*&Nr)iM5a zUje@NFggw6;v2;Ev+fr$ZnHvR{ zcCO zqNjN`Ydd%l@dx>M8|h5i^yzM)%Uj2PeT?_!+V&$x-?8AljDJtQIIsBZ`L7vxnypxe z`eIG=*vrNx;N%VPRlakmbL4sQbS@m@X4^028W&3c z@C-cPHa}0Cx6WgYxR@_5tM2Vq3J6QV2dG|tP$^#4>iTn&!IuixABcJMZ2RT@vGk&z zTieg~|2F#GPT*bhr=H{1eapv-`dxON$K#b{-zFNem=F?+~ziv{0jC!BwH_Y?yt#R=Q^4R^D6Ogagzf#W@ z5AECIc`v^+CtSE0dH~n`ngg3*{|1>8$cH!F2h#VR<$fJ;pCSBEn17&-zJxAn9e<8| zd0F#ZUHgTGKf7^<^8oWU;Kvo#cL#lQK7PY^T|CB9_@!0+lRjk~E%MYh>wbW~{1fWy zeCtxaYEmzbnGYq7AEdu*{rtn|`+0s={6M~QC>-H_$sv657tKfETNb{L@Yd_#-N$Kh zuj>%MPwace`y0#+y`s*Vfv;MlpSetbFW)&7j_ZwMm>mML#KP zobSQ|ZpHc(|JIlz>V%#Ryh}gx1J;)h_qx9<9(xSjv0=R-`k}7tW%_8Qy*=5FB4>`(C!eCWB+_voXqp`V|GC&-t-6zAKO{PONsj`!-2bi`I7xZ(cwLDej<*T@4ZjoXvuyz zyyKA%Z>TK)!RRd7_=S`o{aK4sqx5)!T z^vBl8U$f{-^XWyUS9AV4Lfy6B@dG^E3?9nyLjHo&=ppi*L*c(R{r;z(SBp7j)t4Rb zU-UEG|59HfU!CGUk9|a}6B53^%X_IE$3ODdp3ptW_c0%ia-8;h)VJFziibz}Y`IU# zel9wXp;yVbU(%Vk$;(szbYB5}z&@ON^Sk_rSkyyF&~~69;(&xCo_(4ysLzN zo6I9_5kIav&Vz^Zt;@dndPk+-H}?CsdKaM0Jnts{Y^#1Zuvmwb4Ij$S!M^&F+?TV| zmxJ-HwBtVfhwE$leED>G@||m#mjNe@M;&54sdYq(6O;CXrN5akZYb|It+R>ygWqQG zp4zWl+_(AAZR^+b?U(ZIKwbLY--4exQ^Zq!!hBv|pCLT>KIilldgpxWdXw)RCNE7D z=JSg9`|wloJZw>YeTH|n@~zA7n=2kn!Otugc>?IG*}3_+0S>$KeZ(gG&}G^;`1QmC|K&xK|IOzii!_`FHDx{H_@~`tpq) zz|2>tXujrE_&WMtx&QyQ-aifY=Oy~Xi|FdViO*xc{c_!i&I{gfnEdrP=D(P8bKkG{ zeH_)>S?^b^Zof*!{UyHtWYKwR5KrENS6eOitq|XsFCMB7-g5pzA392ZVPB=# zxjq5U?=6a>`FK&yJG=_Llk$^wzN7FrO?bmS=%Xgc+Y|W3YukS{VF5mkU?g1R}MZdrYzSLCG`c=?ZvKhRIM{BA|mW#X?^@gvO_=T$%2SMDKv z?_Kt9FZ@~)f0$3L->c-C{TyB(E5FgTe{J~o+OJLgg>`t_kp~ptXNuyAf8W*5yZ@Bl zvq9Xr5OGfNV{g&?M$|FU2g8pjc!GDok~hDBeQxdZ1s_1Xw+}^!`}JuNj+!mn&kg3y z^Yv4N8@krh6#FgH7b5=IcjO{IxEt)_$J8nL*5&t=b>0g7gn7>8CUb_}&@;R5VjYX# zPsoQ4H4p2*Z~L=vz`M`0&L(+apZQbHqu*2UzyIbLf0sXLp0^!&+5EflpZO@y{hANZ zudjlC^Tm1VBCVfSegD{fWafps(GR2lV1MvRezN(_p}toeTsv8bum2|c*G2BtE8@x$ z{juNlyLI%B^6i)A%$-*kiuztJ!Smi1 zaYOx}_}+defPA(e-oC{>T0l4Yg!<$%JXO9pufB1!V!n$waVXvgkp9psY0qlbSuj~7FmOkF&N^#>T`i^{fL-?W-fB(iE%GWC~Po!VBzUUNx z-tPzGiyO|X`rga#m%G2me(lgtegNJr;Efq{iuu;%_m!pp--z`npFJZVxA3o+DauzX z=r=~_OXTB4bx-^*%m?AW=6QPSQq(89&OBvteGT83j|b4YT71tzbb8n9&qlwm5%se4 zLqo;7F#B9Mj%NAvJf3)_Z$=#=e(IB=eoOy79QpZ-8}}BiGv7H>-fG%cgLrT-bfL-v z)_n~cKZXugzK{9(mBthHl~euvypX5KYo3!j#XAg^d-!{nIDy(c|N>h-bVN4G&u3@Te_Q8;3xe(-1Pi5I){AcP;tTUVP7B)n727c z9Pfae&Y9OJ=v44k&9^S;0PLIp(0mj)@HYID-$@vIO9ybD_s{d4L-mI|7y5*FH&?`0 zk5xLSP5e{0;Heha*QKI9S-$?VbX$(&dn)}rXS`lKALjRaq^DYQe-C~*A0Op7@BY8^ z982)?x4?g`z>U1CUHs0EeIc{p41W*B!6yC;5AZu!he+e#L4&1Liov;4V zy297dd>VYzIC;7qKf8{$?lLQH^Xg}I6>PP-easZM2MhHV@v%RzCw<~Qe*X~sZ@&Ab z`Q)9*PsT~oQ?7B(I?QXlATK|}zw#wKK)$?ee%^d9eg5F36fbLa<}T^0*r$B~JdzJb zssG({ezGrRpx)d`38R+nyVg?IIa zqVK1<(tL44c(4&V2i}PVcP!F}4Zm3G%}4M%r-_UC)+Lm&) zW*J@ki$DGwe|DjW2hYdPEAG{JH}D#K)OmDjlM%-`pWxs7*6X2{&ATqoQyL$EgW+r1 z+#_8sxL&hlx0Q1$m-!s8x+?|jbOVTZoMNX(f#k1O8i z<9mhk`o1FS%MP1AMBirJ*F)BM3trxpV=gZ88 zjnHS#w=U}e(8Y&egLtPa@t%}$@4WGqbpgcte0W2CQ62oRrb8Dm{%wJ}rS(=nVv2J* z!M)43F8O`g2k>gd2k`^r=%3uL9D%>S9{Aom+AML<-$QuFx|Cz^7emB{i_y0;&N5G+ zpZ8Vr--!I?|Li;e6u-X1K21|sj-t<=3jZkar!TGB;C_8l?8o=c7xLB@5WZ+~U(Q6| z*!s2C#s2Fp?eP0Y;CIiupIJOl`__NJJHT12QgQs>K39ksBGXdY0uTHVv z#&a~{NmtO1ucObl9{YT}C#8M8$h=}ce#87AeJ$fC=cgk6qX8a#Li|{wAAOzoNb{XT z#ltVv{XVRGM!rQovZuQD=XWP$+tF8-&i-5UnV&H~l@D)-m$6RhDEl*Hojm=1>tIKt zPvpL(c<+4uW%cu0^tY^^Ipy;R&*r*vnD}@w^w{#d%!lXw?k#iJ_=v3V&d3xxdiz-H zCH^f&|3&#P-?|j%y&va_iJXQFNF2@S%NE6%URT;s^QnAm=c2De?XR zxTNU)m3;Th`XzQu1HgLnR;_!;+EpAa|3iG!=*-%DJlj+`Mc`+f6#`!#o9 zx1-X6XE(q{=hg!?Lvr|*1&-ueRaZwNlOVxEEi&kB9n4)KHf$=bSLy&;&nISu^*wIY@$Q_kY9fqJiT~>d^(3WvKXc@Xi6@TVFu^pHAQ=&5^z= z_SaM0;&&~M;otUc_VYNnKi__--tR=5(|uVhj(^7c;^Us-8@ODAw|q|X;a=gxSNQKN zM|>AgwM9N_S>F--wQ;}vvGc7<-@k3&sJH5{N#2F2)s?qTv5y~6*Bqm+%*TuB+{JsM z=vqIbF8+}IUd!)ibFU7%|5bD!^YLtN>Kul=51hBT-~p&7(I+0DuX39HSibz_xodFk zTXpfCBA?D$U47^aMf!oc$jjy*-wLzu{73tWXwGB;{md-7h6kZPWL`9Q4e2qTpx4W{ zU-ri~pTTV|T&_L44z)=l3eb(|r9e&FOaGub&ss!3sBS7w0z}tOf4h2)f?Y zw0J{!wnd%1&O0xQ#eP!G1HvD7nVWs=^>YsM?U!`0EpXv&=D_Y#H=g31&Vjo19oCmy zx0f}}OF!QYyw1Fue-1paI6uaE_lF)pJYBxHp?a}xy&>Owfp@;9%P;nbjt2b966etU zq`S;d<-<{mgSEQi+-c&t{duSGk!nU><2|#`uXqmRohPifzJPsR#6Q`uXNovKN)`W+tgYC4zeZ^e%ezItPxH*#Hro<1Kw6klQA{Nv1_9EiDC z@wv6S@rKra1pkBeB3)R%`(?br{A9d81W!}sKiaf@KKKpQUz_Y-zICauw-Y>o{gv{q$JLF{MTFYWUp{7pW*p}Ctz zvBV<s)>6`A~El>&y$Zsbl8DugLF?Do^J- zhj)HpHxtJ@SzOUt>Rq&|&6_htiw8^4vz~=GE_eT%>1fqwjhUJRI*B z(RazWU-H$6z5;Q63LN-^yw*myH*Vg(z~jWb)9)M*zV!t>-)~=0`-iCB+C#l!Kg*lc zF?(3|y&^xpeEVfzVQ`=OB=CddF&AnbrFDm{Ys9nVi-+PZ+QfOkCv_Y?eyx&!cE|Iu zmFs5aaQOG~%`dCJU#pY1z#IFh|2K=)RjVtGY@y4!YJM^Dz+1=ro&Tu$|Bm}4;a>yZ z053JDGxx#QUoq}SFPiThN^et(_c)xN-A~rMb|3Hi_th^yOI*pF}ql)j$LKGOR4w&=s=!yDFpsqeZ5Kl3u) z*He6d6?5~vyF`6CL>|kxF5MHqgLRyKO2;!@gGLr=6d* zFSGO|TQOHD{C*|s8uO~}Jg~p@1?-n1|Di?mgzxvN6ff8S#}D)U$HRxieOUUh%*_|(Z3_H-zPRzFUg6uU z?6-l=CUl0*1FFN8!#7I(_CJY!zz6D=xed2KRRTKvwGT)qUzZ4!F7y9wgbM;!Ubq<^6MLlO(F(3jLznCr!TFU89PY2Tw4PuK~ar27r{nTP&Kee-Sl zl^5YZw$Y{J+b`<@bPgXlj}_Iy)5P^!UHEs>IFZkNmF6q+>|n>odNeFx!mxdhD+2635HX*Q?IX7w5&>Sl@e>KG6Q~ zkFxIw@5F$MPx3C<6m?3zdRcRy?)P6N9-Lx*d(fG;xJRE5H%>6;yIM3Cob`TrKKVv< z>-RtX>0kKX8RMwv!wTmw1+R#H1)XWWysUYrM(AKQZ}}O##zXF#`(MY=S@wOI&8xme zx4!cqrDJ_X{=JN@VUl{}5Or8P>PYl%Q7?;^%ZKO9A9|h({et;J*4s9JT{OpH9mG9w zalZTIIYj9~?{WUtT`!|!aR2KTx{cY$XX^Xqi}RivrJrX!>-@}qG%NhG!H3tVTb|HI z$`|Lw6Z)OGm*%CoXP@Cy=DE=k`YktE&t3W~`PQZRnihKNg_x6)e(x!`qE_cU&Zz&< zJ<#9Fw=Uf)&-q@D`bu@kIdmFrbT}h{i>?1voX>|3?LQBH7V%sCq4nqk*;h$;@&Uo z@-zDDAAo=J#d+!Htrxw4-tc?$zz*o^&7fZ)_q;4A9*$SPsNp% z>rT$)5V(I&C0~ntal`NUh}T#s>XUDTjzNBTE$Wt2-cNoWgx|<_zog^xT)?5=)8OZe z>Pf#t(x1n%zm)3beETIF<@clxf+uEq?|Y2<^9udm$K>gUaSr_s&^s?o-ueR4_15b4 zY2@8D?_12fs_%TzxG?wt%_HTDhxVD!x(fSsA#}0kO{KqFBOV^)y^1B~r1I4%!VxX& zcdh3Ne5-vjfB%4ex)gC!`7B@mOZ8;m@5{cu);(}+>stRw^Kg}QVd?|s!%z4@7iHr?-@MQ`_VUY{(I}%zVjc=i>m(LEUG_#t0?|IMJHt+!E5*f ze2aBoV&9Cz-uZIk3#iVh)%~8d;=_pfdDho}Z(9qzAw9|}@jl-_AC5T=^Vrh=en8w91uq_@PnPc-ez|wS>;w2g z5zc+$yi1?Hp~IIoQl=dEJ@E7#A`<8+LJ=})c`KaSz=m@hBu z{jXY`b5>~{?^~S1)8MFqI)17}{LQzRj-%n}dQR9Ir++etLyKO^HOb>h1e1E3s^1v7i4|-Gs z4GJR+HtI&$qLI1LLda?%gkGssN=hm83WeUGct@v-Z#9*r;F;aZ+Xx1?U(VPbd?jvQRG|JUALnirga~$lwS{0*UZO@3g@NJ@tlzMYV3P12pCO$|H|mi18-$+2eqh?4WAKVg^zFT&E|#xe z>3(tY%BUwuH?>tfXVvu-^BTrc?!%A2s3iX&zPjtaHQ%e~qjVpK`!-mw_4N1_=fhFz z~XQ89l>>+VJTv+t<>cen7d$cLkJ ze>&0ED_!gT3Vsy*ZuFHh$6`A0WLDpA#q~p-5cQI%GrN9_&WL?GR$1?+bC@q4>K*Ek zcRVS=A3s^=%YJ!I!eY@m-HrTGI5;1F=sGO%+qf{!ndY$>?+<~CZ-jnIb+CNvk{+W& zz3vQkg$erBUgIy{p+5d2`*pck|K_?xl4mPEL;e7LbVMu0we}CeKdKq}a`7P(=pil< zH}m0M>A8A*_gL__itpp-uY1(f=Ry}IeRg4g^W~M|Cl2))itoGO`|9~%iWish6J52A z3Y?NJ9$E)0-;`-i^UmK{KgvGl+b^B7&|8OY*Z#}mGddOX+RbNs zep}kPx8gPIdoUgIQ#_wSxV=`Ff8#p5{aSII!v2Zza3uK$=}?;Cv#Ge(upWoAr5E$Tb^q?z@1{~+Fkd_rj_7(W z7P|OnMSZS2^qV`6S&nYg*FIItOYSNB7BUWfYn9>WJXuH?%r zRafY9uc8hFFE9!()eb&bcw_`V_zHdY`Ocy8`hM^l_PJ3#;vsX}+p%u?Sga?CJSNHU zi_ehXMC4!SZEl079x$K2N1ioiey3=Co8&F|;=JOU=Th7aKV{+g*ZAjoUg8LNW669R zzK{9vhTd26qTkskCG-H&wYgvRIyiE;2={&mp328>Xikdx_lNktEJxi{@vm<`J^M@% z4=1S?<%=7}8~BO9#|&4}e@)^K)wMnpUBjbzm#sIA3!CI0r04Ckjv?;XB6-M3&SfWj z+3Z^+-^3;K+WGF6b>^y@Y)~&50pISBC!3$Y!MvgddCddz=6vVSI0)PaZ}ENb^E!_$ z^HIdZ2d;0@?~yOBRDY%Q^Ec!E!0)1uvLD!e>zmovW7Xd}09yHQlz8h}-F0!*!yZK* z=6qSa?(^Vz@muD8<;zp7(*y6~e==rV%lqs3DdX;w1$TW5U!MU#pwP^?CID zk*>TG{D5_c=oHCQ^1aLAr)zcmHOxbWZc;o{i+b}8_iBy&WxaTgLK0XQpP~9=n>yv) z(0%zmb6jEm13dQxf9^GYDf#wG`imBIym{WO8{m!=`hM*@cZ_{}&iy+99?lmx6c760 zGlx%h1Wuo_BwVc^c^`x8a?Z z!cSRx*@c4s6`#Gu@KcGus3iZOc;`9a1K_^~dDu+o*`y1&5d9A37hM-jvYhw~^`o?S zcW*@=qIY+g{(#^&`0wYL|Gmh0%y$lj$7^-w61h&qIebt_kJxeDi#&6!@?VVgzy3YT zPw5o)-4_u1C0x~rI+5`FUJ+k#nmF=v5l+mPSBl^0R62Ll^nvaLU#LF&9(ZH{-|Suc zgOOL}!yDEe>;6B*=iB;|Npw)wyG`QXuQx+Ryk+krx$+H##Pem^oWY+PVSV{{QSqDhhZ-P$--vv~KDfex1HpIGPsO{PFCN+-8{RW`QRU6g!G%5i zzuv~Y8|y2DH}df|%0nC%P7@zaa}F=?vseAk2ZFv8aMZ)_1v3w4T$bgh&RLWF@cz8# zJPxrxoxqQJ*LLyEUdPuW-}`0X1NGmH5-0BXJQn5Cu2;SZJ&yN9=P}7M(uMW z9&rgCH1t!UON8G5|K{5--LG2R@lAN|XX>yo;otXpzlNwMzYiZ6`$2gBviuaD@3_7S zA2k^Lu=X)id{_tnUUmO0d|SSJS-hd~UBkL~>nh`%k)K%y0l)EW%oE76PyPDGu~s)U-T2Ho>{A_k7cpohXrpsgN{DmIaC}toJ(PStNCB8rw_k2z?|E6MRDaM z=QJPg{kNa&MgOIBDWmXf6QR2{Ppxxz*Ssly0WV5ceqMUKTHU@^!d0{A%-5M$VZZY! z@@LQgGH;r1ztku6r^q`L7q*LdkNfm-xL$dOb#3upPZiHa`}z^^^DmH2v{rXsY5XL- zcoRLM>t8p}S$^ccjZkOKm#4Tt(sRnJx50mdeYC&L74wJGABK3(^6>!T*Zao1p(9tE zoB(fh=u6opUwKgUzuT)6ck=C*&R^71!1c?Obx_u0J8uy`+JKLH0p8CSH|&4-r7yPf z|84qGTHyP;<`K~ejL^rIkM9+}==vFJ}B+6-tu+a`R#Q}VHVe&)(gI>vwY z3n34CM*X~F9VK{v7oX*YN;-&d>Yqp1um79sL2dZn+w7C)MZKcF)iZu{zZ>!1eWm$y z%j%==#dm_&qu#H-`NO^dzX!sPFVG|A%TwfwRjYeFx^FMxZAO`6>A2AFI{?4E7WvuN zKY5>jf$J&CFBfB<(6dno>sjAWp$`ikmFN9_{{Qvs|Mq2GY2MvF^m+%H?uYO`!Xqvg zy$|{FO7qw{htI?3OZnAxbVbHdZ@^DCz`wJ^!+bc(I#}z3bpB3R&lb8Ay$4(5lV^+l zdt9ge`X}%6FK`_doD?_;pLTe~j&%3lg_p$riz`fQV-sSu~ zW1ht*xHn&%m(HQ>{*CBMVICX&R6G2o(4QCg-^foTAD&lT&baoy<9N(X^_)8KySvn# zC%Jzg0%v`Gy1)K!iqqZTuZ`=`Yp{Q9{5I~}hdywZ=ELUOFX{Zl=f!%_@VgQ3=D8J% z?AH*y)Wag(bH4n`{VDRLEbPl7`N>ixUvJMzIO(~Z=B0SA^TiFVs~hJEUbmvYf=;7N zKmCaN*<8Ow7m-gNrFd}2UkE2&DdG`7!e@8w=g2v{;dzOiN8{qJA20j`;xC%k?^+Ll zj^-70*NF4*rBl3LOZW!l%a?@@Ti}Nw`{=SG=5Mo>9JaDerHiyLSIZZ2*6H`Vnh&=OyMxH7|EFZGVdJpXXsu zh98gn!R*7Xet~P$PnOVkye3b{mshIJYQL|IN_C$l=PBr*`n=P#@otHiUt}(6zIEvw zx?gCd@V$=r^pi)t8xYQQ|Mofj+w#SE;dkr09)L4ui};SW_>DH5-U$7Q^x661hUYJ>0p`akfUQ=v;%9lb~0Z`HV$KE2iO zEq30M<)?A2?#ZC@u=sxIY+CHwSn%BP<5(}QcV=0Szr%V_>i@AG`%25-rbj>2Ao=D( z#1Y}Fe7IM;au;2I`=}b>7bBmMS>Ee>>+(6I-y!r3!tH0k>;2GSgCD7f zt-(v>D?3`Q6|NHNU(c`Izdj7s<=sQU}Y&vnda5RnDvY({qS4-?zv4+yUP| z28S%;W0CJ1ijT2>)GP4Ob`h?i(GRUqPi}MXMya!m66bHT?tFRWUmg^=+OZzxlaIhnBk1JY#QD?kq_3k6 zo3$@MII3wpQQ3b3{(^n8-`-1b?tJLoe3VKfae!0;f!t8d!#PC*EajQ z61+gxb?JV!E5(O(@ZK}WJL=-KI&=4oORQ4_2j}BA6ffH0ujKpX`oH|J+Tg%9_USY} z4t@Ps5A0ujfby(bUG;|o?!AOwqXECs4;;cAw}>;=bAR#w_N)Kud5G?}@w;U`p8HSm zyNUSj{LA&}ufC8kK0x@@bLe)_x!$KSCG>GwR}@woF8@JYV9x%+(S-z9JMyqm?) zb$KqX>M!pr>al74=Y5Xs=fGU4z*F`S)cG7Jb0Rgtc5&#()&8+_-!0a+^f~;YXjd8M?BQ~moKlBKFWTt z6Yy-4t{0(m?U{$NZUbG^O4_;Y@-aK?Z&9zj;6C)g9p*>A`oa6+1H^kC z`jxw`qP%Jh9M$9e-Six(;D3d`@}5KcL@REbcfSxijXr&rtN3O=a=nP(^YI(*QSy!Jz}t?&N4g>gzSDf?P<&JP*url-1qYraZgfKrX5TMx7ygg=>gK8&*cbIS zIO0XzFZ;j<|E?7AA&b_NQn$=^4n0>%yvzyW!YuWLAK14R@4%eECFVPa!VUKGx`-a-Nu_mO=4Xp~`a^y{5$~Jx zx38WcfAImTH@e?piSK<$|NL4}Jw5bn{_p7aKEh|`!yE1=7fwGJx+lL+;-#9=x9$7o zx#7ZL`Q9(>R~Mbvs`D!A*YLyc(C6C-y^Ve8)X$NR-;mE{-}Nr?ukWdwj26v>?eShs zS3bt{y-`ofw=SKtS{>io=xbA6wo2dYVV^y`6mfq89wguW`rSc5Ydg+?-^cmxAd}8!H@a&%k#gC=i!^&e@&d|1fL^) z&W!ugi8uN3FU9LlTrw{j^)BtB@w(?(HY(MTO892^z0UGee5m~m$31sB`im4N z%>z6L-bQ+eQT!eA#d+%w@maPmD*Cd8Lt23keb3alvVa~tAC9sQjQZ|=D1Z9^$geuT zBF}doGa7SM`18b#eETK6o%drT`cljv+IK{}(*nHpnsFp^rSio?;i5MD%yRICp8IW| zGW!PD4@P;)BJXm({jxuR>j}aYgXS}+n{+Gn&3=zAYzUo{=e9duW{HNin*RQ~@HGYB zvgh2ddFnv@z=zgfx*jcEOTM^a-&FL4MfJqTX^L@@Kuz`S?Tm_jaORPq@%{X(IMR{QWud%16oOmOu9rA} zpFHGsk*{N|u6y*9^Z7P(N9I-YyvX_z)$O|Y3cWAtx7ns2d!?uj+cNJK_|QC}`?|BO zOL4tp+(e%I0DVw{bJ+{tUN~z29Cep>EFbRGclM&*B|hMffd{2yXh;69zN()+j}ARf zzIf=l(pujLeNpzcd5CT(<}y{fPn+;^&+$#o_bw|>?s?ve<3`jUHTw@+ujbxE(=t)$nRL+82x&r||Oxo^chFz*{a zs`=h8>ym{Nci6W%>I(0pPUQYe^nBF+W<$r9EzS#%ciq<(IKulUe!d5e8sq+5aXtf| zoiEO-j@snDj1=#S`zgi8dv4$uxM|)#F#OC{Pch%C{xI79s3MrXdv zdCiA6(t24rX1E@@e}zjuheF2a(6v4lu7tehIy0mVJ|cE$WrQ^BVuTA6+`$7sgM~f0>mR_50*{19^(^2mEg{@S)=1CGXFG z=E~&5hpMM^!oNXr{+#_^@iQ}TI%GcBeIMvp^7SvP4tu07zJ0d%{HflxSbjeB36UK3 zwdv12!|!0TD4ynhzqmid#p8~L;kT@FdJR5)*nNo1xybi^Sx1D=f%9PE-cfk8cFbK? zzO_-9JB?pJzWtIu$o1uM>o~sXZ}9teo;WyXeE{(zU%sq;YSK@*fqr}x-gArfHA7df zJo-fVBPz~hUDq%43yty4E~1NH;hr7FL!VdUF6o@}a%%Xh!@4)@SiKXacv{Kg4*nYQSvZ?;*-Y^D0_JM>cd-Y=b}ed;C4;G(g>gL;qq zoX_3p#}*I#6Lpn*=TJObpLlq!nAZ>z^mm9iYtgSU z;d)BcC&jb1D$jcm-Z)xW-=;a@`S789P%7c)rEteR@P58{s6O9L z_(+S#*$AG{{u<^N?YrlGd(Xr1{%85AzUx0xPdFQWygHv}xOYAKN}CUk`BIw8neSaT zzoEGJA^cs02Y)K|-KUP^JO+P?Me5M`;-TscJ#f?(eY?Z(@0;PLto;8LIHbY*y^OzL zzIWMinmpJ#HE`7ybC=ujI`8nceHZ<2!X5d}p>(dby5oTA$$RX}QS!;2{b8st@0zb7 zkIA<#;UeRpM(|Pg)wBN4@0aHpgR{7g{@-VbhP9ggpZ@gsKShX*=tB{|Jx_m^>*}Ye z;|;Q(m*Erh#SQZl_NmZ*tedw1H~N0fN1iOb*fjVmU))eUZ-wunc#CP{D*6Iyb@7WA z!MRJulf=t>JhtnR+P5L{u2Q7?e}b=mi*vVYpYzad|D6LtT)w`We{xVDHox8oy{r54 zw14iGJqymi7-~&4D6D7{yalaJ)qWN%?^7?)5 z%X8wzi9+2Z>Ra%I=G(|G?RTEy0@k-E{TV*XzFK)(AWywOHC zzG1yuC0~G5`Yeys(YcXt-ZIYyzmxAAinoZm9d+=Jp`Vw3Kr{N@qz~8$orC+0@*oX; ziS>t`Hw2$XJlMdWw{QI``PoP74}<557kMe%! z%fHk=ZvFff>OjxQQ@$&nTcP+eOdr^{_%1Jyf9B({^$vH;%i#O+rdXFOJ!L=qQ4}|i z2hXT}%zW=M_o6Pn*CM!MBd zdzLT%()%0!P;q~VBlr`w;dK{-zlYzUpC#YBtiIiE>bhr_iGQ;dee>|Y?ciyw@3oIW zRv#G8r&Jws#PvMmMD!Ym{%rPf^WTH`{d{;s{xs-=7)avRpe;0fl^V@l+ z^YPfKFLzz{Z9PFB=Z6GgHP1IKNo$> zS?AHKU%5lRd^`AFe4?2T({X-`ugD~Ilu`O~^6i)Mua&VZ(F}m^xh5_UvRHF)S2IT zz1H`lOUd^x3vYNorj0kE4^cS2ZQpiy?s<5&=lIa&rUAi#Hwo1%Z$xL0#4JWqR= zxH*Fl;v{pj^6i)VX8!2`(3>ZI?-l8b7I?4vfuE$a{IT#3pc~DHH_f6x6pK|VQ6e{8o9M{6^%< zs_QkaM{&HQ?`Apr`LexV;+a~}$EG+xVx2s=s#e#$`bF!2i8CXW=c8v^m*Tj6<-WHM zjO)m(>rbvrpbMLhcg%Pt+q$g7bKfXB8_wHh^sh~L_@9W2i}3g#;Scli8#;Hjx_w>L zmr>xIZgM`YLmvzOdGjBhW1MZjq+_eqg>xT(fA@UusH?Q$tG|Oc+>X4({m}d*Jp}zI!D6D8+j; zdB>g?;gb>g(^CAdXN!kl^p_0yy*1yfdP|f1aidbbauWV@6<#jiIh6nApZq?752x9m zw|sxC4nGzAn$~@$lFmFIj#3@6@B3pNEcw@|(CJxkV;@W5l*0MVx2`My=JcvLv_8b^=;(GPm22z?Q5LvehCM5;~c7A;|{#%jnE~_|8bqTaxDC#^yl;84b@X> z^>;f5|5xc9J65S)z5y@aiupzQ?~Tej2jyk?@=ELHt<&?o8}!-K$2-IazXOBz!39s| zyI;}+)a;OJe$3Vl+~>dNdza1281HI5JFcHZ|FX}gecJTrtqaH( zH>9&`vyMI2mBH)(8+ETEb$k%4hX@^JRzE$(dH0i_Fiwp7BYwN-{vq>Jyw|mXtaUxr zy*uFd?ch7nlNZGe*J)>6S2sQ`*57A)m&KcTj^h+QAwPLe555O2aN#g|!=H&WkC|(f zFK!sul3$?%eE?rQMg62!SKVvJd_nlQD?iJ(F7btJ*H6sbfG1v|A8Nr9KBS)cg8h6( zev=RPD&KFp{*Mmj0`X%6e+}!|7K`|{x7Ok95^lV1 zUI$z}7P_!(aYOp34!p(>);Xa28YkX|k9(X?@dr=ft@803(xo)Lj_B*s-+zO=B<8{d zPk;`rn9s_#U-ALmhmW3!I;+08pZ0uxh{p5E6EBc=EP!{vL(kTQ z=lD5rv3MQ(rsT^j-LI#5to5K{mGaEx%Jba(ZrMjldCOX5-~DWH!+4%K#g)GMMf`MK zLGQtac^%&6J$TW4I7;=emd_Wsa6S4@+;8suS^4!6dapNW>w3yZI_5jk?{2WZ@50~4 z^-1t=5g)o-iTBBeqjZnj<}c`Dn=I()sRy|(F%tMu``Cznh-~*u@2+)1?+QNj;KEgS zdcVtC=sRZ2OBL~o`S#2CvT(;Zas3V7I|Kgf#k_j?y3f=1^_n~&-}|L`^tHPEyuDwJ z`|eNCdv%I9_`UHyI{JL?m-IvJ=;Kv?>?CugHp8z{x`02jo-zCo){1zyd^pOwW%s#i zzb4q1r^WG3IO-Dh=JCiw)nA#fzO4A&Wq#SB`}AV&i}X=l{9ksWe@b|Hf%yXY@Vszr zr$Xm|E@QL!ozgy<$DX8rXx)8B@B;bvOE}4XuXE&AN8`TP=Zv_4p7fG+M&vd5;=J#@ zzI&Q|@|>QjA|9>@?%1S`xe(`a^8f4pbn5@k7dOPSbu0PfZxaV+$Rj`ChtdZJ-+}*F zW?xSfX+ zv)|)9?y*n1=zH_UL;Iz;uMNFup-%wZ+XwGoa9zcD3cP8)cqqMShdRQy)E734m)M^} zpFsICt;2gS#hkiqc;53Js6SF4nL`(F7GIRE`}E*R7u^qw&qKa^S$xet^~(EE53+t& zakBqK{{Ow}=#_k$@|{E1iOBEc{E3&JfVZ|U!g=21SMXI+=)>}@OLYhPeJuqapuUe3nRL|>cd7#TNbyI=OTLf7kex9IoN=x&`<888iWf(Cmlv!% zbRD(`C$;du8Z|D&*LMWJ?0oOC>ZomawwJ`glfn0@9@MX-XZVr&%4_;JekPyEmoG~f z+BM#z-taK^sNWwDyFL2B#sW{7$Cs`xAMSNsPddVF*Nx%RK34ko#=Uc~UwQ{di*RH< z-bV4=@$hQo&HDGFSog(-j!Jc+ zeCJSglEZnT?q~Z42f)z%0kyjL(H-hy*Mbig|B){q3itNFOWWwJmx_EG-V^UT?z6GJ zx6ofyDZb=em-UCrtDb--j$5ZkobRIVco%gS`H*Z?f9pUfk?$O;|Eo1K+pKGR^XY7utby?SH`i zAusMnzGXd%;>%LW+Ba%G7k>ucqv&5FZ|Q>P9|TW@e@YQf%y+-k*VZvmB;Z9X1b-`$M+gq}S34Amog^jWU?yn;6x_*v%TZCp>WPZ@rx)Dy-k z)h)Y~&gl(whY!tHrLCL0?qGjxtz)lPry@ONhrDbVUy3z!+6(-i?|x|=ZFngA3@?Q4 zUv*jcAr8m=av!4S*{2Ece2DIU7rvqq`M%=hDE)^$^kA3R$9MQ*y}d4h7YIt8|RAEGl`TOY1^X-@L zxZ~Z;;4SbggU>KOKkPnq?<@IqzI7>Hw=2!_-ih}YpECNEtuxwYJx`)eB>php`=xzp z1ujzDf6IBZ?!ootv*3?K{`*DhOZoUi@q+FbnggGoD9mGK?#(xK`_frYWIq_a+xhlO z>*|7&+;8$n*Z1f@ZwI~?pYaj?<{`XkzI92D;yGAvjU(VYw*&vVUsO89N8sXRc*lHs zig3E;JACi{D0rJI@V%Yj3+?ZyKJH!g0r~KT?oHqCaJ;+d9eA&M@M)ud2jYEG9VuU& zx9&@Igl%}XQ^xJ|B_7VV)VuI~%&q_T4j%4&=g|5>>ageu-S=<}J&pSmA6Sg=r8aVTY>w`qgjXN`PusS4}wR{iZ{&5 zP;Y=wTQbi%AboE;@=njKN8gO^dA_*ufBZL}oH*N2r}Uf$;|cMD)|1{T#0}!hFmo>Q z=>hD+sXFs;;B@tOJghu#N_q8r*DH(ifDh5%oQ>bmIkd0$B0R<`e?R!X=Xxf5!Lz6v zDX!$hy^8OLbH4Ta#o#a8x2Afz^?7HC^BBeXd~;z`zY9LM&=-K;_j=$%`r6EwhMq|0 zG+*4X{}Xit`dxQV5lqYwBA2jczI5>!|>(GygI3M~Hox^;* zjr~rTcSv42Xx%kDXcs)ZTpa(kze~jVd^k$_C;LPX5btK=94c?_M_-uY%3Rztz2{lq zuRgkr8F;p1#dFStpStiFFUVVl(R)nszsrZ`7zFHJ_@|{ET^UMJtk9+|CzC@gecN=^-!@dpR zQ<0Ci5iakbTbK{Mjrf7ZqIFw$*+4(^G}f;==P$zlJBY9{WWR&YcZkSmUrXfshZ;K6I%wOfwht zBe>~wQ66JG&YUwnF!$ zckF(F58zz(z`Lj5J0|%#R4LENcfX{!sntFAK=u1s|Avctd)ce$$rkmLzv*9_N&FEXv z`?eeXV&?a=ynxWj;rI#;)tK+_QXfL-ir=*HQN+ zqj&hWsBfSbdTse|EW_hJOxy1u9MldTjlNLwt}B)B;-Md`&g~-a^;7U+zH?~2E}VG2 zavo;fu6jzxb#r_Vwxdtc{StX1hQ7pgarYSt-)|Pr15h6A{b=xBy+bEEQ?kxCS6$M6 zLvNi|MZHM6y*9qDlknH$H$)QQMb_JgOf|F`z}KKW%nJg>gjE_LEJ{QWN(CsoqtwY@HW&W7J@)_K{&tw)&e zFv52~16SRv{_-G^X$KEr{W9@y!2Oxo;-Tsdoxl&m?~mZw8pLzYS9w{f&ho;#bNKIk zdQst`7J12JywBQ)1^B~G_-*+9sa~-}eK}v;5RXxdeptQFH__ku9@XgoT8#Rv=NgO0 z$rm@o_jco5lJ4XKzn?8yXUqH(exuvqtM7|=$9(6|I&#hx@9|0c^p=b2uC0hC!dFL$ z?}Oyg`SMD|@qWZV-Jf#>p0pB9vR+`G{W}FNzR%o*eCJSnhWmxi5brJ$*N>Br)#}zY z>s+3Sb?Y6-$In|&!yE+kYBMn(Q1PKwx1L!#u;;N~np2W59;)xDZ(hxOC_1Py@M7D2 z%kHD1uKtMq%vY~8Z)0CT_tl_(!uRWNz8-!fp+l5jCm+wI@Bb5VeT6u8tdbwyBKdx| zQoVMWKldnf5c2!VcMeq#uho6O!0GU44~q1pE$dJmFL)2Gr=72-0^VV~v#*2B<4mP~ zJ;&0?(sU!S5m8e(65hH)K5W6ZLnE!-w`N`$j7s&Rb6(=QPhw{q{=^b^pqL=i#H) zi|Z2Z3)6eBWPMax-Lm+N!?*#zh7NWEzNU@NVU##{HuS*a=kxV{$zR@i{wesUInLuC zalRMl({tU0pJq9S`RXZ(f9`9$YhO|NL?@_E?z@hJ9%`+EmnzEF@~uny-gfvkxL?iu zpwFr5l(X-~0vt{e=Hq^}7Q>SiXE&f8W7lgO651$E<$zTIl$Q^HEoq&qKbv zQgOOZ{pW4;g^Dkorw-At%xlOeb+ye< zaTpy#n>zDG(LFpv9<~B6oNvE`8}`8y$Nom$M7-xAE;gT|c(}$s=i4voZOo(1I)4nl z>2p2KshDHw{s-yW^5H1ydYkB6?PvHn_+8DlZF*jXaU%UjtMt$3+b{d!>0V7oJKjM#Zuk2T;`3(7&L*ak(+cS{i zpUsQ9PA(nT<`?~jhjGJvuh*@g`EZo-$y(idFRg3OdK&l)*T>F--#u6G6z@R3_sjE% zgnw75H{2-lGgyoHuga^Zi5K_LA3o*X&bMEBhrX#3=cBI@-(lwU*6O-fo7}54;`}cB zV!pgmaj;F@;kNfH_+H_}cFg}0k2B^zXW~Y_bt&HM!&8it4-XWdJJnf#iGQ#7^0cD< zQ0(uPAqM{T8`Q_q!cXrd{QPZr`Df^qyOrL#?Rd9^gSYAb%J+Whes$w}t@AbB&|C-4 ze|V05c^%#1DDx8YokP!yQoZ~Ecazale`epS?-1G*x8ES0VUIXCWZj1OL+Z2p zaX!_@U0C-7I4j@%5?|5wcSk=B{nzyUTGzY+k3Z)8l5?3a9tz(c&a=@u+bHPi@p)+( z$A|9AI(xqpzdZvP{@Fh5pXZZH!MCX%5p&H#CyI|RaWUUH6uv#|C$~Pxx*G9tEpXOr z&g;eEIS}UQGVIZBzd`u$2>I0vJntg-ex>-k;kW6FTC;B_` zL@jXEe4+1>y36IzLHzdbD#JgEm+nWMM7U#+{**oRuGTd_0DpW(-TV=}Tt5EL^Q5#t zOXMF*MY`85{G|H4Yg_O+EByCa;(fk6ML5WF10Ul1vT8mGAEg?7+sCZy;2l3#^e*Jv zFZste!Gja5Yoa)gJI`1A`-yvX)Vl59t1{fF-+qJmv<`gP8uw=0dZ_Zb9(&6;3toG< z(z~8-zq~HvS;r0Wio4t+>+4@xC(1i^C#~K_zC5+MbR0jzGhISQv`e1Sfrqo+a^C%I z@HzSNuiqUM9{R+ANpSCG=tlJ(_oz2K4IWYPa}+&CKHTd(Me)P^3%9J3ia9>+)3%?3 zeIHb3$%hZ^+hbj_`@F1A`A-gjNcic6FEf1|X?-2l|8HNBLE_={$V=q+^-K6ypbLPX zpXT4^>yy%X>M-wc1U$P29{d3x@P5>jnLC9pVvsy7Uz}IntK~W#^^!?=(P8+2F1oK3 z^KZoYIp((IiyPuWYjxij>U-ebxk@~?ePG^!H%4OrR8P-`4?UODc|G%9$iK$XM;-Rd zQvZwhOgJl_uZZ+`ZQkPr>MHNZhv(Qg^Yafw$ENsxlsZJd{gTeS!+knmTxT|hbl!=N zBl~zg@@n^I`p;+isXWtWYn9q+;Q-S9zBpMBxIp69;h+b`Gk%)?kGs&h6_#DCQ4 z%3JQaj>)=DhW|~Lee!pxPpXT~bqjuQjJdFz_^~$wH<|}g-E1A+H($L{xV95KuI|@R z_@am(wJ+9AaokiN?I!P6zIB=RG(J}!>0a!c*42u7gZ>^q;=j2KKa~$}NM~28yRIx9 z!$WwtW%^-z;hW7p4E!iPTt0j#yjQC`Zs^^e2pxmoZTm=jp8G@hOW{A44@dorZ~y#L z$voaA=>V3Bbd|&KyT*?X@D2Px{ci$%obP^#-|d0h=RGglb!PnL<9&mtFQL=Z{mhqF zsxQd-*D=p)GybFBp&9QoK1I==qkYX6H-yLgQAcpzEWPD-)LYy1QSQL6J*>p1F4GU3 z4{vB4t>AI3+t7NiRGz0Te&ZGRai|g=8RR_XtEcE4c78P<`6TfJKEpir(ZI#xJ;s?! zpD%8F?x**FyyOM(@hrTkbq=S&yDwS)skFSP=jRFU&QpJ#;@w>&j&%KA75Nt}vaXxx zaq`7O;iR5*c$~8v!FQc}kA7jj zK>7W_Ly<3TNVnS$eh}SBrFdw4ax>=onBPzwovVbC^YI(@bMQPp-Jdt^Cjws>XPt~X zCH{2G6UgV=qx`ZHIyL)hs1J4(zXs0R3pF{G2jnl-t1=nGj z8y!4c(Ok&wSij?`?`xKy#=Y8~Q^vRK&j9b0`ypOiN8~(|_2btD9@zCS-t&C!S{BYk6E8++)+Fq z!vFuW{Ivd1xTt||XaoGQN*`~tQoi}Y@xwkh%p1+e1Gs(+Z^%15hkwKKVx6q}1+@NS z%!64;+h6H(WF5J5%ufPOm?u!2+_TPx`sPRY#eDS?VMQN^Wk343xLmn=Y16GQ(fIW+o*9d zy5HyY+vnRa&#zRS{~^5SB>3ZpV!tBgpDRUvZ42mQH;T?{zIdoUjar@l_@a2{{?HTX zRNPlO&HX!y-{Tzm*nD_Hc)nH_f9Af8xq?0eeO`;WXI_399P*a>SH65%_2oXe_B-=w z5%+w*=%2Em3V8B0_bp%kWq(oTv={j4V!qA&uGa6t576JSRjFSwU)*s11P|qULf}RD z#M<}qmUUs|FAd^izIR#Yt5(f8yuYCKZ`gptgI|oJ- ztCjgf>yzDg!@KOhW%B^w`+V`x`rY3hEbtxo>jm>eK8CIfpR?fOq~pF$A8@{SsQP%v z`EfB$^c*Jr-VXjlyul*pa=p^N=EG4se{IK!%6{H@r-)M^Z`6!-_~Moh4)c;OM#CXLYIVi|-MR?j)ksX z`Q|)*v-$9b^t#>9M=4)9L%q&(9go!Ax1#udtBB{ZUtm5Rk@(J!85$M;?ey*YKgB47Pl-E&CgGd&eNj{SWdU$gwwyVP`E<@r~IcbfSy z-Iy<>_;D27;R^3tzP!@@0Pr$JeBxRqo#-xgSMTTD&a50rxr&sq9B&Ul`|?+RwxM%zsZ=eu_tPUilRL^&;nQ z6kU0nJY_!i(Qzf~93Rj9bN`TayNVk(E9n9Hfv>>7;E_@AN4|IYU+@0CUqe98ItS+` zoWpbY1Rm!}~eev~K8)AK$(DAM70&d3mRMdm{< zCEY{5c=%}=;@|(vEIv z<%{7X;Jn&82;bK%KkX~R+=FdGaiL=pud(`Q9((iAU<_zUX`SSMa*y z)G?d%YpkJM4h(=iyB~-#6d=67KEr-E+*9 zo2ldjwox2csEfsV$-~eQ<%{#`pX|n5Irw+#{>O^qL(h6k>SOElSzbgJkWY`TdP*BV z*ctHoUGq}tPux%Rswi$;h&(0h`=xu|m{qtlJpsE z?~idJdgLW^ye;cg%*XK_p8?91roe z^}d+5A`Ocx@bgi!VF%$cM-WR=4JM;qD$18CT#e?KK zht9vO4}v!&ZX8GN>iV*MeUHaGCO?&Y`{j9U@E1kr@+A4>YSDgrzQZEAhlj?+(dY5? zZ`9{sAfB^heXo5Dz)x3s*Q^V>?|j+&h0kNYy1DTo^S$`qS^U>GIFBv!hQ!S~=12MW z`EalN4f}!9bq>d4p0D)G-8gsj<#@lq!};!)aVzy={8FC6(_bmlu^rA|)}OoOIS=?H z_p_P5QGdrB>RavuHgC8X@3i>Te7ucth5c*B9p~v6nx-$* z`Pqd+ozig;Kl6O&Q1Q?GuHJ_w*IV%obRKgd-Y@!&m|u~vuIIWObFje^%jW0lkL^|V ze-&?4$g7u%^hNp3p?H9<^I+oo5W4e4e9Fu(&Js5l=wn%eN6dE)rSo_F_$Ko0A*eSB0&cesi^F<)M3yrDR-3vW08 z@BV?lD9$)BKUh_Q4L-XNY`)|O1FySI-`7&;*UT^4&rv+cCiv=G`gHQ`m)_$ReET)NcLIES9lc@Kbv@pJ*PPD{ z{8IAe%hIJB@`H*W7o3N&&RSjlic>{+_@Z@S^rPqFqs(u>v!NUJT1 zIsV^&e!9Q@Z>zsfu|0mJ%lo>``*i|*xJ$pQ=i)Zt6Q59Tm;@*0!%?aO9r}x+ry>5` zM~-P`?H-1AB(TQL!W=a?+*wJ^A}f(^GbBr_@dO%K|JQ(&A@99R+?*^Z@tt1~CDNPoQ&`LubT z&u{J5|IL11>MviR{^nE2?pMBQRz^``;-msBYzaih67I^otZ`8Uj;g1%$>XGx9 z(8-FQ%7+hy-#hSX8=(gfUYbLfT&q(@B42s#IWbZ9`}!yE^Dof8w1RI^oOlin+zG#O z#leeT?UBj12V z@Brqyjd$SR+r9lhWh2t^x^yfPnGXoRvurgGnc5C zM}rTbf3#(tHvj#4(R;kZxy=_3rBm3CIH&XY!hS>Wp|!f|>l@(4@5ry0!5#T{HubT! zt%nc(+WijN&z^OB6@4o3_+!jR$k!L3@89?LR_e3dGjBuOXmgKNs2gpDUuE`srTI18 zo1cQekbhv;`g!U{AI?C-=qF~zKFkmPycqlb&1z? zz2vHOzQnzg^y~G}P1y%5=BK2czpT9DaGs6g$Zhm$gZTMI{};ITqWiS@nQyad^trf!5kG`iO!zr7xD@h#_W3%v0XJfCmB)Hf6J z4x_H;IU?qR^^R>6_ATzA`hN4_d1Wn~@T-BR_WIDDc)y;5H=dAZUXOV=pJzW`|2Nf_ z`@T=q6$gsKuhwy$A^x1>i~I%h@rU-I(08tpcdZohzT5bx#$0Lo05+q(mlbcgZmzu9I+OeG zP*=edt-$Z#spz{g9{C)_eEr{)e|6o*0Zu#y@9w!$jvLd_pCWue%sZa%9Lm?KTcKa$ zJNK-IE&thpV6A1pJ=gV${q=-P^5H|huU+)<+o9`HUbaRZuSMN|De9Qadw`$I7w4@z z*7}wsU-9qGr^Q!|yWa+!v54+G-#OGhiN0&p#{Z z%Fl5T9-sq1a2q~q3jBB%omjqesK5VsmmTMe>a!*Kzc&A;PYmfi`05Ng(KY5H+k?=*NNztZ#YjwACd1Iiihcf2giu-yT%pNjbhFtdDnWy{0iRZd~x3M zHsn`hzs524>n!z!nE!>2^h!}2JPPlTZ@-jx^?Yx5Z&%n9sN?u-}2!@ z%^mJ??iK=1_?%g9t^H}ZzGu82KAm4b5gonEDFX-TAhT>VTLhnTWyNkZ- z9sQ*3>hAyZWbMEDhnvi&|G;~_4sOiH1K6j+x;*h6%k~Fg&S==WTw4W8iZBf`(WK=`i>ANY`d$uaV)hvz_>OVga)@(a| zpbuJSPFAh%xhd8O$X9=^(tOK&@0avjoxp?AWe7KtD7RI`q$@?AQOzbJ?{|=RF@a_(bsohrYynpT^_-_jBNq zC*b^i`Ir2zn&63z;_p^nd5ibAR#zS39{vb#>E9kAU&|Ngh3|}88$Ms~g0uJmHY@o* zE*Jdi?c;_XCtsWwU(<43oWK9aMfmnP^{vCXf}T(0JVp31Upy3l*o?Sg|9tZ}<~^;; z6CQqKKbRtYeZKe0{jBiQo)Zu{2K#2A8?p`^KKNwXIPX4s`DiS0{(dyBDymD=>c%1B zyRFaYm{FGnBV<39Q3S>q^hWWKoJIgi#$+Q&ih`wF!dC&9pe`!9Y^OO`+vr#3!H4h@r|etDgJ)|?|L4` zBs}OW`R93jVe;`d!nK~?@S6LxVm<(0N9)kk=Q@4xUN%*d25nBj{f{ zu}{*KFT3w8t>2e@TOr2nq&nDk z0Y4V$#ooco<%=7_z2QIa`~*ICf%CXe{p+-OZsO%h>e0?~zJ8wUFHqd;`u_Y4Ut-VO zaQ@}~X60?0Mg8je)+PO&{ZekmJS_Bx;g9Wjp+4+K)R}jJhx6Z4mY?p!5MI9tK5E4M z@|-zvR`Bw^kM?c#E*P&!@k18 zg;%*R6BYR~{xxlWA2ELj5A~ubpUD^Jtp_k)DEzQOpYl@m;2(by+}QW=OwqYqH4g%g z`ljyv&+=3I)eYZJ#km>I-wN+p3tWF8_#NTNX>e7(b7;IE{B+v)CUl{S>mBQ4y)M@! z_?a)xs~^_<#BRhv<9Xv^@rJL6?=M2%p}IxB{Zd`4#XHsD-gpjMqo_XCCO+;K<|Uvb zT4R0rcmN4_kKhCN6M5tke9Sg}0A1GA2;K&so4QfH{gQsS7dkG-d*S*maAD8)tB7Zt zA^wk{C(0M+&5H_8y>$J-`V`)+mftge?uJg$y2~u_-QOYJuoL;e@s#6+`(>qrc!FO0 zICZ{b^rPqFZG@l9=S~;-yGFeTT-A)c-?&~p{aNzQeD_Orlcx3a!Q-mGa*TJkAAW7r zAK;vxKBK%Ii{gVGir&9f4i~DmE9_K^& zIk-+_9@spH@|S#gLpZNixBgCX{($8?f3YyP8V?FASo>!?p zy+glcozH1>_0L#eKHf&Y4Q=`;H-mq79Z!9E&B$Yf|5xJAtG<$Nzl3}3V|L4YK=Aa= zV>CZz)cSL9*2VB)%JXvROXUC5A?}&KI8VRB7<{&Q>}TM@(?vc@6X1N`7OPp ze*C5qf4z$@aR(jB%iuY@F7c%KcmU-k-IyQhcT4%pV&qr!ZLnV#t;eB%Ip4a(a~e-H z$XABRFF#cBSN6MoEO=k(bcWFN<-@(w>2|Gmq7UGH%mWbb(Q*Bx&`$(T-Y)VN$fxgh z9TuMu-rd)%^9OKKi+-a?S;B@aG}lA4PZ4jl9b|l>QzDsK@4em#w$< zoEz$u&YzjbS*xqwJQBJUd@YEd`S?TWG&~3EWYq13-~+ zFJzyFf}i>K4;sLXd*8(U6CXMj{-fFOhWvYbj(3g^__QsB-bV5MW5jvaTlD_rJBNIFe#!BpGwslF5 z;(7GLQ8x!qF@LjZ+!S?l{XINl9%Q~e#rc=|eRt`LTBg3R4zBmym{In#QCWARKHz-! z%X1LK7y7$T@cqZ=^YYy3vH17q@l`*{w_iGMo#3Ule{ZPojZug7IXw@*HUgjhB>XwD z-7n955iUB0kK-u1j0x(IUFt-)xo3|m{rM4e9{F&u>;1}C_KXwF&!dM5KUmN0pq}}f z`fNU)O?+=J`ZUDn?!q_Cf`47FyoUa8tHR$0AH;k-fOJG1`~bHL@w);~#QjoSIRjsH zn|JJcc&dEyQ1b%Z$8pc+i+ai#_}{MkI@s57=P~fu`FH@=kJT6T#_pkPAcn1_`@~um8uS*n^U*hHsyo_fL-xT6DJdJfNhCHBp8FS5N~!twp+V^G{4ujn5F-`eN)Zs057 zuLX4J`S6DL-rz;y5wB9`x>3YycRe2g{nS*%P2-hp`=xkRtE;}Xhz@1l`Wo=9`Nh4W zcVX3YV$%AUIbXIv8G0J$#mqhIgkD9sX*To_!pr&cO7~Z~|I7TS-mPKuPp&^t1+U2d zqGQf?zr?Q@_fALsSo(!=>X+_o8!he|HGe1^k}qG@ciD&YROBJ%v8CT=2k#+YwhjB9 zG504QKD6J7<2t?@j(hk6wBSjnsIv?g&MWV8KE0^=D}x7s$NrYMaUK7!p8IiJA7j0{ z)QR%#mw8dWKMnhEIuApC(yR2&*>B%^lx^mYCpAMkJo*pUWfg&l!sjg zm*jhwg~yN7d6xsH`<>EH`yalrj?ZAk5_&G>FKhG@wZRu7 z=vLOkFIM&Ce7ue74lV0Vh=+GPkF9u~w)mO{^t(+K&$Upz&lfk;Pt%P$p7~DU`Q`AB z()S<3m*YWE{P+QX%Y5fhymYPpIet1B@xlHa%D2vXJrOrF7cJj8G(WF`&-xo@qgSKjz!>(kzh^&5|5!yESP(K$P5odfUI z6ZB8^OIe2Z9Y?qE5#Au*{W71VcWBc4WgQ|sXe;o&{``G-#d&zde16O3UG#Ix_5Nbs z&Gj#Izu>Lo1>d6JtFoQL-yZuX*BT%0feQeUX&Mcs+M zA@UUbV9n3Ja-RqH?>pj0zI92D+{U+ei@uHZV&6HZ_Af2TSw%29d!lQEi2yjTs%k{{}n{{uN@HL2jba2RI=(4l=5_?{o^JC$u zwWycK=e8aDve@BMQA<$hOqU&llIh&*THHF?(z_wZThf3w9y`AB!|ONL(l zA@A~T%)Mov6nV>O^Bv@w`SMEjv-ZG;p0hb;JrO#}e&{Lne!l=mP0^p8FV2f+tJSIJ z;hVG@Jeu`|;%WBKk#1Y>Mtv;b{jxqo`*6{9%BbI&hf_Us9A5EE@QA{Z`Swfe=~%ZA zbH9Y=$1BB4`&Ev>PkrEh9t`}LEpCXnX`*|*i;w;ie9U?BmbP_XMe`FIG3Q)<5Bc(C z?N_UU4`eRSM)YS3_j(S(xzP9Oe|I|OC1%?%p=6RcU;WMs)D|*yp7r9?osZVS( zXEopb(!1n7!>8!*-uN8CV_PSCH1aa{{VJa1(>Z9~VV5{?%sAcn7JXEU_iGtm?Hu*; z4g6v9-7n`U?%O4v$9`$0EE!1dN$%V*j= zL4&@iN!KT+OXNF;^0(@M-#4f~%tk#(ypD0!3F9cNi<*eT#nbHs^Gjdikt%MCeI>vCcOQ{FTnZKAdyl*gr)7iF`#{p_|fN)otrH&vF>NKNa1#CEl?=poeO49vAU}-Ln54 z^^|;i8{vlzef}%N!6o~_6ycGk{T>SE(72fMnXeA3IOzF$_lkWFnK#G&?7Kb&UvL!M z|C)MbzP!@Bjq=Au;`a`9@{wx&S9kjV;#jS&__F9ZS&kdjDe}Ev>f`E$f3JANm*`(N z9lz`N;5v^cKY34HmT$j|qjb+s5GOvO!@iEMvimsR6EA*t+=N%mcfUU8MUPr989H+N z7Tc#%>p#Z*D(qjrby-hsA3*a^p5w0iagRK5ioE2F^J(f8S=Z%xhvUWiUg5$9yiGHB z8r6;NTBjI1Tp9qyho}x)tBXJTSX@s~JlTtWBK7sX;C$W(2cIfgeax)K#+Sn9tC)Y+ z{`4yQf{XusQ^fb|@ZROa^Xkj)`8|TCc~GHa0N;11&u-w8`U3uDxzhUb#d+&s(Q$?U zy8I`e^WB~B(f5AYM_PGJzWtIvO5ghd|9%(0rB(9E4)69H_v*Oomb_p2*5&ufdS0Es z8RFuN@Yhfu=BD`%<74>Jd~x3X0P2gK=DodVpI-bbYjyg*&^64Nj{}$F%fEylj?|_9 zT0lSkl6byYG{3?9E6==M;>JUKIP;xD=U>K2yi@kC;oh|>>!;vPy}qLOpYI&n7fby7 zv*Nm?zhAs*+jYIr`SO0_XPz%lkq)*SJe2kS>hD-D&SSK%1LP+QULX8#zIuxJL3j<~ z{447ZnGYLtw21TP>1&&T2gnyUR44D?bNCXRF~fO$7`hYbwMU76F>sgsjPmW5`l>tbAA*-@_)9M1w9bi$htw~vw?uCMRQ?o&U% z&F?>R|2&@~U%u?VYj`H>u#4k}=PH?pQXl4%c)zmxZz#_9tyd-sDH}|MZHkjMluy1Ty93}sk7Vod)h37jyDe{A{&)IRu zOU~(I-m`ppis$&a-&%2GHS~t+J3Q>ybHAeWlxu1I9Hb-f6aT&qe_Q2^FVIz06%r|GQUi9y&udQkQ z8oq#A#`oxN^1Wa7^Fn`UKFalD`Z^B#m;J7}K9TkP5?>wtU9NAD@8877Hs*j6_s@}k zJ!76_Ry^MgUA*?;Vv#=YIr^IxI;izx-b(!Z3iv+XITYS#I^W`bp5*t-^x?RFd6@N2 z5kDXE=koDUs&lm*2cljs9Qa+)yVkb8#{25NbLP3{<8ACuBc0*v&~516y(Qjtthb4M zQl5DOJd*GI5>L_Q`k@DRI13x6hpNwMFyT)qi*JfBDX#>*jnnJnt)b z?RnPOp>DE9TzSL0uvnxI&KKu3zu|Dsf$Bjg92eNPT3!6wasT`9tB}7#zWe2V2I+2= zBM(vCat?m5<9Ds#pB48{aU&m&QXf~x`Bw0`)Klo^b$xl3dv&sCe)t+X*?hRyzH{`W zxR2a*Bm62{Z?R9M^_GpadQsQS!RZml#baE>55V*FuJhlI6DN-r>16Z84aNDMrTIU;eX7w@V!%@QRo}c%;QvTfFUcF^qhd%9|OGLdET%T`U ziVuDBg>k>&)8K9PohOI>mwFL#Garu9{qkJ15!Q9qbv)jwp6hJYB$t+B3W=pi=n@5z@} zDnB`#yM%r@^t|e)>_^`(zCQHz&4nH(+x=49F#h|NxX|!6}a*PnZ6ydOTCiud{U zOL<`*yySV6qwpFlywk3yY_RS@`fc{$0rK51)rY&G6ZLy5{JTN@u2vVnbGuMij(#Kc zBjw{agkx)U`$q9Dg>G5ruW5c8eDx#xn>YBh=hIc%Kgv9>bSiU&{VKx0_TM;KgtN|| zKYxQ>Am2Gu-*U`RrCxq7{2HuJq`$e?XURBHd|SS_VIEuG`5^KX?bpq~6aHO4?YsN{ zejs0-A{}`r{QtGiE7T`fqEAZuG-!Q2zCFHQ`PL=BuS5Uq&*S0oVjn^6(|Py77W&1A zFZuAH^jjTp?q%L5$A_!b7hFgBf%@zOab*~sm@m#tzvjI1r0YNt?{qHP)-hL}S7)Du zSO4WFMR2}+S#twh_z+FQ4?Zw{XI^`)?)Mm63_iSrPe;CWNq5(;lsCTwM>KeUpMUXw zov>~I-t-3XCSTl8ow6JCE#->~;X5RJ-*Z0-^`yJe&*yqfHasui1Lu{)_!&-;e+|J4 z*q81;JlhNSnEkNT>A|BC}a(Puvgqn@Jo;01M~eCP0Y2Lz}2-rM*y zYzA-r`5efb=)iW#tFP0aoo`)^^THDi;=+TNBdGkuzQhB8>s5C+O5Du1F6H@c{7{!F z=EBk^v_hY9n|Y}lj{oG7%W2P{{1^`Br&y2dJW@J{!?*%I1|KfsCzbF0Qhe|ngtOqI zo4mUh@fkIKycxPEztiq-&KBq0myOOJe0ZX=kF9;_7|2{&!??-eH`SMEf?4eh) ze$xHP=sAwm<)gjizH{EWwMu>v`ReA<)pWo`=g32r{9eVJ5#ita_?m6;4*XDhKb`tj z`jIal%2%#y-+X*TMy$gt(ib)1@7KZmi>^0ET|L|VI%u!_^Dc4XG=99(^vAy~?#t18 z)gbOaG|pnKMZUaJ?{O!1GvSY)BX6-EtbKgcU$o2muBG+U(>XlMD_sxQ{v0dz8&W)< z;2d6weA#|{+0LPOijMn;&`Hh*?oi&+F)#1Fy}&0~=LR|+x}I#mE5*MzmFJ9TzgCOR z;~qTXZn2+Hw*B&4Y4q1cdC49;&I#thSif?|QDF?&UVRy?p0T=c&p4f5m#P zI!_`0@5FzHr*fT)x_Z8KnYU42(HeMmlDuUN{iN&W!_JrCqfVkL&G&xkT(#*ZUnB2| zd2H;5`_Uh>PsMnix@Nw5igf1IrM#{b|K8&>`lwPK(W{2Ooj=2br^ykB_pC znd1Cp)V0ie+E>-V5joJT^Xi`<3_&_xBEXee_S|dzZB@N234V`@noNbW`^G8uoh-`B+xp1NVPf zCu#kp>#p)S>%h-UQD43sys7k4`Ocx@L=Qe=(se!Z#~0!ErMk(g^k)aFGu48vMr~{Cqy1&HlZ@foq`;l1^#^|Ce6O>$JX?KM&uL4n?gOMR#%*uhsU0! z-)0P4lJ9NaaqH(?WDbJOD z!+Hn5xPSTLq4?fb^x+A2JP!WG_)+<%_2rN86WPFDCEtFjKH|Qc5&MS_HUE z%0s6L^$GHsU#Mf|S)KToeq~*X_H7RwH5vYf%0CzJL;QpLIl`AN&x-UV_P4Q5 zxpf-q<5*;_Z9C>pf5{809-Z&~a{i?FHU;k83Vnd}QOv=0JqbK@l)6ej-p0P&dXFEt zuLwLb#{KD5_KU@@$aQu6&GYS-eXZb|f=3g+IvaD#+}CEj&-=wa%g1k6e`tRS*JIJu z6y3j;&t=rTtiSQ!OSZTn{fvDbpTI{o3hx-c-(CAHyC0S~xJtf~Z@!+szXjiUpLe%J z-lBNXs@&%wJ(cFKO`Im5p`KZyNeYsJ| z?{O8KUMK1&I)BeS-y-@~vc*I57vg=_&3|$pXWXhxw0FMSY2T))|qH<$J$`&wIRI z7ton(2G1e<*JOWAaSqqe1)OERUOwJN@#0ARzZLN1{W=%^mGZ$g9yuQV`r-kOGbc7* zJT!k{Ujy)8Q9Stw?l@AX&jvg^=)AI+r^^;Mq~mG_uGRP3|8O?uVJkj<OJqtD}>oj3n9p#6$DC26)z^~qk~B!9p0l=!JG`}x}I z27e4w=gSv2+%KejWsQE{$;v#2c^jR>3-+f79pC4Aoiy836Yf2nTcP)BhWvOuaFqC+ zrHZ+3tZ%B)e9L_Am(Q8GsnEWq#Nd%sjqI8uji@|@*jpE>K$JvT*haW>Yk_aNW8?3d!Y7wfT4 zG5KZJya)Sp2_4@Ucroj`qR-d3FXFl4cq`s3*I%XYTSD)UZ(Zt>>Q~~ccbHSuV1L#^ z_oelG2d^_^Tu+{sFV35{aUH>Z=8k{j@4LkFv)rq3c(^n4H|L9o!iO#1r!n%%8NT}} zbH3ZI!-hW0bDD+g^PNNcI@$0xIbv*Vh?$ci#B$_cNi+DK_^@prHoASms@728LZJ4JfzIUSjul0`? z-*Hx?s)p{1eS2ytcehZ(IZ(W|pOPr&R_ai>V z_ltO4=Wbs-+g`P9eCgJ#d+ac>oabI|AhGf#TC!RZMgr?{bA^g^1aKd6V>YE^~CXW=qaZv z;U&kzG3RCYk3MBSY`*>Sya4=oqc2bAZykQ1hyTI6<2(Kpt7&;Q;iA6lBgDZ0@|6Fd zxp#@NGuyVbc4S@ALM2+XI6^9+qJ>a3&~2gr2MRh@1?JL4B{vXqr@#$UxM6fG%NUns zET`?Xold9Ib~ych#LMxHSUY0J+waW0b?KmLq7o7Xx)R5+pPY>Gjky)RN7-c0(+P#c zC%sp!wdNXg%rVED;m`Yl^A#r^qVKrH`tt3UeDoW&aS`z$`sSs(vG4vh^Y*O!T9J=u zKK;D=IP^XpwGJTSg5v&Hwcg(q;{F-uW9XUl$vVizw4%XPwGSs^W3a!9ejL( z`2zXgW%rB2YuJ~E{@5AT;dfvQTtAOb>{{h_ARCU-``Y6@_Wb1u_?a8=E~~Dxg|7S& z@qV54=i}MLFAeZ}*&^@wA?8x3Z^QmK6Xvtc_cFIR-~EykM=$Uo z`c~BUFiTw`AKuWs-tZ~2Pm15K@Q32v4qc+|Q zar`;`5%KeE`z3xYc*hu0E9o9i!3SKfes>U1^Wj73Hv<2;UK094;oLU) z%R~6a)XU)s=?m=zzVN&$t#=kZX}&n``j+&0Q`GNT)^Ak* z?ZN;0evd=<<$hrGY3IWm;@R7gci4Ac_v>|0KSQII-{y+>xX3fJ_DQMU-wnSM<X*BrbE6*uT%7M+R$kR(?!5hn){E-qlW`95Su&nSzdW0^Ur#)|=X>3DA4hx@ zF1D}Z6ZYo{ywo#%!1BdI)%6FVyVLu0F?4yZj~NfrUy4t_1of7D=g_>T>IbXndS83a z9y%2Fam>KqJmLL*N`8}1r+4RHJ=*$IorCV)9X~47r5(w-kA5TeutQE4vrcx_{Or2 z`R?0m7F;X5b&V!r*d|AzRCCjFJO@D^K@eIM0dGZlP| z>z48@%7>#o51amtqB{RH`TsWW@>nhX?2r8b-sX1b=k3ptZC&Pjb-zvrj<+w2 z=lmGQ+t1Q5R+BjVr=`t*GGP#yqLSgv$0k8p1D<;%hu zZO+w&sBd|Gw0!it!S{++d&E2D{;zy-L-~EzbyjpybJQ)Ti{9Ts+$-vR;EeCt*L*li zx{~k}VNTf``Tk7gmEfo78+HGe^qBeXm-4USJbH8)k*_>B{;LB(zmiUIH~0_nSnrB_ zwDYaYzE^&CwXT;uaQq)BF}U_eZ+N@@Yfd)l24DV zeHw6|_IPhkqo-`rC;vHo1I)vjml8hAhoh`JMjsUSOP~Ki_@=tw%YOUhGvMHS_senG zJ_p80_OEpRrSg+=QAg4D4=drxeDAXQzZ$jiu6+UA@2j}p<6eDTtcy~9GgaxG$afBP zz6Qb9*ylz#_cl1my2_K*;Y9sGb&GuOvOZVO^)2)C5$}{Qw$a&~u^v5iuzvrt<(10w z8@1l83F6^V$NT6f;@u{XnZ}>}1wMiK@)W(x?cfVl_jyyCk0^e0m}9vYc;j~m#P2)y zIp4eN`jzp8&Qp_oY_U>))(u?>`YH2^m35if&Y}8Fx>4sB{`fw8E1ZYemqYP=misnE zUY3s+^?Y-5U+&w3N1Gx}HtOXQzh~lVRsJO5XkNds|@6B?)O3^-bty6KlcfEyoA>aFTxUcP2k>0n-=l!OVo~CPl z13b8Ce!fTtmoLr>-@4!7R`443)swG?>*n^unnItmR*WCB#Y5fyZscFee^%Aaa^2kiF8GPS1DvN%BjzvLPXyd?HSOFA_m|-R>OQFGkA3_{mFz$J zj~e*j4E^WLo2q`4FaI*%YaNmJ>C51yC2+-{vj2_iU-+{Xz1#WXyyn*Qtj|E-d!4xs z_rdKQ{N|>L-!<>Ae3m-5Pv>*4UB5AkbQK)1Y4pVal}Yt#B}F&7~p-Y{-8zsB65 z0v}NPbIvF6#wP|JOURk6#ENgdzY1Wb$x%LUgGtM*R@akxbaBn^u*8Q!%_ZR;0$p3ZR#fD zyt}>dhm}6+M$B0e&dL|(RRC2lYvy}BPchs5Ql1hzha%k6bRI^3Ll0i(D!Pu_!5^A8&9*M_PT_}A951N< zN8hz{m9N;(yIvpu8~ONN`B4neQ@$ZCZsBk99=^#w({G3;t#5 zY|eRO+#ly>=mgy7%YL=UXY%cr_M;bbqs42?z_&H|^LFSu6yI0D{j+KNznnj5eXXLr z>TU3Bs{45^=ZyIp?%PBB-17M!Dj(?+zb~8jtf<41rwr!`D(_m1_e}j``S6B#&wk|j z>gU>l7hEi!$Kkw0eIG6Qb0*=n^Wk3c6P?h{xW20M`GG!-v0D3dp@;|BjkqE`eLkMe z{!#S7^6pMkH@5%vSG9cV->JL@7ZSiie7uA*W z#d-Txf(Jv7toQje{cGJ=H$GG0^Q*ij-#JuX*^YUe^o^3|e~Zqy=Xz!6F5wA^@~?dS zhIr9Nt@^-V+_o6nsZF;)&ja z+cB?0zJdAHrMMpcqEQz||3$nv&*u5yQ@qD7$lLPaUi;H9SH!-(F%MDkqwDvL`Ks%z ze?4uVl<~ahmJ8Q@YaMJcPe31R=-$MazTkf4!}HFc)Gz2eOL)9P9Dm7v9i`rSi@DJQcGT;63JSq6=V*b#4w)_C? z2X?y%|2N^;#*6xV^Wk34A0ocNTdYN#SASFwU*!_|X6d)zz$fO58`f1Sk9q^{eab!B zt`y&gx@P;on3s~iFJHcF-o<^qK7Y~$Ji%A5QM*oQ9O9qr_mMA8kuO#^>XXvDZc$gU z|J?8$(0V?>+uY<`$afBfTf6Y=-v#cIKHxfiQGMem^2uwiw-6uy06xis=UpH6Ipll= z{uRGwzV|--)C2bGd{I3j-+npXx$a}17wK>Aq6;1PoW|S~@JRVzZ~S+El+Nb6U+xEz zu6HZ=4ZYVb_O0!An|n7KI!yZur_#S?-<$K1@^VL%{ z=e8X@6TTwWn^PC+Rj!LEk9h*VS}O7*$@hNgJ!;h4EA(mi=y!Mr-`jV;fPFftvp-8) z|8iZ=IxqWw>)xz~|AznE{T%N1kiIG(KGb}yn1jH*nc*FK$6Q3e+iP(ig;O3^>hH_P zV=HdNJbJH-JY*HVu*dtm=6soVe2MKIiZl6m zHu;J6;X_YRH=d6AzxYw-U(ch@!#s!d_xavs<(0k2f8FnE{!?*$=-(^fm^H^s?pMAz zZ$C=ETh`-=H=K$0Nk4xb_eyznlR8U29zc107d^&o<$2%kOH{vdqqctq^_0+6s&1Aq zZv5rJ2X5ORpeTONyWU0pzgLMDejD*p{n+l`%ZKOn`|n5orT6&0c`5Lq=LIYj{0%e!HXELQ{%`%tT$mp`V|+-wnB)9;UciaK z@%ByC`OFvR6&L!%?H5IH@B#ev4f@VI=%(H~uf&IQo;l$8)}=hZQS05B4BW0beiYx~ z=nLTez8LyL&BMtTH>@*PetZ?);v~5D`hohW#F-zV#J{C$?M(x-KT-*r612O;Wq z>SNiYzw(v+VM|s#uRdMtgqG>!{ic#$c@I6n=fGL6`{Cz{&uu;&<#=Y_dHbg5^Dl&- zm*UBW=OB0vB>toM;)dcyhrE6%&Y9m|^W5eYxo4p(b)RsSd*#oe&pV)=*Q8E#n)vZJ z?iczz-hmU`N7rTZ;SJZn=p!-@hj=q=&&^kNtyw`cR=L3(ZKPBHel&{<$;6K;MSDso21-=;i zEt`*Ho)Pu*eCtx&7^^vVQ5WaDF^A6m%jbfZ7oT`0<`_E;W%()|yHUHpQv0zH_`~`s zy)yPO-*NK6HPH`^T9> zPv71$`uL7{4er%S(Yt)5vX7_lYnHF}r*Yq;_U&@zx~%l^gG#!bb@piv-NRbc{j#jb zpF=u;F860A{2V>ULVWNT{QPRv$;{Jd?f=rd+9vMpg5%#t-9$bx1Lwy@eI1`7uhjga zeDP3x)IPX;8eQHT{cY3XV@rJ{=0Mt)*?Fn=Kg(C;Po2=6xNZmURP2wfdpAKI-9(2o z#ru_yw=v%<9QX|V+A{I{Sn273nRx>F_DgZV@$Qb_WBBR|_@lI0=TY9V+vf4p`W|SXy70M|tY?dQiSC2X?{zyTp%txL0^%IKKgX7j>c)=KI(;>mmJa>qY#*cv>H6;r7o_uN1y$S%=5G z)DAq{J>taua{u;Q~cpn~{`1p0PPnPNy`Q9(pBlrC-a~_YN&wE0=@0+*g=SR7Jr^vJOou4g*kL`Fi{6DdyyK5}r}Nz}ox|V(tam`S_5hq4af9`~wB82( zCg1y|zQf4lPowj-&JtYM!w2S7yvOSQSOy>GtAE-5(EP39!6o_{9^g-59Cf?6k3;YB zP57#O^-A?|jMe6Oty_loBwh^tr4-jU*|&3*^bWD#S)S#FeUe-sab3^)4CTefhbPgm zEr5?tRsZh6e<0ue(!0CQyY-Z~cejB5cz>;TyI8cZ$MIX5qz^1#oVRXSpZ5syexCg6 zOr^dYzuQk)*M#2%^5uN{C0&E(-pd~sgyQ5!w$ zG`wM}c#ewau{mC#2cuqjzf%5|um4MR6ZgA5Fn<$%6uy7Xm-Y9b&{w~J&NAOQ6wlU; zzDepT!M|&N9OqBDo&7i+T^cZlcq+`uXB*&g?5yRt82wsw3zq$R~xX=8Ex%;{H*^Shn{7JadLpZnos&bw{=SY@4uD1Ym|^AO&rvJ9 z+_Q0XD*5)y`IGqRUF$)^XUYEbx@Q+0PeT8zew}>$q268Bi7wEmvf#K!{xw#EgUPE; zx{d^|oi9%jzuUu)cZ#_^+p#a=t9oDPP}V}v=Xr_Q;=J&zbt&)QH|CjR^B%p;fIMrS z{ALY55748(b<%Zz z_U}3T+J5*ki~n8W9oq?gN47Yxdh3ACId7dq_`+CcgwCyqCzuJJ0PzR;;=Jpy&J!IE z(XjP_sViyOl0 zUHf@cf1C#Qy+E(#dD_oH*P-|PIs29G9Ex{oTdx66Gg%x5v>#os2c6B8_&o>@=i4vw zoSy@yd%msfMOt6X|AOau?e`n}pN~IOyc;+_re67V@J`l+S+`<7PW8YA>Mi;9OL1+# zQovgW&z>$ipA($R9({T9nHE@#Wv6%-jAEh`k74Nh7tbF%NdNkwtdGgBhp38wR#eVn^N#Ay{sJ=8& zslO#3->bOSMt66DeA0D?>zv1d``U`;k~Yo9F=r%ST~EAc!@LZ2yH)&@HrcO6Ek14? z9&tVJzjVv__DlI!%nK;=gBI(C{_(*d^w6c;vpx#Htf%OI^WhEaCYgiHdpl12dj>x> zRy%IEu428Bc%OXdQ23%z(`RiQ75B|^Ei~6{%lip_e3zEr5T58b4v>GXksr^2^9S@_ zehi*baq%txzkK&g{=0qWB{9ED`O3@6bF`dSQcq$(AHav?dzYn;Z~Oek`=xc3<~$a7wM*Vt)|oFJD*qo)M_!A1k@XJp zUvuB~1nX@%PhnnLzWoyK(s3One0$70iT7>L&v%-4daWpamb7!|6bHJM>Z)_0m{r2w{&?uaG`a|E&P?HxNk!orMl7z`B@V@oG-6bo#eA|2D*hc^e0d0 zE83?&benjw;J?TJFJIgc574NkpP3=AdO#e18hrusdwFAD-|*8@o|11}%Kz;*blto! zywiI4m=X7#r;xuqE7E`D!-w__ucTWUt9h@2-w-ddK)-pu{7ZH7PQ1Is4eK<>kNXw;8u;nEqP*=! z$;x9(x7_3Y9CzF(#6$348(ei2UF_@NK|G%?%lY%?(C6>QJWlu~vS0bmq2A>#@$O3HdZhcatv@u*P<($j z)b};ZSLufALve<8`M%E~`hcO1NOkt_(W9)vd*sV2-9L#RO6Uyv{LBgLk&oOaZ&_h} z&nmoJzIWL=2c5HJzt5F?dw0>%_t}@Zm=|Mz2ld5@$^8u0J&Dg)K!4cb+)Y}y zLLS{DuH=jJ($Cl*V9NN^NsT+ek@@lz<--HNN1=}=tcM~$gRj|)xRNEl`*Wz@)p7oc`+Otrxlf9?Vm$!wz$?yU zKD;5HM&GY1p##vqoMj$X-@e(7AJ)TGde`&$eHqV#Bb=|WAEo;D4hlpa`1)IMKE)5; zMbDORzl2-uYw*H(e)u)$UiCsR;QFlIy%*GJ^TiF-i6S3H@A@(FO3feY@-9EZC+ofQ zue9}Lou}|;4&zn6UbC3~B8<aM_-2rhMeZ24->v8VH7k1qL3;(f> z-eEWLvn+AYpF_H8*Gndf>X)m$ySKm}ol5;VtInG_k873Zl=#1w<*U}wwGNxUjQ624 zmycf8^Eco__lo_P#0TW#H{QMd^?&`!^%VEF>E6v$(!X}OPbd8MT;Bs%<-;55uN)93 z=Hl~f&W+~>+CTfT^@$by6~1)&cv1DEShxJjx`u{$(d)#KzWKtyyYM{V#C&mHeC}uW zb@4gp!0Atj`yJyda8@hghWU{!@xq@&xYd5;r=lKf{h|5;2JoWKBTg#bTq0iP+b`(> z{4OteUHA>X;J$TvpBE$VRK0SpsBV$(UDkf>6W3Nl7bP9&V&y!s7oFrx(Yy5^d>iz+JI-6E zGrTPBFH)Y9udhw_|Fhq%Vm^j?KXYOF)LGs}-=%QIUipUy1YpT-+l=f`W&7GAKeQa zNF1o$PwDk|-m3ar^5H1=cbNw;-lboU{;ue6F8Dk+Pb=0ZW{HFT9MV&F<gL`R(O;^Edo?zWp*k==rIN z3sXftdpqRM`^5h{!RzQg9)ZWn$8WffN*sj0DAqIZ4i5qk8}C|o?mpox=g*%*{jGi1 zec*8~T7OL(Y|tllk9=h&`rFhek#E0LZ)~_v-hFz+y&2|USx-7;-HPXgRgS0rJ!Sc7 zJ+|uzii;P)^GoELUiV|KEBd&z&UyUOrwrYp`G8`5qUU`XcPRdE;oFmsw^5!ya>s9=-gKaREHxZwOub(Y!SZ!||GU!LOrN}a3I;P*}QqV%oUSL7J{?i;V4 zulewXbS549z?v5=+Lwpmck3X2^!o+wJz z2gP#-v)n6x4(0b9c&EF?_ssE5zI1JTvBC$JzjqZ~Uq1fOa~y?t*EwJFtg~6{>qDI( z>X_QMx9EHGyuXV6LqJy0(THjIlt9<*Veemc0#&t03YUoQD&dJjKTjO2ZMc16~{ZgJW zu+EkEy%@fz;+ys(FHzokv$9Wp*10h9MIFv>kp6Si{1m#PK03Y|aS!#Loj`AtkDs^S zEAgFkHAOw)M)pFRnPUT_bmEv{V!T(bKi@J+-D_P!Se-8W4E1!Hs{<}f^c*;2&Sa*q@Xqore zd1t2X8}I`W$4~3`uonC2d8Gb5W%+8}1|K=*M8D{otYOBL$yJeQ&^h+6W%X{OJF6>etFcJ{+Zdw;y#N<0#jeRTt`0 zkC-m1E59`!0e9u&H>?LxoO{jZUBoxP$vfsbkVnu}u11|!x|V$Vl?A9p@K2kA=&mDlAyDEm3+`_G|I6pnAk`=$PseCJU7V53&OXbYYE zZR(WU=vDgGofF@$d)^fFm3(;PcLxu0qZV(y74=H_6fIG|8`v)(@=p5A*|&WAWqq&f zB;rS}5FaPuW%euoTztW)z>DHd^W87i9U8TDH0BZU$BO#0a8`R#fi^4%}h%lA1~cfqqq;4L1**S8%H`T245dkfUZ z^6~S+4`a3K4)_c)2VjHud8{_CNWUAt*(=PW&$nOpF;hHPrmk>;eYuPNdH^r*BKA%9 z&;F13)+LrOmRLVUyFR}vag8t>2l~Q_5I!OVdq`;K1F>` zf1ZyAFdyamqx*Y}JLKEYtq$YIx0Uiy=gaxxyz

yylfin?AUp0E?^5boc09~Jug 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�{>_5iR#q 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 literal 0 HcmV?d00001 diff --git a/examples/water_tensor/polar/validation_data_reformat/global_system/set.000/polarizability.npy b/examples/water_tensor/polar/validation_data_reformat/global_system/set.000/polarizability.npy new file mode 100644 index 0000000000000000000000000000000000000000..cef15d667714de9abbbb4fdbdb0fda6bacf3213e GIT binary patch literal 3008 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 literal 0 HcmV?d00001 diff --git a/examples/water_tensor/polar/validation_data_reformat/global_system/type.raw b/examples/water_tensor/polar/validation_data_reformat/global_system/type.raw new file mode 100644 index 0000000000..6c71c85e58 --- /dev/null +++ b/examples/water_tensor/polar/validation_data_reformat/global_system/type.raw @@ -0,0 +1 @@ +0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 diff --git a/examples/water_tensor/polar/validation_data_reformat/global_system/type_map.raw b/examples/water_tensor/polar/validation_data_reformat/global_system/type_map.raw new file mode 100644 index 0000000000..e900768b1d --- /dev/null +++ b/examples/water_tensor/polar/validation_data_reformat/global_system/type_map.raw @@ -0,0 +1,2 @@ +O +H diff --git a/source/tests/common/test_examples.py b/source/tests/common/test_examples.py index 49abcf2f90..647bee2bbb 100644 --- a/source/tests/common/test_examples.py +++ b/source/tests/common/test_examples.py @@ -33,6 +33,8 @@ p_examples / "nopbc" / "train" / "input.json", p_examples / "water_tensor" / "dipole" / "dipole_input.json", p_examples / "water_tensor" / "polar" / "polar_input.json", + p_examples / "water_tensor" / "dipole" / "dipole_input_torch.json", + p_examples / "water_tensor" / "polar" / "polar_input_torch.json", p_examples / "water_multi_task" / "ener_dipole" / "input.json", p_examples / "fparam" / "train" / "input.json", p_examples / "fparam" / "train" / "input_aparam.json", diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index 13e47a953b..0a052103b6 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -34,10 +34,25 @@ def test_trainable(self): fix_params = deepcopy(self.config) fix_params["model"]["descriptor"]["trainable"] = False fix_params["model"]["fitting_net"]["trainable"] = False - trainer_fix = get_trainer(fix_params) - model_dict_before_training = deepcopy(trainer_fix.model.state_dict()) - trainer_fix.run() - model_dict_after_training = deepcopy(trainer_fix.model.state_dict()) + free_descriptor = hasattr(self, "not_all_grad") and self.not_all_grad + if free_descriptor: + # can not set requires_grad false for all parameters, + # because the input coord has no grad, thus the loss if all set to false + # we only check trainable for fitting net + fix_params["model"]["descriptor"]["trainable"] = True + trainer_fix = get_trainer(fix_params) + model_dict_before_training = deepcopy( + trainer_fix.model.fitting_net.state_dict() + ) + trainer_fix.run() + model_dict_after_training = deepcopy( + trainer_fix.model.fitting_net.state_dict() + ) + else: + trainer_fix = get_trainer(fix_params) + model_dict_before_training = deepcopy(trainer_fix.model.state_dict()) + trainer_fix.run() + model_dict_after_training = deepcopy(trainer_fix.model.state_dict()) for key in model_dict_before_training: torch.testing.assert_close( model_dict_before_training[key], model_dict_after_training[key] @@ -141,5 +156,191 @@ def tearDown(self) -> None: DPTrainTest.tearDown(self) +class TestDipoleModelSeA(unittest.TestCase, DPTrainTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water_tensor/se_e2_a.json") + with open(input_json) as f: + self.config = json.load(f) + data_file_atomic = str( + Path(__file__).parent / "water_tensor/dipole/atomic_system" + ) + data_file_global = str( + Path(__file__).parent / "water_tensor/dipole/global_system" + ) + self.config["training"]["training_data"]["systems"] = [ + data_file_atomic, + data_file_global, + ] + self.config["training"]["validation_data"]["systems"] = [ + data_file_atomic, + data_file_global, + ] + self.config["model"] = deepcopy(model_se_e2_a) + self.config["model"]["atom_exclude_types"] = [1] + self.config["model"]["fitting_net"]["type"] = "dipole" + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + + def tearDown(self) -> None: + DPTrainTest.tearDown(self) + + +class TestDipoleModelDPA1(unittest.TestCase, DPTrainTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water_tensor/se_e2_a.json") + with open(input_json) as f: + self.config = json.load(f) + data_file_atomic = str( + Path(__file__).parent / "water_tensor/dipole/atomic_system" + ) + data_file_global = str( + Path(__file__).parent / "water_tensor/dipole/global_system" + ) + self.config["training"]["training_data"]["systems"] = [ + data_file_atomic, + data_file_global, + ] + self.config["training"]["validation_data"]["systems"] = [ + data_file_atomic, + data_file_global, + ] + self.config["model"] = deepcopy(model_dpa1) + self.config["model"]["atom_exclude_types"] = [1] + self.config["model"]["fitting_net"]["type"] = "dipole" + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + + def tearDown(self) -> None: + DPTrainTest.tearDown(self) + + +class TestDipoleModelDPA2(unittest.TestCase, DPTrainTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water_tensor/se_e2_a.json") + with open(input_json) as f: + self.config = json.load(f) + data_file_atomic = str( + Path(__file__).parent / "water_tensor/dipole/atomic_system" + ) + data_file_global = str( + Path(__file__).parent / "water_tensor/dipole/global_system" + ) + self.config["training"]["training_data"]["systems"] = [ + data_file_atomic, + data_file_global, + ] + self.config["training"]["validation_data"]["systems"] = [ + data_file_atomic, + data_file_global, + ] + self.config["model"] = deepcopy(model_dpa2) + self.config["model"]["atom_exclude_types"] = [1] + self.config["model"]["fitting_net"]["type"] = "dipole" + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + + def tearDown(self) -> None: + DPTrainTest.tearDown(self) + + +class TestPolarModelSeA(unittest.TestCase, DPTrainTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water_tensor/se_e2_a.json") + with open(input_json) as f: + self.config = json.load(f) + data_file_atomic = str( + Path(__file__).parent / "water_tensor/polar/atomic_system" + ) + data_file_global = str( + Path(__file__).parent / "water_tensor/polar/global_system" + ) + self.config["training"]["training_data"]["systems"] = [ + data_file_atomic, + data_file_global, + ] + self.config["training"]["validation_data"]["systems"] = [ + data_file_atomic, + data_file_global, + ] + self.config["model"] = deepcopy(model_se_e2_a) + self.config["model"]["atom_exclude_types"] = [1] + self.config["model"]["fitting_net"]["type"] = "polar" + self.config["model"]["fitting_net"]["fit_diag"] = False + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + # can not set requires_grad false for all parameters, + # because the input coord has no grad, thus the loss if all set to false + self.not_all_grad = True + + def tearDown(self) -> None: + DPTrainTest.tearDown(self) + + +class TestPolarModelDPA1(unittest.TestCase, DPTrainTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water_tensor/se_e2_a.json") + with open(input_json) as f: + self.config = json.load(f) + data_file_atomic = str( + Path(__file__).parent / "water_tensor/polar/atomic_system" + ) + data_file_global = str( + Path(__file__).parent / "water_tensor/polar/global_system" + ) + self.config["training"]["training_data"]["systems"] = [ + data_file_atomic, + data_file_global, + ] + self.config["training"]["validation_data"]["systems"] = [ + data_file_atomic, + data_file_global, + ] + self.config["model"] = deepcopy(model_dpa1) + self.config["model"]["atom_exclude_types"] = [1] + self.config["model"]["fitting_net"]["type"] = "polar" + self.config["model"]["fitting_net"]["fit_diag"] = False + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + # can not set requires_grad false for all parameters, + # because the input coord has no grad, thus the loss if all set to false + self.not_all_grad = True + + def tearDown(self) -> None: + DPTrainTest.tearDown(self) + + +class TestPolarModelDPA2(unittest.TestCase, DPTrainTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water_tensor/se_e2_a.json") + with open(input_json) as f: + self.config = json.load(f) + data_file_atomic = str( + Path(__file__).parent / "water_tensor/polar/atomic_system" + ) + data_file_global = str( + Path(__file__).parent / "water_tensor/polar/global_system" + ) + self.config["training"]["training_data"]["systems"] = [ + data_file_atomic, + data_file_global, + ] + self.config["training"]["validation_data"]["systems"] = [ + data_file_atomic, + data_file_global, + ] + self.config["model"] = deepcopy(model_dpa2) + self.config["model"]["atom_exclude_types"] = [1] + self.config["model"]["fitting_net"]["type"] = "polar" + self.config["model"]["fitting_net"]["fit_diag"] = False + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + # can not set requires_grad false for all parameters, + # because the input coord has no grad, thus the loss if all set to false + self.not_all_grad = True + + def tearDown(self) -> None: + DPTrainTest.tearDown(self) + + if __name__ == "__main__": unittest.main() diff --git a/source/tests/pt/water_tensor/dipole/atomic_system/nopbc b/source/tests/pt/water_tensor/dipole/atomic_system/nopbc new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/tests/pt/water_tensor/dipole/atomic_system/set.000/atomic_dipole.npy b/source/tests/pt/water_tensor/dipole/atomic_system/set.000/atomic_dipole.npy new file mode 100644 index 0000000000000000000000000000000000000000..2cabc71e2166e0e0d33989b0b4d5d744eb55fb96 GIT binary patch literal 184448 zcmbS!_g~NN_kWwT2MHzZy;FKVuhSL{J5B9H0~N|hI~7f7A{7#{%Bbgc2_>5(BP*MT zB7_%xd;bCF^TW5_xZQ8(-0$a{YhTynS+c;xeUUdm-+I1HTA^X_A#qyPDq7rdb1i)p zt?<=x32{NO0juLe!~XaGbAzJe!^ZzVJ~C)c*!aJ-t&H_mEG%d1=&Ni|`TzTybe$W= z9J;E(#UY09A%Abxdg*UTF2rcfG^2LG&EMioEbF~!{{gY zQ@8iqCVaCOLX)rf-`JoMbnMF`LOXU4P@%lU%xv7iBdF<~u;+0zq z0V2{U=g=)TjDibAjG0h>PJb?Y$gcL=4*rioVoLeR)uarISZ}Z11T>|SWxK&wC%h+xPg?O_F!92@T0!C!|I&X zcb}4(2R5K}*kS#c+=?=RIKeY0=2}BAScD3K=8-V=b9<({vd~S z*i@swtd9NRdgxhkCx?WhzI?YH;J7W0h7-jf(D+QyxK8ID7J_~O+>WB^R`}4W$EsN@ z>g(9cCivcajTUGALfd=viA~gN(mayp6UVR^mQU>_S2$Ow;zw;1GrU+B+Ph}5ZO=nd z%<3C1pxdMkL*975;V!iwI1}#>z5RIm>MFfJ@6NbF(&jm#zV1EAWTL65BsNSOjdRD@ zsZcTE2Z7t2Q9GfMQ;B`6HT(Hp3dOVxy0FVe@{rl3f%?+^FdugA>m!X7ser{<`?Ng` zK4=B+zm}lBzJ--Qqf;;4Xd;H%$^NJhTh2Go(~oDNn68V5xtBgHhf9XJDCZD~6|7qN z74K#EQ-X26dA5lB8$b%Fol=JuN!@Pok zjZoO4gkt(O&19^+i=G@g!Z6O#BWck1E|wF0TB;4h{9c|%PM*nznjMvBzj{3F19$g< zTu@HbL$yblAE%ZNMd8!SIVhjVv(?FWH>(~Crkk7N8KylZzK50z2C?kZN6{La-no-)Iv_^E!~{`0 zB_eTD|6~@3_ZJh)S51cod})^fF@bs%b3LP;S!;&#N?hzw?Tw+y+*MlIwEk$I-^VQ5Z)CG-CqV`6} zdAfqCpKwO|7CxPwuwPc2ySrgG8lSKC(!jH)izMuviuziw6xv$hoeA6R@H*6+e-2)( zn9Yt4)uX!1+g0d`YmG3tTLz6`cW4X8{Jsb%MlM46T+xhxX*T@aUuVtHIG@?017QMl z=(NgBC}yTlCpqG*$KB~wi2B-*XUTSy6~T*MceFq3EE^%eb#35p&KWe$O;R6uRc_(1 z!CV6M^}`&uKVQ;7<%?(x^RFI< zN5#1$U|0*aBRD7vf8+At+z>tv@|s1M*S^a%(N7cQJTY(wENGYpO2@XNajx-wL{c85 zvjF1-D5gm$8HQz!(}InoC}u3KnA=xB7fdGSqkKdI=ds@Nt>mBHbX2?7o?zyv5l5%K zJcru3G}WDjSlW_1iIFIu4Cz~Kclf-Z;+;H-*}PwX_P#cxVtj{DK52{8X#Z#?tSNLr z`5Zd6mx@U4q`v)gQH;IJ1@3F139$0acC@{#4~*|8+!kb-f3BeY^W#EEFs%#)j_7QZ zv+e~2$S#N@T|0lE+7E8AW3?$Cc$XaU_35v6LD;)for)wSqrM*HtI~V7a^TJ_SJanK z8iS>`FOUh!uTe~#eYb)!IxpZ$&EhxWFMft4wX%0udBcNc-G>Y{tvfauo z-aVUIoC!m`3y$nSn_rZ$>FTUU-SS(*<6$Ksn3;wPP8(8!E%w z>1};{PGYe$8oa_E(`VVGsIG$&nefB;Cf)J89gWYD%2Uu1ThBdXDS+~6A6pG8LQF}r z!!p#@>(oC)r7{&b0xQrM{`T^PIZAhE%p-hFl(|v`JVo{4g2*t+x%?y{)J_*XH1DH) z%*Xrg`^t;BOS;r2LF45Xy2lL zX^K|{2)j?b)>;`fKJ!{CKx%L)3(2AA{KsJFKKOny zk>fTjfaZPW&qS!yxWLQW%t3XTtlrGrrHnYb8>gVYv`%=Ca|%}Uv(tH$bA7cH_`@Yy zUTuwHswD2ytp_(j!`MxfbA?(Vb1|Jot?una`#MYeOeT5c1ub}%jp~ZAHiZWBGFlm% zj@HKhl30j&D@`M!pP`s3#2Plv=VNp4;_HHI-qS%xV=Cxukw^J7d`M){S2O8_70XbJ zbGR`mWTk=OgpVkn*~2ohf^Rjmb;8G#l=Cmi+)QI)-8v2RB~%yzM*sZa+sZ&R7Z*M% zv94QLuxa2T+D<1NTg@(d&4c^5r!cIauTODhCZPa{ky}wdQJ1TsGqlY*=(R0s=SxU2 zdqDzW)p|UqTisk)_FTe5c3nX^TabD5&Zf!C>WUT0=kwDw)XXplau)NWx#;yRB_4D3 zuzn%D&pe_o3dcS@q`?}_sIINg6reKkB^lc*hw^!RJcoUmWe!{I98fzsR)@h!aC}eV z*BO-al6fM`-`jxktLvk_T&L`WUd?kvyzwgk18@ST2$B6h^5T3YZ*Nxi|2wW1k$;cb#(s34%80apTL?=UE+MK=b@Zs>Iv-X znF=l|oKeolhWQ~eXD#G=+TwQX1>oDzNqX5#7LD`p4lihZvVy)!7q-UQa$0>e*ILw; zefk=X@==O;$+^fq4+NN&mq8+J^sW6HeA5tLxpDBn6c%i?#)3l&2=xy@ju7uQ4@SFVXxvx9Z#O-mh~2)+WC4@;f~3A`fHgG+P9Q9uRf_TgL37Gpfm{| zccSiz!iwlX$Ti47eF;WXfydU#Q1WmMis5%X3{q11)cZ8PW^Cv(r;Qhkz}aXws{OK}P>_6V2`eImZl!0=dl&ePr zV|s|48Sd*ryCxjdxk3Ehno-WDs<*SFpCZ_ZTLPL3?}~@qx|(7($WuZwJ51LS`#~Yt z+h~XC(yiaeNzs>JX>To2K6QyhygeUWVMl2hYRCF`I^-_qQWbiNU^!jxq`)+1E(PgI zRn$&JNFaFptsx4>9MH8)so4PW`qBi~?$1Oyj~V;I*7#n+|6dbo=l;G)?5Tqw`ztbl za&8{!2Dv|%$=BDlXx?9R&7`jk64<9FOHo}$`=ns)my^8VL=zM<*;kBBkrx4Tg&wqD z6*51~F#!VzGR5b9cUwKVKGQWIuT~0;Pto#&WY&=;W^yY9jZa_?KU>qa7QmalaXybDE9n%D8!rcl-JV=sr?}@{!j$$a4}%2W#D8bgm`!#t!UO zn6djmZ=-y&p1dJ8-i1swd>E~lr2Qiyni@0gC>n;z!5q840`d2C|0XEjfi)ZqkZP{inX96mI3kl?@?bP8{f8V zi>qXvBKTd<a z3C32Um{2}SS&akBh*d^0@ADGLz~CgJSFr=-;}^V_el+?{?f2q$Wk;8Z(mlBvuv@JX zwX-Eh9+I|Rrv8^^p?t#i9aufT(-gqh)H~7-aMp7_5aV8k`kGXBjNBd2g%s^_ zbbOE2>L(*(D`AEfzRvY*>?iY;U(=JXMHn`AKa+LgrcJ3}&UvvY=2DO%JQQ5VO*o>C zVm{t-1DnSu$UAipRM%4VZ1TA;h}}ppKy@8TY9OU~5p7DJj-tLS{L`46b`_kuGzqPZ zhtE=3k?0B{z3?E)$0+hC)i(+Op5!7F^PnXjX2w{+H?vAK@1yQh;8~?E>=486102_~ zf-UzOS)yeiIxbHi$|rL-xU=`~IH>l$2WD{RB&tK%x@{=tWbjdtvTLG2_oC4F*gn1i zA0ljEf*;R+Vji`^f>kCQtmcQx{v#>opGo7RU1;9ZMzzS>zD&67 z$wjq4*)Ww#7G8z&%qldekF_?B_u+M*^C1YeBlg>lT#LwsKjpb-d_;SVnDO*F`u2M+ z%DH>l7HH~@2H&mWsGZ>r6Uo1baP~W`2<5Y|E_1&#M52n!QD1b97n}$+BfIV1qwV67N*W8R5@vRN(@|gA_Ue#C z9&+D>;(Lr=^j>mAzG<zDHM5<4@!DwYU@IV=%so{~&4c~qCs5l494;K||#PN6wXKGsQ&#@^vpT;7H9 zDZQl!@{=@~YxZe0KGMt5$l0y&Ae?*)#hmE_VzX2V7Mfh4ST4xh1Hk>=N>WaQpqx8T zCNlT!$LXi)2$XZyuNm+=R2D}3;!wMlT;Tm{I6^0%z~_ETv{$pZcd1Z$3EyXX zd`*+9c5MaddPbtTaEe+&3{3T4$0s9{v)z>=WcNWk*!NEv#hiN33gG^dr1bl8~v0urbBHC^ zi(E>2hw@2tn!?+_wyk&(f0X)IEP0 z^M6o|@@ctqk46pDf!WX?x?k6-imdFF9sO=Jz~E5zIlI(G+wRM}zFS9n7h_hNej+lPe zXf7&rw=vn)A#RMHF6!&xlP2hS#-Td-Q_$MbUaH2Lid)!;ji=EuWx;i0Ho_mvzH2$5 zz9u@Tv7;|ViQ_JOjh|pKkbjZ z4Ib?iU?O$+cyXh2IV?RP&df%*D4*q{wUEwJBRz%AsIOMDujID=esX1?4aMxYujM}e zIY^%Th(UcluquaRn|E>L-g% z<}iMLHOlG_+OHa3FJP-r@1fCIa;Tl|VoNwn*F(T-yfzBQ@01!oQKJieU!id}U!D$E z#vYJxMf{!bRog=_?SvUM)WzS4&s-u4sU-`+qXTRaeY{fBM<~$OO=4;XW6e8xM z2KCGy)xIWE2|U^-uo*W3P&;*R{mE6q#c(D!9>v@!d_c2R17WifpqRuQWv1=PBL^Sh zeP-7$5$G~eqSmSS8vn=M`K)v-fJDB+`|xQ8pHtFZ3o<3w(HK_rDY12K2g%eR{Qdi% z*bc6Y^&MC^9E;j{ekGUN7@)`ed|grP>T(~+#1v)Va^^Fvy|<*~Gxe!UnfoU-&wG@x}#`Yjy3sAOG=i4P;3y|w=B8d&wY}75-Pl`QSEMZk?`>P z0eE=l8)|27YZQB_ev0P3HAi#$E&VOe=3*dmi#m(u^p159yLLj4x1}3j4;EOAzgv2v z3U;IVsGY^dBcvdH4qX_8j~Co|3eaH}H1~$0uaLos87Bt!J*Gd_JTJ!5+sgU^;-W zPm8`O6K$u-pz^{J<#X{DAJ7OLNR7eY<^+kC5#>i0d1hjYXf7sH+#@G_8aW;Vm8h?( zz$t8GQXPChj@N3vjSKN8sH6*z?L>X8ZOGvKypaahQTSep&#eqd{y7!+CO4rq-(b8H zMo?ad8ID%)o?CUGHP7yzA=?!4plmum zeytxig(c%}fi$g3QD2Yl$APlROx8I5Z8>bb*gmhFd^s`1ty0eh3{z-i3w;0W15M#d zf?+&(YAor>RF?GfD5|U1XDxUa%R}YwPiWtwJiebUvB`-2vfP7Wj20P#--cs6dvScv zv(t4mSSAw4J&=fUzPn7AUA-Rz{%X2te3tCI%d`LZnw&j|j|J)9&+-%<(qXN9BAN?f zVg?;1vCz_u?_>Miw}nkNe4wLw8_GHE%OCp5HjW9$yP)|x*w;#`?yh6LRr}Cf7=~&? zyjvwSIas2;Bt%wFVZ|xzLdtg3*QZ5_Z12r57TM^D#!$p#2Q==8VMS~4`dL<&z`}n> zkH6vwM&}zzrq9XMi+<3ciSI-H%&{5YBU%T)UGO!CzIh%*Tslrp*E6)nOm^k4hUZ-z zzU%m#;;$bTg8K6+`du5pAN6AHHO~AMk?iC*1vEa9yBbJ|)JbN2Qx&Zrw|H$PF-H*& z74oCGu$z09JoY#YCfkcpjJCc6Y2}&2Sg$aOnRsUvnbDQT=6usYIfrzeqkG$2*>y#{ z4Iau{&enKu2c@^}sIHgRH6%T65*USMpnP)ISDkH(^^{|q%L`FoHJ*m>_>lp8CNd~ywx$H-&btWZ_q@>>Gid(C zlg`~vKT9j3e5`g~U}N2Bv{N$@ZKtbEd?4wL472@_irNViPy_kf4@s{rzOJl%=?c1I z%h~qtD^Oj)RpMz4ibFstzNh!$;SPFbbQi>RO+@W%JFHV7k;++e%{~qK zQiV|MZGQ@B>EV>`QOwN-JCJbqr@}k& zy)RL#yQE&Vg0!r}*Z2yHSAs@N4cQoekz(U%$l+}eBE0~#(nnD~`C&E0Ff;{1{PFu^ zr}}GXhs$~B5xb4r32%vD#z#yDf7wbj7atXiK{VhHuW7^uFiL8<=(ba*cO)Jnir)pZW-C>3-<=qU_&ci>Ki0kpSu$HeE zZ5Moc-jKa0pX_PD=b3vKt!5L9mO`z~9JE~w26}>T`D}7}p&A+=L*ZN)so6t{1XWNz zFW-AY*?>6+IMkt-XxaTN2%5PyVhX7Cgr`Dq;OcrZ@fBWU9dX{w{nq$7eK&qTD$$~i z_9j>}Lt%VACOH2-HPg6CrU_3#weu$|paK?E@IeqiU#?zSPXsQUfoU@MTfFK$uI#j^ z9p!r{j{1t4{Ezm8r7-7Uyj>`t_GL+};zW4=5!6oYM@iV|agQ8dijT{33+|J5Pv^1P zr(4imxH2m+a+$zf8?(rkPG>ZR8zy*xsL0%NQ)}!|?dBiva~|FL$w?W*bJ|m+!ZaH8L!$S4xpU7MP%WIk}Rtg zXhAVwpYyQ^uMa@Sg`X&&Ra4qorOzy|X*oua;goOdrD^QShhXsT%tiARHDM_<1lCgZ z1OZ&v%7ft0`H)uf;rj`P^ksO8#CAWi<-;+OxkZ4_;jLq;{VLQD3@GCcusZF0jDk1*&U- zh9MIO{7Tl;OEQelo8tA1PihHN-NkFZdCxTFTqX@8oslRXxhpmBcqv+^H3{`~@Ad;? zl$^_w`b1DWpT@sCD^v5Fnx)&IG1S{923hCj;l9apG#Bm>$?VaQDUhs)=Y9I8N_f8T zCNG!MgmO05)MGQ}sIs0h4V3e)<`Qsjr%Su4du)^Weu?>McJ*s z*{H9RZ6@sJ>H_L|dmb8}$A2Hv%+GCMQSku*)K{yqFSBXs=6pHTgXU|h zrxEbpxj^3Gd8i#NT@jcpx)Rn)8lg3xkSq%a$p-4!gx?i8{UHF}=$)gR`$y3|#^$}g zBqe<&i(Y}>W4tx;h$~~$1l-5}$K2RelV~2yg@k7sXq>%$Lg1$>A7seVMpsV=CP|i2)O@SpV z!4ijAsIMY(ZB}s81j=~$UD>QdlbK^~Aa@)8E|ibcWpAGQS6F&nS%mYB*)d|I1S!WDyj)*h0v-p{d-OC+R%x(c0d9xGM-dr96DN*+Hy=nqF z#^s(IKf@O?qweka9AKw*C$Tdyg-nS*sCI4ZC!C#ceBiTDJlZbo=FeqLefF^PG=4v! z{QLN~S9_btwTC}YUoXDtKyr#R7)<$!Vm_ILw8oCFmw)Lcq47CiD9<7s7sH-`6oQSp zqtUaVahenuRhpyu5)7CO53ePGgJw4B%TZt_{EJWqao?>dCPVTBs7D*q1ILb|d;<5E z&~uVTa8zz5s(qcPDiQuv!FB#zf^xn({Ws}WPJmsx6Hz{|dnDmoLM?l|qXJzwT~p~I z>qFIP_%&bDm$2zk8o}pHb5G*udj2yG@%WUcfRDi@luxVX0=BgF2RYrp81>b@B8zVR zc86reEk$G4KXC)uD|itWJ#0fU`d_kHh30nXn}@GGHC6>PHGU1Idf64tg|lrVeWs_) zw9QRW&fOmrnbw1q(0}wV+D-#pD=F`iCll$#=TWb}eI#Dvd(bM!-=TcohZ@2fd0)7) z?hmSc6V+p7?MYy2gzw=eN5z76lO6mygMW)))&xGX;?QbXbpYoqN%g?)p9utI;d2Qc z+at`Y_9+dmGe%=*sZt2Rjt{BtGcJm$$vIDhdUYsAU?aoY`^p?udUCQ4b8*1ukoNvd zS$vE-><+F(^R<1oKUZPzMK~p^gZ77_+(WzzAKhTpA~}?^LT@|o!Y2v#-^n{fS{qj_4Pez_ZQ@X#h}tn~v;+A*akvq$jMk6%#w;)z z|2|n)k`Bt*RsTOS{pAp^-s%i$NBPM#kd|>^29xo(_7>Ja9?9&39}$hHo$x_ueR!)G zqW-F(m?<%VFsr_tWc_@L=JbT)QCMhO%p3B@`{x=wwvw(mDQ6!0@0lR$i zQ9eJ)C&1;o-fVNI9Exf64+XWv)8yx^E|l}5741y+%Hy+=w|h`KM%xv@_mwqBMBYU) zDOv}prn4X{mQF=6@%{-=a;lj2?8E0ancqu^(ilm{y@DHn6p*Q^_Z zZQuXhBDb~Ip*7}`E(aQSs;G4sK1ceRDFQeDI~+hTvtQisH~VMS*TRxFJZ^^ED>Oa_+UG(1D@#}~BMH@hLaPd<98IG! zH-}L>L4}rZVEr@N`|>1;8QP>ll6Gf9$uWGNEkIp`H7V@j4r+%3mM=Ba$7E-}4pghF zqVc&I^qVy2+OnuVK@`(_Wj6%>NuY;i98q8UkJ{1teZjDEF20WIww=u)Y_)+jN20!d z=N*E9?Yqc-hGA%YUXFd?dMuv?Hwy5(Sh{bgfOz&Hc)L&w?Kf8bTEKpP@Ptj%eNa9< zk?}0|zi4(^M-b(6v&e(UidL}gX}?j7k-P;_oh=Wh_jOTUp%OrU$au5dBly|q<2Ylm zo>mQ8|N5f7A{9qSwyYd<-^cqpk&}|lp*;)AKdq!#{dis23paTczr z4dQ)7(b_n&YzjQ!dq_4PVyK;OwYDtYDVFkD7NB-kmP`hAT8*Se<8y$UYZ}_FW#`ac zmUvFp(q^(t=hK*O)*3Vy+NUbn4gC!A$3PN|bCH%fQOnx^$yxaMU@~tbn>wKc{*8Z& z0*kXMCyu?Xe?h7zgrl_@E@QXAzV&lbVzO)l&f%<&HzRea!V_3Dy7IJzYaSl@zRJ-r+Tkciq`*a=kN8`*p z-9w(qC4%nKXcS|iUPY?L@*wz^8H#x_IRdPtCNcLv7f@Z!r*`l{#2Ud&Sqas3;A|sz zLGl9lF#a1VSWe%>dP2Hg25;6ke6DWktj0V%rJ$`g1dUIl%0sg4Wei++fRD?&zn^1v zm+a{JtlOye`4pB4ZwXp2{=KQJ6S=|p&RIaT~Y5ad6H00Unk;c(A!)U;L`~; z^7ZEuRM*a!GFbnji#zF~G0Hi5W*q!_o8Km8mV)NX)Y%NwU!0&px!+LCw}M8JwIzwc zC2KTa94%S+kY)@SZ@f`EsWlusPJ*Sp!zj z)1Pf1;m;?6Q$0R@Rcg(JimUu=$I1qR*^ypd3FjN`aRct)ckJ)|P9+gpdCSAu##zbs<^sOF`@6b!y7|{?_&&Zq$Krfm&=f+v zi@{)t6zWUVvyy7ptN~SJd~Q6sb%a(=HD|d^5@>v4o*yEQyXDx6_zINI_3>{A%&e$} zF)u**414_L`TVz&nYwhN+HW?5!`a{AY)A>;dlx>C2`lf*LfF_SI-ZVfcVuF9bK#S) z1IlMn-ELSnynyuGS%7Lc9qk~t59`5M{3Pn@j)W!L_$>_^VUw z=BWG*$bOba6IF81I7hA1U~TyVOgvKy)#bf$7Oh_^!lvE9?|pTy$zjO@-*`fa_(O@c!FmrVJz@(lI*QSK?2`*D;q%=$ zWMCR{f-8E%Z;!}a~kTaasOG;HR=ivt=6J;I5yMS z=ik{NCvy$Wh4kYj)>GLLXf?;Vkq2 z`2ETkvu)tS86mhkQ5xkOB&1FMWXX~8gZLR)t5zi4t<*y!LocEEGE6XlEjJw5tol8u zuL^-UD$z3qmd)=+*D1^7^jOoHdbkI(P|VmLLw0Sh5zkR_Ey_pq#u2)o?1n7rhURqE zCpSu04RNe*Y(P2NJ}spFx2)j%Q+&VWyz~wl>=(&$8}R!8;=H4Tzf}~Bwehu#{`YLS zrFoGA>EmnIfR+MAwI%4!L;C1guxzdm48PI@X~zIGK7EP>@cP#Tm=#ol_G62f49pA+ zhYyx3&^p|={TO?d3v~OJ;Zg&aZ$4yD>_a*!e=hg3X*i5U*iQ@ zKR!0*Fi}YaYBTsz?KS(8;2g&kCVt99^VR*znobiMBg6muKI`$LCqYe4t5 zClxl@i~4$hNew=Xl<|rV<8L{?3tl3RC4SSEf^sxI`%1RJV=FyYei5I~>y4icW*kq3 zxq#=?dA$%^Zy~Vu8~z>gf&M*|wh{;vKZNS47AWTm{W-?+x%l3Wrk)`oFCOyjQY%nh zM??GQ_uC#YLlmF){#kFuo=E7z^&I@Z`n)#^jDM_#9NiI(<}@~k0};suPZAjd|$UK$FQln9(kSVxsn&v1>=|S&yy_is9{&f%(tVpvu$_jWg)y z@isjeAlEk+qMWxnX|hGR_UuDsAL>i?)JC>}Q$bpfjkkH^?^W3sYU{QI>QDVb?WnIE z9}6xk!Oi{vl(Y4XB&NZxa%0>kp`6V`)9HtH116b>x33&4Ip#Y*izD&h8r04P$3|`_ zpCF8$4Mh1Qrj635+KphPErMc>dT?Py@>)*jg@>s2kC%O6;?_i3F71V4ihs%QCcM#M zdt-}HUsaD6v~A{9!=^L%Zxn7lXbF>ji$JF0ExP}c_pcd#i@)V|{lL!xger_8(zORX4i}DQ0>lIIuMmt0@F6w zpmi8}bSY)V^Vo}Bc>g^9i(MrA`*PT#Zi@PP?AA*C1QOw$Cw}I;XmJi)zAHnToeWVs zLS6%;VVMZT4!fY3xpTHasC6@a<86r6Mqz3%O%wEh<{etf+YtYA3ivnwM8=XV<4U;gcnYWEv6BjfFhwnsFgIc-C9;`;geVSA;n~4~|LFZ8} z0?K6Yb2v=-;*IJ`xyQ$RDipZu#V(+_%F1VP{6;e&tTqPKC7fOjnQM}v;rlz3&n+dt zwgH`0Y&2yJ>Pyu)9iH|ya~zED^#ylQ0SOxay&%_3`2F7h;-%qj_H=lD2=6ofEjc7q ziUSKhj-$CaIzET`D{sRNuE+OIrDqayzCMb1bcdjJ)Wo*X#!GuyX;eGfXHMX0Gp$9Q z;67n9+HdUe>TZj9l?RJNx1za7TWiWq&uxI!Zx^9)j&1j5Z!*;Buk#`(=h;K^nGfAc zUt1kT>t|bG2%E0FmnekvqnryA?s942WH>Lq6tyF!z8&&%V!0ctU!iuM1xUb>zh@~& zMF7?Qc2faNuGXWkCGfL=D${!4?%&6i+=<^w9xe$77+nZDhx*WZu3qUf{=3paaIeZ4 zjbYJ@7?|3x!os%CL@^yzb0O)O8LTYHNAq>>Nhi6>zXLAwWuV$aCpf{^2up|+*^b89 zIrcRuQC}aH ziZRLK@ep-A71i}_VG8%dD-j6WkGJLiT>+r1wgV=wFhuKTujyiFv*M7`wmqn?IokW- z`|7jg$k=^!KVj|7jgVY^iOLI)jH0;U#l8p=--~ZFp10g+sI=pB*ifMD24?XoC>FB7hkj&NR0nx z;FNa@QOr5lOfYL3qX7j|P+u>~+UU!H127`L9F5QW-EB$}62%^MkY-GbVwi1#Mq{`KtcOhD}z zkN<|F)|^*VI}#t?tMWDB`n}skd9ey==Zr=!+aP5R4Dhp(uyrTFVqX9~J?RV@pAgym zRNC5ramVkOVeQ>Tvk*Qvog>31@oxy&>+EJ*>}ANCn#BaO(=MX}7b8y40d0JrtxkZj zzn=tYiEs?V?2IWl(aR@zRI%0y#jHwCXI=CjQ7Xi>C+Pqcx_g>juf_L`A2!NC>3ku$ zcX-}k0w!T(}zUapJJYJAWZ$$5+s*+bxJFeWVtZ#x0x3&*IlUkPQ2>KVlku4?j z(R|fc^MTuqNKl!$4$XW0p?@TlbW%ZUd@X#Y!xK7~85Nyig4R6Mx8%z2b7SpZVJM&M z0}{Lkofn{}3Ez)e{PQePUvLUmm^-5Jna3XwN^8x*gZmnN|9($BoUQ$DHESR3K{?M* z6@|OEqG9FZ$!Ps-%b5o5A^GH3!zxr)-SKnK(zu3(ofk(j5o30+-rW@DG~jFeOERmO z;YwYQlC46uKlJ@dZ$;XY?&@6B*W=BDyhCCe!B(#q)qW{;A#{e!0%_~nsGZN79Y95U zF6`Q8f?{$j8Hrt!PTTc$P|Wmqw)EkYB4pNXM{%kQC_EC4K!D-;+`2h80JEl!m`AuOmLQ7Cz#u346RSzGmuEf8$bLqA*6q)=bTkG&X zTy&8HJUHtHTb0vMUzwi85XAaBiUpLZ{M$4%7iaH$<;lm6uR+cnLA6WG z>Eb@*Ig!gljeymL^oc!0H_{S{6!2UeGWte38}>637kr<8eJ!g_VY!3S>L}; zCHxX-zuguTv)FnH5!+%60;|%{_-qy4%*(9lCuye@P>j+URqA+Z{H_%TU#HL|I-rmr z2kHZHsGarigP~J%8HC)AM{_Y#xq~jW4`k-b_+5yq4QEK7b~4NR;Dy>*ojnx>9#lgK zAHJ{N{xqG^@q5%Jr^?V6&Iwj#>7lVK>^2A0CGJ)PH@qU5RdzPYr=n;vdupKq_w*N` z{l?~mi=^H@1ity@qIR|jTY}8ZWbkxe$;ImMcKl^01==-)p-1 zlSe{&Yv9hW0+i4A5kGe>mjfAf`2Lu=-X@a75?P6rAbRm}W3Sv$$CI>b zq=)~>qI|l|8OwZ~L%jyqp}yk&3xTB~vTWno9Mn#Bzasn=5{Ako{H*rv`%+TY_C(| zGx8#gzLU*@6$be4w(HG52=zV_nfRnY)K1>yJ)oaBo2+%1fMNu1`@+`>eX6eQf^xnU ze3{(QZy>kd&qpy27RABEi^s^Ye+R{Ce!6uI?0v8p^tzs-eCDkyB`0ref*TEjs4v0Z zMjG_raah#ZjM@ph8pzVVM3ax}R-$$m6%@hjz{9-7kF`T3ME5IRrx5s}T+WcB}kcLMqARJ%<7PR@r1Rgx&5fueWh^Oec4>rptW>&hW< z@LBka^gB*ReL2W>k>9H=#=jkg?~@dE=d&ZV-^nLO{2pWV1p-pWD}dxMbf16l>lAog z;LL3P=Aq-DeY`w*D#oQdMDVlh2?8l>#e@}9PbC@EC3Pu-?Hd1$*;fsI=$@6?+;i;p zT=ZAm~AC2l(lHPUxLr0{{CJ-?>kk4{gG2>t?Cx+q6N*- zEZY+w2M;(G@Knl1=<<9Ul#k>2%k;6AIs}wdqH%uNv4`j6CdIfOQ&2mW4sNhfEQMuS zzChzsb~6*UOmrh6Ka)|+Dfb<0giQjCN%)z|u<5n1HdLN+q;;-$ecRv(DgFQZe zNe07s~%J|#=SkimL1&t5?k0S1=^r`HTvIIk# zGGAt@Ec;_RooSDcg&4-!w1DlnVGmK!WhiFPwwsi1wX6r)%8fDf;03&#VOju+wRnn4L>S?HpE>9M6rnL3@Xa9pDH&|a%b6&jmZm~7 zB9{u&CH506UosxMcup6VLYW}G=Q(Tr&hhWl-XZ13x1)R zhVzm!D5jbu(X-v3hzOa@u$&qOEn?}LdPu@#d|#A*^cR(iA0*NGpU}4K&^n%T*}Wib zI}PPC!!C*GlQJlEs6p*`nr&vz8Efck1$<2{vons`h)-pyy^g4_Hs8n}Wn4AU6n2)juEGW`OYeUhrfjgtF z00u?@#k_7_%acgn4Ebd#DCgoEsZ?v|C|%J}hVqdaxk4q|HCe-}Qq+!CnLkZlHvX3S zG(Nu9+){zH^B!^k+m4?%eQdCSlujo&Tf7C;wePD3#QzkdTmO&WS(86|jJTEVp;txs zpmu_J8USB)S^t?RRF{L~EbhuBJK*SPyuI&#mqR;RPC#?S3v^83w^|JsJqx(Waq{Rq z+|B0+l#5DJ6U!u&bKCJ?Sh~|6E;&Y0tRH(5kWO^3PJ);J@OLg5|1F2IlyH{#`ylE| z?_M@+OY9+k!otxryK2cI7IkYLeLdWT+8NN4g~t=M+`38kGnP-p%fwR3ZK zE!_KC=we%!!dhQ8wb^LejusD03 z5r((1Jo>@C8pSlfHUOJ~xhz=F3!Q8Er-;I%>(;PL#tP*VwRR!U>kGj6o(PIL(Idrn z6sUl(C4OG{|Mihow0q67n`8>{fo^kK%-21%uzKy4EW+n1+uyyfIR8Q_wPbgn%4t#ox zQNIEV8aacilB`+a4l2`iE{jFm^3$yupHN#flB%IONQ*pHz07`qTG^$n3Uf3+%q2G8Hzt7Q&9};F+BUYD+FymGN!y2OA+4vpcfJx0ub zh9yXp;#`v0t+zDsVJ_I;S3&i(Pc^5)WztOE1@HX|&z<7F5O;$x%ggBetYT`z=w%yL z@)YlTubA#2vu3D*QC%mB=gnmiFkVqcN><_fhre5_SiRaW`dJeH4o0Rdhadm`5I*}9 zv^E?EyNOGG1T;jXqPaMBY?$O|*a116hGLH2wUZ6LY2^q#+l0nvW`jLs#qiPXuK3*h zITZO?VTOF0T_AwE*>X#A!aGbA3sy7iAHUfKimWem~y_)OrPYnG4${_fAH??-$@n6Q%@$i2LSj*Bxl=h60Aq2RZ= z39$N^UAYc!@Jo<>Pke1ok9&j6oN$`5V<~E@J<|x!_n+h(;U>+gbr&EcG^dSf8guWdcQerxGfS=PIRDpwp|Zl*EzvdYCGQlY)}Yc z_vYOq$EVq%Ij!?u1WT9S;QaU)i25ZtF&i2svss#rFe)=kc09N`ajD8NoGWo@$pqbo z59Bphf?zeaSHz1Q3%6!pPH|AoduHr_gIW(s(I5Qm)6}2MWW~ogR#$!O{hrO^hr$ zz6?I&m&0 zb}-?J-=vX$H+r|iQ|S{iuqtnZl>~5F^_isE$_F4?x{ALN7uhnRF>S8y0-pPd0Y? zqnHm#SiyWNUQT`RvxR)0R&k|Qz9;i+523dF8)woa&n$@FiSOEd@H;%jbD?z7F%%27x03~;ZKgOB9E zQQI5Ev**4Rds4xPIQs;8e8v*Q7uh|(;aM|2rAd{G*cO7e!USpsM- z4hg7{o6_6ZMYU#BkMA=$M@|y%U@H%ppbn<<2)N;X~~Un_Qa+W#jNMb zdq>g29)$Dpvvb)iok4rwHZYxlf4}zSq_Y}>KhsI~}-o^BT*A~3icocc6L3!lBm4F_j}iFpmfn? ze=v%;!@=TnkoRtjtQn8@J(h?1wNYpZ>EGtS{zyDVPByfJ7DPXmtc)$A1i+4Iq-jVIC#J@X5d7sFV%xfHu^9j_}pUs|7`ZAgOPZa0K zq-@v2i-b0+o`=`7?ae%BH&6u05c~|)hnJVgE92d8LE#Nr8{5a0({)?CAY$isG=`3w zY?)zN6C2jY`LC#hOW}9xEZ)1#c#jn6^pjrx=>eZFiJ-Ou&hX+gI*uOA_dxTmxzU!r zuiDRvuUd%8B)k-ZBfrkkM-?a0T&#R52>Go=EIbrn@3{PygWJ1iGP~t?-=bBsk!-Q7 zfboli2v+k_GD6IGi3L*{a6{)YzGzkGxu_43HGOElzSaLGA^A#dORFFnLvx8_knRqn z5zY9y)15;eaQi_jNT^Dqc-(@gGV_b3@b-x|YAbR77VtC|C8fPn&|H*+39#{XTcP(> z6e{!Crjcz?nFb$Dx1qjYv+SVHzodX2TZ7_Bcob{-ar|C*{7e*;`4V`8hOd!lAIGWbo<` zaXfbb^2P+DBDU=Z)I z9}CvQ;xhu^Gd>iZqaq7cz`H96A`7k2+K3Z>Ouc-<;fq~0>igiJA5rCT@N>fO-lp-< zQHu+b{Omz@GV1#Za~E!FZ~+*WzIXPm%S~>XG@$^k3 z8ATe<6dH-zs@!#$bAMzS=w&#fn0uoQK+x#`tX^J*;<@@u1q!7!pix~2wKZ4U3&@f@ z?u?B-s7z}71CH7_F_M0A6)H1ja3biHWwJ*v%+UHtN}dQQ2c0|jwc)iIMYO=`%`=kj zFbVZbp+}G%yqyYDTyLW?uWQcH>FYPLwLkBoGN(BiWaHF2Dy*P_`js;#&SIjoVMVqf zs%OS9U+S;7K!m?(wsM=hV8$x=Pt=;yUhP|WIWlJI1|E%Y79L&tm2 zf*N9Y@eS>AU5Db?EOeP`-`NjM(QT-$A60YU%FPx?<1a#MY`p6_CMGz8M%$&LIsJG4 z65Y$YLuOQj?@^!fze<&s$U}E2e%45k_lVBNXH~@TYBg$WVSYVxp4m_6w_OQRM!(J4zOqA}e6@+18_M;_KMY@k@)EqVV}l4qvQeu(`he*Q41;ud)=TTea?oI~Totv|{l z-^a4)cW0n@;<)J`lQ{<>PcBA%_Yt1V-VZ;bAK(s(IjgOo+-b=nA2K7+IHyUu)0>vv zH0|SN6i>TE0$VxC$E3fmMP&k~?PFatI!NL@iek3p52Jr2@MsJwO|F>!5gqlcupI^M}+D0M%3WHjw>u$OGM}r_r9jcT@>>^S6=CK8nWK ze47dsi`haQAAYXj(z!|!sGG%x!~IZOYD5g4|7)c&6<5*uM@FrPdYl;J>c?fGeoe~c z2UVSFn0*Q7WfrMTBkEsD;nJzis9!Db(;>Wl1?1*tq4li1RF@qRUd@^^`_R0{3CNRl za}&5B+J?#$YON&al{U^@`^5?MOSZfg&dJ#`YcW$)rZA9lSCz@qDUNs_u9<7av=@1T zXOkR?r}X+XI&jd2ScEsAw(LHSV=||fLyaju&wuZ6Cbv2p;q~PLRL{Fj!Z4h>8|ELv zd+hDYE5Tiusks61W_1$GTed(K#X1WfnNb*s*^@5vVOw4j)@v zA;tez!c(2kQusNqe0DO-cX#4O+}$tHQ?YHROc*DGE;0E;5?^|vw!Ag`z)0Z_ zd2lER)$>sJKHb>n0n7T^(fDM&$Yf9YS2N{()da)5>){lVR$9sCPQt(41_dKj=QnR{ zF+ULf7MM_Fs%dY|!gN(pzvc@s0BOBYR#%Ge31^Pl!P={QP+laCV)i+a$$ac??2M`7mTmPzD}mxE5y>L5jb;$FVht*D)W4p)!{3eF6Z1pkTs?CN&}T_h$t2UZ8;XFTG9RpIL6T&|S$Z*=Uaid(|z zW&QNDR4V$7+b6lM>*JD{Ow7n0#dE_jhjc5Z!kT+xXg$B<{kLvG`eG?Uw2msfaWCL|4Qq7qjA-@#a2y zrKqiliGJYB`{$||tMI?(7U>Z1twLY^H2QdDR7k7LFKa+w8s3pK9Y9t9Wd>i35v%} zWCIhm@Foj}-lO&7IH1O|;_RWQ;|VGg+zk-AQyknbi=(!TYwKySL^0JH#kn@~tsEwF zHWtzh@p`_aTMZ|COv#dO?@`~2{AJ;mZyM}Bd2R8|HIW!_T97{u2Kmv3SWmT6|lYNnNr*^&I=VkxY9h1HWHupqTq6 zN`U4WVUYGLKx4RY^-tQadxF@#$H(9i-hZ11^9^wpW(%WwO5Rp-@~16@>qoqVa zqKdA6Sc&2pU!TR)CpUBThE}68;$!xV|8YLpK0AQgQvUnGvNB~Q+$8v3?yHg?blvNo zPNP`-O{Z&L%czbyhjl-|_kU9L+^BxK8~3ItK`}qvH=k4gK?ADea{%k-l^zWcks)We z8ADg+`Bn- zGAvK}z;U{Z??YDE^wH3AJJ?t*gy!N$Z#_AXk^^xnU(va@ywRF?UTOhNo%yJqqMOO^ zR8hdj#gB?}z}Y8F%AUKTdmyGVtz_tz z6Kv8QL^1zu_`(fQT*I;sr=fV9Zm7d~zhQ3Pz&I2S|ClUwnCJ3JlcZqM?Gjv1-sESs;-0cP44`^$%a+Q+<qkA9>u&q%#>?=Z4I*?kV7#aKXQ=$TCN7UCiwo3(*jM}cts8F zJjUO630hOY@#u|(vE}tHGMS zI*QG8`^;|;-TD>OBB~D6<8s{rK73APOLEOoJyiV~u~3X)OjjPo{5NDX3=4a)`eW}< z-%q6m!_CG??6L#i0|bqwfn1I@bo1WO!u)cKCScu?Kn4|_qcUBqf+5?iom~8W7{&A2 z>I60adKd;wQ@EI|9Yb%ZVx$--DB$bvcX@`;nmZky&X|tIx#x&K9PyK6XJ5IaGAk!JSYQgx{S^*fSq~ zw`~WjgGy)&mreXi?-cq$t*bK{!&dh^s$-qbDj)4a_0X3R?B0Z>%-|J%Z_DC#Ti1aa ze`eab2GwJ5D}+Yu-%VCVY)A3L2(?lHYe#nC27b11kJ>VtUwo7FK59nu6;ztbIUVZ^ zlJ|U2JXfbqXI*Ybh^e?Ss%Lqo9ay>W!NDbBX#I?R^_V(iI9r`Ja{hw2T-iKAFLn)dZOxIv-YDhC(~l# z)GS|S(A9(bUcuWxJPl#Y%D5l(eMZ|Jy5^`h3z9KHZ5_6iha=Kuy#GRYgVv!`h9)$4 zh5)@8g5vR%sfOU4Gf0%SBdW)0a4K66G=tnct&HM%(t3!U&bAH*7P}W@R_EP+MFxJntdHh-2P+K$V^TvjVDMJBqnK>nlBTJ{jZ;CZaNhX$fR*$VxVF8}E^Z zhEutppN=Oh+Vs&l%lGNRJ*%?LLnHVa-|0j%F?jfbE-u`I=3Qm-T)0&^7k=MvM%TGb zw|mIv_z1|l%8%l)^v$J{Kf8ne&3x3>@a9Xj;K@Rm7u1B-YQ?Aoa7O0Slbuh|e95QF zQupKgY2g8UeOey3m*qD6CQBwAKso4C-kWp-S7(!Ba*AladM5D0m%mByc@w_w{;9H? za92$rBWHTh_+0$$2UA3o*;Qc=)Yh%^RBry1zhru&9E$n#eg_uupq1uE$D?{SZ;*pp zO9k+%Sc2x_n87Y`>!~SouWv$So(UR&_LJ?MpQZ3KZvm6#*m{9ETpu6&+kIbR2V2=( zL-Oa~{rs7$4(u&Uf&<_1d(gAB7jf@ru7HOD;;8Q%R*JH1-;<%&X){`5S`In%snIHy zcOo0rGwtINmg#WQGCpQ1!{$1n{?+8-(E>K*Al@GaNZh4)Uzftq&d(f7&rf!MoHw4p zj+p;IYu-`Gmo&MKar>;xcXTheU_%}szl`2d6qyTUHCZl>zPMHmg{V`CS z!Y$KP zs4dCrOREOu zmt+b1!EId$RFA{mRkWz1gyV1>=VAF2b)a9;mp#~wpPgGI>z) zajy3Br6oSzUFR8o=0szlomia7ggtJv(Ojgg`9dm^s@UbKwP>HPS6!X_8g_xH-PLG( zPHRpltD|Q?>SKJo$C>`7HLi}#`8U4j&=A+o3RKn*lXu_IvwUU$L};WF@9xrEyjLI9 zT*oFwxI=EveKdwH1_|u%3J%=q9goJxF>sW+EYe|C$}z%)E3F2dKN!RruLQm%qPVh)x)Q=i%gxQ1ZOVcF}(BT8z~xnP8O--YrXuQ7qo7} zWpd@95bArF+FH08qCs-xaIRm<(a^HU+m5O zC7FiG=v&NZMjLm7)tXu~r^h~aa5CqvgV~y%fMK4#UyN-zbaUNHzLe=FQRL^CxcO=it4DMFjp)#QtRzbsDpnEcx zqqY`ZjDxtDli2J*{0!cNMF||yp~dXwE48lrK|d|wEk)-Q$+GD}fCi)tssu>meM*Um-tTzDn}x;t-k)?eI!%6uQY zLS_HXg04H?P+JQYH;}sW9k4gj6qV7@F(iT$k3+N`UO&%1$G~OtL$oKV68$b8YM#an z?GJXHZ%jvPOnfj6LSBz!`|B=HY<#V;5ClOrKDKDDI=Y{bpJf8i^A8doj~nRx)8h7o z6ccS46gm#YW2t9BlX?61s=T*eFg%8zjam3lKG^SozXuW#x&#D+b-9OXSJS;Mr&{*w`LoFQN}K*#H{nx~E^bYu^U3 zWA*r)xw2gga=j@^VS@^x1 z>0b|ULt5b%)_O?N*Nh%})xXZbcEr@-E{R z&^38sE$FOUj^@j;bUieC@B#185<0%3_ZG5|uld|568QR}bNh0Lj!WS>{+NT>+7wt# zHf=w}4!=Kv`tJJLgg!BLVw#G0f4Jm+D*LualxrQj8(kNOP06JD3KoElpCy_Ls}*hJ z`>h&y5{{o4kU4gUJ)UW2+0%Csjk8PTYpzq%GM0D=-?tg3oyd-!f6f^R#phl-&12l- z9v)EV6NbicQGEe?e5=T6it+zaALsRyyyBg8JeMYm`W{xF0@l1T$QY0Sx;Xq<-ve{pIxjM);)OjM@F;w-5$IZIOF z4x^YS7EWOH^Hi8_H{SaV?aCuIBk6?S%K^2u{!Sepqaqh_MEESsNkdRI%pwQ2Z% zZ+fl-Z2WVXv+<`eYRktZ8eWOa;ja0Q|8X`p|4k;C{h{+V=An3ghE9j=>gBX=%n%(r z{9i4=f1f0~SG^j|*PI4zvTC%6y853+G1vaw3Eh*$sL2*>RAyDbH(Q^xkmb&Jh2pW4 z7lIE3UrGBwDC*a?R3j+(?nhU42cj}#b2iW7rq6pWNB|oeaG4M*Gaun{Sh@8#k%`O#Hlr$u?8+ zeTEA7ye>p-Eqj^AE=WD(zBq!)i(4(hg`0r8mWb&TQWMv}uB9 z44*Gu%hH3A+26Te(VpbTAyqIq9uCvq+(CUWoxYX4>O2mzXYuz^6lX{gg=I6~o|-4B z=Ym@Z=feJrh{k&F{OnA%3{dOx6dOwJuevK|$O|l(!!Se|nXl=yCZD7-Hsnhe<|KhfC zKXT@IyrH=}?xVg7O*bYho8OS^X)96RLl1}(ca?ew+q537vEuUa;GdaoIjt!Nl{q!! z2eQ}8=x92=Zc41}A#K)r?4G73iut(I1yZ!^A6K*|8kO0(BLG6{CXsPE8&R3{oEv0h zq8ylL;A1&(r53aX*AW(MjOL>F-92(&%M|!faPsO@s@@F)fg${I~GPCm@a4sxVp!;i5QNJ#w z>aj+J9MGPqhRSTXgd ztikCdJ6{uxb7@vs*NhpjY1Se9%x&=4zpjQ2y#FJy7(n&7KCh*l)=3b5Z+}2agD*~2 z8-!X-VE8Y7$NA%GLGJMLAsVQiiRy`vR%8tiRG{tv{ylYDW)2^B7Qsp5=V%P4cdE0Q z_ojpH&;ishp~_3-y~YO6Z3kxs7J zO?p0FM02_~@4n*Da|~U_G5;c^8mNhj-%^(>uj@-Z#0maGix6nV>Dcrv9|Q7_utAk@?$7 zP+Rj?wvsr}A_&szMP+Kzq(Q)PEhszS_kjHU%h;DWp>Rb?2Ft{B z!#G+bkj8G-D4=-a7j|~nA;}TN4C|vA{F+X?J%Ug@$9?n3 zmz&D)ap51ZFIetGyuz_LA~ROVVd>KE89gv)h1+1?I!RL`dLcjTkX zNg8%F4_!0r&CP%=VKF8>B8JW_Q+2dCBj*gE$O_-<)@m9i%lfLBYp(|yAJqwypt*Yo z+}OGW#bZ7@MJZ~_Kx{ITjM)nDSs&5b*j7G=%1EnQE(pN& z97vxCy!%UZ{gZL1tx3wY5L^D5LzJA*+W5)4Z@kh%lr<%OMP(WqrC?1#FZHd&bK0ew z38Ps>bjHpbs9)pz9&=o_+@*txM^Vhz6cpjxDlU0+ObV6hI=P)%DtUCR|A}*3&9mn~ zWkDO=T8^J#VCrYdw*#kXfKUj-#%X+E5xX`zA2u|!pnjc<%V&v;N;q~&_<6Rm-V}~o zax-V1Nj2)%`1Yq{VfkITF7V0Gd=M`#M33n@i+;MVf~<0@MYwRKKJnPYY+ z2ELf#W2de^jfn_XQQG+k^QSu&KLD7Ug^)=Ljj%t3#<>HUjZ5V=?j zwPo`%7_3Y)sbHT9s>kdemqbY9Lv$OSuaq_FEcsd~i*jCp#^=wiTO^@+9Oo17jS{R@ zpE-Hae^v_s(uXJ}QxUn46Q&T&VvX=VQ%CSRX_B7Iq@Ut@GBL)*V8MGE^41?S)Yi6} zyV&YiY7jE4g!)x{I}V%|`;pWRd@uLfnqAyMIWdq+!q1a<&v{EuOLIx>yD${bDc)V! zJvJZdLrYOKhJ)k1U{=|BdOjZC=U?YjMc)mk(I&oyXx^(r1n8sPHoUiPXQMKo_!hAn zEl0SIXGWni@k`y96Yov1`FgL=7~bFXhMem-Ncsw zo3gN44OEYmLkKfzTuns1q)Vp(BOJw}yXPl&Q~h|hPB|O3bq|Wkg*EHoRgVnXlME*-l2!W-!S76I zRFCljZ4fN|L{>yCL-VC3(#k#_-bbvmgSSY*zT+Zu1sP7h5tH7vu znx#|#zTa}~X&NZ<&FWgV=N5{2-C|9cab1}uY@CPo>P5pDproP?d;ZNuoPxO`<7&0^(<8pn76eyqIOH9V}L!g38e9@o;eMXHNX+3)C-jElc9RN`y9V z#n(;2N98rWXYx_ZzXR$!w8U1zn_>Y}X2ZGPT%B(j)I+lY#l!Km zqpi&`ux--}6wkr;9n||;1$9}5bK(b&R&dV#QUKGYT67Jo{7;V+4GJ;lh2LT69utA` zmNk&QRv*P&Su`FV>!%RYCHVf~<|Y@m^6UurLp~qsm-@GfP&Id$n-m$3+A@9fm1`K2 z2MWr|(Krt`a6#DY01?}99F_V1+c#5uYw6hwdZ;aBsS*&LQ$@{wrlNY(qTJYM$$i=o zD2mD?J800!>r9yV<*n$Mu75he=rP7ic6LeGX`)K;RcImo52h5eHx zDOU4~8`RnS@sTw7y+3Np_}+J_Ep~%b^`HVUJ==9wuz(F`xe;Gyp?V}o*U-#zZ77wT zjp}ipzYAzh2Q|J@fcn*$zZ_;Lchfnhr%}I7f69W&j6S+`06%ASeensXd6vuRYOe){JFZ_2LGo}#I!9#2IB5VCcI4-S7(JQ*Xi z$Z|PN*!}%Ds%L%wR9GtdkWA^)N8?=oLJl1AcGHXZjZm3i0xl3^HjCUG!29|0`6cwS z{~pMbmq+n%g?^C5g5!yR7(Rzr@9v|slXgRWE*_r|jbt{dev~ZrEJ;w2 zVNX;h>qQdO1yHgj`ZJ2TzRV2v?72)D`SJ5ew#HE~udkomA1%qSaj|52F~F!3dwkLh z?Vnxr>cIB58fP$t56!zB@4rv~%KzoA%f;6q^CFYkya0hN#aLrY3{S##X(HI+M|;E0 zqL@>pwZKB7mD+#6`>~%t2dIhT4zP?mjbhHT$Yee)sx&!V1npZSZmeb9&+pM`Pw<|^ zuGbWH+&Dl})C|yktyJFtH(P|Lh#`JY!Cuf3s@{*$@_IY8Ha^_bgufFf!I~R5zq8RZ zg{9omqcZF8J8Md=ayH0E$3XprMk|^dYra>U1 zzZ4RFtVQ)KyQRYfpJ%{f5uCe`voEEBf9zTPXe??g^9S$$I*Lr8BIhY;OL~11O?Kp#pR8ca6?H-^Z;?@rG&jCMf1uT^s11=EQM3hpz{#gL7Gf`5n&I zsrdP4dowNA;E)W5d~t5QT_uPtN+1xOw+PMGYjPMg^N*0L*YQ5V$$BYUxwMwnFKUM*-0rEH7HefMRMwGZD1m>1><4d0{ab)Oz|?3{geinyKFXqmtF8(Po*rZvH* z3{4iIsEf+n*qa2Q!KIc;ALP;a2uVmnKfiL9Tk+(0ZQsz|r!OoE{{x16*t@PYU|QQCt-Zi?%;O{jx8!gGX6P z>}eT(Z{khQHR2cZm~(T>5;Uh)3msYC1z$*A@)`ZUA3rlnCTnDnX9w_o+CH~&kQAK< zLVnh$tukIK(OdhTIC;sV@llZDKvbQh#3DtJM-#-+377Hgsx01y% z&(M6i4=90tLo8dfb^|K2>r4oQu6;|p|2{zLXKPzA3?DV5Ya?V)zbx|#Ky>>%&h$Tm zsLcG1Sxj!=1r-W``TWic9OgARMByqis2*uS_iwV@FlYaJs(7VA-&JAFO0P3Ii7@5lF7&m8|lxLa?NyxZeYJh$Di zTV#c7ZFIdpho>ByB6&2P?Oz zGAv&+vsB?lP8!HgT8GAG)2X%W#7{~OojZqOK3`e{mrb2Oe{upk&)0d_!=2NC;P_w` zsz*n!9L`6(lUki#bbi+TQOr!$G?|LwI8;xsk~z4DU*KkYuR#4O6KSAriomYRlu#@_ zp&6;PyL}(asXC9^`jQpI1cy(NyI1i2s4w3Sk>aW%@C#@~ZN0DG0v@%25GjRo3MTSy zT-%XKdLr=vn$ugFgbnKJvpbeJ#~PzAz`oZmqUG)Qxq?TYm7H-C2`pE__l*Vqzt$_b zMe`e1p>y5Lggz1z63k*B$)diC*JQ)x@Dw_~5I^Hlxjg|sxnz*9kM5zlh*@n3;bZ5C z`hI0JK6yhPurp8uCLRhz-_0;SSpYs|e%vPpC8%F<(#OeAXgj2Cu0~~!K6pTU({_Q7 zmOASD3EqE)cq%R+?nd~%$bG3M;4pq1bLJ4Vo{uOlB#-+N$uu0P?E@Iqj>m&yt#^_eYD98=N;1L zhJ&HNHd;35fcnli^&dS_$lH@REriPK-8PvSjhhN}d)3j}$SJu@a`My}H!mNR>9bx5 z*#W8av!yrc*WukWAX=r6w#yztWg1>>?lLX>$ek>_47GKD?;deimj`QKeE->h`De2Wq1h(Rx+*k#CoF0@6gFPP& z!GB{L!M-~)7ksCypZy~nJ}f}<<#$930tZji4LLa9RC@3%SJY_^{5HLf+5%19f5#1- zfP+tY|ICW~T4t-$JS!3M+oK4@bMxf^&fv&i(7LCJ)_j%qE1I>ajg~%;L329G<^#QE zu?BRf_~W%vkj$37pTgd|;@^Tfqi4F*thPf>&o5L@2j?j%7@1(H@l_Fx&x$!B+~!R= zOy3A!=N_*vgLLydWSPl$G(MlKWy$!KXJkWUJ!-3*?=WbecZc5(kD~pL$^0X%&}}NU z3YviG>A!G~tPY*WYM%L^aejU}i8(eIv$yK_UO_D1H##vYi#-~|&*l0|%V1XbRY>x+ z1~kqz=NYGB-VS*F=OjA5Msv(y$(arapX7(eF#S|IeYVrGYf4=$Dl?S7iypn}&1~l4 zYoeto{LJ)D9pyVa3DxsrBR^C5q7Dm{bI>@;@tXlvbYOBDT~V0?+2dsW2YIF%h_Cgu zWenkKz&G09){Vwcbk+{IvO1qsw|Stpp2IqrV0n{tHrz(Xpzo(=WN?>0jJ(Bb)%$Y@ z%$g?yWAd}nIusUn0x$7}5PAteyRw_sTR1g%!G^)>D4x7WWyIxAKD@X(jLJBAPKNE9 z1K7Sf;;8R8_Z1SO+Or&czic$8cSSZZDKQh6BZ8l$k$QNA^Vc$hv&sU$k8eJ=gR0l> zhDgmq6wlF(ww!&-3dnOvM`f&e`=R$TlQ|#!#Q>Rl`Q&s9VL|OQ()!PpVluL(;js6* zGm%~)hx#5~=my_CrI=EOCMt8NjQ3{YBUN_!sVHjePEIuFZX2SK<@nsPSH1+)Uxm^3 zP%l(ZT}%PXyEK&w*y86CE^BaL{M(~+u~!y~dGZDwqMSNNqowmunFJL>_MxSf_n&^_Vc%3Y&`iPkrk6d=%qmiwIn2Yk$k$ySP}3<6e|6`eF+A3v zO0ur_Gl4gFzp>c&GI=DX1inwAP|UhZS3;}30`HxA{2WNT%Vd`HR~L$2nxZjW8+?+^ z=jeckX99}3+iW&nZ7T?xYpPH@EwA>nnHx5NQCKcI|7=oGAZ@F>AdH%$dW_#TvnINd z&TBcu!SeOVQiG*U=3}ze_w(+o_x-~43AAQV9x~f- z-+x`mg@|{SFe_&*ns?JVjdPM71J5pepMQ=(W3Hp)RB@aSxYiYs{crJon-7~;ve?lmHZvRlo{Cd7X!kE6 zp5nQvUlQAu$xeMuSo;9y89lD~z?|R>tm#J^Iw!>c*~2-gYTlLqYX@q}Sw(_nYZt-U zciQ(~Iwf;E&HO%9` zqNeo`YrCpHRthP=KsNe?bjkqoX3B;b8!Capq>DWs$WD;wc-5NKi^7@Bai=@ z`XBsP^?6fRo_~u<{s;f%v8|7s2o7eu|AYUE=kZ_H!&0d6E+=$usps)u&z&>KtY`S1 zXIr=>Ectbw#MUdL^M>;q59kjNfn!!d=r_)Z$A7uk_;IbxasDeo>Ns%-Yln%6IRB+{ z{{fNa@n7*X@jmLyQWtp4jf@n8S`ga4YKw2+*?nn>FJga10}R7g#E z{FnA9&VP0C_^+)z{%h3?od3$@@n1^+X2Qh(;Jr9;g z63$8mk(K|=f4R@w14nx2kWEQA|K-c$zoK~jm(wYn|C*BI%`N=WNBjSS|I*;`UyeNf zYvUsawB}uT{MRiW|5bYm=f5uV_^)Ul{}oh$^Iu1cFO$nW{%h}l@Lzv<{MTFmR9Z!F z{!5L=e+f-1q<(TZ|K+X_)8)DABlpCA@L!hv_s9z#|0T-XXT*Bidp!Q@_7#1mJ%;mN z-=yW?E06!0?1%GT6+HfHkjHfA5EqxL00y~aYp?&YclIL{$ z0wp%Bpa`uoM?P;_a;^+^IekNINoLn|6)m63E*O@eGEaxcLzbXEOcJg~*Ls&DZJDl6 zDLvRDiu&I5Vgk`U>I|~f62<&`of;IS>?8uMct7?+CX>Zqxy-r#Wfa9cpsWL#bS1o4 zj&nDaPY!YHdG8U9yPb$)PP$VK@wbG?^NaYsNck!+Hazy5KFYI0{aSiJ6kISt&ro(|?96660n2z&$N7tCMrQ&Dk&G4%zo_UXR zV8NGIy5mzK>Q|kVA@Bnq2^Tg%WfDXG(eLUm@Gyph`emsR$BgZtkR|F2#q;5)9`rt5 zNPd68*UR7iwc+FA^(=;Wcn}-QPi=Z(T!aX>^LZ6&>pQ7I!u5W(BL>o z%tN-IG6SP`srSDd^sD~?RHoa_5CW`3pt+PE#iL@glH0s@7qfP#Lj6)@t)%+8I5Cpw zLTv>c{vTu49oO?0^&2Q@Njs9ZQby7G-lLM03Q?peG?g~VZ0|`^NIOcIg}(QiNs*CA zMp8!h3K{Wyp4aR7_xL^Ezt8KO_c`a@`@NrY&%JN%%<7O9yj3$r^$cg5u&ps+aQ*#z z6wj(-;jBPtG2}fOMrA?{88ZbNT?h&ZNAdWcDFMaQkEUzd@&7NU*D0(&eHln?@~-2MgGwC??C*kXmBK}*N*NndLYqw`fzzc%Jd zf&0n3bWR|?9)99U!lmoN@Uo7ZLC8{sPkT)_hfna#{oihOMRUc`5yaj5j^^zI8 zk{m{>KjG(JMN(&Jf)BSIcp9O;KObAde77tk=CNl{-{V_X(4WeD;=IEd#Z%rf7A}}} z6MK_T)UVwFZtU#o82U4GF^cDU6wo(|&eON+@H>w)O?{b1)d7o}8W&K^*|WHQuX`r& zLlWQr1V@jAU+EiR-1Z?fUjp|#z8pWY3^4DT&?(n%k|>IH#QmfaVHm=xIKP)`-q=_rtasZ)c9# z1!xREr(7UBeJl79A%^;%obr`KP0l3`Cgz}iJ^6J7O#HQI?)WihM)ZBViR3^KlOY zW}VSQ@=L#Ngrt6rgMBIx#ah(W2l~09jSEOBxe|5 z;fCh)K$shUreP}NEmuHexV5aBMi{Im%c@VJcpjHVaqsd)z?im3)Yj#-=Da~Z_P?m=aY zKD4pgs_#5y&Dp5T(UH?2R#Tl^6o^5`hL(B`lbO7qbbaqc_0&}UrHhx$W@9z*T1u#L zFtdm+AtL9Fpn6392=WCLi@{D%lVJ1WK$a4e$@sw*qXu-%951?=J$|s8)@I}D;c}bt zRQ*X6o0+i=we@7L9aB17$}=zAfsSXJ)qDAtm#&e4skvwj=LM%gfV((*etZzcyf$GK zIeNB=`lcwLwqCA^06B?~OeNn3#q9qw1`b9|Vf&gZ(e>x_q$^Nzw1T%va2{&w`}-~A zK+PVwVu#mKK6__^QH&T&xzmZ-x~Lq(x9doND>8~G=CL>LQ@56tq*=EV#gkO^k?4H- zMBFdp|G8le*T@>Xar8j%A~Y8Qm7hpd*#b~|c8X$Sj4bqKSB&By>f0d{vvbl)2-fYg z5L<18+H$w~OiJo3Bk+0En#-pDby%oSI{CXYS>mX&yo`r&#<-p#*TV4{!4&_!=0fJWsdI6G!pr---e= z<8Ab|>Sc5s1_=L&+Og0Y(w>5`s^z{o;UVL!co6H z@MtRj{$b-AXAlo9U<)1a8c{-yGmJTr%d;uK@o4&NftdIV*rfXf^?h2oCX?GELyZJI z&=^h=En$)qcpxr*6V-FKMUwqkrvzfdrf7TwE?$CYktR~qgO4ApiUB(1)*f)NzKZ6; z*uaz3uPSDlH}M|VUDy5To?&%1yKXy*XH?)F?%ntd2>B?8`tG-pN0sNTX6h$aqk29r z=XwU}{kXo1Cun>$2O}WR)&qWh`i%M|TyDd{Ey{Q{^VLy2ecaiO@>X-+K$s~i^FHks z{otMiZ})1VnEP8I!1MZJVjPQqQx~_-VCRCYsocq@6kF$y1Wttmn~%^tiHfK#ontfk z+hbJOy$WR%^N*whRM69yB~(sCWfsy{R`GfZF6av3_t)CX>p8>ju*t(ZxuE#dQSAk{gV-2HnPjgR&ZZCK+!i@mX# zh58kDUL8&s#=-BZCKOLxTqc=5Ya0ZH;dP1Q=7~_?F9sW2W6^V>N9n2T%l#;R&3t@~ z(kKWg*Z3nrU_1VeUvVJbLSxVqj^wzae!Z8N2&+Wm*oN`=UR|@Et99f-#L3g1WM zYxj{;Y7wYRSqJw%N9YqeuDAik<7M=bZg$9o2MWJXJbS&DK<395dXeID=ZvK{+?{ZP z9z8i9)gxbL3gusPK_F)pD&taR%iprK5#p^1(ed+T+ZXE8;sNJXlo+;-U4Jdb(mM5E z@RSQ`>w^r}&vj52K9{MXdIV;clCt4J8Zz=Ds%PHyy-YwMj}*M~A{d@g^a;7IAx3V6 zj6n0XW9J}we8m9r8iY|iLov}%mG_9aFT(GC-N=iEO%FC?L&Ai_orqVts6IkSX_!lbGpv7ooD!`2zI@DiN?qIYd*cHEzWM|WTH7eIQcG3 zSRW4N=U1b8&RjgoZq7EK^U6Zd7+%^A5#pRK1p>S;(YqJEt)nC;&oAr zW5ayWdB#-2i=cW|8%6RmMQk9@53m1dpL7L@W_8w_CWPixvaO1&x7yCuw~a<^{a9%a ziqF5%p5`Hf<$a#gZrF6S{Y>=3AE?Zot{{$|n$i6MkI=a@Pk9CTnid8-ubZHF;@c%4 zWW;EAkaGpqqtWgIUnJJT6~inP&pNGOGG4QcIPQu;b7~bQ3Bj8xfu)O}whp)ofnCK& z_HpC{)Yda&0r*=jLw2bYpnmzOn=sz7bO^FlLiGgv4x^?Y>{$9;{G9RHvPX2FUKMu8 z1fqVu>Pe=T6!t+^R3kbz%KJVL>v!oyA_4Ddw-mhy2e-}TnUG1o*I!hM%X)ZBJG z>bu1sSLiO6poVFyQJHp*lUqd6i#bOHp}A1&D~Hn1IndpJ*WT~Wih`#La$(uU-#lzQ zgH0;S>u{hB<$h=kwYR?@r?V1iyjcv2S$t3#miPRm=OsN*ndGeAzP>^(#Nr znaS&E@=}B-ig}I23O0G;Xxciw8!*52|GG!A3MR5q;<>0_xl=d5@}25XY5M`i(4`XuayME~%;RG!Nbsc) z)~}O-`kqxf4s<0&;0i-e)pnA5}-6!?pNzgbQ@7oDk)JPu2 z>O=GDYSi~5hw@pc#{-`Afz#;R$+?jO2H|aIyNXQ!%U5lQI;;2*!`FyCfMQBEepn^9XzCug&joh#t(dt216i`@Ihs)=VT3K9t_G0*cXpdwm)_Vo!~qfWNh0%9h_ zI^ytebH61f)NYOq+;PIs(P}ovvOl{c`Cj=ws9#%je3(U#0^A%OkLJtjd_2crJrTCe zo{##arKtf4oi0ou^gL?IHp&2);WB#8OdYlLYg9e_eDQ(25W(Zz`M+=fteM7~1@XQn zUF&3!`SO?a7veR>(fUm!S1pvOC;vn-U!5uju5rJ355gW2Z2XAr&V`rFc2H|N6V>yy z*?^rt76Rhc=BUgrM_-b(C6SG^{(;J0^=#mwX$Oi&dW8;CE;fefPJ9ng6ej`? zrl#^n*T$i_NUPmW%4P?!euM4kf2HY_JZRh|!xBWf`YG~rx;c)g{QC&;yW)?IpCtmC z{K@_$)G*-}>eoz@J3M~wEb1}}*K>ESAbh@IME;D~hvGRoqkwsIThLt=-_SU_pEv_Q zXLS>E=bfk?AC+vTGA)*VZOB0NjP0z4(xDP!7KqoMZ=}R9vyr=qbG#6$=VN;TIOIo@ zDR&Z3Joie^aBs!jvIx4m8;vuO8AHyiH_*E=wW!QbZhjTMjf0mOd8nsq`@W9VgsvWNJ&)!jI*Ddkyl=)fK%sfLMYEG;| z*LgcpbBKPg54Zo(OMEJ60o+?GOC?41QOpThIcTaT8hraWK+QX|sFyjI0*WT*+ti3%K9th%dFmh=HvGtzD1V4qNcpTG9 zS^oiXnDz(nE9mz2qgD4NK+l#2RFAia0~AChLh+U3s9*fod9Zdcn%_L)AS&}*WFq|) zI12j1b5UEGStH=GydTF^JQdaBc76#_zF`BmK4by1Li61xrL_Mm7#IoeK&-KEEE^ z*0Kk?`7>94CzJkhmVN$~25FnF0FRufpniP~m`J-HM3XOfHli}m7M9bu_tT->46nsG zm{98cREQjHu0b&`C{88n*9^GwXp0%PZX7)4#Ei^zn93C zL19e_ITA7x^())LmkAB7hPjii(D}92b0wVhu_Ti%C!%^jcE*F}$1);#d=xsKub!L@ zd9{<_i3bXC zg2vhTH^AY|3ha55IBKi=f&duZ^I=)uczq+Y@+j#3yhq0^*ox-;!@Dd^!s(27W5>HYPNq`RZKiMK0v7h10{|QJG-5pHz0b6>JT5M}2=W zuK-ehW|GR5m1qoaPIrg=>L>hMMP<}?PnT&t#Ry&AsjL&It>sZ_?9;ajkh++Sj%VE` zLeOfh3Z9Rqqj;`!Z-ajoD+OC)JkAz&7nv841P{I(8lN(k0N5tUy~+G}1!_yrCI)(b zk71prTBz@1Yog)M!Uo>jQ`b>j)>}HslsVOO`o?H9K0X`Pz{hPOOj`keS0wc3UbqqJ z0h%dzFILj^Z{*7iPl#N;6UFm>NP;=^D?_cHE}9D|elrbJD`vK#H7Mr$ePcmgQR0hu zEf&7~E}6xbMsqs<=Qud>O^zxCM54Czw``|kwLx&}yag)rR`x5Maa0}Tr{H_Y^P|L= ze5okU+PECWqnPQ;t4foF(%wyI3>$jV`FljivC|jv|EA<6cWBK+?wipF@74EsGnId0 zP9U33rl5XJ)^6tUOZGzN)fFh7AVorEt`C94Bk2^|YuSq?vu{>+>Ek<}(DCeiJO^S- zuJFq5cA$FnL^6oJaws&U;`MMv>F4C}K_?h#asgJlm`|PRrhjyT*vN;vsI7Y@qnK^+AF^Ji3)M5<{1=^)6$RJ+ z2%s`m;x~vEX#=sgJE+WzlBLX@gwfmg@LG4~)JVEOKdEWNnn|ed(wPP9n$;CjefunG z>(HC6bmfsXkhs+z)#IV42ztk(SlsLaf{mYb{RXfxNQ!+p)sEU4RCa(Q4K1d3bPIal ze7t}gc!oWui_hTivqkb2g3E$=*M`kpimxroMX}_s=X(C?ZKqIM<<>DYPdXH)*W>fT#B>Vv z6q!N(OxuC#8MEIEjv7}GSs(oWE0^PKI2n;ZpRa$3j-QN7S2)+@N-9S$LVee0enYJ9 zmXW_kvgp{5`?dvSokZ9T3;d2)U&SOaJMfy{$xTP5E<9B{fLr2kE*!tPP@=_kV%+o{l zgnZJ0m~A$s{S1EYFhWy~*=^#%AwPV+tNBb}W}-KV)pY!B;L-5`?A-2MU?#W;HMrzlGZRy%u0g6N2U;FcYD1EKJ7r?n*@e+JtFQ2^8~`8(v_lsKb^SJEFe3c3G3SN-L)4h{rH;ej0V%&;)0NqYCjkAJVCQ}L40PXSko=rSl_Yu86`7GWJuh-Q+m*o4b%Yf)r z?@-_MPAJgS%nS%kv`5E=;={8vOrQ!PealfjGdsqz=H`7+{apw3%QjC91SMQ)LP;?i zpL30ei2YJ)CMS&V!wu5?ShIlucZV(y{RUa2Dg$q>RFYk*BOANtK=`02YOC1i0FkNDh6!WtqL{ryCo!-4Pf7p87K*JKlMi#e7UQoG z33Gh>xGEKsA-@zz*vUg}=@mM|{i7<-S&a8TH+??Hg8JimgIvE1hNmUtB$@V0noN$w z|Cd88UHI#7&1J3kQqedcO^_pfWtprmbS>(8Q9%?7@VZ8f-s3$vmC?IsQr{j}GKjyQ z&{KGcZhw;uVfTj7_#C&G!zLPkBc-z!p?FSMXfdtouY9p5_@3Xs>jGV-EdqlY__?Qo z^A?yUZ$&N|i=ei=x66Ux8Gm*$1z!)ZREfdhdKLc5r6bTdzb-O?zqj5}oz)_!@7qfK z$<>uYOz*@pH1E$XCd2Bn++EI?{V3)!0bj^#@$sOvbUbS79=O2~i;pxg3m?z3Rk%A7 zYir=#M#a zy?le0*Y8KLwO68dh-B+6g3!hI+^JI*1Ru9xmg~71wY4O+3_2TPdDms}9$>r0{va7B z0WP|D{ipB1ehX`U%UQDN06I1_r7U^T=JwDgHWrPKuFhg`I4H=bTdJeJ_peAHH&Z1b zz8AN3MK6W7Vd-|_eK!HcQ*+jV{hr@Vw%Ke#@ibSTf>m?t=>cPWewqI|$?k^t(GS7G zs4bP3Px&!g9iVW~1@$XM{3*XvSd$$|T#MqVou0ur`E?%7CF1=p%XN!r(0EH0%2uOz zB4vk~c6QXm<$y4BY$S}mMXR;fvvQ?{s2g7pjACB19N>q`Y4ae_xu~9G zsR#7dmSp}7Z@iY&Bf%VsP>J37RH>bF~O^?_v$tS~11nEvT)r`?)vVo~~!dZ1G-^zrQM& z`_)*o`3T-CBD=tocVMO!bY1G;Ve3ZL#__OxxeE8@7k(Bw)7*@W-&e-Z4Jk#(ModT@ zEw-^`C31M3M4(Nb?I}JqLHY~z9*L;+(3n?`RSmxdSB&%(Fbv;o79AUi*gwXhOSAHe|0?WW)8NSY&%@rjNw7lWV zL;M@|#jdQuYP%j9T*~f}$ zd{`Ce4dZu?9tK4-`NnOuq4pR$ccj0%z?pZFWSsRX)c2Jqmx1kwT?|^@qP0`~hCMLK z?FCtV3$Oo#$^Ik@nq670%?EUByw$%F~=HLkxSqG zV2QLP8pFKoO0w2Bmg!#EhT^%jb2s0!R}1d1!tV||6fFT?(_2KmP7k$}eN&j#s)|Ba zT0S~9>W4?c7tsRR+KkUrt*Sr-xre#hpF*44OT9T-zlE-Z5dB4{2&Yoy+`A`eq}Bzz&KU&LD4qr6J8yt3>K=Z|W zI~p!N-^0D*s*K7w#YVH5;RIq)SB%P#pkh)g+E1^HIu2Nz<(I{g@^dMmC2xoN{@`Z_ z-RpiAY=6E*ec$kv>+N|f4R+7MQQwab^^%$rU>9m^P??M07sFXian`WIuCfMZAz#f^&ulLnz#gd zU9C|)Yl_8Kx=$h~x~xEDPJUPbKgxW_gM_81t&Yc&VW2aa%!VCWb*hIXR;ld zP(9b}V$PmORe@Dc0#TV2IlJNAlWWwf73cCBU4I+W#E+0kVR(=Im!5ZI-+EnUCKHBY z-nKRohN4uMlkG_!7Q-{+mcd3p6ELj3gT_!rz!Ao54}h)mhta%uJc?yb;tL_mdJ2lC z{*DY;&dQmY%Li0X;Aa=mzmP)xhsUFOUXRdcWy>{Sws9q@M`vd=OHNzKJHKo;DkCDW zl6u_pgRvfX9l$|ukVyEBVY`kQpm-X}6XER6U7#6(*X!!%UnWCd_H5@#HNbLFq_uv-{V=>KW9x+6UG$p;bDacF#Qx@=-Qx}?BcH51K6VUZ9EuA9Sj_BEn-0ybx`?WyWe zxBV3wA3d#7dSsq4=-P#$V_w$1oF0F##AX)ZYs(4!kK7yE)evm(2etJjUYTB*5Wu9r z^`bJZLfWwFlpXxoJRbGk?wclCb4Hpuj^?9!s@HWB|6_ANzkekf=bjO&(7k*G=;T$P zdU~ycnWfkQ`pZKCjdSg$_0S?Q9!zZUJ#ELlb9~hyJ&5hyhw9OITLHH_`z;#E@bk)f zyS$hs$EDz!Z;kpjcD)fKj?trmYs65$=Gd%bky|6#jj2=7IKLCUPNS=iGuOX3zg73+ zV3wSrOe8Mppm;tjRP(30F9E|bBT(O|bRaoWpa%oa;V9;Xqo=cDmj|fGGyKdYxN9TL zh?8REQ5UM`c&)%x*Un^GC4+i1o{%C&dTQxwFYu0x~Si z>HfNYG|uYYYVdGP7Pa612Hn>gzN`R`oSoFh6Ms`~bxQ(xt9>OMhpJIM+w@$}RZ+#UpCMUw^j*QZM4)DbqHUSnQcUKoUmd=U*orn#gOr5%5Dm z8;$cZ=>TYo`9Tc^P0*aGC?pfN0|op=S3Oa`Caj60roJ&Cec%B49T!>uf%x!*K`8z- zif1552D$<_f~2@UYOAx6TO%frB`)*ku2#q0ko4(GfMaNI< z#|XI6eU03{X@cT;u~!@>J#}RYdy7y#=49iyI8j5HC*=*Qi`G`XkqnTeSyJnG-K{A>3CD@^0=$$B*o9JvKQz2pXPc zp?E$I#6ma!Ag}z#JhWe{Fm4Zt-xUE@Eg71ND|U&zBCRY~Zf1bmy1wZKUE8biasS&?d3VF=UADMP<@T_F2%_r$lrw-g7%~X(oFYvYI`7hWA~x z>3tx-_NKw!U3I8m$-#b*H}f9Hzo>!cqM)gg%}bsLv4wbV^8*nlkeOo++lx=3w*LNj zNo*<-p-&L637-?XKo0DGO$3tgdr7eeYuS?ZX}q^l2T@yfBYu&L);^0vG2_uVUv1yb zg6FQK(*|Q1_U-U-btm=DS;vk&>Oj|xW%G*I$I)8ktQ_83cypIAUHo?^D7{pT^idjl}FF8CSl)0wNMdQ3yDiOXP^rB-@i_tinj4y;t zwS&B*Sy`y9AbakvblnRgd>B6iO8nDeG4L@R_Oywke(hd-9+X0POsPf!wIv^FNdsgK zL+u-U&sMW6#X_s7p9I=3M|~eN#~obFYj~Oovrs&X)=UJ)M16WR1OJBI(|U+NYYZ>0 zG846Rh`QJb1R1mCv(5 zF|P|ug{=5U`ggGu8lOu?szFVA60{u%q}aNVGi@CC)HM!%eZ}{Og(G&b$8nd5!8H5~ zF<^K%`*dvsGnl;$&BbWr4d7PZM)d+TQNQ*JjbX3L13_W~em+~0 z!V_6{pS0X6Me`*-r-Y1}+Da7cjS0x~?`74^L`WC~a+&Vx3GXBL>P zs)!OMwoZY#Pr~;Aqw^QQ?e${x(@ZtgR(i1pt57eokeOwOVxDK$N)wCynZ}GnG|qM1 z#q7uFLf+lEeQ12_2IkT5)@(=!+mGr|Z)zoT6Dz^|a3(4v>m9+04sWLqw&L@mx3Z2N z-rPuYGsmL7PcYMFUUUurjYT%KPQhKm;S_vgu-@P?>kRqha^T zkxa39HyWQ!xr^X*LmvNq3I1K_QZNJZjZ5f~r+5u*_178jFk2S(ad+vl@ngPT45~}D zAm=UKb5f&l44zjtlB|#FDCT#PZ~3)WN#F%||DlET8rayALtd-*pn5_#M#IWeugJ4& z`2NS*zL9*2Oygh5%|YXA);9yXuMU$z{tVRjxqE=g2zStB#)&8%IA;JO0u7bhEyD)%WErX_D>#Smou&^v=G&oqs=1y)d7YJHqro&(CU{Z+G;{Cvlz*F#t_cej zmcli$*Nxhz@H2(vq%H7%jWheb`WCuAulh2VB>RnFbIf_D%&u$epteC4tkvYuIP2{u zB=4yUY+WjY%53wS$?UUo>FFbQ-DXqwQkK4V2??*k-yTuTi-bj7Ppz-68EQ*wZ7M(E z&}v9si}Q>$?>oTSWe$+2_tI!iXL#_**(fy#AC5=!r8Dsjna5iRYR-V-5z-t7Ifm(E z-IaN$o~MONpd)My`RLt7u(^C9)B*&g6F~1wCptE?yjHPx@oc)KeKl$;_G=8i&kKgy ziQo8`Eu}_1cxA+6lc)lU$6BNi6pFqRg)R6uqf72GYS_03tiR@?cn&FjqdQ-z!{iT+ zXbg{X{9hY6{x9c$@P7?({9gqe|JRm(@PCDJ{9pMT|JQ?m@P9r2KmIS9fAD|J=lH+$ zIR3AcfAD|7|M7p_{RjV74afgg@*n(P=Q#ea9FG4>`XBsXLmdCteUATY!9V!Fr1MQc z<3ISn-f;Y12RZ&P?|<-rUE}z_{5bwE{eSR(xp4encRBvAUH{|FxOp|LXb&|JP965Z}Nog^m9Y{;#bZ|JTlc@PDOo{9m>l|JTfa z@PFBH{9ijc{x5}bSJ1gLgX915=lH+8|H1#2&GCPUa{OPK|KR^x#PNSMbNpXN|AYSv zIQ}oufAD{Oo)O8*;P}5n{=xsH!SR2oaQt6S|H1#&-&#fHbNpXH|KR@`pA`}eUASt>>vDJDv_$#%zWP@qcUd0;!HoT0)BrwhpwyZwkZ$?--(d> z3xCUE)uJ6BlKkiF>SVlTb;DpW8{zE6RJk|3uo%YdZy*=KGnmOE{2O+#(U4x2JPUJ* z@Vi0+4Zp~?>XGbDHQqlq<$@SJGslqSb2V=aPpXh8Y_6#wYllXnn5(vKVbe6H6VDgQ zsGc8c@(^cv0uCQeK*x{c#vQEOqJrKloq^iApPva$%C9Ych>4)K#C;C3xxX3t^?W{R zi*L6BI>qiZ{aTFoRgM~2Or3A68Mb?9|}xTCxI_(a;!6Pa7u^q5LdV#xygUntc{viFzI=o<|k$XjQK{6ZFD+5B7}O zNULUSCdc~m`E{g43&v)1_X0>6if3C=G<1zSNHV_Hpz~|2S`5TCX7cRcKS1-fBJ&OT zQ>@DW@Ujl|EAmKOlSWGb)1xjZ9`z6baF}KVZ%bF9c-$vFAur?nK{xIUD#PnP0JD!C zCXy);D4vcLCs^#~IdpJv7Fs*KCHc+b(li4Q=gFgb#<=m|v(Y%{iMB=kD*iGBj*BX= z^oRHw^~77A_xM~q&+<5a59qyyNz+&7Lnk%$atM21Q+^3#=+oJbvjG(8s;UExNA^^~=@VoGMpQ-eduMe%&z? zWYIg05toV0C}zpG4xo{IhHv$@54E*_=|vjhY77B+>L_NHKv|eNpbFyOoKcyMce$|d zE%)xjVw}syZIv#WtH*I>-%Ua1;MITy;^&;2%rJPcTm#^-m(8TOrf zo7SlEAet{%KM_{4fKS3ZH=wqP)r#Tgo6F{I()jx=GGBsd;E03pNCLl4lsLYgj%jdY zBW3aV6;^thFTLR>^)Jyu@q8$_MH{lGK=oJ~)UQ`g@A)U><=KrbxSjz=TbNrL&o+O$ zh34ycc?hh2KZ9g-IHNL2Rk6&nVkEQ{eL-b9tm0wjnw`Y$i#f{Kak8v~4hLt!(p43x z?<4fiz-!a_ByE&A4;w$Sy_;B{X*j*p7J!b8p4@FP_pue};qg#gyKBx_1RvF5R>mVx zJ-TAD>{^a28?*Z|>U-6u47S700DctiLS<&%x=S-nTtIx+BUFZed;t_Lw1f?U_*pL<@PB$wSNxtQNs6`+Hpoy%CVAvUSb2PC+N33%yjuhTGe9F7_L$` zfkO|QLFIZd!SW?n`m}4it}A)+w-W31k&F@x8>|gI+MrbRB#ga|6XZWw#OAcqSLV zN#g$nC9nYG3%5hqXS}cbt56^RkEspNv+Gdbf6`Vm6fRBAyW#z!{l7}dv`#@VVF{@3 zzPdp$i|(aT`+uNs1qZLWN3CW~frY-RD4x8!bEK^%gLXIFL*pZ5&&6iqF?#dz9aKh* z6_EiGcUbk0997c4vnc&4V2$!_ohOGHx#> z@9>cvzORAmY1?<2w_s3_dA`JJ1#XdN*f*6%^2K~MnhWt8aWG1umt4xk_r^hAZjpW8 zZQ#l_{2i)K@^Nt3Zw!cSG(~M`_esFoC;@t^4u8i?gzrOg9gN_43x3vrF7X?EKK~)P zkZ+ILx-BgOf-~pCHZ2u2UtL}2$i*OIcGv~)%gu`04sU29G5?f@+G_t@Odnf_0 zTbh+Q^p{=)B!~~A<1jZ;oHftaW5TJ|P#Mq7M~L0JhqQZM5gH%WNO|IIRSEnhx+tC) zfe6}LmcvfF;r+Bjf30AbY82aYcmk@&*=skfxHJmNcbcFvEbljC^?8=?yA)sRn0Yo7 z@JxvOWqiKd#`B4MSOq+vr-sI-^qMrxKFhr?=3ar0!>uQ?*p`igEZz!#E9Q(`EX0#hmTpXE|Uzf zuGd8UD&JsF4AvxrUDYfU^X$@K+Ms=h7O%8HF+X`bl8$WH#H=SXG^efP6E*dGMAY18 zqk5cz7DICISF(L}1d2!edlT(yj0HM^JBLSpKOT9&<=Z+;gdd0Ief_xvxX_kJTUXkn z@yWWU2V2jKW5bc$)#z+U)kbrRL{>r@3b z&%8xtTPjeQ(+(T?^DK9Oemee*W-rODx#f%5f%CRk9XqKwx!pozxEV6-I9=V?9M$h1WD%#APr5tS)$ z*J>xl@SLbv!PZyIWvN&3dcv*XUGV4LAq$_8XVLhayt#`7*X&|6MisSX`=^Kw|D3}- zXTIcNcp9_w*bb>_O;ykE|ALz95vJYE88GJedKAys`^(_uQBCnSy@OK>SNAVTq7Z)FVHsqcvL39G#OsH zRq%rDI-$017p{h{$Da_%8QQ4K;ro_wmYm|pYBr-X-ojB(ldQe@)KHDQIQ3YguNq}9(`&^>>|mNceOE6hyh2%vgudbGgp&jOkt=kf<&mxnCu=o9O9roX+n2eqJ_mV5#^Glr z(|#|3#?1zBQXa2eEYtcCSA{t1^xi@56=1$aBXG{yd z0zS;?l`!hpk;!tz@X%rK@4JY`aJt1&XyVSrzwOCGeV39s$eQ>O7A_NCpnly5IS8p* z7wCfb`2KK#-b`p$C;^#S_+7l@Pq{p|^bk;9DTw-hulpGDdVHDgJf%di^=JK79XR0a z#*d%542_TN3u!ji_oqeG7`!H|!h6h*O!Q}&ZTR~(gAs>Wy}KyPSuTxY_7*PYm(Mf= zF`*$eh7Wfh1Yg(n|MR(+<4Y?6@PE%=#NY?dAA26Wj9e1zSHRlzMCYNf9wxbX7Rd2uzqU`??drkqd}KN@Mc{VDO2i2@oaF~48n#5#JX4& zwKY!tB-LC~2gmF@(YX^?I!xCc9UzOdgVB6F40M1`m(9Vj53ixwwyyxM=_+h(>2fp| zJZl94_AAKpsZFS@V6WxuPV`!E{@{mV&h{69Y|{$j?z0}XHT_!x9Y1af$eH`2dZs?| zW24m|HS@_ETx5cA(pXZ5S2cy=V{f>=QjIJ&$*ZM}IypzGZZXbo>gWt7sd(u(s! z@MPO~hD=j|)dA+jKW!#c5H3RXoK)D%d$#I5-Fr0>)nkxzoi?9e1b#bZP#Nt_2Uy^` z`_y9Nd(>9`oiM3slU0s&C*l*nqrUGsln#rwk0y23E6|+ok7=W$KRqHZ)&`-rywhxW z3h#Zn|1WzKv-s3Nc3X3ZK6H#g$Gq(_PdM{R4myrrKxHl|T%l`ATuE2HDQYV>FOVIm zwdd}4<2Cb3y>ppZ<<}-t?>;m>+sm>@BPnA2T~kos<-^m-%PTWrdl`PVer0$*BQu+5 zV*g?k^HB>s_G9oFz1)M(!J`iD7LMc(f6e9uRL|Y@KH&Q5F#WU>-*3F}_2vf!hCxG9 z9_rV3drfedtiaxv-5Uzwixkab-qXENTWvRl>2b$Hq;7)^ zYU@smH<6Oq0;jb`qk66du7W}f748ijydS{isT%){v@Nqt#CxcOZl5NZ?LEZ$L=%c9 zGu;Ym-fO`zix?EoU&%S}T04%KDQcrKudh!e21T8`s#66F`!9I!{Dmq8IK$XEMQEH4 z--sryPlt$R{6^H4!SV(CjZ)F@;+Y=mSCi;u_96BUkv{tdwI%(ro?6JbgTw>ecjNO$ zr0b>y`~C>uBW0-=^CycZ@zS2&LVdS*zYG*26Ns-K-lyk75@6+hLK~MXK>d;xJ4!@W zrb1Z9X;j92kr$IXKMn*IiK4zoI7iWZ>qdwY!1o&srH%aAo|~Xo!w&W9;1uqgiSi&; z@(GIPj<^F|khGTLIdDStBn@P;;rcV=_`V5f-ZP74fx`1=n-{~-szpR>axyB@KB15QcI;I0_~ThrMtJQ3 z`s~ML(*ANkiYIC8G1|M&3RXC^qk2|MO5iCsra(!U6e{zn>M_3~Dhnnk<7d$IS4YwU z(dH#<4G-)}VMqL`TuZ=>f3$%^UQ*TUnW_ z{oILVm*3(!jbB|yE(ga!-3%PhNP}3|v_%m5NHscs942Hkla(jQ#GUxqknC0fuW`>w z_htNBCVN;J*o!RCtXqlB(>JvyuyHmI(tE$6F$_L5lRe`0NB2JVqcUv*63`(L0}J!# zpmC0mso_i2{2;N*a~bx3k>)7RCcU*~nomEYn5S@el6Gg-lc28)(HI_E@thw18vz|H zc<-D>i#X6j<3M|<4~ltda|jc!?dJv8yhibC&WWV8#=suG!Ou!O%4OJy(a+C{=B!42 z7Y?~(k+pOLwIQpyn1$Q=pT4g9AM5W8Qz3+`tgkpo z3MJ9{Im@Ypjn)Gk-QD<@?M3s%9(CPeXzaz$ir563(vT~&;rgH{8fV!@_c`RqC6fLg z|Gsa@l!NW|^6XtLKHoVoOAiW8#zEI{e9fv&aSyFt=*(n1P0$!lTh~J*g1lk3r9O&T zSldgNsk(xZ{~R>VMd4APJZIu^QUJb|;x&~Iv{&?!o@)*$SMIz3*j7?Qf1RF&*1TVD z2VEZE2dTbzALaf$362f^qf0(cMe|;A@ja*BJB4kNT#x2UGHDfDwU&lb31?IfHJt|4 zt+_<~|GfY6{*?qXP9&h&sey9MHQvoaEMrOi`(~8OyCfEjZg!Aiy&b5XP^}U=v7SH$ zpW^NP%eHN7Jfw{*@4Jr1XP0ON5Fs9T$!|@tK9i{h((={wA)*%F+nRkg1I~!cv*H;+ zs2zU8mmJ%Li`bh4BeXVBY@ZQ_n^s_HB7kD_X3b^e@R&XsG(fprt{QPCeQ^V>RXZBz z3_b(cGGfQZf0d$k4lPY0fA6#sr<)@vX87S%vOdcg+CJgyy^d2>vG|Z)66&3d=JeLJ zVcO*-PHK3spqRxnJK0PpUeaMIhsI|}gO`0(_J+oXTBx1;iTj5>X)a)4ABV;)W$K<9#0$9?wDJGym!!(J`ILwhUIGIZa8J%-t(| zYc=ds%_kjmPN98xH(xnfBN7c27oVYABEOOV95i8P;Sg%a?@xm1?b%)|Bg_$v;Xv#d zX<8)>g^ro1?|s9HpyYLz7{BpH{Tl4s2#FbuRH+nS$DV6ZLi8W};q2DH?*{T+8l^Xu zxq;l0N2u>9Dluf&-Xg-yvqtsoIaNk)Pu>o>Cl90XF(}nzvz5LRv#iS~*Ur|HG&S1^ z%HGAITyfXJxK;oCqDFG-P|Ta}iezA?Bbh5X!m#ghm6;AC3~eVhbTW!bUEv4Aj%D<# zNfMf`f`TM+r)3|cwpXK=CWQ^`LVy+&H8160daVDwqZzNxLM-H>c9a7%=_iY5I9reR zQ91!B;Gw?~E~MahOVuyy&?z!*Y+vSX)OYcPsmwX+78&=SgvPnj#tB5e$w8!s6^d!z zG8bAeN5LN6r6^auhZ_8s%m=ZT_)$Au<;BF|J1?6QcOJ#8OwT44cUOVhcO?{~TAabI zyv(Bd9l|K4UCtU>`trbZ0e+tC%))sj(PSRG{XHAClRst#QYx`T!XqEWoQ;3j_*3T= zxn|#jVgy#ybM=$dKznN;!|JEEa4|49PsnO5N88uAI3aMhn-7Dg3sB6oz6Efgs0@ZI z@b&N?d)BjvoEQ*$tBJ13e12QP4x0B<&8<#oJ#Ui@GJRivk6z_qhp9gv2VV=KsJ(`+<8Eep4^Q6+Pdy)gpK z#ZPV(4E$}Tac7sIn8@pju(ml1oNnN2O)F1rAdP#r0e8+^)XtZGj_mqhH}1u0s;FOY za|Ge0i9E=zjRUNHo@(@Pr<^onduHXMcAP%FqXExS8F%4nw4Z;Seu~AmJ>bT4?m_Kj z9^6F+f_6dh!Z~Q1TZR77G1pa4^3@r|yo)%=oXh!mCs{Nl39hBcpEP?MgCrtXPR+ z`g7($_@xUZ@g}|>7s+G5l%L8o{fG6a@4=ho$ZErNWI!BWx6$}y#_nn4!@3wF)UUlO zB4G7P&&G{=Y*EekA6UT#^=1;R6Nq9OwRVE8iz?NKb*$(PcPociYzKtOjMg`z_8DERLRQi#Q zew75pK}%H6;el1`SDy~!JjL%){a_!+cei8UKC2AX^Mn-8D?87F%B;<4urS%CAPZprL$oX@b94?e#-`)zL zdU&q!awcVaLDpP#ld`pM=|+Fzi|${7lGen_!x4|qnq61x_@+15xze- zY4KayP=5(BXW?@_uHa3&F-Z@SAHC#Y?IH`tsQvRsu7oiDec%2?7>>`&fJ{jrH1CZ| zpVFbotMqqI2m_vLN<2K)%R}G0*xPT*RKrwQ%xiofZQoKcQjxg_W~=+5IklSD zC<)bm#VPT&LG@S-`moa8NJi(iqwxv9egFpbw=~))E23NmiKFCZ@ES1otU>F?_@*ID zyBiMHcLGp5T|08w`LP>hn#XBWPt_MgDE0iyy)M5M^(${*EaROwiStiS3(eOP15cW) zm&9JYa7FusGY#w6s)ox%qY+;}v|qcIrh1ivd4?qF`(&;LcowFBV{a?!mq={`?aQdA zn|8UO@!5OxDYY1%#)@yPLVd4%yo*&^hS10`{5(L5Lkhj|;DG7gdj+VSL%&tvt3Wkf z^YT3I*T!@rRke=stijI~Npjf}!p+}&62_^5h;B#uNv`SzH zrP!b^er_lBQXr_6t>!%EA4a)yHf?|txff~diYT-#pZ7I{v^yJF_ZZFJkLn4~%m(SnF9C^eaYyLBPTm0uMNws`_zwS|jfw&rWH79~ya zTW*BLXLCb2y=h|u&Mi%-?_a(@<2+VPg`F>cuGL)e#YpYC8B&Vkt1C&e=@Z{&Ax&Ns)6}z(an&A>>mS0<7&=%yr!oxzpwj&e_1%5?eIm4y z4}2YV0rtJv-QvrXW_56Ogj_=H*y{OHuU*SoG4~RxCt;O5JLs~xvE|ST)UUH&i%bJ+ zr^1ksFREwatHTs%F7!Fz`!beGm%y2_RPN2i_}QmDxlSOUn@l^bxoA%L5&}5JA9dNb ziFcH+_}Hx9XsUZX1J=_r4u)B*V+LbtSS*O2+)8BiDY$;og`qhxk1JO}@?9T%HTcDJp4`R3dzv{2C376r?7X#n z;{RHYuzUaKot#)3BM9P3z}DmTs2+~Q6qq6#OQfU~(7Ydf#m8JXE{1DLfMP7ThuPwW zc+z&X73KO+8VSNL?vQ%>pN5rh-_ zQ9IQwi&^x<-J5e=csuPakmtx;_6Ku5J~Tex!mL5!+B){8wFBi^>t9Bs4L_4M6?`tE zq<)n&N$G*Oy#eai(O)iX?sic~Z;MCuEc&3!mFb#trRQTVYG=b|d8RfH0wquJHaO+U zMviT5AFi=J(6kP>I%n!ictCs9;Sfw2@__qht*G}m*me66w@H_48#Z*)jkLN)&< z^Ne_(UjkKwVkno~kpa4_#FaHk&qVET_B)WbAu;f~6`x-mRagMa1|zwq?%inn^5sUb z6$gtr@?Du|-rL#_!lL7RwET@Rs+pvoC;Gl|6L;XfP&JJcxfqL`4RCMvqhoO!KDM!D=yp5^krI7Vw* zqEX+)Y-cfX3mdp~7e6ohGN_2mn{UZU*+Ee|`s(WN{`)y5be{*!*P;!c9Is((w#E@3 zv$-AJ2#zz=So}-;9xF=>ht8ZC)KJ(A)w5>r#JFZbo#`HpK2*;Whgygd|3*T8#Gn}O zMc(jui!Y}`z!PoDMT1JTC?*NS&*A%Vajtyq`Z_z15K~3-)x1^}c3!fDB@^!eV*Q5P zdVQe2;!HT(0JA5YAK zja=Y`K?WMb1wS=ek?2YAGjl=xYQCEZvEj|!wAb5EOpZ?~7znOps>OJp5Zdhk^N-!; zd`-Z=xiT_N(4O4NNl3=mQX-;bK(SeY%|6S6>dA8aOY~(3e8t|t2l>8dl^e9{fdrBGBs{<9vrFRj!NU-%3Z9f(J03Pyx&Yk zxh~|zFr%SQG&u)9x4b^fkPXUJkcNeL-_lxm7_7RdarkHZpj@5j?~x(>6HK_r0L9!r zo&i%gnN!iIg@CmS;k7%#T`L|WX4IhdtYLGDZm+gr9&hk{{N2V@Fr)n+S)62w=4)NP zIC;E0hF!VBgKGBZnF4~TKIDEWUi0_GqG85OZ>ZL9LUX#d?KTyTT>*oW@xCQhcL8zg za$|Zqmry-HeD~=U^=wGnGlZ_&7<`-tDa$s&qM7(QfZD93?A4nnvh!6tnu}DKI`&U_ zIgQyQkM`tA;(zUn@ils zSH)1S(6rk`)%6)E;;%xv_;=|ugTi(66K^HT#h0;*eS8uM!po&lE|r96QnKnS3%a9+ zww)34H1;l3lnHCjLSs08-xE4j!Vk`e37~qiuM|`MWl3P}(1l{WZyUnJVG(HQP(d}n z6w_rL-lBA`m?Wyl{Z1s^WV40d=Fvs%R1S~QxrQnfVi^Al`lwUKmO(n&kI>LzH%`~ z;awuCd5B*OnkpRO-5)#D_c*V!$k}U0ob}GLaUk{%tv59rwHU|B@$5GAOxPHuI?x6bt7nPi9~((Io3tmPfmyb%Fm%(T7gpR$kTU3ef}WIb=p1@%=%?b z59g+!@p+#yM*d#i$t*u!M#sjLnqyQ(?ikCMcM#Lqd-Gny%#K-ev zEi)nNt~u=aH68U!Uo(osZ;%1Amf-8^3m2t8;CLpbF?jrxs&pY z)QlvMcbbK$9tp5!cQeGnIcz=Zd*HAK5sI9|yk;Lnxz2I_(2s&1uq5UeYNvSO>|ym^ zTSz*^kJ?H1;{(@WP4epIAX=;M1(U&R$pR*MFoTPYZDhAyfaR9_WR?NmC-C>FkokR1 zG)$xpjgQ^<3b1t8SU433u0Mlp5PPHcVmda7?I zhsJryfm&)OPz({ViD;jBA#IfN$3cS)x#pwsQ4$G)w_;XsOZghg<*cg*hwikG2XtA~ibW;(;##p0&h zG)`qI$O%3{x#nn`g}fR$qAh(Gweu%Xo4K`n!dAC0=pNb1&o12I8A2@m=1r7~=b<%m z)?UFLaedJEDDwtzH1_Agcw-RSc1l%~2+!HMtS43q! zxl_X^SFTMhQ{AaxGXFjP?(Uh@9za6fV6g{&&Ov5*F0Hv6PxS$x6IDq!(XT4JOe@V5 zjiGzG3aiL{MMO0FQO&a2om}q=tBLM)d^{#3H%@-~u7T*|YG{1=_XGPGxQi^P!sm?Q zzR#)i4odyk;%j>S`#;mg!a3ZuX?TAK4X&WWD*(#IHmIJ>C!?S(#{i~e<7+#bQ-HjW zYp3PHbI|yx&0PmwK3AznFb^K*{c7CTeVf@<>piHRu!L1)^@n_zVo`~5C5~JnSz0Th zrK*Zx{f7IZ&EPiY1L=JlfVOvjZZR|4Q%Sc!6h<|xpPmK&UhWX$qm1ft_mYOk=QKe6 zVkqjDdvG!+oTW7D06wNId0+&U$+pBZ@j1$Me~&+F9@+`nqWHb+dXZRqJ@_xj_`5Tz z=kqF2_HE4u81s@xxq>;e@Ko#t>6qA?!r~k%`JDvpa)ai{6Z=KTFW71=+&C1*Y-0sb zI|oejxOwN!lauiU3^BvABJVPs>x?vgnTL-h+%G7=$L?~fwNerFi*;=$^0^VCe1aWJiBsBz~TEm(1*3)NF?ITL)p6p>{?x~T6b zOA1KDhr4u!$T1X?K0S!FS93{c6CbL%j2?oa2xabUKUp*vYpFLx{f!|8f~~0LoUo-Z zxl5C1toK6oXg!I5(gHW?KK2gPGo#}@ea3Nsa})1mV|kw?nL*QAmo~U1CZL+ty9&u? zA#)grRYvs~JFbOK2_fv|kz~{_i^uawL+C81Yrlwc)o#B*Bk~`RX{%15n9y4hRL`%R z6zelo&*EG)48;`Kxzoq>Nx(mKF6x&`?H3ALG~q?z z3l#JG&1JgI^CWajta~i<%#`OXZ*a)hh>p)F*1#FJaQAN zdD?I`xoHswPAT}CJR7=|Aj{4X`YP}_(U;j7Ebzx+PKD1jbgX5Rw*d(E6#bLE7md%z zjoUP5w-nt7+fmI&o}XyQ5kJAW=fctW=sK@wmoovbmoXG$w>84V*z7nIU&Pl}%a)sy zyO%_vxvK%46KN(CQN7Jo^!$n4sOG|qi`=`__vmidSk(6u(w0nlsuy%C;rI29j@5J5 z$V{gVE9+1@g0&)WJ~fsNdp4rJcebA+KY#nP3&MCm|ECh*i&B6JweYP&@yZxCksEl0>zaQi0F6@*?sqFrEShM2>YR6xA;=jh^Ga;b>U#~k@ zRSa*F1n6_mm1vEP-bsZ?gV*T%S$Ln|bzw2cJTT@cs^jZF!^b&L&t`LEhr-eLcvqLe z$oEnjC;AM{d&R^Z&LYWbC>lMD`o2Iokt7ThL9U@9ipd`9rQxR@k>nHjTqa^+BxiG{ z5u+WK&_01DrIYLHDnPs3Dp0?`K94@Abb-f)_&iSi%z7r{Xu?ciw4s_Cg5}uVT@qjb z_}-|Q3 zE8N^g1j_LFMXa|YTjhKl&ac3~ad9q@Cc9Hg;C^%jTB}Bj6RDHI8Kx#5f#zb@gUQe| z>i{TEyoZbBt3vc3+xtF_X=dSli$~8a;?PYB^lZlVjW!yKgK83?vDUyt~!TXJOIDR}fjHQ~R zzGv5Hu)Gj|kly2i+VNYg$ML(q8p^hBMeTTIP6dPIp=7&KFzT1;phM3RjgO>WLewImky3 zQXk8)U#svllhX^9z$*to=slW@)=#;$JyA8k43~HYP)xS^1(<$1pR+j!A8)ihKTWpf z-KTcT@G}gqNe3ZbWE$9b#i4OlI&zw6iRuymGW`AUyK;SW+ri1~q&&XY7E7$&bl_D8{Ys|vG*`pXf`*3J09---x6wTMRTUM~oLyN`K=cAe>XU2o=k2)$`m5XZL zp&H1}xyrDOSKU#}n=U1ermr&V-kXYQ{#auTZ+kk3YP1oG3B0Y!9=_3mv7ezRmsoWP z6P$Js+8jCv_T7of-paC_6~X zdU^qQ_W}RDzndCEd(%@uP!wNRfB0f2Yo6Gbk=p(j<$77c3)4(OVC`{yp8G{-4Y&Vp zJ>_kPLjCel%!SdRa2oNU5XD%jD$+@|N)ykkR-<|(_hqn}*|9{w9$z<}8Gejf+H7Vr zoGU0-+=~Qu(9Q+kI9)-xeh6KmUB4CqF-}Em{&H?VE#^t2s>XO7KA%!UFWlb<=Yw=m zJyyOsrU8D7SZCc;RFA?!Z%%i-CN$5)^(1Z=XI3*tY4!MgluNGriRldGy%4imlwtMU z_F^)OY%+tb8}M(+=w&rVrW7omuGxrc);5_BiZ3kS-tiSEm&{ii+CRpDbrDjX4L>m+%JXxM2 z>{HC*zRYgK^oUb?cK=OyqkIrQs>e(0C)eZpQIPy%gmMMm3nw)NmXJKG3>eqOG#S>r zNsRUw-J=+$XE2sVO8^rrP(|$ocMejuKeAA>Fc#IEGkppS8nzMXmGY<^jfxO>Mz1Wwv6a#UUo zZOi;EJg_3_KJgOxhw4d8+6JSxTiM%id|r@hkxl#j^TFz0FPc+3=|<}NaW5SBtcJ!} zHKUMZRj!1h6Rv2Sm)jkITQYL2ly4r2aWx(%XO@qfp6v=oxvsTJGd>3!FgJUH#+fHo z40Ik}B>hSmDAzpQKg9W-8A$LgN4fT|cuH}a$$g!ik!&3;=j+;J4eOrHIf{=2Ho9Jb(V z1?8djMEyu78J93Z^@u-9CacEdSxQ|y>X)pkEE((Jg@n1asGhvjn;>^|C&->uLG8Sr zd7o}M@|vU-ilKTIOd+hy`vh70x(KbGf{)QGAaRIr=6yoBY^H|N8GAN>-gkU&bl}`J zW>y&ldM-=R`te=k%)U(8#2OENL@{9vk758uR{$0wEB;i~yP)UQtiL9l$v zZ?e?mA*xxa_7tfaoCarVm!n*tdb7cB-%=*jJqNX8(wfR0;!STnI*#vy{QMP4Tvzvz zkOpIv>ynTFXZlkGCiFG|jn8)1=hVDAj_p0T6UAhnC}1)f#?0A22F0v6{KNF-lO8h8 zHluoW@;oJ43q>LKwg!s%)Oeh2(Vh&CJrmLR?EPuN1{goPF!3L{*cd5l;{Bw^{xWiO z2YxO$N#-OxGvH?rS8qnS=KT?Ec=mNMd@DMPa*57$1nC4zHvQ9Fw0_o9SVBgOC*z&( zhn}mHGs-5zfxvdWY)198d^tv}kL`!I>isCDW6c-xwEG0`-;6WH;?wtZDdc2+CZ;R# zye~=4V!M(QuViUhQOr)zVqJ3Ssy2MjyMto>85R@GkuG8-im#Jg)3$==0z28#^GzI# zOX}iJj``6%j(|}W!!TW|BiO8hO|bPXe#RsGs{&c#`}q~V6XW%`u7@sX2AV+8q~xrfcY_kD3>|^V)k*(5|}If3FQiinZ&-# ziG{Ek@~E9rH6f-L6bm-t?Wmn)8{;4^)Q3dOU545*Tp>**^7+VtY3eAZafTstVUt1o z#wmi;&%kbR7OY$ZnWIBB<8TGyVf(`g>X(H{F_j}?>N~!dlN@&l?L%BA6aismX#h_p%KHnMIq|Z`K(ul?$GgObR z<6D{}8_D9O@o|E&mM}Y|u#H|>qlDU7e?S(V+ZK^QeRsh6y8U9BaIvfcJh<^FW|Bh- zr&{_XU9#mP>iffmhbI1;Ab8?#QWRSAJ+c%3YjKC4ZSu53xlI3_C)=tf_Scp1_Y;D$ zcfh}28qg}Hh}OoanJ;{{m`u+VPC+&Aw+@CI?b584tu-$E?XbfM(og}(Vb zUwjtzecE#s7RmJ?g5~&FV*SOPaD2#;1qIJXb0IAi3#K>rvc%u`yx?Ek39xwP0@F{< zM(zCDwFDfD6=7dc1*+NkkO$qtdxzATMl-B^*`Ib~hu=xUR5J&ZOW^7q`1VN`9tO*x zcB~ATY4g1%?(3)o6!SaUnpOEfBacLTQO%BCPGFMA3l(O;X#LC&oJypnc7RnMzTcgD zU=kE;vu295$*7)wrFfX(FTm>J>`|^#y@~%DO|K)$4*O8dlWb?2nD0g||0+N+2_r^O zn4JO-SK&1_IFt@^CClhqcQ;hC=fk;B`1e0z=@*W2DYvInt+r88`PvD^HZ2BRg>G_JW-1g#kOVoq_s75L?Y?&p=bm%!oj0SW&V{GXCxUCi zjdWBf^%v7^sq~?N)^sM79vCW?i}}Icp<-01zt82blnN{FOPBM*g%$5Qus4%BxI2>? zN&WA$_Uzozjv)4!o8BEkjJbA_dCX6Lbs~>BF@Fp(=JDm@h%xt0b|J?6ZHzUW4}WBg zIeVQoo107Q-Q4({G3L7=&%(Uz1#31>_H|%B=D~HW$GoMHy_?^UGRB-(=3eSD^916a z{mf&oMVz5|W*_sIJ8!Zc^UEynq4`phdCU(Nd8X!DU98z$xzBpcx0g5{^Tqxg?vJ@# zWsG@ih4q+MUu7P1_<{2=S2uG$<~vi2G54L|Ugnvv%wt}2fqj{)73MK#r#M5i<&J&! zgZlc&9QQIOe=^3rbC&Zle}2q6W`4Gv^D*x^!+Ol0%H6oDX8Rt;=Tl#A-@rWPx6|y) zoCsO7dHNn_X#NxO+|3VWxtF?LDMj+C@pBB1xs9sAQFqR3x$@4Wpz|c&P{p zAq^$!^L&5*gs_xjHDtG4-ZzZ*JOnrhnFPc$;s4AT7n`w?-w z{03D;V@T$_B-zCO2j9OEbmojIB}dbu2Q@l`S~u ziS1)bnAjM8mgSJg{15BU^~cZAY&*a@&+Cy|Q4Mr+?NPm1oqGB{BU`+Z`Cs0JzAx2a zovlo~;3)D>e8Q`5ciF~fTcqVyA=3g{NBSf7G~?hN>`zyql*f)VZM->Z+7#&7dvmI_3`5mS zeUf`7Ou~XG%;$>>x!5TZ?@St#k#wYj+x)buRS>2@4s?FA9i6n$rTbr;Y2z_>x+A1c zS8uqJb+J9IvbUo>x9#ZupH8?M$D?EaU+fZpgVXo2(YaZeRvh>bu@4h)q2VK*3w(m6 zB_GAUO2fX115myzLIs(}v7%xaPbvlI=A9yJH#MLu5-wDqS%{&xwzOv@Xn*N5Y#CIe z9X3(KABVOaWcEaMNcrDM-7k4$?WVA->s~)_2vS$M7~* z`q`1m%qN&@@($Jxx@0B%mf0(xg#I}(QnoH(R?AWlAkI%2T^rbCt0??*k*3T2mULcR zm~0f)>A^oea;y}gfZfuRDr-Z#9p%XEwk~ORtJ97H7rq} zl%B=mU(?Cv#RGh~bOdGNxc9Q}WAMcp?BA6D+p(Eg)3XsT8}neRsX}Kr-@>o(?-07I zM_~=I=!+dgH%i1Qn(rol{riiHi^fv(<|;PVBNy^sB6J~C99|bMBO<*Y`;&86|Ha#I znRc3`q&G1|{S%Pt>0pDR3eb>>M*NL9cIrw#JMD8FYxoV37Pk;5Z`#x4f%ORd=fnLi zps4d|II$}nY7X|)XDLF?alX7C68sQ+Awnw8Yj|0w!dc9A8Oj)HE?L|-2H*3JqR`3( zw}v0HeSv8xnCTAT1s|B@)8iP48i%Qtci6%#Vf3BVr>U8p*xSI}=SpSjlNm(OS$BjP z>rzzPAZQxluUZv;1|^_({Vwd4euEv8GO@AQ0J9FihGNAr>{57ytye7(c2JIHC_cq3 z)j8OsuSL0o&v8b}4I^KKDKcdc?Q7CAC}Wl8Pq}W@9%)#VZ7lue!r&>S?Sx7zK;e z>6r9+DN^NUz$3K+BX>kx>QA`hu-jeiZ!ULr_;CQ!W!iA#=L6oB6IO_KO~8Z5FW8n- z2|L81^+FpI1UjIw{XRO+bYQD+DJJBq(5@BodJR=YN-QQmX<}FIkbz-_1DIavi-gA5eE_H$*P|U~A=GVwzqw z!lZOjrvDuW4ll&?c4egfEk)tlLdfk}fup`J;i&c!t0LE9YhOBo|CFOT)CG6+#v_}n ztr$KDp?mV!9;!)3doywG)N8i8$b#Zz&Y=0X4eczPNJpMKV%8)Z%AROP>m(0DUU3q= z-EBqBr|ZLbAP41WWHsw59@ETy8Hs{Pjn!y z+V8B!{{;;8TGK8gTQ>j7O*l`optLF}3OH+o=u|JfF&3pAe@B@8BSY-VQKtn<93ba7 z2||ZuNovYia&_#2-cAL&a-#<`kM~&iqMRpXIT9+iy=degw;b?OP(MqbkwNu zt1quWwVJ*EWk5dpmE1M0X_=NF>oK&U%m~mO-b%J!-kHR_9jH)hD%14k?!A63@5{Gr z#A`ifOO8~O*aw$mb6hb?UhT;?AH9Lz3B(NiMX0^8k_qVw(DlAi9M}BGBwNJERaS^r zs82}BFgO^a)Bdp&SIY6yY9XdJ>O*=)I{L4#Mn|6j;w&1m zZIdgFDRx7SfD$eaO(D_o`;iPqtUt}84O!+$J2Vg8mW52dl#dj7tI=?ykI58C(X8Ws zh`M)*y?pzZ`+jR;WvvZeIz0sGveT%Z=0#uzt@Gmb>#h zE7Ow3U#CSS|78_n$@fZLkZukn>#sH36t1B|BYljCH9Id{Kajx(S+&8@JS`35OUrc`hE755gDL0v?J_VhnT zgZKbUj_!fXDo4`z)rt96cH@}#boyZ{L{Is`k?m|xF&U;fFz}PPy&gw7+hvd`%tPn} zA+mbz3H2?%*slV4O1>mT>UrIGKhu@+odu}zXcKPEaG;PiGE@@#1K}SnsDJw_95)$9 z{QN>>u(l7CQT+6B*%0p4G$C-Q1jU-jQe;aR+b>b*+H(TcEQEdb>eGXEWH`@0lnK;?aYhSJ)n(*X%`A2cG>y6-LiZplpr9T)j1*s!<2}^e+p2!nPEz=|J)GD;kQcj{Fx#qXCyP-dj zXrV;GN6Oj#jPHnAZ9vZUU)cIP4G50q^u);$NcP!NUG!7zD$9ge2xyW`0z7PT&@^aC z&wtf3Hh&|m#`BTrogAjUV;}4veSppjVyk9tg8ad;G&oM1==^w^Bdtay4~*$JzYO^X z>r)eyNb#90Irzy_{0?n$F+YbbW}IdSS0~GzT#s^hB+E0FbgJYeo`yJ*MciFzI7LFu z`8kH)rJ|%U0mIu0akV)KRkJqZTg@A+nJ-UMwH~1(;V%q?_2{N`G4_NAQLLFT4ffp! zQxv2b?kdz;HvuQIufb`F24$~u$A|eb@CX&9I>c z+KMn18`Z#~3ngerxiq(n`}(s>cOKiT2H^RPIk%k`ix#`}B=yk1YV==0(A zgG;EN-@>vlSFrGJF4!=@-KXNuX!_)bVFN8X_53%U&YuU zA7f>EHL5iLpHJz@ z)rOv&y^q`wZQ8v@ha@M>$LQ2)*yJ3J9X>(0U+00E-eveH3WUxMMwD#?-nYI$O~Yy4 zils_ekkXItM_D}C8Y$#&e*uxBZM>C&b8&LPF+9KZ84}Yvq4_Zxfs=;tsQ()dJ`KS+ zpL*;w{00AJd2;6ZWsyKGON)`BQel2-f2GF!^t9$BKBIQ;FX&m z9V#e9P;oSRJXFau>N>Wcx`&MWow$7K5OPkM5+lEO2t65&b*20O>7b5HwqXK8?|ZVV)6e0w<7-^*C}Ys6 zMe3~fvOZbWaw^Aa+MRLj^~fiG~jt>K9-b4AH-5h!n5%KK`n zi<_S(K`dx8OIyAZdswO98*sD@Fu zr6|?BO2iuH>12{Lf&S}^Lb2UcO3fKVyN9yi{Y;*zzI31+vs&<O+0bJb3J|!?c)8l}oaaEQO6k1JGm6MIo=!T;>S3L_p3NFf zgxLLJR7>1pYt(P!jKFIYWj=IGA4)@7>|0dxDN$FW5Z(y?fypr$lFSzY&!rLbNA*ee z1y>7SzCgzpV=Dgp3LCW_VbEBOROuP&X7@lX)R6W!zsH=IZmb+Sff8#^-qr>|x<2JF zoDRJze!Xu1`j=1Qeq0=@z9fauZc*%|#W=bjYYO`lN15JL3jd<5+Lm%V1(3HkU4aWcALCwg9D7`-@LHiQg z@oIJ}u7vW_aBe3nnt0pw_~LskMqvj_*{jI-cdcS0qwQ>X$=qTiEfx6OGpB^w7Sw+B zM3J^NWeyMEvuPk^I2uxiL@$D`D&YF)B2;bqgu6*|&^JC97GnqTaFROawi_d`yaAH+ z))@B9b18}yB%!||lv}P<`cZB??UI+H>(l@6mVT3>SZ!gtl@|kJi6)-Pg*7blSsH@$ zP8Ms5zh<%7+u-zjkhg8=Q8w$zBNn`(14{=DNObi=HX*kUWwPqzS}%^q&qHu1F{8=% z9qCr`DQM4>q6kG>8e_WyB8$dR!wowU{v3@0QFSV65T`#SDy~_E&Xg^$PNS|0yn~Od zX|lN>J#=L(C(DLTgjtc5o+0&qdc)h$&uFZWJ`FF5DOy-*L7PT7Z4`8wWoLKcxoJ8Q z3ixSZ(R)lU&B7mLX>z#L0G;YM*advWGS&yBCnFekyo!!TeX!gvNJfkHqieAc+1UTW zgn|sLE-|G)iz;1{Ki@;pUk!5GYspfR?&85@M@p?!WbXV);I=h zTHI;zUS+CmPsiCJdy3pCOmhB#NDi&Q)>q0j`JM~%#%CtlpSTUAc{2u?_60i$0yrjlr5NvSM9-~1vHROY!8gE9 z+voCQfW_dPLl=xywz9~BC*U)GJZ0J?rmbC@tFuq98YgO?J;Ha93(|=v8ajT*agpEc%1)_8GbKhQh@<5sqkTU zt@zo?<~a17vqEXXOhm=*LD(3o#3?n4`y>QmAiNAQSdZx@2PW3@v0zF>lN$Xz6!ELv9fDDPE<$Z_qyp2&ag(o-?@18{y8?^cA+_4>M%?CjE0?V zq$D7RvYKj`&UGc5@`c!96b`2rJ~|%u8#zU3Fr6q!=l=ah{njJUHtxZsMj>)Jp+X`5 z`01?qO_ssw{sB1&`XH0czNM(pUaQ~e9V^Lpbz4$W!WkSdh=s;jGm1Euj<6{~NUJj= zv;B$47`esG8Lz;TkA}aQB&w1J5RemqzsCCTSXPTD#Z)w8*y3N#T`cn{M1$&SY!Iu1 zS!4^UHlK$Mr%BrQA0fX!f}5#VLE;-XW84kGobi+Jc3>jSbvTN2`MKEpNsG?g#KH85 z1^v>qC%%s^aQx>&d}8Kg@Y4l$^``Wln|;j}?ZVB7H29kiA*PhaB(!qzU9=rVyp-Y@ z!$nZ|B}9wOjx&K9CTvV#GN)x->5l3y#^3M&e-=!nl{?az{)#g&y=YJG-Na~AW-2;) z3<0}U$ci@NRfsKGS4q%oJqh@~(n9xT74kkPK+|h#A@k6LN*x91rF8u4bwgxU=ywGK}+)*6Lh*%y zrfmW3v)kA+FGbo%JY+Lpdy>!)jxOx{dfhywZD7)!0(wUcJgpfCM@ z@O4Kf>Le$V(FzILdMq5tr{}QU67Fzp%1MgO9!J_qSU_59HoX&S>th8@`#rte!tIfy=F{rIgN8& zbpZdJHl)8+a$HU`hN`uUh%VOQ%+a67-XKFuGqSKE=qJ_&%hKwuCKOE&BK?7JB=K7w zW4al=t|?+uj=y3%oxJG8<&Y9T-BE&n{&nJ5Sx$7{aU1c;uWe7d1Cf6 z1yahqj+Q$XoL)NsuSeWDUpdI$22F&^>HpO`A(WTx=dRn0f3bI1i_!w#cS|v9I@F8i zmD)T%pTFRn^9IXi$&@Y=5T)6If01_m9IS86WuduFZ0^Y$P-e}%uhXxvddKb9{8N}c z)ji1aBm-4iZ~#Hb>>pEq)de>4r^0<`gZ(nSEG!tdX#f& z6L?>&=tqJU^*y)5PCjLte^Q;=XSmRP5jToHU`h|JJ5jJDkHq;Esocwfn_nGinZ6Ql zs=6w9PjjS6#VgoJK?RaHM`XWuBTsU^F-6!ple&E!ek_UtuSS5RE$%|wEd}Ay<0#(Y z2@1<15zF5N#qn>^W-LGw`(ja^@&z~aM9JP~4+dWJBjfupcugtT{ltj0ep*t>-^Z9E zZ%JRPb*X;#Yq(udp~c&5$#>*A=hLo3&Rv+PVXaQ*j~<)5c0C7CjJB#>-QJeCTNm#$!eCICrPmfzhc+z zD%d*NQ}&Yqyjqcg3y1CL?V2v+`!vI3kuf!9`Sbn=oyLi=VzmF))8fxgNtm*Yk6doZ zl?KTkh4vIFy1&DmP6>;1xr#DbOFEN`ALohf)}zM7T9oA_NxSZ9kdw3yrMx%Le9NBhOp>M-KmR7rkcCNwq)Q$adE9SF|nc`vvP|Eb;B=Rd#X zZ$dgEXA4qV-$fR@I1Nj4jM#ux6Z4-Q1QX|fyhpO_Y|hN9coR{{;;V%5JY_q`SOX5( z5g5t3=@MEhg4CHwFl!YmotrTqw>wY5xcVheeZ3;tR$8;%kIHz$`D_BdJT`RmF5}y! zOZ#3e^}DDHGd}B%grY3tjMhcRiL% zrQ>SId-U~Fq3^k!~;wA3Lf-WQl=YS8Lt2g-F}xJJuS znYA9>fq~Gy=nKPDo~Q^m#@%-4*% z%%mvyt`gH8uSbD%bSWnK7;9OsLtWZ&*urJlH>y-f=TjD%&&6T#1{J#8a{){Fp5W!N zK78M_8*9r%u`KTcYGp5?>aPKUn+NdLXe!oDb;6F7kMVPIAtHY-!{Cft*nF-E7P2nf z`Fn%HMRlmV5P;x!3^%q~Qcde&Ed6!}`*N)*GW8$=o>^m}i9Kz(oO@tIKL6;8*S)@vj)z6 zlOx^P)7bLRNvKScri+c+Y8ab{S#hzWFH20Pgg}Qx#`Hk;5Y;8g@ zMt_l6;zq?=AK+p{4Xi_FQR(A*;BU;wHyH=oD}4*fIS=roX%9?fRd8?Zebk?H$EijG z+}fOu>HBu0@4N#x8NWjNUKiSuwFV7yl~EJxK@W_g5t^dKX|&4`lGfsqhXGAJCPWnz!y)IZPk*{4sH?;q9e3>M z>LFngh&+i%wG%u~z6w^#WeZj7S269?UTneV8stjNX0?m>VDY~6ep1$%u&A#*4{&63( zuE|16`6z1rxxRSR1_up9$Znz%74eV7a`RsZSJNl*v&PTsskjxON8jsPk>i(u=w<~< zROHT`S1Ee8YmnM~E>}7G8;6%JMc;c@a^UoKIM`pmV>?)p_CCb6NzlY6dUVZd!4<;J-Pz|6PXK zEX+yb*=ZzN>rwtfWvbiaNG2_A?0lgV?O*IbBfC!(^Rl(cYSU!8zqXQhafo}gT52SC z%APJ2dXvczSNrGLQIqBrN+{K%%Fsy^sMd)sDyN~N^BX$L-r&df)2RK`3~R^#Fq1zE zT7LZWWNAIhIPSqf{umyd>f&<3UL^Tm!dQ`aaFP9sn^=aPW-eD)u0ebcOAtQ6fF2I% zQlrHS)ThbPzF;$2*jWsI=kw6$vL@9HYIM)*G9=81s7RY;UAT#jji!_hMG9Sf3@B+v z2v-v|@6N`Qh%U&h2y@x~G2F>3M*CfU8m-F4*{k7f$`*5K@Tr2{jYQVAP={eMhR{t$bFTK{ z9A64{w1PSpHDg-u6QsLX(}ba&?Do$X)HV&nS>KOcl*z#Giw~G_`3R5mYVj~?9KHXb zL**|tDe$KXZILpiTiTlBf7qC`;xy>l0dunFsc^oQ5#0((LU;2dO4_PRXD!PyBbL#? z7i$uKbs6tB&!XFcuMsXa7m2wX(_&o#Gd(R_bH0K59$(S?!39zN=_nUgp*wZCDEr-u znfJBGNHYZ<;{`ZoOq8TkGEvw#2$_5Qw68PJ)y1L-a$h^}rtDzJYVGIP^F@GUuU`v7y5cJ*_iwvB!jZ>~~?? zNGQZVSkYq7a7at*q29=Xj_Jryp{ysheD*=;GHnV-o%pYI6EaH0O4EAh62n>*IoQF2iv22vv+8_iFb zU);fp6wXJVH;9u`bvU^=1*;5xBY$QxR%NM^m?U@3rGGK4ry7*es77sB-K=T98g;r0 z)5WEi*|az#%A6Disq^WW*R4x(4spm(egfGp1JaX8gpWZizG=P3i-nW1wz!wI%zlH} zB@3|ZofUG!I$@?Gh2{q$5aF`YZK-l2`7QM7@1SX zq=XzPNi2h_ozmF5(}u7=A4grDG1DtVyol2tB2N2`KWoS() ztmniK5;%X+Im()u=)Oc@s5$MNBtxDVf0*d9E0|fML@v!P5ZrYMOHM1&u7F0iZpu+~ zL9Ye9O<1%Zk8*C-GGiY{iuk&pZPw>e*fwWw-i%~wS!VR*u^XkNJ!Pp+J;;2{5UX-; zz=_Zctn--+>OJ2f(O(Er!c|PAtslwHCNVExMapgTpyxu|S!tUC*`pROwr(5KZe9B)M`nayMV~UT&iF& z%2H=EV(jdjOnsIm-dKOaJ;6G5a%nZYs#$}mz=v#Furi*{@St&qKiFW9E&Ma=sc2a! zlL?xP-r*V4pp=K=JRa#dJ;BzztDJ{nOaD#fJdUI^BqvX!b@NK#vc40U29jj{bchYg ze}s*MI`L0X!<8Wc(zcVLPusq+Er!pqM_ZjNdgl3 zu599bx`SJ=IgH5MOOE_24_oqQ!mIZk8)(q7Lb@ri3tD;I_}27>fPL6`F#!!a}F zFD^B%NBGnY;0u)|yA)}9c&i5@>)mMon=y1?dn@jmd6LH}Y5Fhf8*bTH(;0e!oia_( zouovg=WaqLy9tvg$kO7c-Kd!}fa((2|7lw`95%;w4sQw$d{benJVmze&KzvC5`bW< z68rVY0*B`Zuz*c(AUOCG+qyg0DVO)~leh~=iSxu&{zJ4!5j14Nuy%SO*6eno<>ss4 zy6qw6#d%QwoU0JzD@JrVqs0y{c^f!eivNii9sg6q^Hm_Kgc^;$k}S5cwV^8y#*>k3 z23Wl(>CcN{JGBe2^ZO)PL~1*(~CL1#aoLtGiBDXi>hAax1EJ>{v$N}c94I?<** zJf?rxkUaN#P(oq|Pr6x}o3R||k9vX2+E6!oH${{3Ms4Zx{;4#QDn-MqCenm$(v7b?O2$?g4@_k_tU(=1j zeM1E`*lD3w>E-?D17?okDo9m!GX26GC_uE3-;Lu%NkMlw_1z&(W2uaQf7gT>F%$n&Zgst!+ZYm@zc8cop_*eu8Afcv@L= z2%?quF%uccJ_mpWq(C2zqKEzh5vwO)dbgXnDZBXuJn^$nP z^_LOx37lqYmx@z7=Uvu$<}+_WVX_{43%y-sn4=A9uzQQ2lODr4!<@cJ_F>KuZZ<07 zk?v+=)-8V$>u3Cd%6VsYob#AZgnh(_Rc_Id+<1upCV@d|XW^`|3GQRw;x9)ME1<9A;tKdaHxNK+Vr&6#5M9c5L!syQ> z9D|;TiQn=uW~)6)RkNXy)Qd}(Y#_M(Dh@tWqzuC>+`iF`@*U$Tc|s!M9R~1knhqIC zB_X0>6j##tsi=A?eF<*na=Xu*$Kp-*IKFKbm)+{lpG!Hr>+tHdI3qJJT)4lL(~pJh z;L}Ceqa23)sfMndOK0Qi*=Y1DZ^70-3z3~+O5zGzuy%0-q;A@AzI!Chck5v}m+SQ( zS0I~*p0q&F8)xS!Q~8<6lyOJ`CUIQ<^>w2S|2A-bB4H(KVowfsG2d6JsF>W#Vg^>Q z2dDgD?i9_|oO;eaWjf&cC36aKA4B=^k`NxUqa|-eX?)viWO6K9_#!dV@zX~4yRTT$ zb_8+qKB)OO3OkV$e10R27@h#_{ckNod;1}AWdg$L<;lwZ3uKI3aPpQYoqEH^<)(U= z-J?YpOjhG|wkeIf;6Y}0w!tLDiq?rZP>XI5K9p$Cne3@#x@H*S7p+M5#YFmhq7@rC zJ#jXNV|nKaQL&mC&A4t%p+^?t#j|;+NZ5)U*B4_}r8@YkT=4kqemoKmhs(PaI5V?} z^KKkyMSwr56n|msY-ftl-->|Hmw3_TL?*&#VRYvd4qxRbFNq>dY&?l6t^N4f_7<5s zneb2i2XV1<%u!dTn41!Gpl^`v6V;}Oo-s7CJ(I2N(IoLTDkLS8!>({S-_=E-&=Sl< z$_iaN*m@cVcUI%b0X>pakH?vo3pg~`g7|1pj(`8iQY4;X{JUVJU6jQ1b@gb_I*GmPb;%WUH_1@Lzhb6Ye-Il_C{bdiHH1H3#zp(Fl%Y1vvA_?o zhbKXb0q+rP{Rj`6r6_TQFzrw-M@5tm^HR1Vfs^)ZdUPDi&Tyq0TT0lmIp5h78%uIZ zzQg15ccPwU3z_gd9)*1JpstiyCY|U;x4ziYaBKoQENDa9mW;BSGu~qBmYb|m%M4-n zn=m6#8l{8nEcw?DY`S=kEw7QI4~|pltD`JwJQt^XWiu(gNRv!Ah?8;u6msJ@Pq}bm z`c@`L?bjr!E<%p-!X;^y>ti@IR@MCj2tD-5hW?< zX|%I!0$s=)w2m8ijfHa@;x&hATU(LK(i{m-+Ios-a| zvfBMuf-6^hbQ;e3wV%{E+?C0ow`cbmX=!g1z}@ z^oJP9=DcQaO`qXJn>y{>fu4kK(@9fI^Df8m)G0LhP$nAa*v zuPkKgf4=UPPwsTdQ-P9<1i60bOxAqks8X_-%kM$9F;(bS;iK0M+`d!q{njczIw@eL$ zzD>f8Iur6c`5ZqyMe&pKYqK&xVd0tOcro(~c)PD7rk^{5jTv~-T8OcM?v$bsj1QMe zVKvd6ZpbCGMH|Oa;^-hgzc|DaAN|6{G>)rE8sZsiOVIYbG4vpk>qTK@yzYa0S-aC6 z+%UVr;~)6M4!1;OsnUy*S=X|e{Xv1IL_LzmX6Bi(Wc2g*h1 zbhJ6SD;>Z_OBF6#HKrP2;8eIA$KF|xUC#jorx=l^fHoa?bj@{I_+ln;)tuwQHoKZ0 z4(G+)RG^L1Yk8`^$9bPTKvOm;lNOi5UwY(0QS}_JlOHYN|v=5Lf z6d=JHq43=!L6&RGsh7*Vox=Dz@63@FB~{>D^LV;@)sQkjwPNJbF$~q%Q+lfmow;%m zbMq{SWr~pG_ET8>X%a>3S0)*sQ;3_HjfDMt6iJ7X%rV2s-h+su187{#WhcF2)V2E# zX3M9r&zYw5Chaz4mQH446kSMGrxuwt-AppqgsN|t(*lFjZ1)5yTD94cnsSyg5lKP% zK3^*3mUztz;u;D5HCN5 z{v4<*Rx&w^Wg!yOyxfpoJ8}v4cKtx1wJcGp66x$Sp>rC#w4g|p>!Ic(!ewUnoONmV znhmAzHzjwD36)&!PPPkdsQl*{l-m2y`(LIs|4km+7jtOojyj|r^oNyF4)&?Ohw^{h zQT6g18cy87@+*Ew^Up?0gDU5>B!Le0qN`AZmWN(JW#a&DebJ$k;R}#DE5I?PAEDg1 zm`$&JhDp{BU^MuNrx|`>%;=nCAkvICVua6uoDxqVAVvdU*PD~5x&+-grNO>H^%HU%~gkWapUyFZxOFBfVE zxr1HrEGU06qrJR%jQH4+xu6&Q>dC@JMUIc_bD;10Z{Yg2N9evFLB<7Nc$X&B!B2S< zg`KZ>cGv1)@m-pF9_O%0pOaaxMm@JLVMXdnU)j8+ZJ4C5PohT6yvjM%$kaEX*vPT8 zsAMXni??F)kSL{G6Gv&iFCvs=NO!d_hNQw^Z>LO4AAiLa-*RXiF{GQ(W9WBPK2m4s z(020=sLpzc$?IZSh?WvXtO#XZ83GWMRiQxh>%1Mo{;bW}fC^4tWiL}04WC!%HICSj z3b#|!V`;)##&K-ry=hc&dOGv2vZv*K{jBCfA-;Qad$|QQ?ET~l&IjDWi@G=t$4cwD z+_Zz~W(tr^JF&1zDeB(z8~Pa?Z21KxQo1w>lhV0t>f?Xh9v(lrPT)SnZhkVlK91go z2~l;;4-}sfB~g7@3eJ|Go|_BVGmq!+kgdd-z^82V$0KO59OU~ByBhEwL2@(~iNir$ zE~1PJ*-tRhWdqu@EO17%3$|Vh5UApY1O4|Pks?U1ysi%pbxOUXHN*nQbu3JHf;}uJE3m z`-qf>W!xV1I5-BoaG8Y~x4)@|lyC={W^;hqu6Mu*IimDq7ohsyosL-DL8!b1>qe z$<4$a2ueytwyzY01bu*pFiO6 zEAPXdAzT!aiHP`1o z26Sfq7!rHnSh_q?nyyd$23Lhc#rr(U;Il!9o)__x?NBYAt?q`bj37nSmty(Z@ie1H znvS*)GM5K;U6-{~ve_w5S*GHvl7(toD1Dj6blXkXgGb4%TFajPQ!PV)=4|+Jym^67 z6Nc{vATP+AnoiZ?SF0)VMnbTEAD6Sk1e?Ccz-((jVi&w)n>;+>o6qeayp_jDQlhKW z2T7{kC{4HR+)9JS$dlzsY4R!+XRQXx^kb$db!0}PTmC(7WHgaIHopq7)0ri4|JAdp zUTYALtjIgAag|BAer3|(+z#MT?kuI9V2A65u`XVd+MYdO zCy?hnYZBy(=9VlqsPVlu9jlxR@gQ+Jxm}#L1cUe`oauqo|MHJ%w4>CDDk4bL2P33UT~{3m^Hp$04CdloHiBZcr{ArI-I9S(SUvy}OWiQJ2axV;S@aBO7(5_>dAS8M@3MW$lS2N61YC=v||QrTN^b9%sk z6E3N>%wpJxu1t$YoAe@9e94(kw;Ges)n}|pL7d9}Y0%(|gRD@8%lHK>NWg6e3u{&2 zb{NaC;lh1xFWiX6Z+Z;ZtP6M&$8p?Ol3^tG0B2?NXuHb`CYH#}L{9|B=i3zKYsc~D z#==xF_?i_hK7-76JxG|Z#xYuIWDu!B+h3~E``ers)xqtc#BogY4I>IaFo9I0O{jPz z0dums{|)eg(-7L3NK$p8j#e#d%gIBo441WK6`*8aG-$l)YWOe1eqJQ8CcifhOvH$C(%n?z`F0oeS>}vTTZ|_A%nl z#Z){m)Vsrv|U3map%(5kyh8{u;%APngQ0d z?L229oh_-}$%AbF5hlbB#_s)bxVzd6o_|*3^!yZ@su~Xa+HkCG7>Q_y66p2wq znC){NvMc?m+s&LXvAcX=4=E{^n7ONGq0AC+jf@EI8X8_PR0B!x4&kg;?M61!Z&lniHj5|xcl zN)O@r!Hq`79m1Zvy_h_*0vb7+SjVdp%9qLzVv&W%7gce|p%Gm}S71|bJy?h5AT{{` zyvFW?)tD3LYF~xygf&Q9T#RXQWq3GzFCrp_^8L((w9c$VZ_5eDO%9;dv$mk(?nXqK z+R+22IIQ&Xpq9tpG;GiybYCW+Yn%Vqiw{9AzZ2Gf2%>e%*T9JTbT#__u=0|ENFMea zjbR;7Jvc@vJh;X4BWI!WcLq1fuMpdM z=U>K_p*HmMqzuh;ev41?UqxH150%t*rMo&Z5NocGuHTGzz6sXCanZ5B1Jzv0D9_Qs4n$$MJ^ z{=k$TwRI!=sV`CRP={K7*;20AYs7QjF8ISiSq4)5>_ND=k?^cBi0;+I zVd6CjPV4lbGjjb=^pd-;ZQ4p;4z!W< zYL=tq)=Y#SP7uiz($w|N5que6C0SsqMLVrG!$;0aR8G{A^q*ADJd0hT>o2_`^+E6O z`E|00_VE|Hwd(L%cb&vdqf@ADG!l~jG7_sNmY8-rPwdK`B;K-)Y!c}#37>lvmCFuE z((ms_kF)G`b+aw%UUv$6Dzh-TqKkO>`7A2d{6%(GRnqv$I_DrIdXTP6)~D}_G__hR zu{EO9W+l{ZzKo3RW@My)8hS=A;a;mt{w9a8sg^^{y9{Y@+G7~BH6i}qe9T+bmE6?c z;o_7fm=P38cP$iYa>^ErKI=#3iJ!$A*0r9DR3^98pGEf^Zyarurxof~M8(lJVta}q zi2=%__w_ydcW$IDrAiB;+Yp^?PqSXM;M&o6gL+xfDWWe-4OKe^pN%Dx@egfLEF1Ti;tYuA9I%P zggb^IbeRvy-;}1Wi#Fk!wF7;;*@?>^d@$^Za@kCwE(KP-G*7h&HyEyB}w`n(49=gFc#3XtWxsK1r5*aquRo%LWwh z-zf=;ccj7+8L~OEOeh3;QZFUul;~MgT1+Udk#T2D+MQ~T4W`X?L{VktBza9__>4IQ zVMp*_4$p6^iV=2hKbF^ZV%wHVNERJJ=VW6V|KbfSnDcY}NGckB)*!Klb2OvoVf(5M zq|8w!9oDxEhq}J; zcm$R<9`t;j92Fi;Mf6t*#jiG?gN{>Sl$MX#rn)4N^S~CDVjP=j%Dwu*xaW8Pe^r?6 zF=GqAGcSqAKK2y+VLf`Bn!m$|sCf+pV`9U;HC`RrtM{_+zLQmc;h_Dh`FrbTU&&tt^WZO7o*szHNZnd8vMEtnA?O_l9;#QeZ4_;0QhSO2C-I+NEUYpxEC&e|@Hns67W zSGicOa5-3QL>2}=34+b-DHz51sEwyBrOaO%;%lJ3=sVV$I!ty;`jR|m_FRwCdxEerra|l)cnCvkhofw|6jtgcVECA3 z@kIW)=(bl2;|wjy-Lex(4+HRToIRy~Qy}G8M(CBP$J%oP5_7GPcl{G)>s`S7J{IuX z_W^3_li8mc4JFw|82E60W6m?=`IuwYQAgU*zZ%Q3LlO1JhhFrm#nE|&oU1XVak-Hw zb26m0xy(;l84I;+OB$@g|Gx!m(7-&C@67jz%c#TO-X^rnn0pzQ%MsjRM-}@$sbQfU z?KQHbEu~Jhc2O`Q@&mBAcoP!myC7-65*TG9a_03vO!?0P@^gkE?%#XtZ4IHsC9YV# zzXcae`cb->D!hX}VC#}TRA8f{q#-GY?VAE)?ieP`IE^jL zHyAR|g&9=45V0T|@Aj1)7YI9XB?G;nv_&SYUS< zk4h6U@WKw5t|>vg{t674ehrOo#Zb=OhYzo&<9DSsCGhil_RVaJ-|j_kTT*bkED^bB zCUlzbf-67sJ^OGVZFtfTqsyG>S9<_;(~iTJm_V|gLi9H{92d6i!u7m1jJUs1{8x4k z7KfE6%qUlE+?ItTuit2WQWBh@o+@TW<)D4eyP)9>P2!mDcC7sPsi36aQDKmK8?$E1 z7L8h?M;5L9Q10N2l{>8I>%W=kva|NLF8YkKptfkGU@`cg`1st6&R4k?DXzRCZf#=j(u)3Ku~rv) z{k#jG)24J8T}Wncw2;5AMsB;h(I}%x;d#Y|?xn349=OIGBwNIuStM2ryNt!I1tMb7 zeu>=aGNh0b-qbdr_47b+=!F6qet*y2eLrzz<2N*P{>8%m{|XB4$&(D^cZHVTP6ST4iX)}fLS;cd&J4eW=yn@%_FxA5 zERQ4nXdIN^>=Yi6d3ZK{7DB>$i^S8V==*H}S|iJa@{dE<_2sxkVZSqfuMX&Ud9c{l zIu6n;#<)6DMY6xjjTs1&@kjl!@Xyz!jq|)QVAMTPS**-m9T|QH8>9K94h=~kfWXt& zf|v8XGx*1G9M||Qu};^bL)#NkIkK0iu96{*(zB3GSuFNA2Gg}=6-aYS5LG*SlWu4u zd;Hnr{26bu;y&J}@Jh+hdq0J7Z3y+zO%UZdwy0pXg2~{c62H+#cxgM3Dn`CS*G4H( zRdWHStu8`lcZ?`Kc@aGnD`6I;UUaYW68BGEAZeH}Elt+L(TTqx$4s`G0Y62Tx^l!# zaHb=|3QzA9L$=hOboM_*XZ}0?H!S$YKihRC5lmhoMc5OrMmBTzi?`gNUhda+v^O@C#{8X37`A99>E=Z<|F^X6)gX8Nn$@e6FF!0 zW99ty!R;3;BsL~pDLP%9u4+e1T*rFQpLjEh-~FX<&SW>*o1sGKYDZZ!2qWF#vEr}h zez@?RNbZ)l*n6}9X|X-&appxnOWn{hM~^}_$6gg(2{^A-zlKMgx{^MayGbS~b{));$0{dt)Zrnb%YMv>Qzy z{F9APXByEdD1Uf(VP~y*V7j>>sW=g_Q>JVs>%=7d}H5c-*Xmb~;ll>$quGo$N z58t6}zm}wF!bu!?RECzAbyv0bB%{Z)77RaNM+@JW(zBbk-2bv9Cr4Lm7~x2LcY4tx z6H7YBn%?I#U1(=o7Tjd`I#FXs8pD~}!|XKszyMn2dKgjly=lV52Y92i6pr;bVYo0K zHnV%6&FMOpwAa8%bphWcPvTLd1)V>z4?*i#Lo{^cdr1ni6l6%oM2l`}a~JsbcWAC| z!IP!Em_=C(ZA&RyTsVx1FBialQ6=8Y??oHNJ%CBCE0PboVX)mh5YraM3%Pegu=v0% zj9O}1=stWn!e%eVxw~!qzLC)ftI1&|$!#e)Vk?4zy>-wXmbOHOu3(<6bF|v=BqhUxO zE#U6iKfgct5hz#&H`kLp@Bur>WYT@2Nk|I zXJg;J!|3Te0FkFNaC21=B=vp>bS}Us&f`ZeT!8VimUzZos4JWc8M?q2IX(K)*pzK3 zUC%7qe!ZA2Pz3##e$<}To9-JN!l##l-l_(W*|>xFph@(|WiUP0I|sQ@$MC*u2jcbr zO8zMqq0QwhT4(et=<1!1C70ysQB10s^2QLKHngMRH|v8rgt_UT@x0W5WWvm_RY8$H zd@&>UXgQKT;DwPpw&3?{?ozF?f!&2j6q#z%>Vk0CpGm@NA0#s6nXM8=X!dO z#5G*f-^q_&{OnKh?SsXzv4Na<45Bg14aK|(F63AHLzo?Vg`M7;#N{O|Ldp6Ie0#Tv zmxGFhM7a@p>TQy<{IgZ`=|M-f>QTf!P4dhMqCSh{sc#PFY@Pbh-6{iedjA!jJLM>K zyc$`@$x=NtEaXS%lWasYN|tmWw5kK$1B2<|lHRVn$E zwgwA7e&IQ3tGH!z7*;D{#af|>ZxfBFA%8led|JiwU@K~kbj4K}9V}mGM@=CKSpK&M z4GPyL)oPvz<_)IK-TJg_?k1e!*=2l-J{>+d8CTVI1gCC#Csr>lL-Zhs1OE!d+QJXG z^7~G3pY<*{c>Ouv{5~w!dCTFaMi|Yl{~>&JmGSp?cQSEWCb|bnFxsyleXBWwP3~bd z^v7ZJ9lD2~k^d+&`~u|9B|z;V(F&tuIPdWlGx$5s)NsaJJ9*Y5b*T3OeLRTyh=eW7 z?L0%6dH)z1dfU*4!cH9jegpBpOzB0EGCg^D9EdZe)-!LIcPvj)G8<9wtRr~H)PI=z zV=gN1jubwpb!nN@W|+FCNLH-$!K>a&G4X5%W}SQ|%w0CHuOLlS)(!t_=fFQ;2qHeQ-@MTt$1@jVcHA4hoc3L8O#yF6RS{NPMco@Yo?iB6qN#?K4Lp^ji9^7^#SwC%x+p(X$DhbJi z>XUoaY2<1<(Hv(*8aZGhhMqiuThp|->j7pm72Vv1B0Gf-v7r)#kY z@|uJFJ3b+6p`(ayU5h&>6>0e=8?sc_;vByvJ$+(=6y^>p2uE`p`719Qfw(Jk!Fw$;?c|oLYp6YP%Cba*?x-*;y2{?n{6J_XBzB0WlDuTSZG|jPP-kppVHZM7b zlTS41?ZhWy75lp9#+uOulL6ShC=64GgT}Iy&pX9f*p6`MUk*#8B`4*gvc_(Jw`zY+koW`m|4>Ud0!L>9$ z6>hYM;UNgs<KFKf<8C?9(u>L@BiblNS*_Tl&&kl^)1oW}wF*cdEhw z^yd7qf4dc}TsjL)(wgL(A3$n3t+mRi>zl zz2ZQIHsvpF#dL#@5-IK$sFdl^@{<7~tjLv~u%8tb!5rprE9z#O2dLb{VJkN(+;IU3 zYf|`ms>0Tvi}7Z%4>oYGe)xke=)Pz?`7ixC_B~*NFT~toSyNy)Y|sl|P0JO7qdc+OwQi60U1`le|+m zvfS4V_s933&(+?{jdaJ4S*}#0?8hCcg-EhVMN(gRDoG3wr|)N>|Jz1XbiXX=vXr$d zT{RkZc(;fxStVjEj$?;)FqPz86)HtH5g+SIM}6;!#rqI0?S^f9 zK0gU|gQ}7aj#b;x@W<;oC#H&qGJQ(clcRxqu3&(LJD;R)jU zf6g?dp9f7o9VE&Rac(I)P)u9GY>s#-n92l5o@v~~;&l%AxudVhdUpnvo|{GShwsc; z*()Ynw=lc!115h=7h2O_p>}ya7ALKgym9}FiV#gQ4Ya2cj}|O=txoP+3`l)vEozyu zsHWJ35AL_I1kNnCs1%SG5Ei-y;$#j7UTEEp=Dx$m@e}S zwH>n|Yg;C6amMYV{a8#H_DNjBbtEiO7tWl1eUt*2+&l`i57HZSJlq1|Fk{0uW z6{+_&?ibmGh=MEwdb@H5J{mi5o~k?9jH|)}WjFDsiur}3n)!7BkMCNe2z0)w9$}cuC@!yHLuVS=1O17b@0Z$7}l{aG<@ev zl$BgWPYWCB$r{=NzV8_Kw4srl^{v?Z9ZSY4Nf^cbMNT(;+99bxpshUZ%2KAEW`@km z5Y*nkKxl;P3Uu)f2l{xg9>mzn>(+%)^znX-`ySiVvdCo zHGe#U6}{b&xpz6#3LoRw#-FSy1Y*bLN7(;P5##SJ$B(Mds4LAEW1J%-10B`qymkXB z6V)U}|2nXwYdP8m8WcH?W)=WzxZO9Tp=8StA$tr$@5yOw?yQoG=Wh1Q-7y&IZ7gJN ztP-btHHoRG<;i5MKIxxK7Ynpisn|%3lD})=2Y2x-SDG?2)rKb2#Nfsrb1MF0N8gsr z!L1dZR2OYSk=EXD-EKrX(|K;x4WiMTLTLW*E}R4FPT?Vf=5;ru{&K4y|H zN@nE4dcLWzzeM|kEA=j5jrP}A)?*FHyMpLWdnv+JB;dyiSxQU1gyhW!nU$nO*9?o{ z>B4?~el70LeT7d_vXm~Bj+VT7+~)80df-x=-SQ2}=i6~IKMU(Pe>3>9Cr#A9$Md8K z&6v@ZvSvL-$v+P=P_!fK8Qe*9osP?2Jn1=iJ--}Ff|{i($+pVV;>=~-!<5ifW?MKk zuEm>;kCDOL_r$=l%p{*J7b$C)+bN4Lh;V(Q8YY^`;nZdoysZIhDm`KKyvIbmP)C_NTmO0=lk&K`yN zk^7*xq!ZJ*2esgj8Y$d0p_N4ebf8&_+uC^>86 zNy`sJqp{~&5y1Yr!I%SxSeYz|t1Lp!EdE@)F5)nEZO(UFid^}#7&)T=XZr2Mstdbe zRH#Ds5R~|J`}gW<+==2Zn!AfopKh})_=vGn_UWB z%Ca&3k{n&>y+@25oQ23(XR&ZWh0t5N0x!1638{cuF(zXlUcbC98v3fEVCX!I=`e?D z%4WQ+3ZW|7|6sH-5gUv9(vBU<2)=j#PThjYCEbX^UwR3}bQ5T==l))1s2JLLMy&j7 zN8PV1mt<)8V_MvHgqTc5kmm#86qbvc+%+gX^jRpxq+nvW8+27Ngo^tou{y|?R1Q?a zWyD$D%VAHgvTqT-*%-SoccVs~-{|k428+aSHtdo zCUig4u!qV1W{3$r>imk@c~-1a}zhmZ6e=T}^ znR$9AH)CZBXN}VADdD&j1*_N+Hv(w7RtM%P7}JJ}*7Wq^SERh-IoG)x{r61Z`Hr#Z zYd0Mxte5%jpMWhx`r=)XJ(PECL{h)e%qy=#!MudvNe-s?l>CsjpiPpK??yqPZyTDg zO_$twZHfHI4Oq+E`^ozG*!%Vn`Y^M6b$t$0JN)+E#bf7u82%cI-yUc2c~c3VjP4F~ zJ}*`^J;oZxr7#-{_;QYCJU`Q$rg>mdE_2NX9YE~Gb^Lzyrf0=#SqJi^rGb7F6Ehdp z8a{NV*_TSXr}JDSXnh%I=9-y^UvLBe?UA99(2J5I3m@RRM4oQCofeYHQmh+S$5~X1 z;Qz5qT6W(>(UdN<^wly+5Bm$46KF$kdT$pyifW)zVoQoG%JloM7Bm*b;p#)qxLbLk zG$I3UQ#2@z&jaVQ@i;O-om5TQFk0~`6xqYJY52r5Qav(m8`I#fvJ`ytCdwaJqM}ou z=6~!ZnwIoJbeRt8Dw4u4KWv#hZb#EkZ55lD>#$huS#VU8C#}BPhspx_if=Um6s<34 z`tA1;>w7jdG5SC8cED}U>%A0}C1E0T%x(C*@)tqxH%OAWQ=70%8gjj4X_o6PNxhp4 z{p%@3B@y+KdrRIU-Q^dSZQ=Kon<}ZlZ^kp`{}@hc$41VuT}IoSh1lF+0=*4a5ZD>U+P(*h zUu9#_(SwDkL&Rzh6Nwh>;?JBKq!cX_kqZ^Eygf{` zXRizXCb42pO03v6@0qBd?SjfbHzYweN04DVMe=UjetZwSfXH8Qg(HUM;i-Q*l&>xk zilwLU#j4t(m@R2hk&ON+wZN_0{@Y>T)k9Wvc-UFRH zBl>dr5lZe~LyVOrWlA+*+QVj?mybm9uwc5iOpT_liezSF4~oNAJiR>(qlfgNW%KOe z!I{QipHwK_zoFRFVn#}@?jzUD6p1m0G|=@6UPenJKbYSiPoJTC z%0M_>T881iH!#iew#XU12tGaE@~)iSV&6Oow41J>BkzZ>D|Hd`RHSH8C421#s=~G8 z6Kc)B!y?VPaM)*Q`tGPi8NU(`bSs~8aYIDN=>y1X4k?rb-x0BaQCM;GY;cNLAvQb> zMpAHa^cjO$3vom=;(S*tLKTwo5q-DMRDLO3`t97dz|?NvcGNLjL)R2(1A6Vr@vV|K_r9+Ko(@{j$Y8OX4%ogNFYLB(k-jgGU1? zYG==6|2u@B&u?*}yAln5lLUp@Hk>tTM)ctm7|_6ba#&-^)Hn)bM_rn*nfHa9 zJjUOT1BLKC&?)RHK3`dY#+ROC%su?~rOBMf^B~JA<{;_J!^@Ol@=MaEtJa$^YWD@y zX2_G$hp9Mw_8OK%X;bT>nV2|Y56u6`k-_U&eBE?RH1R%<(nFguuX(uG^@v&5&HLc* zcU?5Q*^u%MA8P0$hvoUIwDx8o6-&Pr$2Hn;CfJ_l)hNJ+c@FlL%$G8M%)Lcl8aL<( znk~*EyNPwkfV=Sc{+RnF_B48Vtr%Mvj}iXN(5Sp3g4gUrYw}M-p2!eSZzMv-SA!lO zVZG~)3cYGJqD5UTsK)_*rgvyjc?I{jr|FTMp*79me!R!b&CpKm^$0qd5=}&|Hoo8n4HWUv##jL{95DAm;&^8Od94{c?(mvSgr{daSL)!7=2&P} z=|#W5LG^Ihr8`hg))R!x>yPi1niM}Q220i&Q+$vQO&=eJSI6yXep|4jyyu0I^_QUt zd;Py2hCl7tz8oi4WpSTQkKV_nI-N@o>x^lL-iEW%*emYY}^rZ+@Rey0RjKnlKWiZzE?kd4P_STZ&^&A z?*)T_`ZTOXhOEt>AU@2R*=KDSE>{4mKUVneEyAI~QDXU*?}aAKk8$i;ySQqiO4)gvg%^9DBfcw8>#1X6h7Et$ z+oecmPrmTxokZW2lxvnY-`!52wEiq2Ml!Rzw-TuFG8B4EW#(a%_&VwV?wwtXRSC8* ztUU;A;{Y16cr>a<8F2n3goaLv=Zv2PGr&Ws&pKb+dJu?u(?SuhpiK2gry#NRi!e@C zW1rszTfIJu#;e@x4vB?Ek}uViHNrk90XD4rO)KiatEcOreKC;2TWa9>`~Wh(zLJC& z1qsb2v?$ z4m%^dTbdvud#xm(<1!ZZEDg3#&Vv2Y3#iYkFPzn#cXsVM2=6yT#EvY^?=O1|tq2{` zo#c+%+5g}&P@8nus9^Zv|1r3%Y3=7Oc+Pp<=amk$e>Lx==*lx0VO{YNeIjD8ocE+{{3TD1d(D9M3icl9GwY_4xjz?9T5!j6>)+za;D5Myr8|0k zs~1h2m(9Cj1_e#-aohZXSFk=g%&WwoPCv?gEzkVoDoh^XL#Zw%WTMoFIUZf;TU9yM z&X8dyv?`rbX~746ZX;hyQ@kd7mQBhu+RTW2^!&u7LBTXcL7jIA8j1_vz39yF%i>u1 zX5qFnl=6JKF~`z`2DiV2)H_Ga-_Q5cqF>xckw!1xn{y!SJ>-IhL&ak<;!{g7^d{@@ zJDl)s{A0+~%3TZ{RpooQiESGr+;6(hR%(5{(5v~S82SS`1wchf_7 z*Wg(cy-&jcen(f^Yjc-sH&(<4Q+BLA9UOQZ?d(mSJ10*=#!Tbx_kE2RgrQ@zM? z%0E%fUCm*2_T0JpBR&KwQ}pH_>S($vQog9t*lqVXS9S-c%>41HISv)KBHSP9NU63j zV9(l#>qsX`v{69lz)i5bp++Z`-w^U@%mw__iel@lqQhhhVhfCEcZWND;=iwQDEr*U zy{Nj}kUDsG!kkwwbY-s&y_l^?7ozQG;Ct>=wL$XxfDLIjQW~UFRv8TGrQ^R<|NTH{s3!FIbxDh zsyLmv2^!PXkYoQ@*ks3I`rYRuNa2NKFETK7;t$~vG9H*9=p2Z2nhCG zTX3~>5!Th+!VZTXlvJRJt*>k0H>EfEnaiWhz7Ta^1q~cL2J% zyFQ`qL<`*Z4#j^DrO2!D6^;(lrJq5(Yt3)4@at<%OCFj~LzmscpO>h-Zr7*gTK}ta z|8}9Uw>we%{46Fp*^{-xZWwkeh1DGH7{(@HMCKv9wPU9CqD1s7RzTkIGAs$mLflPv z#IE6dRNf|pT{lC_<*RtTt(ciE)3M`PF-|Xh3zd`WFuMI9CMGc(|8Y3d@A;tjI#0Uv zEE^Mf4og_(O7{nppnnKIC*@wu0?Wnn~Y$=u}mGE;yo>?yG=Jm+jp(jZ=_gs9sb_UTQ|BD6~L6LWo z>1-m(Sl=!bR^P|LG2Q7)AAMSC>w_bw`eBSjmw5)#u*|f;$=kLR)iMz39bxDlsYA7M zq-Z^RK*~dOXn!uxf~Q|2g7xYx8Pfmrx{;B&Tdb5eC54~KqVB>(vHH3*&GUOAjE73& zQ-2d$vDRLk=DerZ!v$hNt3OR&;6^n0iMX7@{>|gAv~JTK(f$zh=$Di@xcU;JzbWIH zOt5(N_5vQeWQf3y;|0IQK7g~m9+a3Fvhwj=G3!nX`D2(-D4De-!?nr*b%i|t>=}vb5wn?<`$((`(3V!5j(2RM?shH}>jK3;58eN|Z%F~jS;ICoAD zv(sWFw@XgKJk&ul;B_Wme%^^8ZzqXeOHZJT|IMf7mlSp?Wf!?+!nirI=gJ zcMxY9SJo($l3HQ6Sf5t#IdL}qA-=!uLIsIch_)}m&99cE-s1^#(Lcj%rU#9B^#Py$ z%F=z`QJmG_{QebHYFQD77tD7!-Sayl?~TD4W?Mu@n&7W^B}&xzIr|^?OzxP7nV?GF z9P~hH%tNe{ryi@cDO&Fh@A-13NzIltzwixSNn6w40SaV)@Eeq@Y}x0%iI|*Uc*Pm9 zbNvsXd_z6-ukx<25w{@cu1eoovwkNrWY(~Rx|Y_6*ZGzxXyrW7E+1@s|51$e?Mqra zjtG4@o|DIxLMzP;gE>ElxKGft55xLvn$)oFC1xAE7p3bb@!qt%m|T2AM018j@%3wr z+P*;iigrddpZz&!UWf<3bcE@5W{#CL!B}Qs;c2xt?3p4*YCjCbfX_{s6ZQfQ>_u;} zsFf@|N0KJHlNdANxI}McH*wu@0|GBd7w%@xlbo3*R`mFc9=lB_@$(Cj`uQ)MvNXv4 zs|00ZI^l8Blp2rm=N%S{^rZ&WPF={qa~d}8XHH0C@}Ek@rEBg8HUNl$w00 zYl#V&58Ngh?C3=Su1eJQTC?c!1P}V(9#ZL*=Ja~FH~-G~lEE2w?(=%nVb0u5Ff^vE z^ZhBZ&kI!aO2VB}veX>>1QA!aL!wiQ+|@-$ew>A3DK=b)#K1D$%pOx%J(==3zD51PTu9m>a@cpuiRyVCo+#mFyM#Owig zGQ6ur+x(JYE#pQzd+{!@sj&!Q{!<5kKfymX;Pm&481Yb!A{P!qsb?WFW7KF}^jfG+ z+>b-Q*c1JhfPjYUV(3~^id(fE=G_ko%>XxAr+5lC_rDO+&T?d7(iQQ=3uB}8vJxG zp-=e<1Pz=G$3JKAJNXC_+Ov=-%k11fhP0vnGG6hsIC2?tr@tP>$1~hLj8&n5w@xzG zR-PV4v4>#cBK|GAjF@gJG<-#|aGRKoi{b~i{5&uDvH1jcTjogir9Koj#&J-jv!XOh z9%l;Tuqysv@RHq6#H5ykSar-6>gEe!?;$ASrv#Q8SK@F~e+mk37mo+0V$KiVt8znw zu4qpZHx5*Zt3}2XZF*F4Ypezqmupk(+kw0XSt5SbF}w1D1zvheW8;)eloo**H;Op7 zaWf*~UI@h=`QkUPrP2LBbC(9!IV=-BCjquh~d%>MxC8=SSN zaHOcdlks`G9z{iUq4#QA5n-uA4Svj(eKZYem#nDZDf>$|xEs2~kS5&pqHyI7418@$ z>ql5qz@!f-Ph|E7d!|=1HL&dMf2g;Z3dGyt^xDmEIWiODb9`|qDHLCeg0ML95j+zl zG`CI)QB&{oTrJ4qW+1#{>Ja)Zggz~n`TyOCe>|mV_KF+mvH3XO#dhFaOD?K^Ch@+o zI_!*iin6N~bY}$n3)31ohwDUhGpu3a{OiIQ3nOc7M@e!vk5J2twcqf{qKSm{=fm{Wj?Ip<) zfBPG#&!~q{{KCQqtBYafEk|{`M~Yj0tc5fC)YjX(Q9<&Cf`R@;u#WSf4H@wwap4PG zGIXc4jjEKd9{_vFOze_jPUK!Wo}aqmdKvHAJ~N3w!)fSqSDPAp$Wr+G+n91ilYaL8 z#(!1~xim{!@>Pm7L(X&l?wY7EG9tT~)}lq_4QJe)XoYXEIH;_G<2m}Y&&H&%+mZm@ zt@uvzQ)?+`$H?EyC=g6@{Gl7N#ffAT>@AqZ^7PvhwdC@8yCO zDUT(02H!_QS(3Q3QI$FeoDMWCmnFA01-g3QMG~M=i+Sx*l;ET;bWK&sOhtzLN;z-L z`y}RSD%1HXuGBR}j(W{hAcZttdZzLd;Wg&seoYakr4(S<-SEQVJIsvFzJ%P%M@0CZ zYdG5@8}hR@qqEc)y}O@7ZqNqYmbSq)+awG$nue3d?69Bx#oZnL6lgjNJ-#|%eV`y~ zgkhL(1e6T~t+~IAc~#D^xb;cAZ_%KVE`zXS_iu5%pDvvmqz=~zD-@nICd&ge(Y)B7 zWZpI-#&k0?z=NpxN+mX5Ps2>h9(2xKk)qG6M^& zP5gNf5BDF##!-FAW%3!U@wX=6aDhXHIwV#DH0&%M59LOn0F; z#O?n$I`6Qa+xCyQr_$2Y($tpr_+IZbAv=49Y_j*r%C3-6NgAS}fygRK!ycJ8(NGB` zE6E7`&gb{nb05!r-1ps~@Avb$uJe4qUoV<`;168)9%eSF0}XR_ri!;V^lT#0$|XDl zGU4viZf}}o=0|gr%xMw3OPapCLe2l)uT?6P_4hXRn;wSR$3Lhnx`mBXvarmIpSiiu zaj*xoh7}U9Wd9>%tnW(Jk@GRL><2bEsnLvssra;$na+>A$VKA}da}Do-#Un5AD@9) zjRS?TGoj`T?_6db!mkMq^sR!Onf>yyk+>)Ile0!!_hV7AA9?Q3rEN{q5d1eAu5&dg zXDm?2*&Fj=D)i)-3+L|&`7_>)<}cWajLo}6%1kS|vUMAlci4zxbq_jl;Sj!^su9_{ zEGRa+2Ziiv7AAe9DF3q-^%R%IeEN-K>Pg4n+!rUq-rfX4}J05iF;k3pKa>JfD69y{;)(dtxpQ)!xCBnk;l$WT4&lGM27mwu$6ARuz85^xr+m z!|V*Ms{O_0eJV7@ax?#%g@M}o;o7>AmYS&J`@4T{5qXf@8qy%Dz})7Whw zkH4zjg-zTJI1FePOUoaN$CtKaTH19nf9*lZq3{%p>DmWxWyc|y_p!obHhWX`P(Hzx z9_G))V#P_g_b8AW`m1y9eYa4ryd?gmvu9RSRh%6BTx|QKL3&O*gnXEmc(N)6Zb#gZ zeOC&*1J7aHXo1>aoMlYfkL@x)#Tx$>?i72Y^Nk^$nf@M+_4{L@l?CoJst{0i&~7;KaT)&O)hf1#O9ivpqN{jG1du>2hwS-R@lj(d*4!>)pZZJa6zxIu(qz~%50;Wbur+rFR(?pz+S&7(&?qqc8GO8M!siTJ%#aa4b6g%J7@GSe(7hm)Xx1&Cn zxd$M%7}>6|*mj3qQ6q+kEf%NHMVHxNyHmv5+et{?@SXW=!ICi!5#oJbHcn4qzhMJ+ zY8(#XWThX4b%_+J=|%W^pf~%C^{LywncSatfW^9QwDG$oY-VZV!$J$1lD`=ncSzyZ zb{#rqD^I63UB=*ryyqO$&KoAF*z)D$h5ySNm0f_q?u^jBzYJ0eni{~-3u7yRzGNsP{`f>~4( zd}EU(ZUs^_l{tc%29~7Y@EsEiv`CYgXR_LLkYA!sM}z@o&T4>LYm}&*6N^Wc`RH6Q z?Mhh$&u-a^_|~sXBpILK?&vw3xxR&WhUbL-;|H9Li9pw{(!8%ZkFg&o;6X;NaJ^EG z^CL|~l9mhabM>I+t1R}GEkSOn3I0aRl=M~WiiEcQ?9+WOlr-2=R5O;@zg{@-NsoT4 zoQ7%7%SEb*A(b>3;aA0Lzt0o3XxQLv_`a`}3`zK(pQV&Nzh+|0mor*G-%ftKBDzRHrm9~3X;&8Wl zqF%$0V%uJbDU)7{vCIMr=3PorTs1=XIFZ7>uQ1*B7Hi-@``NQ0Y5$4=b1W!=J%&2$ zefoV*gUZEsDDKdp9c#LiuT&Z2*@rNc^NlsP_K0;ahWU@aRwv%iJ;A)fW{G-=1C~f{ z5?Zrn3KfSqk@D4sRigKG}d@p;5cad#k zVaXo2EN-3MJa| zM~!l(?t{1DBuV!1;lgiQCOXy^UtJ@uzs6u-%m+g}_8J{=WJ;QLE*4eAlp|1JNq< zZO=B*vLAHT%zz4d9S}u3ylHEP3@wiR&OTkx>C*q*x9n)3K63^O1Q|P6k!nL<>dNP0 zA1!Y(Un!_1tOdhQWip#bnihLhV^LcoMpiY!<<<+%ZSwD*Is5%zG^3}k96dJ5KvH2X zTKcKdg9|%xW2P)=jgg~>E~nYg)|1v(`$}T}-N5Wh6VCOo6zi&r(1RKHRcF6S&Q809 z($kSJnAVe?wQ5k{@gyv~){`t8Z?B;+ zp%UL3U1-tf7V&cI9wc4sLhnYN6CZ36LFv+@G$B*$ER4aQXnndQ?MN?%DibfHD5#us zyXsx2>5~>+<=*=;U0oV$ZAX{hSW{GDB0^vLQe(LVt^RifFUEmN*&R1*RV*@f`%!A> zU0m#8Gf>7^iastXoXh+uoG;|#ojmgzKIp=0-XT0|{?5+BBGGMH9!v-4i{P90MMLyf z_!UQr9KWk#i**XFO!_BwPt`*D*quB(a>JUtLCkFQV-DdENLNo`Z|NWkny-V$%a$P9 z+?`qtHEBW9B}xANpWcT?Ixn95It-bw3y>o7R%~9~C=v>7c}`J}eizb(N-*yvTtA^)Xkg@fJKp>Lfs|oi z+#7xezyBukJi-ADAL~%)wF?_7!*EUKIezRv4g9(X4`YD^b%wNS@?%_?xDb(?0qMQ2 z3crHJV%}tCMs1pdmbZFz?Wi|-s_sPC^6q4_(}BG5qF{bemy~sxH=_9#zw4}MkbN({ zPqreKv$vIeUz=6*4L46Z(coRy)MMlT)J+LP^`tE@WFFwb?fmDn#UCSV{V@5>KE!(O zz{GP;5mZdHJdr)G{YnwnCx`+*Ohy-1o+J0|%M5znm%U1YdGR}3sCx{bxq>Lakb?dS^+uT`Z~o=^{}uf#2`$R@Bqq6fGfc^l$G$ zOys=S`v~TVw58(rlqdM_8#C}f%!bM2Tx{2Vj&p}s@tyy*aMt^Z9;ZV%z# zn22e~>=syX1B?8MFmlyQd@K47Til+p|7JZNbjik$v|G4QHyZlk?SlL`lN}z7P3rP| zCu5KAf?RYBa)jF;JCeNIjx9V7+1J&Jw&(^xa;_f@ijdIpU@uH~;mWK|Z(6Oi7GE;Y zqmvYA-s|O}VCH=cU7|+4@-GSLpCxc-HtfCacO~nz4vI}NXPDLEL+Z)T#drDZ$nka~ zzq%8W+`Nm(_{h)8D0SKp)f)qz_d?V=O$rXK5pv!xcskygYRm`VY>F+Wj4-F)k-T5~ z@d;C1Y^k5yn6)hni|Qb;M<$Q6_?v`hof3t-t{10V6v#VSo<7%0q5l47jOo&W;hNvXx^7)5 z>!l){nC?woxr=>!WjESj>_!*$r0Bz7WjgQ5uBA2wD%u_`mcG6Yr$N=&X7ySOaJ-9I z+t|)@@2KRX>3L|lK8D0E8WUS=pcL^4O`ldF!Y3G?ny%n>k7bD1s|(rM_m~(UXu)0| z7|pSUiA{fUHdq9GOAXj{8AyW{SRy%pFlr>(B5=ACtu^Y0QAVXAT8F<|!zM%J+zg>$ z_ZOB)8r0!$7KlnfeICW;1{b02DXk@vJlNv}XBM9$qH ztdEqyKU7;h+r>NI#U;!eRVlXqk_WV8VXkMgc$0P&A!Q#hGen;@b+LuRf6uXtzb|cz zxThW}Lp7X_Ni9)=i`fH&U38+|$~73A_y84mO{p=u8Xj{Sp^)rKok}g35cC5zyq~Tt z<*cgtTU_nF9&4WrBHP6(w7`2O9OM0i zKVaE78D^drV=ps^Wqi~r#Vl7u9WWMy=YJAoA_|4;=ziiVV_tqa9}Zh=4NqBRYfb)s8=Lvuno$-N)C?XMsfd*EdI zKv>%svuny9S7s;pe@w8T7xG3l>9~uqo8E&oFR0Oh$-%|TN0^b@aLy|XU5|pH;bORR zr071SFPax^5VJSm5)(%6f?@My$=aUd#I-mT=+(*7hkA47R~3ke%^I|w&n0TLewbpd zLW@?|@m$xRRI|3h&Q+h@w|mnv_7O@sS(9I0Z*sdb6C-prnNea!xyK~rYsYi@3G82Z zI)F^rVYIWrk?imHrAFR`ePOF6e)!R@6;l6NGz4f}EKQKpD~_25k?|VNLi_WT2c8eSN z*enw5>YVN1ol)GGYeJ#>Uo1C#!R)-#h(F>&*L2^(RT9T6X&=&DR))SoDY(G*xC`y< zHafK%VKs7eWv+*CmD-OwcTK9fQ7>ZB(sAW>D~@n~WRIdT-vdqQrK&!=X8FCz3?x_P zS)s(5il5rjv=}GKJC_H|-GVxQTF~pH+swL@Q0rtLDnG}r)761gJhBQ`u53ZazzW## zS+D21{rGm|9P0G0O@%9qu&glmYV7_8`%F67KJpLCES;7+t*rR~Km@;P?Sd zdS?&G-CUuuN)fiVZRnExZv;y@qwbplZ4GT_7IJUY7czHc|36GxRVS8j_=-NsnaKUw zjk&jfq13nsS*nUy@$EP7jJU5F@Ebqf4u}LhcN!xtLxCl-kgoBjj2+VKbl=Tx1}lnK zvK{^A=+dVjpw8+9EL_X?=xuIf^DYLTud7m1c0USUUxtP$*0gP=DLL}@V%-mGa*(m5 zwegoR&eoMatv8^wPkb*44MJ8_G;Z*IW%=isIMrnavYTu%L}xVy^gqJ!g&LV{#yb9hhWB(~!q zpJ&HkBx0DEzx$mI-F+}e*o`$H>HGh%Htx4%J+n0Jl8<2DsB+G!+j9pi z3GJE3;P&2@ck0=A7yS~E*v1%I7iHL;i+R0^X?>;jjhLr*Qp|u zXM=4QpI}1Y7n0^H1;~{90jDKTh0mpBLT>y6W}pb_eWUEiR-_LA1S7kf0|j3r*t6O%pObTNDWovtiu^KimCpA zoz8C{RnUX~`VWdq?xACS!Kg1UCS6NNA=>FVIWm$22syT zoswnK9mvyj7zNEs63^p%Q{vTNGD_%D)TOdFg|dHam+lkX)KEuQ%yUU2hk{n;Ys0w5 z4dHpciaig>LP_QaJa@eIj}B3#m+aD>fo~WL9OiVR4q>Kb-sCU!BqC#^zvCs?0%NoV%hAyOLY|5?)e|qS` z_pUi7cBsiNEQLj$cnDAsH&gX3Wp=eceOSA-_*}ju?zT6@mV{;vQp0VShTn zIkfrbO7Y&$o87Ue(B8WMe<_$O_NT&p-)B@DSEYys1q2-UfWNN1KN+P2mG!bDzn$-k z`<{wC{%jAwZbCk3x%h2$6W@CC+7T{5)j$RFX9D`2A_O%Zooaacte@9Wuj&hNR` zIh;ez_MmBr8uTGYj`Gs%>G=UEI+@algG1bD+^mP#e5M^|dADs6or{K^?HK&uN4_o$;_0fYYX9y1-y&_fyKN2&12b1tRFKTO5(LG#H&-W&1cTU zG-vPpGiKSdJ*`NOxy2{gY3_o>A>o*E=^^Gn{3<-8`P`#dfg!WyG5Noi(2K+=U~xU|41+g|c^);}x_c#i6!+Qrvas?b5dfAENnMXAdu5jM|Da7hmiH9sT~ z(Hq3Z<2kU8ekyr6DoG5f{3@QmmZ!w8IyB%#g2+3dO7e{=w7NnU@yQzWbrSoXj`yIz z$R((f;dcmGQ+)1Ryy8yjZN6jex#fcDrJNH#VL~q!`%&x(eLaZ=cVC-4y~*(BY?1m`m#h~P6?_Yq%+u!CiK`z?w|V<&iS9% z3z$6pIHDISP+jm%Ox+rdf$QHR?!$8w_LZfHCGjYF{}DY>RVjVuN?bekg?&B0(PtF< zQnGqdhkP%J{dyCpq+LkjWJQrLo+0g2cUr3JM$11`prLja(BVSX)tWSfIgUOn?CA|N z&Q$(eji;{#jqh$qN==)vjCZaHVRD=+9F2P=`7jHVr>BQQvDE!DCNyzIY<4ti?k0*C zb{16rD;EU=vP3|U85Ql@hHa~MFbBLh-Dz>6=(+pFy#n6PgmV6Aak2=o{EPQ*OzFo@9MiM{c#As zYS(xS$Jj+xHBhxUXXFlaIoFk9COA=?jvBpZ-$lJ3+sS7pev zWJ_8jBay^t^}>^W22?)FUsSv*5PMc0VBh0(77vxST$F>iN&`^3Q3Tx{z zD)}&Hj{VU6&m-&~cmlplYT0Eh4U;MZiknx59wok5YobYCT)7KB${K%nSTiST8#XZm z?Ld+Nx+)u9Z>GEy;Dg6e;lMOM8Pe#qMTD%|8QL_0EEpDtyGf zy_{L(J9*zL63CZ_pkVeKO#ZeG%AV5^{H7oKM;);3^-dh_GYQ!fYOoIT{XHgi$CLC< zB;E~{Y=|*L&k2v=*nPLe$9DvV4c&$=vh7&N+3ub)%r*43XyMNL~AD(04O+p}oP1%Eqy8V#7A^fqPGCqYoi5=oH2rGNP~H~j50*nG z;kuCzj(-&QL8e$d)homw{EtDZO@3WGndUx?yMUiym zaz%L1J)GF|0a8bzd@YOyiwc)pSmNMFXJ_vdb%wQ^t+t@*+Ora$bSv8eZ@WF6v>R$uduRQ z3T^7LWK?-wa&a>Ed^qPmWS)%JJLWA6E54y{**MWVPK9=~{K8mkV^SUc7p2`*DM#9X zW`F&FcW=I7^$B|#n%RjyF@5MtVF_}Q?!j1RIE4ol;Cs?V=uh>cy{%^uUr>s?^i>!q zWrPQ#3-KbT4~BfRK?0#D4yjP@<>r>c!ctf;lDbmo7#wb}@FEVCzqvAfZaH_uzWvpbTwdYQh zI$M*~ALh}|4MwQ7C%JPzHFCus4DPv0l4({Ze8VfCy=RE+C9i7j1VbfWie2- z4o9~y6=PQ^pzOb);aeJj5?*BP0@%QkO{7W!HK+`tST%C!-zh_InzF-gH681OF zA6R5KzW|?BvWua0tw>|0e!AvAoS&;tAF_MnREjeByyh%)S6OH-`-x+PR-`-3l^OTx z=s&=Ua@W+MvHl3+7c(E8f3}Y&i=Z8CN=tPp9Qje|Zuj3;anJ7yF z&i3X`Ko!g+GNS5r4zr%CnOrSVXI6uAv z^;68C*ywdRq}yARj7;F2AorJYQt|hwpMUnT6by+-#oCpl{AbNxj&FMk@Zr^2NwTZ4 z#6r%U5+l?|qP$6Bu*{jFr8!%i{j6w7jsqRuuR^I$GI94?e|mfCri9M(+8I)b$U3iStA=}&ReDB&RS*XyHbDe^c zgY1iTFASniJ3X@g;z`$Yf~oI!cGOq(qV0(z=sW*@n+yZ#Rb)Hd4{gA0o+q4@euHBN zVo_n#h3Z!}qHX?C3=M6AaO+)o|) z7GOe?w&X*1nI=X5w5MAcMaZu-r}Bdg2|tpB-IK#H{+d5MjW?j&=7Si2&x3L;)aaB~ zB>Ra6kiI#04lToQ>DFD$4Ck)hy1uw6brFqTt(dMl1n-M#;rRL|W-dyD$^2qrDx67G zw-l$CbC}D#>Df=xU{thBcm(;=&vXY$>?{!hacb0eMGwlca}_@m}tgboe zFV76Av6HSY__q%a@_+xo>wG?UBwyaPpmsM``nkoFX8Ajk>~j-JWoOCq5jOND&yjXS z9%oOfU@wFfedt<*Q3as=+q`Jm%ycB{2hp^l_t+b?8Xa2YFy;J&QDp#U_e$V1;Wefu zaSnM{4%W1pkZDs225f&1??qji*}V^XPd=mapfx2c9>fD?i6}h(4aM+&H0SPZjHr;J zM2FGrzq*3D&X*WByC0=x-a}5`0*ScU9~o-HF?e^3*uG>8-|6SW^p-}EruTR_>#X69 zc>snF8_Zk`JGwA)1!i&^wQlz z$7cCGnhuOA75Oe!%vx8Y$KHuzuDb)pF5H20f8%-9&HR4<7;Joa0gu`oC`t1e z?50K|A^Q$;WIb^{qf@MUS;YIyg_x$H2C6GSl%WZR4?Ql@o%l>0m;tAe!w@vD2p4*s z!UMfkSa|F(rd=z7sfIhA7n?9I8Z>iP2KFyEM6LBe3MplF)$OimQRq+Z^NTT}oEe9? z{VAd~4K2f&`NKQ+jm}5WFp{WZ!U(GVn1|iAC!sZjy8{#2BrcJqC8Mq414HNVuj*JED!LE<%nLpXmj8-eLC5 zVJ26m6`nOs#Qu0q(t0rv%QwWp%wLI0_Hnmk^8;vbZcWNgn#$VBFxW|(#@~C4C9^9q zGT@$2USLhFwll@23TxQf*wWTjjzat^5W~2m+%Yjyj1K(Db!ez?_ zsH*!TPU{ut{dq5{=K=BbG^*9L#MSP*G2rA%k!AEive|zVWRAQSdda)QbGb&TB-x+`%F#BcZf#D-VwC8zb?B^ zb|A9RmsT)eqIl{oT&+#=pE|xl^x6Cb-ve9x;|+VlVCyrqdANxqze~mF$~XAre_YIu zlf&)y{?tXbO*F4l!gq6D8X>h@7!LKrw1I=Dn0YJz*#>bwIUNu6lko5F7}{Qco_%_I z@Zti|&C4gC;NODF+y|XAz9&8e%ae1g7TxNi%em>dkXvR(b7BLKbbvWxyweHK>coQa zrFc2dh&KBx(~0D4#C$fQ{f>1|jF6|t>f7=AT&KTt{7)FQF2&iO!NPsB4lVz}`>*4X zlDn^ZLpym5!t?*(Lh!YTkbhqM!4t#W-(s{O9OHOO{#Sww)%G6+FY0Ggj! z&b}|g+QlI-=)8fff?}b#hR<(%@1ymg3~pJhMWtc`=Y^8Rm2u`Ge>Qh{w*7$b=k%hL z{8=2eligQ2vqX_{3qIXz!Ny((aA#(vq`|kT<(laPX_%_0<$mrf?a{@PQeA1uXgBI} z(v*MK?zGm9J+)7mslxNY2{SxtS(+oo4RfKJF_l>4vkysiax_lrDr8JjvFfD?9a4P4 zJ(dHQul)ftG%K+nmvj3oId}284da;K*u(BLyb77W+oVRT%r;@vV;hn`){9=$U&Y~M zBQiP1E9j-?{5{_KqmG;&;O#U%JH|ryKRs^zXG9=b{gbqZ5>~m(WWmlueII_TscJ?a~=t!+K0~2Q5_ObqPF4AVcGaq(#nH1%AL zYpX$jc57g4@FCQN@H2Y)Sey*ofo044)AUhl)Vh#)ns&7!V}b!W-bj<|sQoT3cy}R# zyAdKPex?YRznxvt=Gal*Dei9G3n!Tlaq(f4Sln?GKKK1FlD`xEeJ%>?DfV=5(ntLE zTqQOPF`?tC-ynZn5i$`rG$Q5?qJ}x*NA_c8{O`bpt$Ij!!)(AiD(4;X!>>F) zGPCT3@0<7GQx{osUs8hP^26M{_<&whpP@x74@1|;Q`C(M&>GO4WasJ8^Ypi({e&gG zzruY3bpt#(z^qh%L#lLLC|>pGNr5f<@Hw>*zcVfA`LW}8zKhR=!_8^(Cgx|BrDJvU zGyIpk1QvY8yx#c){mvi4-(RM<^y(vw+3i>zHvn17&T-bb6m4arp=zCkvXm0cm|%=h z*J2!3u0_%ho~0LfAWYqruGJhtDZ5b;q` zM#6vqsE4qZ=E7KZ;7*6vKW(ZTBSjx&@8Q}=Bbv3N0aBXxk&?voyjb4<2VBACb9Y3D zrXgM5t|ByKm2hsZ0fie(FZz9?OdM0Np|I<|!jD--Nfo7%yamqm@0L3=_jicl{^k_K zxiOiwGsM|Vo)qokD8{vx^7ljrs-82%%z&HNBX$U*RvXEKC9g1Gx)ylYMeAyANp9A& zhfGt%Hle%Y6%tRS2)jkKVpbtL@q!%1YL!rlj(-DW`p*~h?|u>M){GaD zGg~BH8eMV9?yi`9p;n}I(Ze$*eP(zRuww~y@XQHZH_XTNL@zpf;5tGoPT_pT5W2VX z1RO`a#d>*d(#+CE`h6L)?5asC{5!;x3ZBh|nN#6LZFn_Zz)#M%)P*%cN$Nk`nP^JQ z;xj@&XQD9BkTx)zYxMhe9KAXo9rDE8^Ka0YI2)A{M$(Nls&wXG2y#@37G_vtTXQ#T zE&PYL_-^QYI|{3v<;d*eK-`un;o^Q(Dz-2rlMPiE$KCn(PBYE}en-nSFM7GK3q=p% zjO%kRQj$Et&c-k7Akd&I#W`5g&;}!+OlcLBh?ACLm$?ZwWGQ0YGUn{<=mPnnm&CR_ zb_+H(3geraIDEm6v~&j}f%h+4n(koJH+}Sc{kbacF0bhUZaUEy+cF4pgLx$1f*LQI|^!J*^L zxXjO4))TXMhX!F^yxltSzPTJoEhG z6{vo1KrvBDH2`S-n2zx$NT7_!}`o#o-US7@}dN3e!hM;rW2dkO{3sP zKju0yuic02KKG+D0}LpmJdnZzU&18#09uuQdqoN-aLJ@68JW0J_0VF( z@Y&yau0Pp-JPCE@Ww`a&h3onhE@N>!~`pzU49oCjtX?G>LLEltH1*#cY4G#{+(SP z;y{B1_1sy3Yhld2HD#C1fhIA#dlZHQ$x`WpU1Gi@8q+xcb8}0Dh~<3gK%TvPyyebJ6|pMc6kdGym-X2K|?XfoJF9v_cwua`mXPDieLWwnDPi zgqBDh#re~U6wF!5)xk-~%KQm`GZ|{WF;9daI*pf0nP2flUCfTmh16svdXjogR5qm{ zFx6gk_IN5XC2KG!qeYTFBu|7#AHuy4r6T^#KjGZ59@qDC7UcUJsO=*5D(E1ZXH6l3 z^ILKgdEY+;ncIlOcqKZSS|iDbmxC8GC116*No3b<5&kQARxwgV^3qHfs*y366Xylx zj>qC9pX;wL`zQWZW($qA>G;Cmf#^BX*y7&Cd0JaKill8KlhzkS}fM?B8|;2Okt+|B6BAJM%80YW{;o>KZ(4Oh#x)88q%XVt|t;N~Sr| z!-WTN-`s=ql-$iwy@Gc}tC8g7M9%Z~aQDiYO5=T~%`*TE{T=BEc~XPkdPH9FrV@?5 zwDj0eEDk@0^KTSMb@y1I_NfRrJC&$3?4}6mosSDYI#Be}N22*JTkJ_;=lf1?Dl3JU zQga^rKH8I<@h?#l^bo7Iy3vNGI_x5H;t~HeOq!=j0r7u00O1Cs?`E{vsvmT>_eTJA zqX11=3Vr$*$4l9txUv%gxC%q|H#O+DGH3lA=F?X(^fEK_`|TFb2fr0l7THjHexZ>4 zXpLd&y40oiy`q;#ZEVRl(|x9m0FXrp|o&oy~=&N zzrxOUzWBcN8eX}K5Pu}uMY^9V_$(rg9*HVctq>)GPRP>jBzbnAo)!s4%_yGQiNL{m z!u_Wz1zuO83-8!7+wliC#;cL$VpsZm>^nBquzRV-kh=N3Wp?vuaVn<-);Twju2U{% z-MbCvV(#Cp$&{Q|K7o6od8k+wiDCOykY{xn`e#heJlD?zpgjXfPlM}_;)w@JDqeg_?^%4cYzKH04eZ`ssdXkgHwrEd0Ac8%!M9Cdp zI410oY-_oM3zuH`{}-B$6oNjSTY#}^HRAUc!=kDKgKu=WQ z6Z;oEf7`(3_)pB=p+#S(t0T4iET(t2A=m%vafY)nZ@QYZuc#Wol~VE4!-!_4zr?BW zZ{gax7>5-6X=Sb)Jv$SI#&d%D@l5z|!es0zw-@Icf zo8229d^^OiqpGxNcsIJwoO|Cd9%RRR^`+IVuvT-VW0~w!b^H(irt+D(wEQJ^3otO+mK4VR;_tC3292MDOwND*yI=+T;O?m3 z#Q8rDb=dD(3RKh~D^Q+y;Gq(kLvl1`nF3`up?FQ9GFdXOFz2p^Fkt??Prekbu8%>X zN}m7nHN8ZG?m}F8FuSOx`yMX)W?=nt&!Vku$3?%2k3wU@8|>U?Mb*a_g5S$-`V{r?HpVn0!b*mD2?&I=>rpCe2{sw$Gw<=b_ol+8dK1(e&lwO-{*Vz98}^>&*ICGy*3sd zobx*MQ=39tav;@aPe%gzc~=yJLkoTA;S)pp9X}J7R_8(IwkA1R_@ixd0)CuUrr=wC z*!+Nd-%f@U&G&!JTT8`JIV&0~zY~RBUr1UN-Kg$bDl~kri_Rh|sxCAoQ)c|va8LMu z-R6!vM(F#j9?u7|>wt4!J$84(<3&0SW!}ME=9L7v??r%aG5W~aP-VAEsIB1+L5U|- zo<1k$Oqhpn{&JLc`KichScd$$ZxQ&;Ks=qg1!d(5Bp+!)`z~rx%RvK5U#d&>lZ?s1 zwky9o&B>tNgu<3s(3p!oDZT0#Zp$$9^OP6aIdVVYu^*MXnbGKnSMXQcjapadz&Sh; zgL2u?6P1P=nPa#!`UvOBa`0y%-_zwTqj_*Qy3kk-{pu#1|7JuB=bh&M=Rb6qDAB`C z-j{Huf2x}S{e61nP=wXB`m7RFGi#sG^TSUONbSQ^)it4fa{MfM*TZ+p> z;*7IJ2lk|5f1D*VlUIq4Ut`g2WFXuIhG7%`%;&F9faRkAyjoyKwZq3?hxT zUyE&LGjQ|rM9>Ts3_tFOSAz}d`BwJOo*9F$`z&amj1)ah8ji#gRXW8k3*SB3kiTD# zOGi&E%Zf;!BxxQ3J4ePP@64ZM@~ViL37ZH`&Pqur7|MzC9Pf)VWR=uq*1Y8+D- ziG_N0)TkPQ$$DD!c##v$at_Du6FQX0Oo#yGWqAM7hfn0b0H z5dN)5ViIf(zn#BuXfxB3HYdW@kjKJdsEqv~6L^p$DDD2XPa{2c# zkY0L`*A#(YD2~c~FK)DgS$yY(;!1-w95e&>Rtmua^$t%R>iR za3mE4#~0vDC_8+NaHtx=54t2 z6MZ<7kiDTzvRa2{h4qf~?@^S9$nQ$6^O&c0JU|3)x1sBGnxc;HI7-WVz{kP9Xtc$3 zM4CHdy@8>KWVWhT+YRx<>>J#+Goy!pzk%#`*O|0aG&WSBtbZ#kS89n-R!X$=yc%<- zEXhgs8!Fw^X?&_KrBAAb(lGg$u!y!><5@t`hpWZPC+8X^tdg>FXo^cRW4o*1OOCKAC z)m_~(SQllRJTbjCSQK2D15GyvOw;)-Cc5yS&!eHxyxuNW4r8WvjRqtwy>Q@JckU;S z#Xarqe5>NKW_1n=i_A zV?LvK;XKi&(}}JfuY#Rvi6mU@k@(O_)ax^IvNtq~pmI-|aYjpc+sVM}&@dXIUJ3i> z5;0ccB)(={W?tlO(HMOekqs{}QFCw6)n|FQ$vKV5>-6bDWxJ?xW{$uEEn0q|RCwK~ zWrwUK9r$a+uI;Pr&9b4^3pFU_oWLIL?Tj&f1HF`N%L?HsX@x#f!=Dm2H;Q1hC&D%QDhR?h|9$tpRv)Xjl=ozv$RbhNH zp9g*%WzY0AjNYe8S8t>W_l7_+EpkR`$4`-_8cc?67LdOxCpvx&A_Frwgta6}GSA$_ zAN5Smawdz9J@2Eh4Ex^~UH4t(`v%v24&%>B&i}M>{^y9Tzp~ph9Q%F+_V=$zVolgH z6qAbopIWlC>XabXuoWC$#4&EYCoq=JHJkoct^XE7iT_VhshQl zI^jt3C#ujY-J|eu>PyC7$B6;E4>DKGpSmk5iPu{TF+IB%rRSa&YO>r9J;b>l<^^q= zy9g4_VJGsQd$zJJB7azt<#1{Crg&4G4(}y6*JHiYjeSbelnC2%JVShoeeq9e95X>%zL^JA7dMF$gB#D{~A&A zEd^tye?fhDBbN z96OG4DyyLRG>F!0HlmVGiR=gJM_a#H(uT3yIfLU%F^4tjzmiCNa0&*=>1&(eZcjCM19|27ScM_FOzEWVg@s zD&(16hKH;Rbu_$}j9;~lyN=BKSuQ6D>YWRbUdg*n>C68DQxReG8>%_hBvouoSGd<+ z$F7)niH`Kk$Ci6)?sW2?DS4fAW^TPRz01r*y(O4yZB4H2S24I#&^ zx|QK*kL6G}%`@xxE12|yD=pWr;q=KjIJ0p9u8+-yay!3!=cXfL{s&x`ZcA5cQ=xNI zntB#?p{b=?QNzy=iR^FeTi%<-?zjV^)=q>P4x`6o3o-xN3#^{CKxtrLR z`O5>*xxG(e^}}IUdT0sfv*N{%&x4rzG8e0_6TCThb%lSH<^AU)<&YV7g`LS_%Pi?j zj6B7jeK9=xHAXFph2!f8?pI4ulfr6PY>&kB(n_cr9>8CIhKTp<#YwcIiL*O#bFLqs zpV^HP-+?j1+>m_RlkKo*lrR^+h;( zb1}XYFGXW%HO@>7ppzH;VQ=^o=UxZV>*2)v`G@db0BVw(3k&yy@ZHM&0)t|toH&e& zpMD@Zwgy&Cd5|t{hsQql^s2L8!APHy75)gdYAb3QZB3<>C1S~2YZ@Y_L7gqT#pqxs zx=^q!d5$v`BZ?L#V}8z;M|Ev79rzA=f8~s8UB&y! ztD;X(Fd66dm-IIBA&-6oDdX1=(QY0+gItPZ8?%*neq(Bp?3 z)tl?konv|!B6|#tUD*lo{vXD^%t7#%u5>v4Ep9F^#XI&g|9YlO;k=XB|1Az(pGr$S zf_`Cu9e2U@O%yS`b!e3ICY1HsBhj_+L|EA}o<(;uNBV=1c^V7l|D>tXE&x7a4AKv% z(+ci24gU8PrJM!)YOYBEbH8G|eP1%0Yeo`XMH8^f1xksT>bg*?l@u)hI{58py_ zpDg(;c#BQU%-MFDy>zh+B8?qNTD32PVRW`II?m9Q_~AW}y~vxsHggZlcoI6sj>CN7ea|}==B|xqzC{D-I!UVc3GO3zvCwYGmR;4fhEk7r08*( zGYxFyclVy1uu<$nK7H+}^jQk}@xT4Gj3edAti|THid6bniF9|^ikHK^X~DlAaGSJ6 z{5kJVsd?OMNf{>^ZrW2Cd)KGAb8p`1Xwcp#uJkP4n-W_}C9{SKI_7IdyC)Bl49KrT z&ea@DNKvBlS$A-y@&F?F=XE3CH3lkXU_q=t-O+pv&vEjUzi~U1vp6TiJinUz3$g3> zPki89P?T*pav~k*#P0w~tt&wKabMcN`Jc6^511WeO}%dQrO3sXpclFmXJ_!4VuK!K z*QcWQ<3QT=wJXh>nhk}`u4J67Otg9m#?<8_GfA6f2Zv%+9CK^ltI_V&{zwlhh0-+> zx}v!evorq_FK5`0?yG}*r!5jA6Rjw$KR^4@%|)8BH^rBj(w9sV#PpYrioWt?0r3UAy~9SRTEPZHC^=Z$6H)Zl`dtJdj4J>_^|Ox8ab)`~LJ?v1ITH zJR2@gVI40e*Jp3Q%{waeqVTXtRZYODw;!>nw+)$=8!*GwkbYlxV6KuSz5Cy8Za_C0 z#GI)cf3zq{#h!9pnL8NePm>P$k;2kLnAkO#y$jYfu&@Be>)03jB^Oh2H}THqC1xm} z#;lwJ@EKDI?~t8%`#los-5*1Dq5)}^UPeiC4H|OHX_(e!^yBW~@&XldoOuaZ6Te^# zyKTcv8^j~c!w9xeq>VqXiKBlHBh{8Y;h`o7nUugCf?A>G){D-S$Kl7SgTmrA_k33! z=I(Zzh)Nhtty5>9>)T$aShj$lBmUIGV>0swr^D`gUusbthr5Qo&=u}<`2jn%^P)vR ztvkX*o@YTa;llV+cd=uLDXj|GF8T1XU1SZ3MRr*jhTLrsMg>nnEfJg{CR7G-yiP$#;nD~^W61##5`z=OoYw+0-60S z7}degJ4JT5lpi}kn{2)@-?nu zLg6OJRc2sp_hNjBn2dq3x6t&X8ZW=EhDyH;usvf=gHCTl6@#o!n%a_fb0+39vtjrI zJE|~SiR$spXx0s)*jAv9J0fq+`cu-2Rk$YcqMuh>>Gm#f7(VB_1^4<_Y+oQ|oZF4t z7rr2GtCILL^c058Qz6Yg7lpQclqibIg|uQ%GMH8_(yMmDJ5$h&dvU^R+f58(uFx3v z7`Uw)hQN47^t3al^RL#x&R!FXtqtkNCo4D{)53+W+*Pz>UQRu`1=?(BSg0yx^3H9| zRedrk{tTt#4-xBfTwHl;O#7ylNaBw-h=QB8)AzAGot-RL{Yi*o(zOEipSt_L0jlZ`izrsLU zmwIx~J|w>$4-a)Ag=NMxp`eCyK39ZF40E5xoX5<0En;L7XR4m&AUda9qWf_to|GTQ zU5^w*>Yfz6Cl+B!Q5-Zw)`@R9u+AEPVl~9sebce+ zsKBc0dO=$TDPTo*5ROg#E*|R|(!dA(vCz#PDy=%yXX#u#i~B4>m+I3_8EuUEdrC5~ ziviWXK8D9%gTz6hKz(|g!OJ6t!mF1NZJoXe%_(7`>25I1oAwlPBU1Qs>P01Es&Q44 zDpsZSr3HNM>(I%Nc)oilUI?N`<}1YD3L`X)?m>$qs|qH4Q$wWn0Ma<~44LP3Mfd3! z@VDv`7VaG@(xon-b^db%4!l@+T;~esKoLJF_&OI05z0{N^F z(Qux-M9B)A*WHF7TM4<@KN4ct9ue%RPGgQ86VqNQBh^HO!s_^Y+Jm37PTG{jOoN(X z4d^HDN>3k1Q@>}Q*h9uH%yEWv(y<)_c;VQx8x)kmBYth>TKGhknD z6SDeI1#cB=l=Hq~Vls26f``HW)I8>DU%-~*{g8ftJvKio#PbwQod4Yul{_!f-Psod zyn<=>G6QnG=D}V;Un+aZ8SY#=@;?wtlP6gdGQDZm*9@$js4Mo!XGz{p&O^tr8wH_x zL&eIL4S4?jn?xnNpD5_Me9Vlo-}^LR$DQDwE*kqt9GLnb%wz?0?sB2h0Yd;o`F2=yf ztC)Ip5YjrzFyaYwKet}Qz1$W&nQc!Ew=Tn3O@`*4Rj1GS7ZITT4@IR8)aSGX_GKSM z>hUpAEYp+ln z9fLSwf?Y-%(b3;m)RuVQ*XtAnO+Hmv)we(7jMHGSHdplK9K!dpKY}(~J1ibsbmepV zw!($gO+tN_HnX`KgZ{j_Dp8%7kFsGSFk$H~anJ8QZuFST{U>=SnC0WsY7=bjo+?)E z8Hc17_M~#S71DA2*t={@4=zj5!`+;xT0e6BePaxEoDs zVAjLsp5#^JL(d{MLvQ~es@4S=JXwUrZo%{@hbV7mCW_YNV6RpsbG>(p)A{)rT=@wm zc{jw|=ch2mr5w(;6D3YjS(36}%>G>cPU5vMR(SM1j;6a+lJiSs#nyw*v9kY$!ZqXc z$xK55Sxg0>zs$xp7&DxL|@eIc5 z4s_8)k!GnM!=8#aqHKg4=lHxx)vh~EjkjffMGs03tP)+V0_lUbU}k9$ZQD0Qq;fax z_{T6xeEmxDW0D;;DF@Ll&BvmYpG#k(Mv0P+$5^*56!OXr;&)OD`}1bN=^#6~jGkcO zHys=^YR9ozMUtnsZ}H!oci7!(Bl%l=4^f;2dNE{G;fnv>!giz?Wp?+ZwZlF@r&~Ar zBV|o*V_smUJ@276NvPt%YdpW*D25+8fOw4(T-h^D9OyiPJkD{>N!CQ_(@QAYa|`>d zb|7qVnb>mc7Dl{Zijg{}L}%v<T?^>ys{f@u51-~s&Ya#&5&xpE)rG;Zi-2bU8(ZC2Amf3p_0Q& z%w0Nw+k?X>CydXg^S5yJawz?`NRGZwV~<=vS%iIlFDctv$N5B4SkEvhs@(Sxcb2HK zN@k6yH2IBDx0i~zCONEJlNzMumm?%uS75k*% zOx9)8UhtqjbN684oa1mse_H<}4d45{$IfZWbZ(*|QZLKWTo+}U)w^C`*>m_Radxhy z3)UUU#OX<0>5JlV>>F|wE1#&-W%eoAP1p^a@g|gQbsK%y&n4e88H=y>CfzzU3geyI z-5tR+vilz-JU@VP^9Yh2q>rBsmEw&H=hU}H;M0YF;!lb(XQ^eOG0hSIZQ5kIRg1bj zV2{KSAF|@?S838u9OiD}zu($)Hh>uo{CtTW`UvkPe8FqJuT^%d!x!G$_9)b(%=4%5 zf;&j&({z{RcSG$%T{;w-k0~E|Vd_&| zdX;w!Pv@%Pc3%^UoK*o6`yLoTu^70#0=m|1V!8ZabQBiDeDybxl)eHfJ3ixfbFFwa zCPlK{Qk8bJwc+`4jY5a7e;`J@Lqc9v;SP3jEz^@HkG7q-w{ED&{BNCbiMf;9Vkkxqw1NKerT#$lF&fSWwGBoyYHhX_wL*tnmY5ZCU z#S%wS=6;%j|7|>6WI`tMy=c(cA`D$*NA+j+r$xhmY?6h?hMysWv$hla}Y>b(|utK9In^fLvka zVMU*lVo<;JfN1JZ;oV#(3f42Tj$7EX8}|!?QED`MSpfa#xmSdY{0HCJ-6?aZ zhnTvEU9)CCF}U9y?l<_5?73QujC+n{oDW&8+RTo*96Vj)LHqXYloSq3!|@}k^fP3} zReP(Gh~nLu-U!{I6H7PZsIdl3PIMuMM(!qFo4cpMhx>MwZCe#+P^yu z?rBYbwk(EG@N&u3leTp1^CWz#N)SEHI#N&Tt<0}GDN*}+1(KpvNQLI%zqHes@h}n( zdYpxk{do*9K8zm&_TkNZ{v9zni&KezQTmLXz@}$`2Qn1dY)Ba$2cfeS@>EpScqfoVztyN?nKp#k@^aVzdaZG9(5Oq zNB#)Kh&|ZRqJzEbWBK_K!g&q_)NS93slootLhX-Pw(-pA=}n4uU1|1rSJ9kfg`~R{ z)H&;6;3!RYO$Hg$Gw(T))F zV*B(j+yU{S%Ykq3w!b-U-Si@#xF6U%Dn$&P%$d(`O}I2t4fl`Uz#gwXNE<1^XuC>G z_CAj5RW!}Cz9I{8TXo3f3um(4Ctzf}C2gte&OIeLTDi!K`p$8udaG}!-fd3tF;*11g_$kE zUFi6Z9yC`!5GOW9LHqkW+%TJg>o!YKQq%)RcTMp|bsHxC;j>$66_)GF4Vu@-9Ot4- zv8=gY;irA!xITwF?*1i`*1onlF_!%oXZ~T91Mghl=U|-lFaG;Z^Nx8V?;u-Xq*a9} z0ahgUS&#P3JuebNIB$POfn=XVie*Qw=p*xrhwPm!dQ~wKr+gcnzMMkaep9l(cn}dQ zcVOrY6Dqizjy*m%pv3c}+2f)hYpuv%s|DJ{+tIDg6)`)%;&NC79uM?``Pu8((&q{; z6ht82uL6e)Uf|?`6*$d)rNz?4xDxG)OLBmDrW^fWmaOTKd8ir^MA-_-$d9tZ^aO93 zzj6=1`+TVAxHr|vO~RD~UmCy5n|`u`!+MZ}+MPmZ{Ei`*zOV#K&Ph>HS5vWOMLFhF zzsIZeJrWI*oDD`#}l5O{Jsm_&5>=neF3}$|AGN;lmJA~1?I_7t}l2wNq zt>g^W?2qeUY@$W!i{FXo|INUyCk8Zle<+qaZ%06W7aFr(iam-I&>Ut?=)(IotBW|S zr9;3=Cl%CV$Qa4c z&t9~5%Ld8hw)H}Ko1otjeG8(uP7(oc*yU^xAWSFS!me5GMcS@Jad5+B%#xZVR;>3a z+;Eh=YZ^DHBFbX!IIgk=g(p! z6=TP=)ktnOz=4*F@IN~gR~K`((me|?KSrQM*$svk4=_~Cj|w{H!r{6j=H&OH0rr0A zvW)wHyrUzv9hj@z6&J1V37Hxd-VaQM@}RpS=7l!hjBr9&sw&3!)23KH_olLY@S zRel!Eyf0jD5+TC7$U}C!zxcN%DxmL6eP|6oElgIQ5%*WR!g$9HNt5+4ylR;vF?q=| z&&EA`zF8+6aSF#SW+6x-RrD(30%ll$#|%$nic``<=@%6~OBzyOxD1v|{)9492ck9V zh_=c?9p`@r{d|r~&(o3aV@UN?_p#lo6gHe?Fl%VQh8tfo@8C>k*ap&85B6U)4MzTu zaB3N=Narq$#v6|y+Huhr4gtf_)me>XgWE-7<^=c`X;bfIqcPmt1xfmPG-fZ)E2chY zW?6=$N#e$CL++s+x>hJX-H1j-RlsbzO5yJJC3x4n4QI3T$k5^hR_U|nB%Jw@yi?tI zv>h8ptI({A3izrKOdoZAh@CmF#H57-X^8x1ap#yO7VZxsmwI;?{?w!wuO1>jWdwYU z%t(G#9p?0y2;0%>v_zemqV4=MJ-+~WeTV1O#iE!a&rKhmV9Oxh-E~`!(Yv0ZZa_cr zZIOXc=&eAT)tUGEw`bvs#j@1QxgFJ~F_IkDRxC??%NdbGj0t)n`ExKRes8!O5+$p4fp zS+9;o+B_p#o9sl-3^;36r%S;SSNijXx$*|~G+;V&xfYL?{J!MNSt;h*q@Izi8Oc3& zM-6s7xfPZC_Mp+b{VC;JH(HeGL8+a+=yV8w28;q|_UJ%TS?fr(f1F8q#|wBZNx?jC z=4XUo!?HCQxXYQ|l-S4IyGi8i$S3r=S_L0p6>93A2HEx+yz8mKcg2~o81)sip2*Ph znd$82av=F@?sTf`3c6N!lk;dFo`;lUQL-tOx%$(Z6$Lo`VIFEudC`Q?8g%eRGSpjL zY4luK(%7&PXNrh|!}aLZybTDXQVhM`iK>3X(Ra*k7=BWq(<9jVwI-LdMIUiDX9J49 zTo;vT#&mOZE`r+1MESC=BqpxME2VtS!L!pP%#CN_Cqzw$BD0_a=$Oh9!JpmEDAEQ z#W;596jUsZV7Y7-?6+x?c4j_1hGjT!DogJ#@ZOrU*;c?VoUg;xZCwzf;7F0?Pw_3w1$ViRcv5pJc9iqpPSchC zJ)8n5o)N#zwp{~^U%gL4cv z^uf)FyTl8yV963JA7z0hdk-{!I|o(!QgrJ!3P&0b z;^5rdDDKA2#LG8Pw*Mu*b%Y|Ll{3ZhzVwuL^Y1DqNvFp(_R=ed#MMh1f9U< zNj?<4y$1zkhaqtRJ1QP~F%vZ&Lmm=4VR}+#*I;-KWq;-PU-+{yDah#AO{liYQlxr+ z@!;QGq^dRI#R}KLxMn?(I{z+=8hmJ-L9|dYeT|zD_Wbu=E|`+S-2rl=LGM)g%xZ|V zvCFX~iW%|I&xPEb-iWd_q~v9NAl*L(Md!GqP%BIGmG8j7(VW5$%TataJDTP7=%&pl z?D_l%$;0c!lRbQ2SNkdXT5E#?Q%xy4+&O5{w+EuewL4w8uvr|A7u0d)kYvvTe#X>? z(n`(E;_@_qx?wEn`VtqB#>^3~I4?2H^&TuNRG_Y}C0x4QLQTV7@y{wLKt7`ii6!=E znZ=pW?c0KC*e(6B@gLN>ScvJ0n&ijNT8E-Ll5bvLv29Byex@2yWwAUh*OlS>t355Y zRiviNN>njan-ueYV26U1xOn3#Ud_G_@6QLg$9b9ai>IN!^hBZ2zf*{Pd;_2E#^dL0 zIq0!Nu$$>pd}_7Cxv@L3cvvJpOf$h?(+b?~@SzJ&ColuP8&0%@P?q`ze3mwWyL%v+ zb};j)q%V$yJ{O7QYGhjwhR3g!`RjD0%l&6zx#C;#K#n^fz8d(Z=R<#NJ8>g934>Ql zXu&PckpAHF_s<|&^;Lzs^p8b;X@>BAm@FLXEAeg6QE}i&kfdYgOQiHK5sdP#LcO?V`4AF*_(Ri_A(j`O>&ku6U%S86_)9^ej ziadH>M6XHNFo|^+`pkeWecy;A-p2`}uVS)S9p_-W)9AB*ME-KlQ!!uJo;&s~d(SXu z)s|Xz)<84iATpB;`JGyYRLuu?72TcY8gl2wy$x2g#^6JfpkWt2;NtF4%sU-OVf&P6 zsrY{f5m!c;JrtW_F^$#SppgMjiul35P9dlMZrHg z8oN@4!Y0fWZ}+$2fX7z^?QAKS(nF5KB7Q%<-;U#3c1ae>3=`UmGtiM3E=gX@UL(5| zxE85XSgRZ$7A@Do(=Tl>a<-=CL{G##m8I2#UD#jSEF3QVg^ZseJ>mSyD3|3}TCYzf zd7deZYBJXD@8rt2uVbDx@)My@Adt>Gv^F_oF3hg*(mx z?7z{Cq`3z(=8}XKr@PXp`QFq&#Glr#3Z#eG7Ie>~HyQQ$fMv=D5cE`vy4ohI@9=tlA@yhwfa4&kk&MmZw`==yy#k>@T&vrD zSr2i9eNo!oeW-iPOpk&5fHG-*NB5|N^vfTkT< zRO|ItTo2w2i|jcSDT#$;NbB zC#+mKl>6Df)M+;q_DvC(4UcJ*7we#Xc z6rcZ$`?VF`EdD1-kFG=MBw&$kmG}^N5ccf9nq^xeb}f&=?D5jr@}@u>U(zW0h1qhp zli&BnX(BX>zenyZ=yS{xj~-dl3wQRPZt98mp4TzhFdbv$tznw?6x%vuG2qc8^x+vv zxXcOYcihA9k366FsZT!aR(wBkK5ER>=yuOXSfU?-1a<%xyq$&3U5qH$f|);hv2d7Y zPLh*O)Vnzm|n9YvV? zyB8%U&BWNeJ#e_v0I$INFjwD?jSbJx64AsN_;X0`{|3#P66EuF=GZfRewIBJz5iI# z_@^c`#9@r^F78fKTTE!6wIRE-yl4Tlgt{^FcX3|{-A!1CYq1w`V3#YMIkF#Gez#E8 z_6+Ve(KxlcN*vXw;=Aq+j2>@{oAeRIMx$_lyaKYTOHq<`6^ERLL4Q^zR{pz<7R7Mf zA5y`5uyXj^TZU>YDbOAZT68@HrZ=?k>54No^oWM@7%5ENX+ve}PC%{9hdQ}$kgw0o z4?Z(Ib_?O(b$fht_yecIsi|(OWXwvdi zdAx-^BA(1SgIp6I(rr)^2Jh0bfq6nDp5-FyOyJwFKK! znWqfBJdhyPui;$tsR|UIcqQ_sZow?{GR8Qckqm6iuP@`y7y$ny&&DSt#=Ge(GWUpiH8i5~s>!Y{u!b*`L==}*{S{OF80 zRs0(_XGOs4N4C)8+1;cKTG$>_EgsI{dDZMinDN1jF1Y=`gJ5Puy|SkVEzDRszXkDk zK>m&G+|f#c>L%uM4Cp3Ye!RoruLWX8&}mWE?!5buM))lJEnXOp zmqdpb29N&r{i`gao`Oy=XPo6@0w=+CbJ}-KGy$H|po1mCi z1^3%;qV(1$^r$tW8b25OtgGQ}sU4ZEbHXO(`R$E0rqj37@Mho*jGJLgqYtwy{`@-( zea0EKm#xghXZAV!t$j=GA=j)GW2D!huHKKL#@D0n@=82b>_;}270K_(dh}SxuHUit zP@EtQr+2(x$Q^~c^-si{!{vtiylN=`1V&>kz@E1OXCKj9_%gQzM4~A0N`%JOR^@(sD+l!9p=_9Pkjjnn&!Zm?8x0L}jVP-q#$jn7|Z5x`%0w4`n z54iY_MDW=%j90QoQpP-V8QY9KR?VXNfhqjmZ}MGCA7ei+lk``SrK=Y5v~RmZQRXsb zI_RfB!OI4SNd^4>WGPF}-X!5qnOe~C=pb=}nTj0~!wZb$QiQ?6Eco84Ea+FYO;oOZ zBRZ;TaBDa-fbS#;pJyM@nrlP`7j0m0vK3vgS&|L=G}T7)y==ESZ7=Ii;YHIhv0jOm zK6R!w%G>a?mi@{Tlt|XlO}KA&C-d7%RE>ELt1q%6MoF}pE_NhemhP^ zawI?NdTa5F$Pdc{cViUPX?C<1ocs&$;-?|WDsI5h+8ANbw>w4B25b(|5tYY1XyJ#0 z%(6Z!8rgaKl;_()d*$$Kp%iV(v7}yKG!d%*4(cn|y)pBR@T&TU#eYu==X0}h@q;w`iW0>ntr&#vQMQg_}N+MWPw>dw_xW#Ib?r)BwE=?oW?wpHPnQ_n4Z{@rAi~zTF{&{ zir=T^6joF z_k?-(TCiGWK$7ss_%=8k2R=LVxpxF6|J7z+lQWgCUx9V$nlvk!?*#joAZo1%#RqjK zd8Ie7oW#7;TsQLjuLMT=UCGAOi5|Rd#VTeRsHA#P^{0_ozHKm`S*5|}_+0$e8_k}! zdF+$0#$uU;IB;$|+A^LZO!1f`BEcF9)_ueFHY?#h-T>dfKZLGpkO+|-f!Z6f(E8kr zM@QeoK_(Xab05HH_%D>!WI(aA6%SgPk*ldoOCPC{O=*r8v`U-)UY90^rc#McHxuHf zD>bcHEAow;s6+1v62~2d$q;MG;4{O5_1BR9$A;|7&LU*kR*XAPi6!2gd3`95KC&FW z%abrHeLlYzT9CJJCUegF#fu(lIKkq! zBb}M(NqJI>pw>4PgEuSE?0Xfmb!B%$+ipAPJ!0MjcLY!F=dM_#KIv{HEGQY*wE&9AyPcIx|=}ZZC9!qwPH=|pSO#yC8e|2%2v{6W{VqLht<<>@eGiM~26RSskB^+@>=3?yGRlyo?&@`M8mw zEL`r~!?*2b;*OjPGvW0x+GMnNZ@&O*4qM?;!RW#_Z>(_pyaeNSc8C{S4d~;~a9AeE zW2{LRN^<1Al8ZaW=$g{SkO=1aSV+dJ>hd!$7w0Afif!+8DX(EGLXBTaZUrflbMY0_ zJ=YMEUhw<>(=$|CB#DLJdeg1rpV9m5I$x6Pq$6UCK0} zUOAAmm#q~ubL5cDEZMM?AV+0(GP|;^mB$*c~vCT87CplPQ(El@faUtwrdxZ4mP_)u=tr z58Z>}MNy?TJ)Cq$CSMTZ8w{>=<^?kErMz~l+NTZe1?QW_wK5A~wFW#I1@;VhglSsO#TZ>>;ae#~+hU~Y>XAoZ4U`+2?Y-q)>5RAOol_m|dq}yj>v9H~Z zw5-{Kkl{m4sb)fh@3)bLUNov8pb*h^^t~a7qJP~jxU;%aa`OPQO;g>->43HP!Drp4 zPX#6Xt&u$K;z~K#Ra@4JOmr=>C0z+M1*lW zhA5{(uTq*Wz39N_VMnl+&#j4?iqwC|Qp9Uo@*KpUK2RBY578mJ1fn|0J)EClMU$EB z^Uu5)husqK+tZ1xnsumPA~S>T*iy8HJPq!<7wSX($eNjX>bKS)T>A#>|TFeKQ(;VVa1HR;FEZKCr8(63&}EC_1hlfjci^^aWo! z@un6Y>C9ZG_)C_M28`{!*fG#|Sesu%R>({3kn7^+38 z3%k;Eqyiio!xs1gbU1-OxP533_Qd$Vyfcgxlx!J=aclSCY}kL~{xJdveK$e+#|+rMT8MP!{5uCnAj~WUE;oYc zMqLaR+~sqL9Wg(-8-=OfmDt&Oqfe9r{qlTS5STs*yV*I?(O!F1&h)o%l1@PIX?wU1 zdM0d{@BOXhu^6!GwQy@?E*ASu%_qoU%&1y%V3!AFpZEm3zUpwTaHV|{WGP-QnfDF$ z)Jx_QKK_)(#(Q@V*MBRrgQ~^ryEW*4aXoYJeX;X#C7NWnW4CcNZWg)VcBVNk>h&6j z1BRkuo-Wlbl7}AS9OK!}K za>LX=@#vv74N`V5Q9dAmHlOQ%wK;ig1JxP@%;=W z50kJzsviE9+#@)23I%;X;%fMD_MO|&fp$$Y4=HD796K5Inla-zU(EmMK)wdb)PHJ- z$adlRnJ4eNrt|r5dpBBt^dM$Vx{mifEof8oS!f;D&gc7=aO*t?0Yj|NZS4~b8nFTD zrH0Hi{emEeAWYjBhMV25Vr^$W3|yx2U9%E%ua%*v+capLPDioUHPjldg2U!-VyT=f z)&AXuvSr%X@!p#@n_R%s>KNfH@g~KZJ*binqQI2qpd%;KP`uESsx>Smii1vJSufD) zq&VUDW*a7iSMVHIlSY~LrsM6;@$jl9pG$|)ZvS%l=1Wt@H&Daee35Kgis^HF=-!91 zV%MK@IC95{a*puKG2|IGsJN59f(|{H=ZbO9)?#=s?jD{T%kz#n>5_y3?MjJxq`v`P8hH+H7C<3FBd<~b|Vzeq*Ng#04>jC+hL z0|JV=^*sk!&KqPt_7odaN`cI)u>Uj#TGIc7ck=^GR&hqvensT4W9R97{#}1(1efZM z=rMkn3;Eo@0o5T3N+epo`}sf7lCPSn7{cz^o-NM(zF$l!y9LajEU|zcYLt; zVxWvRB_|}kJy2MkbpnAsb0pRMv!L>PCx&|W7e#X~VvQBwsYlips=Z+T(S~1`xsvzv zH%*WQZYL6-;w!!F>Z}WA(Acw&YysaG%LssTw=FJU~r+ce-8lhWCH_p%B0> zc-vc8y7V7A(pNEqI)n!P`i)%)i?JwkFrEIyXNi;R*muP_tO5zr?@q?nX-YKIR2o$W zR-(@v<^SX8yyJ3i-#6aSk_OsCn|3Pgy07C5Wh6VE$liOejF6FvjI5GfC`wD(%E&Au zJ1atjG-&B}et-Wxujlm)a(_Peb)DyNyx-I}e;CGijzFBR7NsBNdqYYcPOkB!L}^tT zxcf7z9!V%{tRXd02hZMYY4;9h@s9k4-0h0=J@X~^ljKN)x{$QiWrWq&V&_a%x^4PO z^lK!#{3c&0l}I7Oh22I2b>aUdOWZdbM*AMF7n9O-ss7eO-WLqUyxGhUEc=2s6H|P= zqf8!t%zybl4a1FAK`QD4{ADY|f$m<|@bxi%f2|UZYxW}N*h^>xYrwEoRXiCX%ky#> zYU%r}aMle)nv^e3!*W!`{Xs35Rak?X&xeq|cL23$ToA|kULC<5f`3OY3ezhWvGHer z>YOA4r5g@7r}K|{b-MIO>bIC(CQAy;y=ffg2mj=*l>5z`UXNq%_^VL3X&BNX^dYS? zThQIrmdYmC(wZm!s14Sp*B5)yrchUk+G!$&s%UUd!HL#)I23NIwx)-2p47fb)n~}b z50d)s#`3X_&c#^jVsoI_B86rzfuaeMpAay)O`32 zN4qQ}gL zsf0_EGjk<>VO4bo)I8iM#qt;aE`ExBo@Ufkt1ou#IgC3BoY{{0ukh>gNMZ_Kk1Uh)Sc!%)+T*smJK%K4x5_+74Og|Ddxzk#~#FbyKEn) z6eo%s7tK2%Pcf;{iv0>%X#YH;KuhGohIi?!UvOtco)qIG{r^KSIwb}y=0zXN-gjz*@9pry>~%&T}OF%Rp<97ap>)|BG=@i?RpGh#NC zgQRVBy_g%h3*LqH2yJ;N)W#%1?!7n9k!3LR&@OgHd=l}+IpTBTd*RaNNWLfQF!fBd zI2>R}{r1(P;gb?hL~sUYi!52cmqG4?+sIhMjH-->;#?hfe+I2*e~l;PXWfJE{r!l! z^pg874!AU)&p$fPV3;x$J;(4}nSFNg=lqezSuOclYoW~9o;~ZmC~fHxcCTyj=ZNzT zS9YS}uP#{?*)pz4hTg8Rq5m<}+s8B@XsHF=*yu)!v>S2U#+AMew4n9V*&+F69F+Ud z!XdA|I5N`KFbxB@yo3qwU#*u7f$r6Zd@l4Mxq0DO71D$sFNyxh z4#y$oz04W?&HUYT-aAKN=PmAJTHeRk85i)$;1_zvry=#SC37*$J9d)F=6T>-X+Yy`0;0u z8FU*fKd;4>8-K*%x%@rXa2Uq%kz&9hH;Ux5`uh-dtl+y?;;&4Y^c57RAtf2c`S;%n zprB*y?OwN^Sq~l*x@C^fq`n#W6~Nhq|Ck>;;--)Hf)|KvP$gZrNbzFl6R7COP~@Sk zLeo)sV&2_r$X0iy*z9njIWrY;v#d$-{j+F%%h~V{J9^#HgYpjBplRC{m{b{&;)Ot% zRqex{BRaHBM-i3-CgW~dPx6zNp*KF27=Djk*o)-pr+OL8V|D3i8SfxYe8kBHc29Bc z(#$?Vn3fg_yH}PJU704PY3O1cKi8oKlg0P#1L*whhmy@Ej?C>Lde^W`w5;w;<4z8z z64wy%lfB9F9!(a#Q|=>s@MY2QJWAweRKPapuEh4TsqlGH3+?#(;@H71^!=riXwO!l z=mo{EZixymQ%MM02?deS?|*!)FHe`5WaH9dPR$oER?8TCs=QbdH&^6Ik)!TdI$p!eNYK(i%Dxx>bCXG%NiMW2& zB7Kq)y;!#hL!>Gsp%c4PjFUj(fHd)HE_;FIY=n=k1D$l$raM_@;Cs@GPS`W!=4dq9 z{>MS{wjhPK2cZ$KCZ;u~iz!ckK=pZqC|g${GrEbT4~I;xSbc1JW7D!^lz zy(E;42)V;-;FU30%>OBe?2``IP@!D3)#5w?-tU!6%jNrc!g|IgMF&x_5EG z&6sw*27TJY{0Y_TRLrAg`7A!Iyh04q7q6V|6H14nL%T|>k(p7)ktz$hj#Y3DtalIN@6%8 z?$wbiWY74C+<;_pJ>)8%Qys+CKxasWSQTtQm@j=67&xdKX9qF~@Oj4HopiAsh-lc^+GWUBT@lKBb?iRF!rOa4~UTBbrtT+XOenTWpb85wH&oy}Kp-|Xf>mfop zxA{A;0k60Z^v%!-M@F&ht_yQ$|Nay{pZ;NMy*52>>PL15*Ww@l*;G_IlhQ~>L~)K} z{XYkK@Mb6HDlKT1LRXrZd`z6b??Vy$H0f$`1pAZt3^9cNdumdgU-O_hTiBgpz#dBO zrq_EqQ0@Wts($mJf4#kFayJvs?G7Y8O@0Ofn1jywpSY(lfSvnL)aMJk1FPXWI}d$_ zNYl5eUzmj?MOC9t!Bmy|YU}=DbMRI8mDZ!^xe~>1-^Dxh-V~nKpOSZ$qLY0oH(LhN zQoAc~9pp$~ZHCgVz08?U+={|9&L*$#PV+0{@b;YznL9F9dHGiM7JJbOi3Zh0#lng6 zKdZvk=qb;YDDD{sk5*+q0C$D*+5fprgNEPZd#tvS=n`W_zkHKX?R-*PQ0PSi2gM_D zN~~nWFGot>){pW+4~lQn-N?nti)`KWg>aUl^T#b{?dKFBQR_+p-z)GT0ZqBrz{D*k+JO;&^)tF`ZL5%-(7z?YJ9i+Wngjhymwq|#V ztFfbbP4e_U#DtQx%xT{e?xHAY(DQ72nlZR19kFJAgRL;OwTu{ zptdazKVx#m({t=34vxT{h-TsBVU3v8G5CJwj8K%jCYmNB!uGcf>_W$2_;@$w{{+D! zG!W{kzBIDf0*?=F!zIp>dGzQ;ZDt0N(RZXFuBcJ?qV8h!f7?aedoA+*DpO>b#(#hB zW|(gPZdpAS&l?kP???y+Bo+$=-)(%xlS1*qJTYtbSE29UhfGqcQJHZ-7&1@t@&xWe zUp8Z>uqnM8(u{fG10cKTI{NhBIm9h7mrWn~cC!x0RZZyCDRalIO#@^(cy# zeP({pJw#d_!@!tl2)y|f`a?5N5!wW5D#0m7eX?uNBGdDag_kuy|IER9wP&n2bdK{s zdF=QZrU&Jyesq(0%5EKLupc5JOY7zA`{uq+NMHIdHUao^3)#&)FBuk!mp7k@)5gz{ zwd){iy!Da#vKB!D|HE|?CEU1Limvzbp<)<>6W>oGgUT@LbO8TcA2LtqKJKcn#Wi;+ z+(|c~^9y2-%HH+v>+MKkawt>_|B0ZPz3Jaa-sP6M(zMzA$o7{6>OBNK3+Yd}fBN7V zGi)A(x>I7#MtJwQ0{KncJzp|e1f4JCpH&m`KRnw!_?vg2{#V99s! zOX(tiO@pvHqD#9Xb0iN}I8pyOqlE7(XOiPw(-bN0l9zguybAC1f;Gg$FQ&BccDkfs zV-kBv?uv_DvkODA^U>wzQ<2Q9kEs{FpWOOjC`l!>p*TRGKzcDp9u`zNF4vl9ZYVv0-irCjWW} z=XcM=+E=$QcyAu>ZjvR1r?Q|i>?UsHY{gh@&ip(q#nbW2@$!Qn-nYC*BX=?Fx%0Ew z_A;&o`A~bA2R5hc!}Y#DX_cB_&Ub%UkMg65E9Rj8aw&LJTo&QyTk$Gl1VSCoh+Yw$ zh_&m19VI-Ez0BT0r{%D%??;_Z&5#d|M;)^Tio#xE>D4{(VJ`2QN3HxGi9#;>c^Vsg ziI@H#c&?l;eB!divn~xd^X|C#cKB*xja@ZXJg^W>vTY*1W{SjZ?oXfYy87rnDoO-B zIwvM?v_h`csKWU-OR#dXrqJ1*3PbgDIJe}86^C*#nSGw#DSHZB6K+Am9p*b9bm_?> zJH(IeLZA6r3RUlhW!rgvyws8&H#*?-_8a)kchsf#KH>A>516B6NqrvFGq>XoqHh_I z@Xah52?=5MEVU;OE|E!DAk*>h3w(c}N^p=RbengUVr$Bu8QY=E(dlZ}-tH*A` zNb&sn9^p6ej4=G|O2IE~WAbEHm`B($clQkpzUe}hcaO5J5Bbixk{v^vap?C~^i_3) zZ|i<+Tm2P-n`=Z5`+;x@{sapJ8*EhFEJ+-rL5febnfLEpbXS4-PNy|#S>FZX-n?F9 zF_G^KYoc+WL{B(=-zDD8=g!v<^#U>O2=Bt!l^#~;6V>A!&mF#rnWewbaNLT_<6=cw zo;0OiH=-^^2ApGOhUrslekZz<_OxgS-l;4d?8NhkShSZJQ7rog18z*ka9b6M;%8Yt z!JEdWxzos57IbRU0A`Ij(SiergQ^4a*nnP~z`D=sYG*bByCL zGhc@Gyig*$0~=91R)N0u;@N{Vcd+;Op)qwf!k%ZZo1^vVaOoxCvhFH#Dx4{2b+qu+ zx`COS58{2K7k%W1g6CiL3Nb;7c6z z*dJQ%;y`^roW_=f6pZd}O&_j4 zL5TTT+*szu@3dFK?QslJ;-u(%xJ0nSkQ-dxsD99O@nT*)`Z|0v0c`qayGjm3yomsmR&85Wm|iQTzHD8s8s-^3AJQQesTOc6Wfe{}9~XgSvN=U_*>N zC4I4`o%b^Ed$SB#bC2-j93|*qEX0w4UvS|`nK*d)Ji?5AFbm%sCJqG%?EPA_t7?n5 zixFs@RxKQcmy4(_yP!X(L_{vRC9xQm2&J8tQ1~?iQ69v+M}O$6`{H__D?RiH!l?M zcg5rT{t>m#-1WPrgUyr5MBWts4rYeJ$i0oo|J(=nvh=B7)F;ns>8)Xg{{9v`=*JIXZ(JrXEdm-nJS zX}_^CS`mFW+mmY5T*yvWr?Q`3#6&(QtM}ynr~|Ft9>v{8c`EqnL-E%O(Pm^ojfoay zKZx_^%;fkz!jJ}Z+(Q?23p%^CFZYcsvDsK)@ueO7{#QZs;T6b#b`bv_P2l;-c!d02 z#GSe_cB;70g9XxXc=VpTT%Kg#X$+B&fxQFlXjZEk#G+Byev+KX|C$ z<^MTBAGUtS5OaCj*o|kJyE|Z&eprO58dLeiKlraHP+~HSd7^v=$$K$M6vsM~R+l(T z`kjXfzVek@1ynzxytkyF3qXUcSJC>hQvQqu9%LqYd767UFUIOfk@pUCMp~D6}A1^uCga&7~3w zY271w$5+5Z*`0ndA9+BcI-d4&;@uznd9J354lOgN9oD5Ao@UIx_Q4+BZO+qEpvHA~ zF}2i|G+xWm?A&bD6B(1z@D}Xb!CXP54xy^FyA7w7s?@n`S{-J70@~>D)|J z@h4I3OZn@1ivTxAn)z!uNj+`!d2pdG`MLU2_sA&GXQvOn{hK4sQyr?zVN zn&vk+?_8Sm9JX3o^smE&f|;+mPCrZhnQ;oE*hwvRzZ7jfi%}DJ8fUM| zimoYVaIobf`%kyx>(xJEdU845^a{ZnDHGfeEJl#p0$eN5DJ8@EdIBgl&fntkvY_;euI;LLsKC1Ktci|;QwX80h zjvYIFy@ddTMMUs=VjBt9ilkl1}yzqvx5AWZP3ul)|Q99d|ouQv3EwKf7 zeSNs3ZRRP=J9rkgdjo}C(It$C&w$-Z?V`pu&cpp|#=iUV6d2Yi?rr*oWy~3D4V7j# zj{?Q8%Piy2GqKPs1H&h9S9yCnOta2mm8&6*j(7~MFZW?EOOqz6q%y<21t$}hV&)PL z+Lg|X?f2VJ#{SJSyrX=YydK^M__Hxy3OnYPiV5EA8cMAcYg5ZaeXTYbddp$zTYc0M zGZ*@Fr=JNmh@WUnA@4M3`M7r2F<-20x(q2Ct;UoW*3@ZQjPG6BF{DX~_QXGc!Oj=( z8zf7!t;*n+EKhD}s&v&&pLywO;)rdOm|0j>LN!Ac|VoDd73b0j@i4b zwiN4TL#j>vA;%qsvAhG*8|@79tge)-X->c8mm<5!knaB0B%6W^N&kVabiLh-3{$#` zp3CgW_XL06|8IV5VMk9&t;i#?4^7WhL8=D(zXtPNX2mZtlJ_RQ}hP@HMD%h<)lFrcel3~Z%~65!!od?!JF{43!Oc4 z9(Ch~Qfx*ynp}4ik^OyW*NX<+nLiv;_&x1?z=CWNCvnF59;#|h$>hEpzMFG@DnySm z)+eCk+67_E{Np*^#Ry#AAlju&$<698a>F+Xv)PXHtx%r+d##K4HH|nnQH{1lD&hpa zz=}hEvA&Nx-o$*xucCa&W}U=JBUkFt{WP91(5oI+2~i)1KFQq@cQCDNus|e#NdOtr@T{K{9}SMk$W*w&$Ot^95o7^Fi+x} zRwhQ}F3(%e(1o>^p#N3<bv#n0Am8EI4tOJG)v*cM~ zI5sTqO17#VlzuD@5vrP0t?NW*Hbo zxD7ub&FsIHp|?d%<4xW{N+{LL0x!n^mgdqlh&eDjI*hR3zBFy&%u@Qc)nKA0GawY` zm>0XN#>&w9pSOh94MXbQmmSq1 zFv~5;l*$Kj|7-F-l=2>WL}@rG*9Idk{4MCoU8GD{%LV|&UI}5 z(*V`sr^TY;ORy=q2XC7}LLu#{XZzFwthu~Wa(#)L$Q_)731#h)xYoUrmV`&Rko~97 ze0L9W+@OuQ7Y8BM*^p)~li)(0HFmgY(qOymqMLMYeCn=4S9O%=V?i;0wtG{@BYB!^ zk%NWoc8je4i?=)PaSu}+#*^6hGM{PBu?I4x^%^z|#NUwMRIXkEFz08I@4{jA*x4l3+&sOC0yNKks73i6(#vFioV#NC& zPunPqfHux zMpWVV6(s?@o4?~k1v%efG&eyA>-|vNQ;e>MvL#nP9L8*BVQgQLEu^ldVfAg!@h)VS z)9_zn%%(zo>An+d)3vdQ=gMBw_M&S-jaU`^0t-7WNhf$JJ7c$tl{SvdS!LE=Oo3RM zWI(mC)6r(JS>k(fijaM0PKyHf2;HE1u{5w3dxlPkT_1;uqs&KtUO!pVQUm%|-<29G zPVlTRfHvju?m*!JY(M#vyfn{O=f|Op`Kzb0!bOk$HCS>~8MZ_2it7#BYrAK|4h-RQ zvA7Pe&KU_QSsk<#)Js$h&IxD701P{uEy)@AU!i=S9eZ+?i0u5!D9!eyh5FkO#Qc8K zDgN}X><})mOXU2M9mUMuhhv_v5%@rbu2lXKbNId6RA@kTGariPel0M$q)JoWtA!&@ z!fuf^m90z17_}>CcI`$do)jT=BfAUTnU9)z3n|R`$-2HCm-cy4Up_k-dYnMKmMcj! zTXoFMr95Nwr>E6DvE$7H;TvX1;bBUc{nP}hv-QX+Xc!_FJ{7IMO~|E6p3a)FH|mBh zefi1mc+USWt79&iY(07`Xu}T9KWmL-4hFvuzCLNg@)cLvb<~3@QxvI7Oc7>t{^zS@ z7pi!AR>bxCD7oSOP$=$xC@Oa;im-u}yf+LJ32T$Y;F+hy5QW~PSMmr}2S*~-#)>;7 zUlC@r8DZS>aDDz38^$}pDK!wce&6Jb-YC4w)5W)jn^0Q7GqL3ha3HuG4YIwEsoI}f zj_cFrbR8Po=1+fmS<=1d>NH8tm4=(?P*nFGbmv_dl#PZ6v%Daly?R4$(H3DL7cL%l z--Cb7hrPp}9}@>fllasjLz?G#{=$8{e0$EJxY|%*_axDiI|w6~wQcL>NwpR`aWGMv z;?(-m)cKR}{CjtHS+IY`As%bLnvmMlUi6Wf6Um%m{-DTAJVQVJj_*sA>9#bk)R#P; z+tT%y?)3VyF2z@tOBOfzQS@sKGTa{_6yAE%r7Q#LqHI~zbl?>%nhP=bt~5JIUU2t6 z34xWhC~#p;wr(~$gVpI~>r=E(sfSp)8$G@@!MQi@P@TEQv$G0=-gn|a;6i9+S&a{~v^^b1A!!Cq9OFn>Dp=p>#b?`IZ;H+d9yp;dlS7&ya^CQnnNjY-My zedkMc9i8xt2!|LO4=G;*QW~)wDND0(hPe!8L;nM`k78Md2GM{cSR8*-WOC2q=BXHX z^hppGrnpi>;uWZEOc&Em+K?vaihmzaMraoAf?hk(&3+jos7;BUrWum&{zh?nRVy;g z9zgHRMWmj!p_Ow!qFp}$*43^QZk&OU`x6jU#%vzXO7SA;0K2+nndi=qnq9nmEBuHa zADFj0XcvQJ(lL4AD@BmWWmIZeYD(1SnT&+v~Ii&sX|;AufKej?1h++0xUQvi(x*xLR{PpX=f`;yqhhuy6r@gsN~)4Kgr{e^W2+vz}Lyq z$e&&9)1k>7$=AD)>@Y!0eLslr(>rirxLeW6DLkW$b0*DyWn%4e9n#uiOHH%8gELsf5>3H8}O_4l2L5h}}Llh}Aue zuAlzmZ^>>kce*2W|J{jiQvby6xgM0+>lczTwuv4mOsVUxqMY} z?qg3@6mSnr<+4bJU5|J0%SI~8aL6lax=hn=QlXtHW%}cjKTO2Ddw1(przRwn^N;}dwc@EE!&LGZpScwND|&u?&2KA zP0UQr#kL2Vpmt>j_8;afpF4BvEeA2L(v>WeHzQ;1I&8bl9w_PU$k5^5K}8>OIXVvO zKDyDnpuTjmbQ{!H4j@T|3E6$~z&E}#cA8hCSnzqFF&DlsAHxUg!hCWTh6nt`!v6|I zhEkv?HhqdA+;w^Bc0?ST`2*D@?qqy0T6~$8gPBJJ4eHdSZlB+YTTh)am3x<^KNYd< zkPd>~G^uqRyEVoeq1;WCf*LwdU40!b{WR#;+8_AbzY1S=^RAUW1Xc_3@YYct*VOw_ z_h0?UCfuBJG?wIU;zjz;KZ;HJJ*Z`{FZtAT_;@>diH+?2dpTEAQWJYaq&o6mUp`3G z%{nM~$}H%f|AvXJKOSMj9W$Kv-X_wT%aQrHO#JZ~>|NQTf^!yxwQ048chVFLh$N*4 zf6-)nfV;uJ(apXIZ8eh$f3B3K@}-s!e6*X&i|L(J!0QwK8~DxLZL zZN6Q6Y~Ig)_X}8;HL{>aCI+*EE}+3~y|@_49+4SoXbO&l%pe71JS;+Jc_bd6RcBXD z6_Sd>al2C^dFD!HGUyk*C^IClU6pAC>v{pxbg0=qvS|wd}>%EN;ovqT4=iVfoY!MW0P1EnjW( zU%8A!Xpp_|YP&8rcKBgv-vr6-2ludGaJuBlyAwDRlY=d}ON8T%OPEnr$k*)V!XC#@ zLCd8QznD{dc$X~ZZ)$}0Q+{S{yb+GqZlZIt9=Y`YD7H;Gk7(|%{1n)llN-0(&GRcc7y&JbRfM4@A69m=%%9!`(%3l zKQFlB&rr@|E5Oa82kAfqZ>t7!Pr3t3-YC)ElFu0J>_kq_n)vSd2DP7UX@*dzLAKJI zPq85rqZ>GLp$=I+IOEClyArdv=*UxM?pP)K6#ijaYFAP^-zolQ1MWK&TTRu-z#*RQjU^V}`6NK`g!;+9pMY2@>jrA!8J_hNH7#aK)!7GOs zR`2dYdz|Dcs%#s+jd&zEvb;ueY1$clYEcx+a;6BmHtU z@3G9A;`!nxn96sj5)B_DTgy^$oe}p4Y-wJ=M#Puuai`Lb!s0g}pZ&@IrZ784G7!7? zZm6(Hi+Zd0Qc9;6IWrIG#UE#Oy19|-e`YjkoFARlaHAi=-ZXc`g-ffM~Vw-UNDqA+o__>ab-L5ByH#lM zVZQOhD7ZJYa(7di)Yrzr^S}q3$>~ZDLxQp7vJHh~IMB>vY1k=cPS4;&W=+|6{>y>h zkK|q7)w|f%e$Gn) z?hby4aY4!ylDHE4e9qzVI%#Tku)9j z(xzo29BEOQG7UauP9NBvpwE5Q?~OgF_b)SY$k>9`$Jf2hPFhoX{VZ$^86fN;ENEZA zaoB`qOZIb@;Y$AlI5nvNUGpwLaep$FJv@!S`WYynxfo&HFEe+rCv~1IhREtfN}e2L zS!Ut~Gu;OVX)XvoYxZAw{J6IG=@|2 zgCC;*%ES1!h{*K27A=@xE1{n%*vnkg{QnXPG~_Li#oyO&^jff}Th0{{zkklFA$&vlcLi->(w%Ak4fof(F8{%xB1DQ_iRxt;7ub3IHyfX%={UaABxkxEJ%N!1vO^xgZj*#^sB22jT)~= zIovBh`nDe(H|#>Hc04ohYD~&jyp!P0`Lc;#gpp3r@m-F2C7YlS>wvmZ2e?ZbjmZzk z!urlkIJ^$RD(rH89fjCm{{gjYmmt#J0hJRx zC}~|Z2ItB{bCf%6x^x!bw@>Gvrw`o@iig()PnxLaL9>|$F6}aqWGDDj&3^Vq-Sr?- z8)rJmuAvjm5tBSxHQ!BM~)8jSf^*2zgu1 zq%QACqxc@9n12syV|tRsmTy?^@(MDCjOZS-OmbtGQTF|x@X$0M@8jm8YM>oNoes6_ z8Iiw^d3%R5t?9eAy_mW{!VIot$t!jk)jX3Bv$;jRr!k%8JLVImVPeQ)5ALF<2%5o8 zDTh|!ot!7hK32odIz`A{n;}Na-ooC5rNZ&I0(Ov_ggKSV(C<-JH2Us8bXnPjBrDa(YHlOi%*!QO6@{3^URU9k zAZ|C`!H)g~IQA{1aQ>!?SiHOxeP3=t(gQVIsJX)a(<|?A^Hq z{lYs$1#>6&9qfg7CjH3Vw*$x2cJoZZmmX-hqhDA8M%K8HW;YdzjtE0wUA%Y`bV58x ztc01?ak1W|M3TMw1MbE*i&FbL!s2ob=8YU8@@zUpyNQiRx|=Uvu5?CzimeFt-Y6L# zrHh3|Y2vzBCJa5MNmh)G$7ni*NVjk?*gp~3`I%^qd|7B%nGPelYHZ?JLf*y}abM;w zba;33bbMDxJ(H%q=UTM(UKf;&zJz1Bmek|^TizYsLBDaP)MH;APAQ#1`58TmKUame zlm?ugHy2^a64HFAM6;ZyVc|5;&&m&YwQB`RmkgyXorBpiz}|xHGF0xUj-X9Tp~8Op_8l=Sr2){-MmGG%Oby^W`h(h zd-D%Xw_b8~N0}CIrojDpHTIUgNAICBWa{x* zXpN;O`HU6f1U=17kPED5W`+?uYL9zAUekq+sw>m=`F)DU%~PcI-5O*);+NzO^S#4+ zDNr`=%kN%%B`F;+LsU&lM1f;?!PqxXM9lETxYM&qa=BlWIGb=w1lxSVeQ8UYUN=&F z+Vm51=JcZdw^ea2_b)Og+41bggO0OnLfN0YKKafxG-V|e_i;z^H+!R!ce5X$7db1c zQittx?_oh+)TPOgdadg&Sv!|=Q;J=fyG`QAV`rMX*pZy|Ey;G|Ye{Q`pa|wzw#>Pd z|EbQNhQ;G1!La!WCBt1%f& zuoOX~uAqqLB?W$-6tYW&Ms3)S*bM_nW``c_{(KG!j&6J*QXt*VwP-z5ipN!YRCCb_ zzbD>=?{N8CSzjEiH%8DBR_ zj^O#+WS(_sv_UUTpRQOMAt3q_7S86!?>Eu&J@?W7dya0Ym+@zY z8@)+QL;d(-&Olg^!}c0+dc{@@f5g1yAX!wE?#IO3FPxWOA|g8@(K|qi)^a~}o-K1w zZB3}1xdgN0OesdzfxcF9R{6i)w1m0PHwrB1=ASc|w9S+9ySvb?>a%G6K~(Y2gubjO zW_Bodzh9={PSsjmfApArJm;~$_a6Mu8>_M7C{*4Kz^AhhaVf`$^y@F<_}~v{JZeRQ z6q4X-AWd$8Y9#wQ8)>$6>~>P7)yLH_clt%VAJKuKK`(^f=wxjCr$kH2`yo~LAO`4M z77I2ih<&Rz;X!njFo?|)yQ2(O8EB`4>CX2si@n{Z7lIJeV<+req?m=- z!VZ#c=y*|!iiHYv_Ff{~mv*5Uc`cZkq)#TDGSvUZC85R4!v;rHn%QwqOgA>5kvn-# zlBFUPm`D6~#6E24IE1!mwzT)kd3GCbLZ3gjv^Deu=UCH_c;yMUweCjbd^L7X|H5OZ zP|S0*!1cuYQ22HRtDBs$U{N+E?axIS_A(Qt93jqCFqjkzd;54iHM)r_Zz6DM&@^U@ zSklJG4cu!2dR8(+)OZ_zCdP7y-h}$@K7_O!2kOUN@czrak;GYt@B|NU9L%&Pr ziNEdFuxGR@DcwvGsT231;<_8zvFAoUd=UG#B#@n^Piwl(!)^XtxH3PdYfsJveKN*= z&J_MwCrcmYnXkmpk>`UixYPC$o?sS!To*c^eGQM6#)+)~R@9Q>O*xz~DBg$-cqp&RzVnviKHGcP^)(I&ntVe9rvV&=mp}bc;z*|*k2U=t{#D1RTA?b ztHtl6Y&_1-fv5j1$;L$|vA6OR4Ed~iG$WV)pJh1WAB=PFn#J>8c{ns?9=6{17w7jq zMM}H}ja%i$`DZIc_j9F<9b*x4emVxObfQZ$y5Oa>1M+$<7ROfT&{_ovu6aa=*0IVY z3F(RY=au5aTodXyBnVG0g615RBbhrVP;)7eVA_uArO6ncI)Fy~QK9wh6x=_4tT^X6 zfNtBrXU=S-Sj&vZpBHNWH^UN3<`1I)mu3{GO_lWbl0j=9&VAhwK8^~OaCsC+iCwpd zfHoDVb2T7%dL;@f)WrywGsxgB@KD3u!pkEWuX~llpkYJdc}W(e-@b?JP8FJBXaYm; zKZuG^W6oo(kcoPYN8j{G!Pg3Bx7@=T4|BR7TZ++J-=Y2)XY8C_;`Qw^l&#XH%o7VsrXAeoE)<2Ghf+oD-l=HY+dJR<$X;<;QYpN4e}&+8dh?TWVp+#uc+b(N zc_)<7to;W^zd4edo*NC1jX|VPrNT4LbcTILB^pMQa?YMi7cYSMGj>r_sL~bw{+!(A zO#$-kfyx<1PCg!VXpBC&obhEpku!a1W-hS>_vxpOkW@`_pnL48GCQ?QByhenqSk~u z@(S}S9qRDp$2n&4wc$-(HD*>`z@!P?s7LD)>SCM8_C$d)s;F1BunhJ7omKB5w(g%d@jx4XMYp> zDg`Z^rc4&6IBRiy5dA6BrGee!m}$VwqIwx}h**kMNAr;-XFz>Vy5hB4IZTHelm47} zIQab({43R|StS-<v}`JWM3CGIPX@ZNb~QK{i9C`blk61QrFJx@$Mexjz zLbl>0N;i#%SJGz@>ZHv3b5km07Vhd-4(QdVFZIe|-dD68mJQb8xfdehJN76iCkL4ThejT+SeuN!GX z?ST#*3!jO5e^u#{mkT{n+X}UPa+GXBbhmjk5?^*BPkBrF^z#AS9~rP;$(*DG5 z8LpJF+Z4YXzu+j(t8YEi!S;gd@LJ+cD?bm%t`%#M#P8Ozifznqj6=dmzAu(E<3!*Z z-dDZD=`PH@mogyZCDOEa=^9Zy+ls!d=6C2mHxbpMMQhgofmh5R$zINIOPBDo@$@+2 z_b89pnwaFnc>>I+Hw!-gcZ>ac{Ls|a>&KcTc zZ7n~md4-fI8SmuQz%BCjp6S}cY0;MSSV!X z!q|%0XM2=rQsrPQ?ev9mUtRjF-5)oLJy6nLj@slkQT^T%j@btEV|y2hwk(6AgAsi^ z`4g>4RqXS&q6=CI^w#w%MwGCNp_@I={oFYhTO~$rb)#v22T-xG9M(@Yqt7lbbX=AF zK0f+FezOHF-sedvQJLbnH~Xwd`cj+wG4D?UEXZnKoS0}_iKW5+#L+3$k~^(;AvpIw z^7&q&?{|-Ra681Pa?>~|*T6nvIvvSPZ(6c7X zhClEvH0F1ND~08JhHaxBy?A0sQ|oH6sb{wMb^ic1zevG`gH4s`DKI>1Aj%%> z!@tNB>}cDB+orcg?htmYeGI^w#}7pWeL_Xo<WGb zV>e6rSd7Z?p>zFa^6tn1t9xt}Q_dSu*$-E=W*!hlh26+4!UUcB|A<*9jH&uVAbO1; zI>>kA-Fr?#cXlA@TK_?`-x;(u4k6uFYLuCC5JQEJFnG34^1eSapZdg$*5W`>8D7Wx z|8Sw4{<|=c=VhTLL6ZKS&0<~L9H09Kb_xGv9kdUfSl~S>rXcg996bN65yQ5WV5nY& zB*NkZ9-O~~(D9?h^z~P8P*R50!>{!cWLYaxZ9$KHBEiqVl&PUH@*3-<+E~ z^je7&V>*OR?}yOj3~8r)F_N}FMb;<{s+h#HWp>sm*&EV>ins7S&hDEz>rm6;PM=QDJCW0cN0u~l_fq)_=2DTsyVxou$ z7Ny_3pNBEN`yJyQ@4xGp7s_#-$69ln`@T`t)AiV8N%Tjrp7UGFp>(V#bsVbz>E?$* zr91!srf{}(haH^NRq3UUH}dLFh;^oVv~Zm&HOkjw?hHqA;9clFsj?JqVnqcnf1_s5 zS7vwibS1xvGwnS8nV>)xCGVgx{587ro#Tpfb>^(Nv+0#E z?^MIxRkoz(xl8n#-G;1}=JfQFA)Y3+;PNL&YJKZP);!}FrmRi(xU)y;$TUpcrOsY3 zFM8T#J8nL=pj`%>JDxB;@KaweT2L;-JLDt8s1zSsl*_%g$InXU_Os*89T##M%9*di z0g_KLPBcEzom398C-End>F+MIe4bt4gVi5!_VPjAf#rU6K4wj z9g~_0vG}Ym-L>hB%7+K>ZLKPW9`M8C<5#gJrW5txoSj+o6|qafma_gH#Tli4V!x6N z#f;jBk#0+c(pE2O;#~RmQT8YbX~F4Y6VeKEM#-Mnc#>*G%^G!Ln4dhwGTU7ziquqibyHYMjjrWCoE`~S80{mMJ>2e)$X&ooQ2;I8Vxt$VQSgdnNsmXx}* z0D7;z=yR|pyU zD6no4!g~xydVF76^1L_lz5RGc(}QXc8_>cYy~L)F=fa9V!*{#vkfd5_BfX;@rC+)% zIelrOkl(omi*EKn*n25VNzK5O66be!#E?6tw9B9QcaL7! z1|2H(XvNBRc6im-mWJ$5p^&?#I6CqRd&zRy3FeGrnsr!6yYcT%cPu~Igy2WFU}|55 z)@nWECfU)r$PWnr8pQiXUS!etCvx}e!X>m5eJY%UmPQ?Fnab}nlhwG%`|;BIL0S$Q zFiS(9R*!KYd7E19L+eCWpLM0X)$%m(0C!z~=t5&N$}oGj1G&`O(%gqm*ze(jvFRId zSFJ0%Hs@pUgH%lF+>>1gy|G}=SXgy=jj02IX=|kme4jqUZtl6gesews>$D-NxF;=r zU;%G_j-nE!X^uuSy14T@jHF3(Mk{x6twG<@U$LT6i5z2fsH%j|wK|z%UI%t_t8$<1 ztEGa65)^AJOS_)@l zLp7o!XGDzC2AYxFb_LG#l|nbck$yFEN1e@oJR}x8W2?uE39q@6D_eNEbfFL2 zS>X2etg{-Ol3D;hQ2)^S;C@^BM2oc|xMxfR_B{lJrO=()a#{ z(VBY;?1E%CpZ^xEDc!`f!Y1_b)1lp34&0~Ih(U_Xlqd4tiM>rK6U@kBu@~KFtHw%$ z+ak;-6*ny|AWA_;6#vbJdEH@`d8sd(bOO&4?-9!iFrzKbDi17O$~MDJCGLw^5o&aZ71nnt>` zG20)V=N=JfA2WBf*2E#jH=<98F(vIAge%JRfy*AMa5r@}bXo&Nu_v>;yz_Xe&wf`= zO=?x&2KV%dLUAtW<(SuaT#zh2$cNAb+ut~HW{%kK!;So(Ft<-t5CQpr#hy!Uk{GLs zK;0_lIhAX~Z|Pk^|EDh0j-M`w=Ki+gN!H?%;wj`Ed5PNaqJqL9yuVp~5qDabi$`BC z;N{6m9Gr?kWMeslN4LO73d49=rtc!~DGh#ctc7mxpXXh?`T@7`O!SwCcEn$55 zn&|tB=d-ULiI4GJpcSo7iKcpJiZ~*^@}ITULyyLdtH-f_Ze$#8O#k>?;j_)2I!LKd z|8=!^%h@HfD^t3o$FdDB>T-eH-q z&bv1K$ZNI=o#39pI}f&En%75(LC|{fL2)!vQ$i&%D!awJx9rgAZksPQKM*aq_0K8ZP{MvLrv15{b-v ze~OyliAF3#!DbWAi7a%WO_O(sqxm)1A9n=3mq^jzidtlE%S84EEmG31KzO$#^gB?8 zLDt{Ui#wRNnDb}kz7kn?U+6_x9w$5jz5(=m8Y>rgoa3g9nPlC;!!=m-AEy>=CL+}-K(Ouhz#-yIY zFUu0)&(0oscEmo(HNaQ?d%4vZQG==qp8flTwO%$<^yiHj@25(Ye{Uny{RXAV-iXwXvOk9 zYmQy$Nr7$ISai#SLM(IeEG!zc_dmtlD@Xahmdg9tg)nv8$L{6+Xq$N*S)9?7c0P{| z1Kwj%5OddrgBX}mhqMN3dU)>``ybn&$k~MrFBEu=egemx)TyT*_n?#}L31SU3`rM> zmJd02=XjNUd0!{I*OaxkW%fwn1$9sLssDjOMMEruXC~Iqe9wOK=VD+N zYs$Xy74ZpnII_i#=4jWUHT$IKKHiW{|D1~Te2*y%a;F~(8xd5)83cYO*PLfo#D47& zmfWl9`4~%knvnVmp8v2XA?z1-vmUggRVQw-FM_j9E%u~eW{VEi)~G03g;jc?FmC{& z9}PgjPh+fQYs<%tTk-E?DW**Dp-y%dD4Kc$O-VuYE@TiphCZ=ZQ9?0tzlH3KSoGZ_ zMfrzXp;E92!?^3?Tq5rm%-oC5XvMW}-!Wm9F>N%L<&4vA;i{y^4(S$DyhxXH>d}SH z*lKY8Jy=}7=t6o;JF$5AF8qt*+@C=T4riXh2>vX*9)1KDp2k6Y$|LR;+JewcDhR15 zLgx$nxP!@yJLo^6^wDx`inW2wmb2`&&qI%MJ5Z&;-JhfH!A2zsy=rgc=)F?*<8YQ} z-EdsBv88bG`!eFc`lMkIOpKA@@ya3~;3CJ-i#gtsB0jIg(OuKhm{h zmhA6Kl?NRuq0tS_N!eJnh-V)odWf%KSJ2y7hEAMI7Sb)}a6O|6vxi1Y@^-lhZ@ny> zneIYe%UjP$;yLekyaVUOpD7hS zRhJ^Y{EtX_5iB(Ne8dob9^aMQQpWg3crN7r#!M5+nEe)=N1M~VeU3C?Rs}BKJ0SkV zr~Xe~@8!lDVs%g+GW0Lt)y1Wf{f~AdHaHu}Z&u;um1{y`aR-MIW_Z+osj44Q# z>IgsfO$-_lDMqqaWXo^~wRO6SuQzmsN&H?+yn7t+l@X%LqGOmf`2lim&Jn zKA>%v2KDsi-J}|JMsHUpzvI$)bDq1u`F$HQS{sK?m9pcnGdV^Tz<{5FAVa=?_dkPE zMfZ_uU{1FqUZQA(6v>sW#q#n%+S9KEieutY9omlyW~))4y1ENGB_*uP^>=A zyluNI%nsVY(M5>{sk`8J&L#2wj2=zXP@~ohHSF=UC!qsuz7r%WQ;#|UTg4v{^wER z(6T1f->pK;+bl%A*(4ckHA)1j$Km$nj|FKD2Z&*_&*AQY5{Y^5N>Q5D4~qxaW7#$% zx|Pxom)FbCr0v#Z-Rz7YcK$4Xs7AgM`F^1qgQR!H6n5B=&ctoNg*rP5i{`#$-g_{9 zV@Tdrylb*{NZ`BEBo6M?rB?C?jQnORncw6{d(SyZ?v7b4IcLOoB#8+fSmH!G#&@Ug zoKIF7;zxamsQiN+J?DGj<)#nVZ}U@TYPmMw?V(U*Jom zrGG=kr$j)Z67q)fG*3DTrV&3dVa6xqDjdYM2LD{QBlA=Ft zE9X0UZmjs-S(6Gg`5ABUVy>)4=AXQ1=Boi>wxb;F-t!rehU|#?DIuA66-chGgmt+S z9jK|pOzlhDHNkt1k-EY&n6vN&Qq(lD@v>vnYAmzUA)S%dLh?EtUgA4GI_gk(qdq0I z*wFwFWr{e$UPvo40=pZ#SX7zq4S`>oa~VQ>9tz zsW8xIU)>i=ij2xYV;|mQSj&Dg*U_TuzTw<9zg>V&Cqv0L0O&b2n!8@ysc63gGJ8j%;OAUio#ThHj=anCYzOu(QN{0RE;KTC zC6-w(E8TTD7A%{d5F7tPH5F-Z=mA)EF$v)F!q5jv}F(9_KOr(DU6-5&TLD zJ4en$`o^Oov(G6}^vf9M*QvvH$zh@VuR$Eh_n_&@-=JMlEzWXJtV@7#leOE<84;1d)!hmh8sVTcV&#+GIEh)%zU`Ag69 z?g97PzbM4KfbB@S`vgVPe8+pH^MEm9^vPr;FD>G z=E!2qU%nc@f}6xr_6Nv)zlLGs?Rbv1i@Oqvu|1~`(xzNT?wfGZFG*IiD2A`f`=c6BI>0f zof+JK=F;m3+G{`<`w-K=u!qg5E14eWgv`)Oyz`wZY~>BfZ_zyQ`29FhwZ?||L%Pt~ z)*`Ib4e8~=6Ox~o1evtVkW5$fpzL$pWvy=}BD!=XiVY!u#o?S|^P$rPjbhA%%e?w> zKx~)D!t=sY>@HK|x!Wn>W_Jh1)eg7}E1*SNxpEzOj*A zX{zXl!~10E(=lV}k=hvv+!fpFO=sF2Qw`nYnYcJfk8~4Wz{>YN-kq|b@Ko-GdLU2d zd#*+(8j)W_U7?<%%A(?f5-6bxG|V|KMO$51;f#WohEZXw~4m{=OCVY@=j+? zKy6DfreD(Gd{<}k@~cCn{T!B;>(IfY)i7HVSkODmf+Ekg;qtJ4g*V&z*J9>hF3i8= zn16L?=U=m!e;s1}HLzR;ulrd^zB2#PWd4=T{7ZxRmxTFO1M{!OcK$V$`Imk>|N8h} z{7dR>BRVkua%2A0iTT%V=3gtAe_j7C{*}x8%ZvF}K*vpiW;~~w#`ne~=3lp%f8AjI z^)lL4lF9t*67#Ra%)jE=`ByjQUuuyDC8wExO=te~uj+sKS2^=9<97bli}_c-cK-F6 z`PU8RUpJY5DexJg3-hlb%)eeR|59iE72eLjO1o_5jnY{Obtw zuWIIB`ZlJ!1YYVE$Ff{7a=nofyIVYvX_MuQklSKD6_%Z028{%)jO_|C-AD zi(72)ocWgz^DjTl<^6ly?5Lk@;5}^Dn!0{&n~N!oLLbuQ~1fE1mh5YCHcL z&HU>e^RFuAU-y`QxibH{$^0vY`PY_q{^i`xzZ{u=y=MN^wVi)0XZ}^QtqE6{f9bUI zuPWwW_RPNu+xb_2=3iONzjibK+QIw_%)fM)e~o1R^^5t}EaqRKl5>(c=3hIRe`PWM zGH3oZnEBVacK+qdXNQID{A&aAudDyXziPYp5^LJ|*JkEld-ywZC-W~g=3lYQzgl*z z!8GPyJ(+)vV*XXk{OdLIuPw~KwlM!%%lzv!^RE%izqT>|s$u@cKjHuIugG@(HH!Jy z_;&u)Har*J%)io@e@$lo)x!L1*~upKVE$$JU;Jw_^RGVb{L6s(S4KPkddK{$shxi% zF#jrM{`G4g$dvilly?55*v`MsxAU)2?fh#r^RK1Mzq&I2iedhBj``QLcK%h${40j} z*Ldb%G0eXOI=U=;+e|ah|z67#RI?fh#6^DkF^jv|?VO=JFL#{6qJ^Dk}Y zUx%1~^il}C=)wFehWS?o^RGnaU$>Zlr856o!~E+n z^RJcd{ObqvuVc)=8kv9jGXMH9Lm!&VzZSoa#Xja=(uw!cm-*M9_#lj8{&kZ1*JtKm zLCn9hnSZTs=U?lYe?4UWRnX4A(wTpSw)3wb=3gQ0{A=w0g@5g3{w4cg{7dRz1U#94 zJ!AgGb?@R0^RGqBzm78h+Q|It=T`0vo3vD7$^7eHJO4Vt{EPmJe{KCQ{-wkG>wP=_ zTF3lLkNKA}^RGqBzt%DTI?ep+UxEVFwezpv%)dlC|C-GFYd-U@_wD@aJM*t~%)bnn ze|a+hN@M;tj`>$zJO2t}{x!9oe>I1nlk8>wb)5OvtakpD&ipH;oqr`V|B`3^wUznT zCgxv0%)h2G{|e>1bQ$xnROVk=%)dgJe_1mBTH4OPmN5Ss*v`K$F#j6L{Hu!jmjUxH z)pq_>*jA18%)k0E|H@_lHH-Pz73N<9nSX^d|B_|?<;(o*F!Qh7%)exqf2A`2`pEpt ziTT%W=3lnVzxFf#N?`ug!2Ii1JO8TR_+3nH=U=~=e`)*||60lOpCIO6In2MFF#p=Z z{A&>Nui5(s!AG?|a4GYzyUf25nST{9|8ijd70>)Dr=5R|YUf|inSY&b=U?H>zp~o- zR}k|rPv&19%)eG%bdxN!tO(R%{-wwKOG##zuw?!!@+mhq0xqiisiV` zqDa?MG$`~xdz0tbeVKSmIA1v{E_c`*X!3}&x6NBbpZjw}Yug^NzsQA7_bEYCBj*B^ zTGOJDh|&2BJJ_C}S~YgoFveR}@){2gNVy1(GYfJaXP~XURHu+OkTP zY@I^sE$`NzsB|Jni4~3BF-+n=$%#htS?Wmi3?T*u(lbX(YT6o^Z)^D#SH>Mi`0ExV z9Z_lc;r$bF{O@qza2Yn@3ZwTrxv!lRt-}+0!>Ldh%H?OnDR6^DCL z&>ACJKVTOIH3X2I3};g<=0oo1Wz1wJmv;?iWvIDAS=p zL++SXfnCHOn5-y8N~ddhyMgoaj^~l2a0a#0Y$=`Z$iz9gcd~rvuYV<`^LKU{_nC%2 zEf+`T#p9FiYlIH76(yH8V(MQxnxDvh`OZf4n4O!;CYsa0r{+}Y=1z;on$o>RHWX)Q zNJn`8+2HPecD{JgV=XgU#xDC?kG$#M9d}xOdmolb2arxW`zj=>as6#6#y1|r7rRaH zI&l`h#hK`TY&4Xl+4taTM*Fs11}r(RxZjq(q@BgxRB3X}=|Ceop69v82mD*BLUCzL zqOAWVsLkOV!Zv@Lbjrrl^{x2y`;CxYmI>WEcZL15GD)8LT3C#162&U}M9$OIu>4sp zZhO^8rZ36F)}s#C-`0n_KqO?bkllUWLt$GQLTNJn+1=obzrEb4nfITKR~1TT)T<-t zf*}QXp9s8^Z3Bm1%2d*~M6%*^2IuY9U<;T(kNYjUE#@w)Re!|N3Gan+@Lu@e9EYPx z^&)bxDjKXhQwVosHTrtNsLq*IFO#PmZ=9je*@CGN+>Lt94E`IxVDq7KFrDXt*wk;h zob?zBzxHHy^8;E&8IVf*j2(Hegx1Zj^u+%+PG{I-EVJIO#y_z5K%w|N-hh70jX>lF z4Z7~*NpJL5!&*t6=PE>V*G0o;JhMZ28|r%TK3-hm*~$znx_{;ps)0ncc{21)Y(bObBmCUvMXSm!IOF)4 z_u;scT~!z7a06A1ezc@443B>mi}Za+~zBocSJR-)GsdCFps zoLj3j#f(@cGIrQfH(O2GXYC`#{m>)pMcjF$JXBKk%!!W2@4&&By@=UiO^WrIaLB!c z8DaKx^xi=}x2@w2$tRFIycy;1<&hrs8Jp&>#lp{Bk;lI2eM9%c?XELMOSmiQTmf!3 zq~PhyhcM1Bh4h~cc8z2s%HtsvQ&%IoZ2;b}d)0Kwa;yxG$F%3xTj&c-ho@AkPK65UeOG5ATR?Z%IQ)Jr$G5W}599Qrl!}ZE! z=hvJ4i`|gNIo)|BD!BdL8=DLa>E5Ui9OQl7Z>xBA`dW^LOu2=#iJeHzi5*TZ73k5? zj{A)jsIgZrXF&Ih+wUFeoVPFcjwoQ9rzM>X+uO+t|v)G$V=1KszxMk2oV*RK0q$5nRh`|3J*9-QBkcvdxz}k<(1zUl5S3O zPI%I9{~B07F`|?F_t~mni&OgNMf1TVG-dG4kK+q*c>j5X+Z{vO9aW*RbQ}6^KZ=dx zqtQI>j`%jG6!VUSAZ~1d$TN^=RWB$ z?rjdF`i>KzHp&ix&(?@;FHC4cyce#itrq=8tC3|NOK5&<6sMy*Q49AQNJ$Z$U(I_R z?pf@6>`Ra8excK{1L!*{gqk^z=O3O7o$eB`ryuv_a_8LZcWEN;V^86HlJC~XVnnaP zrUF@c_Ddi_^2zy!SU$@=P(gjG_~UJe<{RVlTf277U#=^Srib%bJXVA+KT9QnDH&Kg zg!iA{2aBFzyyFzY{Ss>n3j)ec<3Uax=JWn@MTRj3->c!idoyybvOw@JISSaQLX+fV z@a$PBD!n_ivy}Irmp;WbosLvva0I`%JU|flC8iIk#1vKD(fSvOj?4yMRA^AgQ){{Z zkVumq3ZZY7mJ`f=a@T6EE5wHrVnD1OMGRD>dH!Dz z$o=n2OEsy{OqTTLS&~l7FI*3=!@fPdo5G*Dm)(BBY9x0y_#DGL?cW&isU!KEEXVb9 zc{0AKM2cVZaO#bqO5=QyCRZlhn?lIo-dRzruZ1aTf!sxIitazSM|D9yyjzAKghL)$ zLG1rp)C(J0_32A=Hky^4G5^nUEW6MBoc4|E4D-S2CHIkWRe|#o%dux)4gUN+FLJvK zmQ-0Y=X+d>GBsm~_PW0~+UW(tuIUu2pH`)7liToa&kii`4HKgh0>y=aIY`&3lDGwA zhyiVL;oz$+QlCc&b?*jI_n&h^PhD!;mn^gtI8T39m4dUiV5O-^v*V5F{e;ex@_ZgX zT{fk4^SjXBJ+p8wj2*{{ooQrGSNMb)($_dM+UpWP*8PHL%wGrY=kz0sG6{{U)~82O zKJ;prFPY^UNnAA86ExbFOl=2B_NcMXQOb|Pw~P^i&YbDHMYPMh6n--{BQZmko~}KQ z6Fc|8FIACte!hi4^Ebk@>?5ApzCab{awY{Pqv|8OP@`37`-vrpsIOyhX)DaP?8n9` z?n^Cmp|7&H;8bKp2g*I@mvJd~PP$O&JS$SV`Gj{Ml(Fn`WeyA^1KXG3kX9KCs*D%Q_DfYAQ7LZZlSn%_~Fw)UUon)(Ofu{#6nWQxU9 z?v0fF%(=*H?)^L-hrcmu0fPgEqL|<3_U=z56&F?5ft7_H52Xw5#TwA%r*7iHLnHL> zYe+hCyRq8gj4;Z!B9j@jBr|rqWBW38UY3W$FXyhvymJcsm13cGx<+JO+zvZqNA#)N zB|2z)5MF&f*qibaXT~2BCFz_+?ehk;YYn)&)`zw){QCTFn_%KVgY#K7;O3dd0o&zWAM~Nj|)tlWDHQ zypZ!!$@^dtp-D4?yvglI92ST0e2v*#WTq4i(XphoAw4*&YW3?G_z4y{QVVHT#pOvNXBsca38x* zZjpT1Hk|)0+YtG5lH~7qQ&dH)MN}H!E1ECi?E5SzvODSWm6JF{iIA`34wAVKFs!R3 z@BiqM`)tl9opYqb*SvRSX)7FOT9IRr6%9&{6DLMm(m&Z<=;w0+{sAWB)af`DG7GzN zmvi5vQec>H3(HT{;am11V4W%~UshuFy%hM4x5Z7r?`VQ*U?@sL6T|XAj z*PMcxWeJ`<@q_!{=iC`oiu+#6z@vAla=xN5HU@k1Vp(YKMg!ub5$op)sc3IX;CVuJ z4-e|u+l$n;&c)Oacd~Htq$>lG*(WE6W1YNHHiLZ`MVPT&hWdD&mE4+H0@rt(r#y8+ z{M&IKTh{URQ^U`IQBk2{ebr4I;d7IN!B)wY)n_q1u`_k+mc;MeO1$;5q>PR#6nRh! zhcnj0KS-C77dWFbZWrcQcA)yg=VFcR1enZLr{+1Wd`^7`)1_LpkUIs>#MNLOcP#%1 zkfohv*N~HDj!(_HoUiRAYOK5C)sesX0_Jfm z1u5&+NMwvUlg#(t^wy;}=i7a0{-WVx_4@00rTjuXaqBM18;c=3-dEK1*(@n4D`!u$ zG){Q3(|6?!$#9;Jyqm-RMe|0nfB`*@RC9JQs~Dg24(N+XK3!+D!S9!nSE*7c{AeF2O7;4A889CmRKI0TFjk^ME-No#4 zGse$_d5~`5J*gS?=umeEJzLqj6f$FBZSs=r>Rr7!RdWxID`pG zm=uKhRRd7qep0OVSE7ULxw!fLx(H}gq(M(jFvjD9z(_r+J2jcTxz?1*zNB*d4Y)JD zD=DdVpmE8eSo6Z0<~>lN54u}W(0{X}Eu%=}JiCuC$HPUpHG_ob#C!N$H&Fat*(O3l zD`E6$q3AVG5!=)I2>&;i1E*i@ge4KHMaQHk;`ka{+%>o&(Fs2UBeThp8{IN+u=O0D z&!P&x{kX(?1Id_gyi^1v9>=u+R%C7X_xf_;j zKfrFFE;Pfa8XGLqG1SF?UKZWQp87(pb26t2hdSIi+l1PFqY<9Y4&w+l^2r>FvQyp2 zI`Rv4CJw>Di@j-}UKjQkjKR1&yvyt#fF70}DEQ0SaK#Uz&MFf3r||pWRu>8$^$=zy z3X=QG)vx}jz|q@R3ha4TE#}}W?Ea8kVBe8*SDg1$`>w+7`ESS?@&lg-v!{p8V|Gu} zX{w7J!Tq54y|OOwNy8f<;SB))-|k}Z;7vkp(M4fj`b4rg%o^WryKq143w-hR!GG@J zf!}K|)LsQKao#j+%vby#H5PiaC*o%2L+%gv!j_sygdeTK)KfAr{^5*1yd(R_v?Hot z1k>FZS;~w1&0VIvJFUzeILZ8uZU`oYubSL}{S{xMw_>$(ApP+06FyppP&l5QfBkOp zF7y%>_)sd&5BA!f#!>sI2*7FOCvRyGnXKG zdGdlgCDCmNPK4_sTVM0j$M=^CDZ>sNLrxSgX*N`bOpmAe*;f47Q=tpQ%Y-$3Um+uzGuh>(| z{RFf;auTm^`IGg!qc9!zQykWD;JqnZn)IG`UA$YMYGp<7e*Z+L;3{14<9l?%9r1mU zES=%LkxWS`B<}9Cq#_+MG1u|Z+K%SMUP6}2Rpgu(wCMO_QHCiP-$|N0$~y~p-5FRR zt4taHaz&CtG`iNk$BlhF?-`;*CpqsvveA$hZ|q3bXSB)rRTnaH)1pIK?Cw^u;x5Ew ztUKmQD+ZhLvzvctAKEzkHm3P=pWNjKXr7mkU)R`6d+Q?K*DoSE zZz|lvd2eUEF6mibg@WWSYPFOpd;W1)|NM=ePt3@(=>(1`D*sOpb@NjVOgVoT?}wX` z?n0ncjWf?~YV^3gR`lU}*_0=B;we>n~lsw`>Bp1(MKQ4{YLzJN-}87yuZj=K0V)LzYp;fOAn z!@lIsCdrVRTaHC{9WnHY4b56rjWxSRAnk(}oieQ8$8PR*!UUbQL zCgvVgr}~|OeLX`k`m;HGGIt@r$Vv?2Zl>Y?94P2cGv2_FCY^Ak--XvN8xPvD>QiA6U(4z^?Kb>~!b*iG~!FsTfj^ed;v5 zvQ8|#Y(lAP4Qc**6AWoHp%+IqsCn~3QNw@dw2*9sAIQQD2Q%7!@+8Vj3UU2!SE_5x z#~DxW2Du>Q3hAGb`!+wROu67vu@ARfPYcc&_= z0Pfifa8rk!ngd^;Vb@0djhcvxa8J7MFCF79_%de(4Y|J?el=4O*TOyYB^R*6+nGYU zdeN;>4v<^m%FaQ5IiCD&a$9a6uGn5$u!x9Hf zo~Xx;3q6pA9&~Mam)UVB78KZUKb;;O9?kcI$t7^SXhhQvH9$SG3?jNS)rBijve$XU zbebtLBDh;W(q6orZ7wcx7wr{VE6%yy6Q|di(4TylfPNpXbGotE z=50W^Up&cYU8?w}%+JjH$>PTNTm(LSFIrW53ZE$#QM|)f{H%KtD8K6l^g`t@z($sP zf)YjPFgf}p_Y-}mJQowHHAr~1Az<=rA+OQ^)m?gYD$9oS7yie0(2;(O_oaLXc~a-^ znIB!d(z-=oVLxn!h|%7R{FD<&cy>e#dUptR4j0g(79RLcdIy5p3pmSZ8%B7_BSYm0 zMrJI6tmskz&pG{Ct~=~X3<)yNnw8G$bN5yZ#xZWt!^xgcwcA68F^~lmVyVJg6Z!p z?x)f^gE!ZE)75wC6#Z-?{2M}q&22xLzW*hveIJOH=Wfgx%W=vrUYG{j>qGq143ZWZ8L9;^h22SaaztPMqE^QZ61t z=z~XCd1|60_wsT0$UR5Y3~frP=z!egJbO#+L?I&`P$kd#Z_c@^`^v&=?{gGay3%pW zr}$XPyYSAAB#yj=dh#RAb#{q>@zj}pCKp$UiH3fvQ+K*N>^nCWd0F#~mIoU$7A4t>XG6P{Klf? z;6Eawz@zU!?Ca0{e;0pAE_r@J%Jwf97ncBk>lc#N&_0qD%LVwWs~)gvgR!W+w;ute z=K^!3LPc|`9`_5dUw)n$%|2kwvvV2R@x+qiCVUa;$zL#?of;WD>*-Oo5uJJ8?)q(4 z%6l7+U(MX9ZtX~q*G$B@Ir>y#ra_~F6eV-aICH6KPEGsg1pW^7BJAc&=h5zk3159E z(#wO+zBMIza37~C(e#z(l*Qi_jxW8ark6eYtod&1S;iik?MUXi@UJxXz5hso3BOmz zls?DFPg@{6hFuu5$}st)B4^E0Fl<&OUOeL-?iGvi^w%%U;C^GBwEZa0u%b6C=mMlIcf&k}o@ zucSy&fMp*L>%g%LWSqw zLwVCpXufc#sjgMb2@d1V4lmN{Tm@T~QtV3ZLb*RKh`QPo)N;Ph1kv{S0olBXCg)Bk9kLMsxvkIC+ikwNsT-ME4;WC0Yy6k zWiE84xjPOcN!_W zq9rMVyV$>?c9awQ1kRwfC{eN`>xl?ppKI>a0g@kw^Tf`VN6>43var7<4{6^2+2(4A zOvlA|<|t@$X&Cm(uYi|%Z{FqBgxTR_6kPTtFL^cU;}95PQ8g}pC`ZA7AIb|KfOl{C(19!21pND$)(b)IrIkL8- z;CuaheDAJ|V_$gAobnE*bq3&xfi4ZruE*Odc|PCs-ka4Jq_uV=lX~ul;IlyNc^y)H z>qF*wvk-!?f6e=r#-y>vi=w`E#FDZ> zu-!2Qs}K4^g*)8Ohk8RLUltRc*P~bbB%En}jK(r%v*%^7h67@Ef_l)y9j$_e7VNnU zrfoAkvEt7*BzAqB14&bt{!Z|NcI549qvqzu^U-l8SuQ(nMwjPk#_hVl%$>_ z*mEIDx&I_#{!i4pO@x%OFIxOdF+e35)BAhj*6TBvv#vHlR}h~hI4qwGrOp0G<|)<}#T zZcjIzTqr*)u6frbjq_DV@O;c29iCyf69pksV9#6=+KO7NPa^8t3)CU`(>A z#Kgu&eEm?2d;>pno|Ytnb<234*OgX0bj-iOJZ@-!4+Sq&r<8hM>{FbIGkZ+vkm3L= zSv?W~R zq#*{AkhhCH$Aw1pr+NkE|NYE-P=}80a>UA9UrK%T7q?R4pcoZI&ue~gx7BXwz7_QS zpbGD>M*rQaW_4$HPS0lvKGxZ|q^8_K| zcwBV+Y>V=Jwq9yO`pY*#jnmK}n6Z3xLcIfZTgPQ!Gy4_)6@ zfK3ZNKoY7&$II1Wl=YhT5Y6e?J1ut9@MkVhgSMB*;d%U7bg}GAk<~TWyC4f~4!Y#J z@eMZYd4%jPru6h|E#jSjU})ZO{8~j6)ARw>10rBGbP)H3E0aI(t+wn8qC`Fi%0?OS z=jk6*SN4VWbXAOauS~0^$YAU1K9Kg7B8ylf%1C<+`GFETEN@Oe@7Z}0;7R2l)v5E1 zr!Z!Z)cxXjkm@5%13t;p!Aragbx?yEIhQ@E?>A_idW(}CWvQ`*&z~>iC2}8AMcBJ4 zv0Fu5;<{4_y^PL_8E*!NeVi%Za>A7&eK;d1GXis!Y>7iP?BC;jOCMj}+iFBab|j)F z4Z`p@m5`n}5+B=UVtfdH?hLvhcd|Y%@a)KRSZ_pC83o$%Y}R+3It9HQCpsuFXGK3Pl5=$8`M`X9QB$R8BNtLu z+>J4{Mr7YXopL*Mr$Nlbbv<_)BdIiBRS-GEXLbSM2-SE{+@Otzgl2m2ybVt6Nz zg0EQ6%LFM&aIzCUzUM-BBBDgg;*TKBLol_Fr(R!Ppn`qR**iK?uIpngc1uC&#zx+= z_>6x4q`0F!9lN)@g`K@Bjq13T`{ZS*56^Jd?mvK{W(U%|8${i%=HjtV0QKD3jk4lO z&}q3Hr3?Orf4f(czIFL9-eC*-G!!do~ilh(wD|VcyXRxD1Ers>A*tdwXyqF z)sW^i0_EHXK75J=85#A*P2(f5F4dvwN}G_RdrD+~v!{6%bCCJ$vKVUaK*l9oamQn& zh>Y^1UIV(4)IU3TE3;F3n=O?am2eN444I{vQYW%TlXL~9uepi^LrWlc+?7J|Zleb? z%a_3(r0mY$7Xi0$ctjVvTUsX$Ic-A!AVu2#aj9rPGCPVisrvI-QK*^-i@pu;bW|!N6ngIhwtOFlHhrnm{abFg_1eFehc^I@z|iz z4d17>2#?7J(BWGI{`@nAWx!_k5h?L*{}<7TKeu_j&#?1^6!n@V0VZivO4x5qUeg7$ zhFZ}>Syk#tR_u1K#j8iTXto~+qg@|i6Z-&%7J1_T%NXN zk#^d9@4fW22c@MYX=#WSl@v`;LZLK>B!wcCR7yqScYc5WJ%99i8n^rUT%Yqij`w@( z^9J+}JpudI_4wGkP1G%QB9j;IkZ4srx!-s0x=Q1bh8bObG6xpy^a*$LBKM&i z@!i~nB(LqM+Jw8=Jya>ODUhzN`2fvG3n~n0NDsQvUTgs$C=NCg?v9MX=eDN*?Gj=fY;T`7BcBD}ZAlC;S$$m!{l(;;~(x^qFy zYS*I2a$RWW=C6`r7R&B}&C zxemvbUSjFF7%cs3hN8MA3}i=r!yQ-r^~*(CTRa{<9R`iMTNv@-HhMpaM$VxGXcpZ^ z^Ms|m!!N)i-c5H4jE8sn6g)7orH+oxc-VUYcHiwu2C6$TB5DEmimd2`MxZ!kR(uWU(ZWN_`{HdVzC`g}k4i7l^TY;?eD48`9%^#ZlD^#PyJ+zSHjs`+;e= zviT!>knaR88#7$A6rRVnklr-a<**p^^$xSAdQ$A{{bK3(z1XH9$nY7T!LP%x=dULm zYqfY{W6rK*4OE91P^m+Ie z&9UrZwB@toIcUr0$0E(bn`SA7kR)9d8#sU9;wz!anVdW9r7tG&zI*d%H;UvL|CmH` z{@w@B*k9iy<}rFSbW)ty+MbWW5*2K`_9C#0|9uSI;KF;a1LB}m77Voy2)XL-+=D+Z zkvx&7PrTc|-`iC@8~6=_c`meT*rlwo?6Nt)Ub7o_ooJSCGfpk#d>!{Ia#nq0c9t>8 zH1f`|xdPqOABlU@4xzd0WyD{#5=R#w$94VFC|g=58a`gZtCm~X!`ZCa3O9sFpW8^D zJ{3!1RIuAA85jRdMUCTs;^Xcj1U3_y=J~*&))_ViffRFaI&)%1@lMvCPOa&NaL!Sw zj=A!WLgXzZgxYN}?~V;xLMVe*tS;0y+b_a|=~9ooGz z8k6+H$U3kqrA@jCP1%W}G?d*KRqvoR{j&I-I*{VO{6gj6e3AX#n>^F%@aapjUVe0aSl=3O2x`Zrt>Sts?dZeri8!|D|nmz*iVnhV&n<2t;oY{aW`7g5*r z8f*Uj%vx!C4Hgq?kbhU5F3TDtHlP)IZm839!;j**Q#Fdeno?Yh9lrB(CMn03&U8G) zD(7l!x^G9{r+vaIvnP0yuS?dCa^PtGlX-a2SpP1FvLy->$vvD+3L!L)^Aht^V=;AQ z7~KweA{J@h7BLT0Nu!G~l%AXxLCN~$wK-2rzGQ?QJpWRP)~1-{O{i3KCo?}?a^5IK z-JGl`?TkG2fBzO$@=i2X{T=kA+fkh=LxB&>h!kzx%GwwtW#FCkgR9W24pNSFd%3={}+`$dShK^W*RM5^-&$3k{j>Oj9eF z*|s@Ma`9y#=pq=ed!G*{JVfTTf5QcB@b|H`T=Ar$0gXn+>P3-PeA;v!c+$pYB;fo`Q28OIKY=Kid!&w z2E=@1M z&14UHd+#Q)Q?6i04}J$)z6Ub7d9X~dCnwd5qO5ivboR@TqjHJlkU}JCr^!&Xd%p0A z+XTBApU`5>9DLr(nbsK4?SFdI`=ve&>Z?LM=33HS7}2tyRgsJFaF}m$qFfuiA;e5$}bczJFFu<1vV{y}A212Qwpk z(8@Jo7;t|gyG?>=Mcy1VT@7WIy$czv)g;?~d&Q$e`66(SIa$>Y76zSC_~))dKekO0 z@&5K=<&Evw>)aduwT&XqJsH2)k99Le1s);$*|FM<=fIW1&y=}awT6_K{TVBt1z~WM zE#*1=MMXwm&;~v;Ml@n2X~Tg3enZ}$gWU^HtT3u!pY{&iGz!AWAs_G`|6567i(s2& z30L0lPT0!5iD$!bbc8c?46VSHm3DZ2TbG6gPevb};aQZr(D#H0T;si6UlR$b&SsX) zeq+kgvZ9oy@3Hxh7I(MZNdHs;{{1gE|I?8My0@c6+k{4|yOa6F;W%Ow21mUE@a|^D ze)JXiJvI^7Cya&O;TOdX2zDkP(b|_9$OoUd@8T1}tOP-4_z_&dcQokJdS#%SXaj{6^dH&+`Mfm9@ z;Ig3@&&u~>l>9_I3IB#S=2P)BgGW==>>jPihEeGjJRbD`%XhrQzONh5;e8x&-E*+M zZW_66|vCIa;D?=eCgW&?rxrAcSVvf z)h=~|+hRu=rQ<_kFPF0GhUZmxa|m^JVnWtT2x9vF+g*;c5XtVf!sY_LkED+2hv zWG8#~4u4m}a$jx!edTCu-5p#gv!KLLOiY<{3Hs$aw09pfe8)aPh`1tF80gc*32749 z6ge~;)uaJ;_GFn3|0z@tSn+*Fg?C~ubSvkV#3|B@hAv5Jz(E~4b683NT6grP{4i-}@##Xp2=f5-fO zn-FT;2dLC;60+Z`agKYx7K5$mmS}-ao*L~|*QGFzcQ8)V;cS{C&HKho3!htJ zb^3l>x4w$L`R^qWriU>)^E}SP{}t1QredzeH9Wn#9ls0DhzQ?2NM)~K20*7c!g<2d zXS1;ONT7J|@G&+|u@;B6xS?pKF52C<-ud*&98He}ZV$8*XTHyZ?^H*;ocd2pKBZ67 zYzJfC20!*qm{I+cam@UdLVq7-x)f{T&hWL8$Wm<@KKTNsSGtPaqq_8O%O1QBlNa+# z<;j=tU`hps;%I#!8BBeF!q*!_3eW2$^DB|4Jzc~G^rOP056HZ8P2%J8QmkK2^p+hN z`|Dnd=0!coYk;pXnx&3I7)TWZEAXhmSGZqK=Dpi(?2uj~3X?7%@Y-vP3$V}f;=Ey? z?MsBM6zan`@4H^d8P}Y1y)MPZnG?w$hPNkyM`!;e7Z0T%EH3AnVBk-9iUA+7d zuP>D`AH|GTGIu$~{VQTEqTuFFq#P?p`*&@_U*5-=J*tOl>^5v%!Ym-o29cnBQ1t(z zLW2%}7pwJE__^1O%ICUaO2~RKV6rBeNvTqtel@hFIMPP$KF(bEiTzQ|WT~P_W72-% z{eKqpt&+1Sq0Q*vY(?txo0$0WIZn+sqX!P}advhav`1;tD+OoC2rZC8tp#o-Oq9rl z4WccRG~v6oZ`Ny#K#DFKjz(8@?b+T#_V)=mGN}hG8u1R79Kzvw)SDzC8>**F@tB!L zk8;j3j|}v7!4hoTnE;c(FtRO9Bgg_m@!I$Ry zHd32&cc%0xFOYsH8j8oqnGH0Qs4Z<#;5%<^o(c4%^_qPpe|iN|MYtB-I_X9aw+1tV z*qlCh@_W_A-7lV{To zN;K+#19z7%B2z8`cQ*DU_bb}82C-P(CxraE8|)e^8Tjn+M$9as2=Wju3MTq@NB)iO) z-hViZp8bMp&+0P#mpudZ+Sk#p&;XNteXui#GdU$U5x8kQGs&;QnY}+_?p=rDmL{Ar zRHu;_(lF*~7xKJnPtq?>qoKY7=cK;kYA<_X`Js-z1kdouTSv4xyntSE6GA&OMZfbu z(UK*|H^YwI6Qi*2#vuN#xMHp12;|-H<-1b|&-^yQdDVC%$A@6nQX8658VQrp29T+= zqt)}n@pi`ocr?3F(KTlAU9lH?Mo4g*{QyRF<&x7UW}sub5tWP^A|{+==W={^W*fc~ zqhwpf!SEo+oE#!7)jx}rr;7NHQZ2^rTqPX;6o~NsfwU^M9g7cL5T{Q%lhWXS*sEcH z&r?AoBV_4Jyc=emuEvFFyU>wt3yV)(DDU=mxH&Jxy5hYk#xLySJDy^@5ka>J zJ!GBng8Nc7WlgxWDG0x7d1o+eKIhHsnG?cmq_V9js5hq0%@UHgo{Z4-E>!B{MGrVv z(Z$4z4x9F*89^0j;jG+xWzLh%ea$&wC)&?E)ckg9SZ2*YUzeqr9BT-r&wJ5db`&CY z&2hYB0PdH1p+P zsM!7#tkxF6b&Ebl7R(ka+p|z!-;FjOHWNeJpW)kDU2-YpJM)-dqPX-uW^(u3F1S-n z3TuJ$N@gq8dE?voGARA!eU!Q!ty=DhvL)eAn5|4Xb3cirg$uCLONHI-L-73QKAh}s zK!I888U0+0t5U|4IP(KyS}XCk$c%n47ty{&F}vmeaWyYknVBcIk1NBoFvKk5r*^(|XhV zD{?fcqXl0-s!@)G6cw(Jp(YPKGS5(^`jK)}pRU6EQ_h`6%Zs7BFY!P71ltpyWGz@$ zhOxKrp=4g7Sp4ZdRy5s4c29QO_4UEU1vl|*)I2`(1JG|xC93C4L3OhqBJ$%AFv(eR zG-L~cf9w|9E9QxLMn^gSbxbr}-;yP-xE9Y+zYDc%cg40*+LZZdIi3~gi8C3@lpE`f zjZV#?V2L?}$wy+v883M4D8#5Sz_rpn-qs;$KuniXrE(7+XhTTo$Uua zuy>}zeMT|^iQN}GGU}w)?-xAdVlY8XgOoGNkfkO~ zrMz!%JsC)W$JCkcABDG#paskSpod&IM*QtZBYyWm)PosV#~Fz5kpktf2g8H?pppaH z$hDjgn`2+GcZ?DJ=xoOw!<~UWR_oEj`RuDs3CT|Sp+l@Mj?RNfb^FqQsRq#r}c};WK#<8Jf)#c=J}&JPV># zmij0f>BXMY2Ap=8j_f6NG_tlCAHsToQGTSM-wGkU7**E8u&U)L=DoDWxXH^n15u3` zXBtHtGwmldk**V~}J zafzR-Z#SXfH5h+e>;mVn-X?~HAH}krJhb&VE4s-&5PPQ9!KU6QC@hg7b(xGX=o+SNYH|nia=!}sw1udVBNsZm;zmd+=+3g_2C?oPX*Oi95D~jf5 zFVgLzN2k-R#3uHszdg_jxqk`b{x@fGo#0B7E9~gbA_*C$Ge??#t>HN@a(EX&J1Q*c z?`uK9ud2`|A_4WB&iHes6nCpKu&jq3iTVQ6C?}vxO(zUjm*Q=e0(G}OhyhPq@He9JQNA_6xe4x4yC~6_ydw^uk+_cJ0W< zNQo5fW^VB>Yjs55IuC{YYV@W{xv0IhAMQtt=+X6oSQdE{QTt>te2k~~^<+C@cs5ZK zSu70iZ^Ipfze3SEN%HRWS;VXsSgSmQoxHwOeQO|Gw~j+UeRj589Ek@T^wGxt)1@Wc zMG!IK_RSkYbA%!d)0-@m?l()q@(gMJL4R>_VZJbP+mGl_NA&vgSG4Esz-Gr+hY8VL#jmOAynEt~-TII08EL@4sPm96_=RJ?Yxr3i zfm8PE%}i)TOrb7SaPI!jgJYskBYS2I{~~$+OUaUfCZw*cLjlPy;>$Nj&IKl6&43gf z-NS#z#2cvZdmr7@oG7m)16$6pJLhH<+DiXp_o^q7JJ{vxr&16;KP_Zr~sGGFTlO;nJ6lFi7C0;F>%jk98|ZWsmGbWBs&CWj&>*O z)bm)pY8m==;a=F@JqUF2puQG8DZ8mBuGtobi5u7FNx%ATYmucAyf13%PTvEKAw7w^23G znr!mBQJ30V`01xZ)yo>$_fr7LicP}S*qDYFcNbrybHqkfd;T*-ihr9Qi#?Z&$ndIX z*0y{XGL&kNd>&~*yV70A>AQz`*r`E}!uVZAZMS&Oyp6=TK#_LvBGMLg!K7I|B<1os z?2mH5nwb)@I`|3(DQ^`czI??yz2l;9mo{|VdXN799Tvrn%;bq^#L+E#Br^y7g7FeH z8gFDxk3}19JyM~qM!IzJ;0I__vPa>Y6YWT?LFw@bF(dB)-hNBRv}?)CuS`dNBXd*l zjSOtNb{zLquRuP1E7p5wiwdh!WSn0N-TNgXzUDh>QL5OhEsKO7{**3x z%h@71k;4p^n3fX!Svf1~`T0!fNataw+zRn1IRz>^%aGmAm^P6T)}H%~*kSs#PPRkT z$d^Ori32lYv^d*Sz)m>_y23f=U3`B!Ki8Z*d~-2%Rt==<%xPA0IULjY`ON#?_$`VO z)530)He?IdCwXTTUTZ?fh)qZp`$XfQZa_kWxcOIs0x~Owmc1+t4Hf9`1aCy_8X-1z zX!2*ML@5GJQEF!9e6tIg^q%~wph>lY=a9*Yr+`W+OJO+9Gt znn$>(+Y_{#?-hQPs0f;ek=NbG;l~TaarPQRr&;pPYof~1>=F0JZfK;}bNh#Yp3U)Er+cdbSCCQk$M-C#)1IlF6hR0y~7jpA@c z0@iIS5A=$^CT^IFz&O2jNn7)A@iqC2xWk+$cX#H~OqwHxFO{Z`lP$SBZisisWN0<> zirnA&P`69FvG0NwrH%I@S~nS=eyB6^*_ZZO?S`9*1$_uGr1-Oew0f~G9Zt3-$GLsz z#wI^XEmNfj{rhki)0d15h?FvoXmcMO@hulL$k>_`+iqp;p3|4SHd->%;Eb4|#~rS9 zx1eF5L|cl>5&bd^4|$*FB=r%A{EQA+*NJGepICRg8~LXn!K)YY^p$sSyLlfkSKW!C z$BLwQX9Herwx#iV97MsT%aAm9(o)@_BGQtx#eGbv?kbC5Iotbf;Q_qJ3ZSGwO*$3L z?zpHv>^Cu{gbS(cKkY>~*e%;KU;)b3UPE!AF&VZ6VM)IvoM+y$;oV@we#*n&Q8w%W z-vw8%3~@-$nWDb!#9F6=!s}K5^&FRi&z&cQr!M=hy=-a6iW2dxLyaECIMJ{W+&nYvvll-Sdn@E`?zV~!(3oLYFf#;=UAe=lSMdL90jL~PZ7_v*I{Fh z>@-gBmLSGWK#wp)-`RDj5#UFfdA4lU;U(f!=6G-bCF{nfdJokzM+ZfiSq z43%J@au+Uz?1R778wH~u;`5I>B!$(9X?!O1zWYTSo%BcY;m|g?BTlTE)F3(3^%ST@ z8C7eqa{qiM&tGj}V`cU)6u3Sl0pBYTsbvlgK?-e+E5l&At{wK=4Xi)?fOG(-}VRP+Xta>h!Oszf53Fv7#vq{gjaYjJjUF|mYuQi zYO`w2n+u*liF%CVlr{1yq z(bd6);ym~qS~H!U0Zx?kpch#NMnk7OfZqT1r1;iJsQj0PFQc3A>dFU6!-?yh+bBoB zTQym))t^COR|^tzNNDY!A)du#VM0d`g*@9SeqE`AQidNDg+36Lf6l>aH)v`M^Mu1X6oTod_8$|!dqXGGU3{i$rq zE3BEIOJ~$9>C4wD&hF-l)T~6jy^;*2$d8gu?kUJWl!o!6n#I!SJNOcQ5?7W-VM=YQ zFq`!hKTPLxFZaEeGwloREuMqbyytuN`UZ4DdQ!#yp%@k$jHqT`nmBd`_G$&f;in)B z66oX%*y*HQ!hgC2dAbLq#N&z>o1jIv6`e5d3TJUMZ0Yc(UT_~4L~EXL2c+dF#$6ji z&&Id&UOfdDSNEp%iYm1J(01rQXpv-2$Pes$vm7z1^F;5iZer+>CRqH763b&OvTy3W z=G{`GqO3@sr{`JmJ76eUPqzmCYdj`-+v%t|$2IJsQh|kleiK;c28zMt8(n{37|8)qyKX`J+ zOzCSsJX8*#`=Q+s6zW_lQ1`cGTWuE(!|_$fTzW z*^e0s>o!}GS!qYe@xZG;+^_C#LYEwT_&WevmB`QgbU(7Y;6pCUG>JG%o-v#m?y6c2Nml*MC;6ha1+wxsZao&U2#~f@Bu-Z zSCMFbnnzX&6!N4JdosFEOP^Di)~5!)zbKQ1&o0z#{{{)#@P1DQCZ4jQ-{D@=+xsbA zd+5-5_9P#;RR|9!zSs7%p~B(ic%L}{hDy$)Xv_S=Z;|+)KS)kihVq8Zg!6WP(z~R= z{fuzbueym#d2%%3?@->s9mgKoF4T|}gxiN7aMx9dipECc`?MHwaFQv7KD~hD--^Zi zoo1x8I0AFtZ4~>DIWd38jvDT66Awoz(u`eRRIp^fa2@xT-HoQym@O-&Y?CJoe&&RI zE=2w+A5#6)!dXS;hfVjRiUnWrpnC;uraRGVy&{RpC48LThUH(? zvLC4IMa)B0N)NT6bT=*9e$SBp8|O{=?)(h3W4`1z{`{Kt>H8&~EiAC3s)_6P43Pv3 zuyGOTYb9dF?1^sBJ=z8`bW zA4ba+?g!?iaP~%u@^z%BZul8IQdgw%I(-Vg9)r}~ili{S6M@wyC4uUfu-c{;%YRFW zk;{`X@ro=h?{!GLEIAM3k{~huL9OWCD;nA{VeHB)7rS4kqDR41kyUAg`q%6{vvkJ( z5zJ;97D!3+Ctz6NJX~=J=I@XWtSb+2AE*~Sh}Eb0-oZl2UWL2t29#2MUot&rzc|}q zMQht~B&$|9p>gak6y2PR$}fUHgN57Qh}xJkEDegpo?2TB?plJ$ z1*fomO*vLhHig~`eM%^M0{4vJ=sSh;jw9Y9@Td>eU9CCGJs$~KoYmg#Ozv;?azg}prd9v@=BE{yWe7#UfK6J5Hc!#Vjs?A94T#=*8&bgTd$ z*M*R!P9Qc7*ow`j4VW?H0Zd}J!?Lgr)Z-pA>9#<|q8$G_|1wW?CzT^=G_QH1c=*Pg zygjAq*hF-P`-cJh+mZtu`UT9h2I zRg|61fX zeJT!bm@T`arxEsD8;3bVwJ3JNFVN0ntaCM@XLEl*@qH>*#}m>EH%P~eipQ#zF1`LjR`E-+Kc?ZIq?6_BKzgy{-mVeoviF6 zB2!}^cWm8hhjTCSV0bQWRXr1l+op^EOiFM;&rVFZu}?C*ZzbjoQ^SGLf03o=A?YnG zM=OSZ#!-pJK&WDk#$7#w$3Jb(5c<3?HuoeO%xc3eGnO+6s)AD2cSr$%&qEfZ1U<)Srr zf+YG+8_(d}@b`RvpfW#4m&HF8KeWe)E!yYc_v1mJ`RlW29hU^{#je@kUT308F$Pw% zT!j6CGjDLShWA3^UI|KCRj6*MJzX47&KVnjx}t7OXMDNfjU_wt8kYJ)HuIM zH2-9ITdJqLCdD$d$*>me zihnDrBc~x%=PB|=Ya^myD;~GhVs6qCAtP^tZN{b0bC5&-Z^2^OCH6h=-lWZMJv#&_mSem?rqrrEi8L&@9C0h zlb(tQpJr5#w4)CPwI+W?=ED|odl1=ZQxO*i^{EuximERT02UyWE zIZxWOJ(Q%)%t@s>klr%;sLx4Hdb`eplBZQ6Wbg(2EoMfPaUuRCoIvaTTD++!K~Q8N zyJn@x^?4bt^id+o;8Pg>vjr6o6lfA>6(+qd#m`w9)Y*SCN|+fJQ0zwY`Mc)8Ij#9N zfz*`E9>s;se>L=EW^p0HD-YsFgd?4+Rj0r5SDEu|L)k`M>8#aJT)E^&^V79yisu?Q z2i--5x(-dz8vu>6G9>Oap(V0ovGV&lJnyf~j2-5}?Yt<)55*0jHeEQXzvqZe~>vF#n7?+e_h zb<{KNP{ z$8&@QPBeEnvsu1t(NP%>>X@ZR=MI@uMW7WKtk9#k{h9YL&5AO5okP{VUcAe(qAR1W zvtN8Dz2uzs^$po5Ht?gfoX>dvVjBj3xQ+HnnFw${j@6-O5Ezn#Lw8odL_Hh-MHtcO zY3#$>+lj%`l=xnC4sRxPq46^T_HL2`8f zshpb*jm}W?89RU;5%(lJHSo;Ei=194(5H{(l6O0sME{ox)cDO-I6Rptf~2Y23aMgcFs87*rY!>x&aHXw5_HKgPZ9`(1n+i*7^kZHO)XOx%ETgN^w9?@Leh_A;YfmA`iqQt7gr zXGrY$c5~YtmI?a|?I^jgJiTl&ri)31y%5sQ zGnq9sgFCUzkk2rO*3tFojEY3+e_2=^@=%h?tk$Sz&SdWXDRH;4hVA@x47_wsvNSIY z_sgO^39@U!hDa!;#~`$kQQ{Oc|5F;k&lO_%iS zR%NC0`5ArFklxw$74e?d>^0s6{lnbBbn8x|LlZF2`31He;Je_)9jI?ThrsvD9xK~| zO^T}6Jfj|^p(k;Cl?R03KjiDr#>Zyfh0eN(6}GvkIlLOxW$|dfb|06`#$vGRb$sbr zi+05*ED5(|-jOTy-F+T6@0p`Eky)V%MKCp-g9bD1AKYbcjiD9y>Kv(GhA#W*YzUt$ zY1pShn4-XQ{~C8%`Lzq)ygG-&Jm(vBY>vncx`$Jzf5BwM21$YH4HVAiZiQy9h^W6Q z9Cn_C)wuxj8}~+}v^;>te19@9*5vNl0W@bYgYbhUXTzN^>qZ~c&eNa+H=4wxVtdH9 zcc*p50mxk`Kw8S&A^w5HSw(pLM3>awe?F&MO>phjpDzHe|54@Ty9A(Z~M~m;51P> zIe?@)W{8Y`8Tj&C3g#PEhzze({CGS;{0@E@Sff{fP$Ry}mogi}Tm`$H{6SyI9T%c`k6y?#yoym!;JJIxEB&bamm8+9nvClkLg=vZR=n~j#Lq7ma~u^Z_sd*t z-cTj-T9`XtNeg)=)GwOZdTwnl3H;=aedgY6LTX+Hiqx-QdU=;@nakJkLg5X{ zZm`qI+W;C$e;~%|(5TgwBL41ktm$h*l^(`W&o0DN=J9UgtVjEUdU&XDKk)Z^_8~lm zv9c*Sf62#O?qc1(F%h-X{psWt4U%5E5z*?Ps-V9JygePi4)&$%le^<=T$OlRAWcS* zQfP{^!Jxm2^s1+T-(~&_7jkCHN1G;Ht%jjU$W<`(qF1~EsDVZ#LT}0T1i^tBEqBeYs zC_SDjs`6K6Ejh0Sjl*uVXZS-bS>b__iR_V9VCHFp9L^|uP=Cj2%nF%|%=-V}wf8f| zv`Ua?7K&jlFA;dd8VVk}_y^)E&IV{B=OfX9bzN!GIu$ywq%S>Ms7dP_nMpXl2OYkw zN*jA=(x=8~eD1kJqBeb(=-zP*iUVI|y{daGGTJ7?)XqTidvChf8ug4F*q<>h+m5_W zZ5AebIx*VLjL%m&wC+}Mc9%e{a1I?A@O9 zQKLwWm8-+Hr>1mJ$ADCJ zvqf&35&cSMH|;%Ug)8-9t%u9;Yo-v(9fbn#E;EVTP`rd-DZwwaf((zaX# zxXKAB+Z`C;a#AGc+>wNDK97Pi9YX7Nu29>v1@oHivE)+k|M!&p$qeKjodo6;1L>~l zhh!BiOdQ4UEWXNA^yREXW}+sRwiuA%t%X^=wcXMGLpNHtAW?GsNTT>XGYUf|`?B-v zrh4rKN!qYAuVXwv^GNC~@6v{Gl&zd4KWk_MM6KqDa^MX6!dwhLS!+CM7 z0(m;!-weKIzH&0*l~4+*j}f_{Mt6OUHP54cD*?*p2fScf;l4C z)rEE$#o_wkGkB}RJgp(;k>Jbso(5*?Up&p8)jJr^JjU(qd$7-;3(v-yxL3Od_fqsw zRq_y>bB-h0sRyLaUPXS!J#;)cgxm>-@Lb_3h8^63L$iwz8B~r-H3zvjI~XUco$0LD zggN%xP&~_wKiU^`PaGhhP@JW889=P|COs7{nd&T`~kG#HR1uf&gR%?qD5 z3uZm7$T^SU(!azKl~kncPlm2L`}}0eMdOyI?3)+~sRdibw5D1da+{3jdEH>^avQE; zKIA^x4f+Ze7|RTr?9f?wn9&ml)&)?ayfjiDaUaxgyZHHmyZ29hPJR$-zVIP9}QO#c)m z&b#TM{cE-Vm%ewh5X+t@bBDVh9+Y1Y z%Qh)-?p6tp_nO0XoIGW6u6GJGvCq<(!b7<8^;nMjT(G60hyU#}4*eq4f2IEsbilrmZ!ul)&FB-(WjB zy(|F{yT^-{-$zNFRb51OmWHI{?MSiy#TI-zI9LP>QxXl8FGb|qpE#G{Lce`?h=?3{ z8p)a9uD`VKlbQQ3r~6Q7hXa)!4#BQ`9V!j#PWK%IY?Tz~qm315sV{(G7!z!`8#cf4 zmc+Exll&%h=S=eyVU_1h3z?<$NH0Kqm2ssBJ2<=7VMjF^wD57bH}}VEC}`O;G5e_- zO_g;dH>Y6OK6(!~6J`~)%CgU-iXAY=&@!MAPi9miwAVpM=Bknh&&(FS`Hl#goe1at z#D#tBP|G=t=UwXII9r-#Kbec+_WtBteof+>^a%sz4fHppE~!aSc)QuWoK;d_Q*DE~UuKh`v4w}5MUJT{Fo zri#N>Sb04MtJo`H_2L9VR%VFL>O9YCKZ%E{whEmPH>y@+eru5&iZ+?hvI+9^_mUxe zRa&q-|0jYSeURVw9#8q*xoN_8@n&-+T%t2kv-kuqv2S*i)-@QJ?tsb?q79K}FyYK; zc7*a>bjCH|(vuxW?uztU^|7QTV=20XsnYZ0d{Ox{62U6J(6oVBS=u%q0f`YuWhFiDcI6(93XhI+Au9p;J8QT7n_zUa3IoZZ}ff=|;zg=HX}{JB8>G z2C1$??3P@ffv~&F>@Xx*cToE81{VGu4)a3hsg)a%Y(zea*Z~xhV^1C5a-h~BMXz_L zQMAcTNaB97&#XIr``annuKpi$?-`V37p3Wvb54@8WDt-X-hD4nQOsh_+0UFKsF<^e zVkDSB6j4C|11N$DC=yJl7!ef_MN}lIXLVIqO`p?ere~^Wy5^rhs8ZzRdG=oGzOO42 zY3x^f|B>gKSN0=@-%&Qz?wA>v!5L&@n0M<&va`0bU(JGfDiJg_Z$6G4)Wy4Jy{T(d zGCUU>A|!ba5(^3?t-ZXkM=KtY=3nyGN)znE58~A9bwa^RlQgDpm#lrCExxVQp~Uc^ zlAIoYBne+SQTl+xd8^+16q;eDneQ9{>pQ!}%RPB88#Na3GG9e!cDK5jo5QInQgoX= z7VCGokoBrhIH|+guybA6b<>VVi~Dddjb|?j9f+*rol8j_9PV7kpznq-sr(D^tVv`YE5A|j&x#fKHpckTV-xQ z2Xy-){d)jfZFchc-x6PChvCD7BXD^%jJbr9QT1pRx_-Tf-Xr|T>&Ywey-Op;oe*>? z?Vb30>?Uq(;cW1MF1QiW4bz!}+H^{eass;WSxc6t+w0Sf`gQ1BAVd4J<>=%MeX1^( zpM31&1q)2hH@xL3Iim$=Wi zWNb2K23&@I3_HIMvI{eJIC3kiA=?mzeHB~b`Mn+5KEt3lIs({#jCoQ;7?XJ(rGa^{ zGJlTH)#(WTeF)(u1-P+d4@U1Ah(X+QJiOZzj{~}4$-~aHSY{k_+1q10R+}F77=o&> zQ2J==&Hb3|NSo+SdR_gg=@Vz1T?f#Dt-(aUx1q0e3S73of$oE|Vqwz_o>4X9;sW-! zt>%0CjUwn5ZxQp;y3uIvtbb|QA+atSLaHXI$T_%JGT>%kx*7Kb2^Q9QW?6dFUTVnf z`tC?zf6&VG&Ukds5MRT!=*ssZaelBPEjw?j>dA9VBvy4@9vmvv^h-XAU zad_tgJWEsKIhPYzU2~=r?}C{p)tQbT?@D?hnm9Ygk6E(p3Of`;FSg2v=hmG6e%zDl zn`a7I=}JBHf@!PMJn_UvpPkOTMWq>Y8m9yAK6Dn_CqG1Qem0pdbP%8Qzr*L=FkC(T z6S+$_1=l=yif0M$5uBlRecA!et4AnPn#!F#pV4nHn|T^q)7f*e`WFh$7|=h`=Cn|Y z`|*eDXi2g=C5-uwL!VQ{-USB`S$7@J16w82PQ@WzJA=QZG`7!Z&Ni+W5u-h5K(}qEujyDEVo@Fi(Kbaqrw9*DEb|#4hznQbu+ZpXGSwT1Q zYq996gD?xw#Z$8nlHs{y@+jH?qip?!!OS=@?sF)r?RCV#Lm99+$~nHyoZ($}0lI&> z(37}0=srq?zEl`Ze!d$YKQd#wTAjMId=dJMRah;tprXh(;_|>&eBwOPMM;53#c|wq z=|rY~_^xw(JI-veq<*JM@OB|{(3v|JvA6)EUMtdEo#j~25JXbyUr}^(GtWneTvlt* zhyigJ%na2M&rWDDC=!wsGoJq|LebHIJ@3X8tI8Rc5f4PPnHgPdP^P7}oO_9MqbRQ? z>^u4gm-vp>$yl8hG7oxdksb9PR*m&bWy#FE1HKwh(6uYiq&%eQOY<|lIHp94>eMML zwnq4qyX5`y5^+>BSv+6uBk?I>=1}}z@y85i~ojV^ldEQZT8xXexF!|F9jEoONRte7~_I`r@Wi|X98ba%z%2CQf zRnpM+qASbT+53gxZ#%>2JnxlHpH!sg7u!+&^tR-a!dfwP*GU8pUYIxaXNK@J9fP*i z7uVuH>=S#PD@C^LNAzN*`RA?1P;dOjIZIF4YMmj@f9QZRcU*g0_>y7NZlv|nAPX}$ z3SKoIBXm`$`IIL+1ovWwrzuIzyN^(S2K+Gxn1*OHRQbWT>Ife zChRb*h&zhR89{VzfCaOh@-RZdhcYKzz_^rk+(|9RRNkMOl?=x28I_pkcN!@cF(~Vm zg{jO9H&}lS3%`9}P7ZT`YngQ_`HMSqwdlvb3$Wkx9_pNv>*n=GoP3cDpWWR5o#=;t zg{Ls{4m+oYei9>VkHPLwk%;tcl9&~5z|&7x#AwfllF!FZqHMYhlGTrj)Uj*vWRoi% zbn4C7RSAt=Fch;IhA@}9FKyz^)ct&SOk{RrF*}Jm?%b9nSE#|Es|mF%D!o>jVu$`Z zoYi}KP~!08gm`y#11|lQfYe)s;kZP0E=)q+jK4y|V=LC|lf$8m4 zm}stM*ZXkHdEw8Q+ur#3k0J7O#-jMi78o#ZXJfGs?bv6DD|HR%e^1b(u8c}A7^oDCub|4w4iNJsHbi@m9FLY82md1yXCzpK3|#LDPc%b41wM@IWqQY z5eFxFx)GY!k|BiX1k;-`e~o9s}D z^4gvIc8x9V+><0`ynFza<5HM#c(sVAyN7vYJH#a+C)C&jcQnz8Idva#j}1I+@lq7z z*?~lDv+DtE(&YT~JG{dDMU3+&&c}74@Y_!8kNSlm-gEwqGo~d`^{^~r2hvF|y3SuS zH$Ge3dAt`!>1l9|zb*NZzXPw+QgEq%1$#HR%jcN}6^}KTz3{F$_MigEtHNQtIaS<^ zdxL|HOVPl2n}ct%;N8QUbVIw~;&2;wr}>e(sS@u)2ca@Ln37jc#PV1>Y&*S4v^JR1 zQhi_CytGKn{G&!=^X!oI$~S#NR2p-bn*Nq<9n$Is_}t3M(8E$1dIf53d}XAISFqp+d; z3_l`GI|ka&rU?&`uc=6$&GKaYx&h1Y$j~757A*Pm46gSzsP(%p9h;OdR)~AS=@Y*S znI0d6(VRL-iH|$BtSAuczlMvBJEz1>X%AXu`UaYldthF@FLkN=g^JO0FnppTIo$q^ zpUsBQA2tF9)63o5reBV=;UaK~Lj?%_v-ZQ~=c zwyG8$f7$u&zFp+{e}qw$CV4$lgHCKa+}>Nztr%}g_`C)W_iIo}wJRMeU4cq>Q#z;T zLfHpKVCn#6@>um30c|fN!^V41aj`lLR`L|mhRlsLYeZ4#e(}%8E}X&XLcYwey&vXF zug?aOZ?X+{l|gpnJjj{PC0)jNl5YfOn>#1LY?B%_jk?2Lm~6aeW{^|O9UM&Ehw^Ds zB=hz@EIky-iq9!M$Je7iN{LdA9EH<2_JlNQ(fitH%+qtA8z20r>RlR^aBfD!)sMP5 z-eUiwHC?h}=B>qPBpDrqSFAffJG7|o?{0LSE}^_61NvHZ8W|s5$wDGeb}19^$1DvG zCu!3B`S!T+^D>sLKOK?J{O0Z?Wihi2Z}eo zlKhzEPW#q7&|{-oK?y%GD8-)C?td0BogQN)vjI?+8=2|adY%&U6Vdl>~kX73>{MFV@L5Z)-=O_&#ohUNqarJv79a-FUX(d z_7I&JsccoPZgSupV%;0z7(}U??W`ytSwRa`L}STHF)ytxXASI7LGxCu#L0!$8x%1XCSD{i~-PT>ki|# zzVugN0B6oEFipjm-uKm}ysC9#UCLvj?5;^NDG}n#Nhe{qoO}OUQY3@_GDkFdA6~oJ z!aDVX@Ew=|=cnDFRVIbII)`Ce|5^kZv!~=^5Q?*zg_>W7qnxEVrDjDZpR~dydl>37 zl<3H|5194YfSnOfuxiICRQ0mJiU-_fD&L8Pn*Fi!++#eRlZo!y72FYY!5o{;L^+k5 zUFps9GcWqw^${x0jzbZjQ|*4c!kS9^*pS8JA?VZAi@npEvCk@s`vN_1{N5O7 zIsuJ0qF~g#8AUA>P`kBT628g^^x-R9hh$0$BXux+Q90VqHc5Qa20^9QCak$o2cwDa zackFcI5;o^t?D~ex2?erdVp=lt*|UNpsN#A=-RSOF>it$XT&A|0=;&@Vw5Sh2Jsns#${;Tdk?UubVs=xR@=+bMvEvc z=r6(Z8ILh^)dBeO_cNQbeahqVu%T@&24w9++T7c?G;bJQ?#e|b=Q79j+l<#;{y~Nm zdw*BQ!B%}VG*`15bl!R7?-+z3+?_J2*^it)9?WC&CgWfBxTxz!n}jztc1&Z3Mld~T z3#7LFX1FkC2NrGr1GN}8F^@mzUDhelW8(uN^@2N<~Tp7|%yb~* z@H2Cw$)%vxQysGpaHrI z>yWTNSfp8LQs4<~avyF-)lO0*J%cme&kX5=!)JUcv*6D!zcc4|aAsz$IHb3o=joXU z>XQ|$AGn*D1nfe#imy7K z5w!N6Wd5*VTz{g1Yr4UbAIY6jGPoNe-i3-OK8x_D!2}Htn}mX_A*nh{X0IOaTvUze zf`=UvU0m>Iq&a6xm&54^=dr9*Y4EorQ1Cwftbx(s#VKIQhl24cV~ zKUzNg1!}X-2#u0%WSjF2nT|0c!oY*PJKw`0y;g}wR%aX@*e&va@#o{a#{`1Q!037?MK0lQybAcLd==2uv^n55qUzg6mYeAHs6Zt2$VW!b@^yuhHj+-Ar)ubH< zF00abov(POEk|zGxhw8?4_BRJ`T1l-!;997-Cn8#fPw55m!$VuVKElkgQ~N|= zbck>ZJSKh<&-BtOa6-xoLu}3I?uJ@O)yVSioC|%)dx?DWA?zcXh&_i&v82r&k4^tU zLqZ+A!sTJF>k0+)0-Wg3gh{EYBsf=!HhJ(l{9~bHD)(^Cw(@87?BMIxPh{zUgA^^B zv=1)6L9`|Evt-KT(|neYP*Y~2STSTh9-eWhtJ6jccg?%PW^^q!3^1ZH*DoR<=?^qb zZK)=HnsA&}kC~j~n7Y)9I?k|PG*XAsc&8QhY7f%N%&FY33mFylg0HkD>3x=`5mQVg zD-L$0U&HzTO=^U&vT&l*g?d!7=(~i2Ftq5l3vC-?PKnxnG~6|avL~C8NR&_?W!}B% zxY5f!U1@mC8yMf%hh#I(FlATbpLKgNr1x*!oB0e<><_=gv)mpVs}V3#nR)+b@r-k~ zEsvSkYL<=gc~7xUNtr5^Zs$y$8&xn*XztoXeA>%y=Doe>3eVqrS^H3OQy?iT(X(%nlta;+wc!FqMWMIaXHwKa^gbqXYg$_II^p7 zZZ~vKJcJoLbxB)&J7ytIdkT$j#$cgKgjoK8s#}qC$(fXPHHbZHo+8ueG6rYe#JgVZ^iSO_Oe{@BqzAjkk0#>&pKHvv zaw28JGI8bmbi^3T(w^7*#SYa-%qy>hjY5A>5I++?hVz_vg*hGI4uCat5L>dWXp%Ft z!6i2I`I{mAn!&u*4s}wWWkpFT@z}oJj}jIc(ALFQP}AW@qxo4nhjY<`qb1y&H1%$_`@O)u&jMfCh5yn{8M7(-or`>_D0*nJcb^iF(xlmxeh$zpYDvIw$@$H-Sb zL^m@F@wxm0mQFq<*2<`3>EYwJ-ZLE5MkBzHWLmXg8sa>i@bne)i{_7o+ueZ}a$+D^ zvWLY}zKT$22gs|#33!irgXL56aQF`IEY?0orbjGy3?IYzr45E{v8Mrd9%Ip^5xCdioQ8gTk9X^x zd57Ue`_A{mc^eIyHrs>yG0WIX$b4FUmp!qys&+^Aly7`6`fal~QY^n)tBkJ*c#FpS%XLH=$P>iniU4`MNaC~Xlfzdf{QPTenhC3d|I)zqbzWIis zm$zWkxFWpvXC~SR9Rj-=r&c`^DKjnT+Hf;Enxp~8hx%m8=f*RBZo;$Jo`MFPK$nxJ zxwmCT6YrkHg3g7oKVn1Ja1i6V$Dq`*7G~dfVOA*5w`;56#w?|s=Vv3}g%quNw+R7D zLvZox6?Ayt!Lj6>n8a+1d%tTS+meV=9sHSaEXIWVIe1(%lwD@*{^awKe9Jg?$Z+Rd zX)mO<4aDp3&QutZf!hr(q~pR)yQzIFl3RZ~ZPV5p6*AsCq2_kQXD@`)>5R+7@#GwcLnZBJE zv<_}mGCD$x8SOxqUb~Q8K&;3uG^CNIeaIAiu)En6Uo_Ittpvpowe5&#Eg} z@q49^=o?9@FW%F10yS{|t8)86@y)*({wgi_maU3f&ZRAO(;^#Y zr@BvSLBSB7W!cz~rP4e6_U%N!xT{*M-HL^haFN7Znv={4yXsPs`&?-c*6cow>fSd+ zHs`^5rkq8)+a@TdDC4f>EqG}zhE+@ezFJ&B{`;ZGcB&USKOdrPOk{AZ{a7rw)n<-o zvqb&xP8jzyL7>iH$)fSS5Eu}Ohu<^A*D^h-j~R5`!pPOkz^SILMjvZzpplV;Y(D6^)A@_lgNOZLo!s#E!8Fl&vbyz9Utf8K6kxYyFViGfrHq z(I=|`s+8^d7H9MW$Ztw31`Pg$LEOc#+^t2=yZz)0Lzn+@|JTs}&Hk?d_J8I5tN%;? z|KtCf>+;|HU;X}#|4aJ+kpHXnzvKV9`M>D@iu*70f7Pr0d;ixhlhf$@ul}!ufAxRO z{V(!=4G2}F`2QmR*Tw(9|D`1Te~L>r7^?wcU z`LFkX_4p6`Us7iO&Hr`t-}t{){4e;w;!gg1|5qvZ%>H-!zfArE|5qpWe^vhf_`ekX z2mY`2J^ye2*U0}x|JTd^g#T;jzwv+h{}=ebuKo}FUsL@5bN;W`#Q&WCEBSxY|E0_R zuS5Up|GLrV-}}Gpg8%pXzh<)kD}*^!oBl8If9?72_`kOB{_lUr|5YZR50hancz)fQ z&dg3_=kQZlYw>MKQck^gf6r{=oRqKbbMP7gF|C8FrKj+-A|uKA@`5G z%tGj1$QIm@~B*N6!dPGgo~jAYx1Z(`umUAT5WH?P3JN7&5li=3bBuy^A9ncZBZPEe)yjRwr3 zwFLcDq>vU(s`X>uRbdqBGnrAl#FHkUs`d<5zI6v#>`l6KD@+;9AhxYjM$^W`hv|074k zzRXA5QU&r}(F{fIpnh=GqVapYsb%0T%pIXjAD1}L1M;Y}u2_cAl17g@wQ)BP`bXjCEUI{FdM+2-QL z$*XAG@)0}uE_Qeba}*3O^X_;U)FUq81Mkifc+bdWk2IZ{Q^L3w=5zi=k&rpx?a`Y1b2;m z4)jmPQk>|_zRHz>)DRsF!F-_&2ic1fdjQu9EUBokgt`2Q2t;vwjW-NR`wCy?=N7N9EzA7#t0r41E+24QE~Jh8Xoi@ zy^9eTTf|(c=rGD$ITVJMuEVUjKN)OZiwfJb2!2@2oxF6Mcyt}|o9eK*=>U|+WFyET zA7dIX;gP2$<)?D4&hW2zk=w~b6-*vm58?5?F4o1urSE-n5s!s{GB2n*?F2d&NQRwL)M3g=gKOfuB`N5l^ zQQ}EY-e^&cFMsx;y3v-YI&|yoCY=cJ3hx2upC9nMZ5BV<)Q zLFwY5l=XeTFq!<6JNxOPV6qJU9+i;F4@Jm?bwUE~P;ZXzA@umU;5Dopm8%|u#fKn? zGds?T4{t_%uUmO~y)Ph&rBzC4YlZuy9RB_Kj9H_YnRRFsE_M0<6>WD?>9Z979G0Vi zw$8lF>I0q3Wb9JcBmMWkaHh2w3CY@YExa8u4-cbohZ0RaT#Nf^3Z%>~xynev+-*HN z&mND^)!ixfO9vL*oQOXw!zpn?4}Lxk#aiBFEV&tqp}hp=_^OlNFD+DdFhl)JJFZ_d z;vQTJuK0#a)P}0kjaT(B9yCN^I?a@BO_8M_W4r6Ua*J`cp%Dje>d@{Br_m<$4wm1Q z$a~&(6yD`L06S=x+%UpxEkViqtHq#$&%`wQ-qe40wb(e=3T+btC`;EK`4V;`uX)5w zeLF0=r$cT}9x;>ho;bYLg2I)n@Mc^H8lH^7ic`1Iea#JKMqA@=St;5jG6-*6j^#6I zc;0?Q+}md%TDr=T)GF?3ww=k_{8E;@!jx%{=_oNR;WI9Lt%rgA9>m+dl#G!?OPcNG z;P3t(S6`e~7UQDQaMkl{aP2R9apIOSRu2Dw#vF6Xm-!*g-Z$cPPaX1)^hN)p9VmWk zNAr9}h1E21WJW8{Bz}4=p8gYMmAp zXszr;owh4en@l9gAJN4&%WtRf?VTfb$4(_+0S`mz%1vFg+E}wS)N`yox{1j zw~&|B;C}aN?)lkMX_5y$D9Od?`zDmXP(rywZotUZg}fGdQ&i(iDBqfkg^8ZDiJ3Xu zx^3p~19YjR6D@Ap1)o|M`twzhvX~Worc(~OxU0}dxwS~_aT;sxw{SOe6lN)1K@jJ8 z-*4CsjpicJ>5mEB(TT;)S$Sf@Z)duzn}SdGehHg?hBTl1Z+*6uh&)Sm8uKTBvcH}c zYt3ZHe~u+R5(k9i5+%A(R)b%h^|9E{m7;b&!~EE@%=Ymkr#nxvNVO9Ck~_0EBcHn) z2XQ%Bo<5J&5Y6#Pm|Ut!s$UXCwh`x+hJ3{wxKiPCY0BGTOi@F+(8(N4>N3ZM>bMVI z^i7KP&(`7dvn{P=zk1N=Yr*;7-D!PJB>Ff>MCwyp(pq{7ZOCH0a z)m4e!E*UPYikX9v*n+CVt0hhkPhs0K4R+1m6@8nJqoY%TWPWP3kcwE3xy~m!tEqtV zYu3T&*#e38zGot-oY`nSZQ$K`5p$3@8!Sd3YRzWI9Svg_k2VIz?Z@P*KpN6hot&}QG8R4^v13iGlHjzJ0tixv49!sVNw{}YZv6Je~G#S+eD8O zM{z?T06$(BVVhx=SRdg`K5-3b_gWwdH<(lQgeI8UDxihC=Rq$zFm|0QKTj)h`Ozly z`D=(n2fv`HZZQ-q$KrhSGjtz$0BWu8u`N{vo}BxzZK{EFL=Qx^X^~MlcMi0fwNmjf zUexhe_DYYA9`&FD$#dX*--2kcEv+@%fyfFSevkRHPoV=&J56|Z>_xqD|6rw}Ass2` zOfF_M5Q-)=H_4AQxPv=<=wSA5OlS8`C_{*S@i#pfqe_+0w|XNoG?#eEisZ2>YkhL3P|CtowBiFF9xM zQ#u<@nUn5s)``sLd=LTLf!rstqTjil@a~rtJ-Kg0CBGMl1bat{me~!Hc>a7GwB>#9 zF%;aXfTgz;g?&%Lo62PT-uDP=7c79bmlf7UeSy@D1u)3%jrCI>U}V#NY}T=YeD_Su z*m4t<(nIk3d;(tnVi)Q(6MVgMojFS{Iky{uYkPeWYvM_HyA#l^sD~Mf9wf`Wo4er) zVA-Q9DULqKzAg`T4g1i9_90NLccE`JU1^2?HvBmhNDdQ1C?j_aQm!&<*OI+L^QA<= zumU{MZ$RW}NpRo3w_zKo#C_~7A}A|N_-PlQ`BngZ`gBv=oLLDdaxUOxY_8>e_J_Uj zqX}QREAljed3{r{*H?uma=&@0eh(BCn~;C`FboQtge_Wn^m!9I<9gjg5Sfs-{X1x| z-+OO5`@ftN>6}$A1{aiw=vpK4-lrmRrIZn#ZbZIXQF(_#Z;BvGdn))6E{1*fr-ZOP ziRrv95DyD~V)R@&a+*6y$cE`sSN^l{4Ku3yF3Wxa zHIicoQr132>KC9!PQ1fYaBYBTcQztbc}Jl3L>Wz%5&`lHBTx0jo-Gky&gV)r6J*B7f(=i)7h8m1;>_iYrh>>ztK^q`pYtV+yGszIO2Uc&6* zXh~&u4GL1Hi)pJH#e)UI#ix*$%-c4`i=8^6&*6BIn&4MQHI)xLn zGjU`*GpvmYk@YY#F&4E4OlnW_Y7%$o5E6OUSv^DG_C zfMqe8&W3qPb!gsp3VwD*r04z+6Z{@w(?s^4Mt#N9s%G5F7=hJ{(6oKmgxYb_xa%K5 zIo;Jr-nHC+= zq9Ml&IIkli!_hB9-(RK(31tW9Di8iSz7ikvdr`MnN5xTf4XQoG9N0J)%y_Fo7fwCJ zhpjKUtEjqom?C^WpMEw8a`&40{gQsC8n~3^#PkFJlhxGcjeqo~SI7 zri|T9SRUb%SMZ|)lhfpwgY{byAKrwK-ySnlVGmr=Yb2|Tx=B9pUgo*eBT0u^xY*C{ zn4eQxb4_-+aZgDTBTYXd_SL`ig(RpclOo+srgXDkg*Y7BjI92e1sLJDLVvqS-D7%QKJW&SHd&kC`{HjWAT!m*bZx#n>ZGW#dq$6C`&$DJZ*suFb= z)gw^l4%R&PWX{Yd93P#6soeQmxT^-etty#kWlL&7=a@g6z^)_(%8A(}Vjpnc#_9)D z15JdKe;oSu(W0_6H?pw!gC5bH_%r1}SAVE+566ne-nFEPUCb(z;f(kbTe|gREt1m_m$|@oqQ27 zA{lcOzoBhZnPfz#GpPS?O5&Vb&YaMVXwA(OZ|^EVYwczPj=K;{tM3Rm&Y`Gi+A^PT z5pGNqv^_}<+t=*F6P*BB_#J%zUWKvgeJQ0%gFZP-7N^<2x#y1&%_%q_sp(M1=m;Io z1@sc@*at6dw+5$7%<*NU40g6`;=XHv7}u>pjQ*9t{x$Bdhbtm<`&B_fj-+`11LXML zH{zf%4JvFvTC6;0!gx^q-az&~014P+zpPE@{4-1(E6cY)5r(q}!e+i3Uhw*dm zE98aC;7OPIG4EZFM8eYM7iwC&=~U@jmoC9VY3xQpMA$2GXrvt^rfgA4Q34uh2r>W z*dJ>Tg{51tL1`wUB7LCSqbDBp4o1hj`&c$dLTkoIan__BYgcz8qgW|)^}Y>tXVCrj z01RznpR0-#y|~WK%Kc}MF<**qj=O-F4!cpJS_cfQ#8+n1YWFvwyS*F5^h$f$Hr|S& zDi!dmzZLB@)1%5an}n6F0~PA>T_QXOznAdWYwX9%rRBIEWkuh^cOgnW0d?~#vE|Y{ zIQbgG=g1xCmFgHx|2`M|7hUJr9bL*u@keaA*VCUN6kkpKu4y+GVIof39#@ z=_roJm2ghek2>CY<@P9dfAPe?)|G~`R8R#LaNjh?kNX>rXbVa%uyQ4jsH*(&HdL}85%NQkE!&#?{D$bj}m#6zbb!cbC zcXZA)7W?{L$5Wd;Y|lL-EIn^v)5c^hGY-#N{vr)GTe6VJ&IzSrBV0}8{=wc2a4I%| z+v;72b@>N(l5CjaP>B6+eaWC@7U$0Fu(+19tF}F1?mYngE`p|i-+^~O9MRbCy)f0% z_?nOLpMM>dilqLm|WX5wWy}BFsAA}G( zcsBQ99YzZkvFT=_I1~2*61#MPY}me z9T!>`JTS{UR?@0!cdcuOC8|~*7rifMA$x^{T)nx+7@myT-NQ-Vlbz6GuOYvrE9c6y zFw&`(&&yi0vse+~e2$Fr(jx`;df|WPEe4obus1~!f0%itTgZ%(Js)wX`UHH~FB&`P zHT25MQPabMy6D!+er5d2wfTS=(ETeD?<4XQip&TeS!}y*s#j&L!dGJx>It z`iey%YsH0A_XJ{o1Hk{e1vNK3xl^Kfj=wV~UB-tZ*u!8jt67MPqd+9pv2gh*lMPxzms2*d6=HN}J@_ zrDZgmvy^G_R5)ih`ndBxYVH~l^B@(!o4xY_{Ypfv-85XwO_ogBJVPj6x+N<4Gd}5} zJ=G8GE=-D=u_n-*#=KES4EtUG_GjMc13$8N+k>wanzV^sKU$p^z-Ey$IiL3;b?-eG zmS9HXg$DUd38f&*AbQZrjQ$k#AhkX2^x%*beT(kPOkqD7TI50J*((yLTqD`q>`9LD zo}{oQMQF1Z>_DH+6qK_ucu>%n~aOYt~2lRajB@{~J&DMt1zz?=pn zb|x*vGKUm2wQEsu67ihiHV&zp(UCu~IAeQ4M8-MM!NBwEovaXb*`4V-_tV}uMTwE~ z{K!1so{o5$L&o_x=H4}-D}8nG`_l)+#@Lc(!xN!>MxOMh6~Wi35OaUJQT64AFuHpd z`Q3devG+NgvcHa%29BhXS1)vKY`~FJc2V^lC`RnxjC6SwGM-T`&g|ZX#+G+5bhRLN zGh=4{SdqefQ@R>sM}~{^Y4KBQnxAG#cU|2nWw|YNUVakG1~TW`&WB1%PvQKLVDdR* zNoI?$Lh+y{jcq=S%RZ|xvd1GV*p|i&;oV3Wl#P=7L+tzLg{9`)OSooED$N%VfA%d* z-gTymq+@XT@fV{8u#4B~EcbxeMe>_d&~+cac4E#H_kh zEP0bH#)L|VhhgimtL23lxbA`&dv7zdM)!%p>`FKqSWUz0IBl=n3QD zvzCYDzb5kTQi7Gs{pq)i4mI&zb^Gy8VwwW?j8axhbib-0zg&y91^<#vY+fSDrp7>i zHet-1FXH^PLl{2fuQ<5=uUPRY1(#E%LS|iyI9{xVNFQ?w<9?;Z6d&9VV4i62b{yJc z1F!4O%*>P{%M2Yvoa3{3?PcbQg!7EI3B|$eO`qV3yScR(uEzXV#rN1Cc`SzH+mr6D zMl2h5PK?(xqT6MkF|6Dc+ebN5?dln*Ij%*e(w;Qw?-~qGRir76!Hi~Q|JNBkdVHDv zm1`d%gF6SQ*K8=W(@*SQZbH`VyoiV`!@LX5q+94j;g$AC-Q<96+>@%}j`ydf-t5&H zgyd)y?!Ao1gO-h0t$ZIY*8)gO))MXKOSu~$==a8n_&ee=64M1uI;70!woQoqC`A{i z{zkt?CzyGrNJ&|37#^_(#;?CHV@rj8j4)x(CcDcYrHY?wM&$aU1p!YgB}bBNsm_9# zA?{PelNCJIU$hU+yjGlb{*EEa>lRaf;`$ zpX3}VykaF1JOx(sc`%**Gv-FqF>Qh!--8nIYj_tjR`nsfem zY2Pd{YT!)_4f3JO+pdV5#j)%^4W`1^Y7{-SJE|MIVL+G>c`lxgICkdr9>DWyQ$2K< zWsAdSjY-c@j;4RTheZdR$d5CsUW=|_{1-FQcKQN`TTj@PxnFcoa-@y#14+773UTa- zUbEMm9%kPcnzuZt%eEf0RLg_zDH)0R#a49rRRH}-+9zUmI8ktlH+5{S3i{#RiR%51 ziIDL{@LKyz1Qo|g?q4qDjIag&-fR)?j-12Ue^Q0Q(0Zuf*(b4SlcDoRq$t(wU0zv9 z3lbVW;P`YG5w!FRIvVvU!rGbAY8s&$u1_OPOi1y?M{Mx0WPgt*ty;z2)X}NJdhQ_{ z`Em-QF1-+&=U+uh`FZRxQWR3#nE#9;$XmAtcdq4$DLrrD$DAl!RSXk1XFNh^&>&o` zcqOvJ^6-(*aC0YnBXfm4&z60uTVXhcE&qo(r`~jNNS%0f(jCU17KkfphIBO)c=+(W zICjf|obw}a=4)@Ue1)P@W5+S@0cukl`4gQ`8u}`yT!A&hInrf zLQ;|SxjwRH7%J1BG}z}_8mcYc*PcN4B{@)xj~4wFq+?V4BQ%UVly^TX0|P4FVCOe= zn#OQ-Yvr+Q{!6UPFd?0ruIvoI53^s^w5B5;UHh>grLzGA_dE|X)0ddy zX-1=uRN*_{alI#PLI|Tgf|<*$>$C}74MOP;b5&-(i^VaAzGStuRdgBjScF{FAe(7{ zSn;Y_SQqJ$(-7v$sygCvp%V4`twEDZ-(!D*6TRdt`Cxx3?w?vyY|U>N9eD?vWlkh~ zYvA;iT`=FJY23c|%wBqd6^hEVZo?}yBr4OJ^L1OKJl0U-{+Orhi zU##J$Hv_9PKBLE_MlpMsDFUb5K@&RR(1>M{!WtQ7KgiSIt}fTNdnpr!E0XS&zM}gR zHPU%0L#ML0qcx*FNOxm^C>%E)?WWebCTq5f#Kp`4N=wU~yl1D7xm+zOKE1`AVV&tv z+*Y2IenNPZA#HZC;x7F+D5%?zWVS1Nxpre~hiVcvniv)uBDZB7}al1GU+^QqE6%5)OZY11EdYDMxlL zkD8ws&-)-nA9frW7fTYvD}*!a#ztM4vOG%gQRxic&NiUW_wT|_>o_w2+VFSyGwAnG zB&{;;dMta1X|vd=fN5CQuN{%oWJs(^!0{?eT6e>jR$E^}^%M32E(oMONx3K+Y(i4e zJ!xU>In3&O05KuXjBi?$xYts(qW-0lsegXcwl<~^NfcY(#%^M(K2|w_}-IUPTM!);zDaNtiLC@r5(niFFE4>;pn{M za&F%@UbOcfT1s0xMeDwfGZfi-KQdkj6zn0Xpk+N2$hs5lqjTu-}(Lh z^}L=xp68X^_vdq6=Xo6O_c|RL@(<}w+n#h~h8y?y`5ES#qJ{b0tFWm(go9~Io&#r@Ml9K35!J+Ge^VR9juIY*kxZfq8I{u}X@ z`C|)meo1`(tb@@J8S>XNrLLu#q-|qJ73l`#`OARDtEo`jOLIzRhnx91OFH+$j&|`( zWk!n!JvzgDpbvStgXqE*fsYu`=~j;oT{ z6HQQDI1TgPdXV*d-tmMGZgw)$fwLf{oy2Yje&)Z`pbGUwA?I8oy2f{-{-#P|XLyU) zJwuH~mM;*a6Fr5h68E5H3$)GuF4i4QN9u=R@bu}90mTWxVFeg`ekEc%-5}M_jZD%$ zL)EZ9HU=ou{j>(Gav6&wKIXKQIbgeW^x@Z3ji^4Es2K}S^egCM=YsWie1FM` zLGg*Z@I7P!E}_!e=`VTap~cQ}?v76_#W9x=(46H=>lTcMt+6(le{iH1*a&Ct8LM9M zq5J_WVe4&7u6$ zB$WNO;QrS#1UfR`x@-wtHjTxs-AiDl@e+S3k4VxSEivuRPdLo&DQ+=aBYMbF+??Pq z?(Urkm+w&sk$k~N^A8w!ISRhId}f;c8;bv>LwiFLJl=i55cc(~ag%a3N$69=2xl9_$*#w@BOwyN$cO%8_9njj?e_ zaDID>IR@+x`xVQ3KNFfTWIsA%reIuqH`+Jl90scIz)^ok`g$W3M|nR~`LGYk1^Ym; zjr)!4;ZyDAj$Tge4a;+<-#0_?p1WKQS@M)~;XiSG%K=PZ)Qle!RmCJlcBjiRV>k1l zi2SlloD4ewHHn1(&K`>%J$7>!%#&j3E()`W*RednhXT**(*e7gcyZqrf6Odta?}=P zigv?aW@NRT9xwOUB<0bSz?Lc zes|Da$vej@LL2Gfj^wf>Vns0vstO>Xl^VGBeud;Ye#wt`SehX;RKx zo=B1;8pmyRyPvV1Zme|3)KAoqB#pvZ7 zkoEk)d&3kF^oZYceCNMVATJhdVD6KJ8tGoKq&;chF|33ANVoN=#I6QbEjrBCaHK1t z<#;*%sCeVO3tgY&;=!6G$)@lzIXSPtPI6dMnx-PKf=vcaZgOI))r> z6Gc|}xZ=1JKl&?(xz9?VQfeyJ?r_48Yq}UTXoNT)y$n+otkBuZy{K)DF*`neq2|yg z&R^80oTj13SltI2$tKJK8it3fyTERl26a(2z~7!dCCkgSNh2}~F^jHCeig`3=&1r^ zsd|eV4_#XJE(t1m$|9Dz((QJypy0hlbnP2JQ$3quaz8?Zt1-7Fw;K0n-j;-YejxNO z`O}~U7bJ1Bw4gO{7%4l2ikT^LC`e|0h*vr9%$$V9`V4pv%ZJjxq2k8)EbPc>Ku7+` z!kdamkRSFE{eKvcjZPQd=e2NllKYa6UkH0<>}1IDdoo8BQ#V}0_FFbIe|QDfe|>|o z?Iz^-r3L|w7jZYvoSxj`-S4a>Pj@YyS&9={vK z-~L<0(^?hsyw?jSDx=w*qC;;Io{PK0?$3Kl6!cbwN;;~cliZWmnrczoicY*gX+ej( zci^dJ9c=uasmtZ_c>U!&+&qj(c0?LhTU8@QM}xV`mr=4$hW_yTQC%?zJerp1+uOow zvOl(ah8KOgI~5~}jiEiUzi_nChgr5EJ(zwMp^k(jw+w00s5=-j=%XUgayA-GI^o-TTkISoA+b)0-f6e8XV;HHC$!>F zN)sxa+}M4sLexiw5*^c_vBIBpZRJF~P9l;+0%+dY<6>pVWpv*5r(BtpYi0)}sU8M&sdN1zIm}NaYm{bR=;OUe@T*hv^PC*Q#rlzPyius&o}=|om5{HQ(En;LKIm*lY*)6%#n4ZdO} zTznkpe;wJ^`#($eO6k+4l0KC2-~}`K*=4!26(L(MVQbo9>@$@jcY~)e`OcY1`!{&q z_7X=kTT%Ixy<4_Fp#AbI{^sRkuh}Q0Dl-43asxC?O(<}g14#uHLd{g0CRF;+B$Xoe z2ARJuk3SnLCai(U4hISw+m+VrUXNgXKIcev zCFx><>81PdHBqD|7ZxIA>VC}H!e`Kw7>ry$M-2PfgJ#T5!?azmMel=V?`IuckKkB615l0OI2ueqoO!&e3moP9q_pS#&7+OEm-$hgZ9>T zr?KURbjQ_@E{@~9Vx2TIR|vXp1OYz#pgOfrW@?&M{X*QJFm^qG*-g1akEMKZgw?2f^>e3B22?NBRZbNL|*5$%ZOJj36y`>^t@JZV+5jbKG2*A>l%Q96#dJ|ME7)%$oT48&&KFKewSfP4KjV*{hAv;+?^cS`9d<90b*b zLu=kxp24hw)qNkl-trE=I|UU?F~s08-mWXB5wv=pHfYg;Mgn5BGchlpscB(gBp3_T`6B~|)5&bB2X^K$$yak__ z1yWe1Bsth+A9^?4V`rZ#O(=7eL@Pakhfy0kyU&r>cHTpvofhqrtQE(u-4&k8?&9D| zR~q^(SEvlk#ni<;=(hVi@m5la$^84s{-H>BHrnA?-!0H|)u7qT6StfZk738PDDb@| zywvAmvlr(khW&<~|6`c%(xE3ic>ghy_oWKDlw2=M=_yxHSZjkO%Wkxy>j~ld$QFT- zHnhL$nRp=I7s^S-6lSU-)-3!j343eex4zky2JJatv~YWZ#MQ`@UZ=H5?EQy}TVp&( zIrfJn`_)bKIdDT5N%fILkE(|EE*UIzlo!@J9;5c0v?zZkMe7SoB>v}>DdUF}{Rr0- zgQVN=eiY{*UhNX)1!{El-5ccmG@yI){vuWDJShl{G*hcJ1-41-Z83l^AMV4uc4L{8g^gMQWg{ymGnr&mLJ(+v@S?LHhE z=Rs?$F6U;Bq3bXS1z3)U$~Zn}M1rPT`l03YP#jeVpmkffB6g@2dIroELsqDeM&dLa zj@T_$#B0&r6mw*&r--aZ_A6`-#q|-k6!KDmo{ry({;#a*1)tBu_N3t^^KP~o%8=sv zJvip6A@0mQC(hJ4p1_)zUjG5Nbu^lKmUQuF9)l@n8*DCBdfwOqkJf>&9p}DE_V!sEd43( zk8wF#*wI6W22^#5Z|`Q}NwN}s$g!c4J#P`N=1YE~4XO5F6>PJ3K5>CFD~s!)KGv0_ zmRI49qXNZW)1W)2*;87~^XfUqbb%S6Uz!!kzRHmPyond>w9GFst6C(uWeKBk&5}}O z2b}o0LHMWyi%mOH#o|{sBp3J^u?zl+Eq->SwTCkSS#DVR!kl(Ge1q1YM`HEHQ26e7 zf}gW~h^5TdyJuDlqc;WO6yvy$$yY+^wg$=v4;J})vNYwB9I2T15X#Ao+>>hOcYuPQ zoVFsBIrDRM^iHfRn!_%(k)mL31~Voa3$G;>iUk`t;lpz2t2fR~7mGOSd7$PyGTJQ2 z@T@B$SISbZnFD>vtQY^-{Zhmni7h)k>9<}KC{3RNTiwWLmcTXcmvt#{q@G8jVL#G} zRz@n(`#VwMAU|^drW=xM)o!7_KF?H@Bj?NRp%)*{RPcygt+E^hs*o*cxWMS&M+rpgPPnKi( z&-gJ}Jd9JL`>WYslNcwq)ksq)bD|yBToY%#<*5sE#XGk>heD(mZL5BRO##f$@%E)fVRl*%8g%#y@>9X@U3OqBNy46l z0IIgTk5LVL&%RQCdYM~zay$ifO@}aQO%5jLhcIX5I%Z6=ps6a^SazfhnjLH?NFAm+x6`eaC zh_qc%aCVCp(Z`E;FM0?vAKHbTf(Cb{qT#it7oHCrfbMbZbDTQ}{hg=b^p-&+dsPc> zmoLZ5{vI?!PLl@3CpJHBn(ZTv?OyZrY%6tbJ<-*Uv0ebY2XWGH~9})DH z&rQ7RC}uX^g}$6|Z@;h=*N6AVx*PCU_)l3a247-H-nt-q3iH zfcb;BvQM=X2Rex7myYPOyciE31X91u|B&C&4AnpV=$)Dq8hUW1KD!S5_QtG<$>^6| zg^lvx;OKG%(r(o-nEwD;t4!#~3+@%1s1(;9^q@jJD|*KJl*iqzC|8?z%*LVO@xWfp zecz2er*iR!oG9biA<{dbV3Arzk8ovrrk=5d)?RSK%nTgtpZa7-{ z1n0k`;sE5aZ}4q&9a@CL2WF%8^9j6+yNz$Iqwr_a3&;#Ag<9}BwD|rKEu(udmnIIy zUF2}Fs5|eO&Y+K`1MhF_*tfxVgH^rB!nzNASnH2h5doAP>_?Yud@x|RJ2h_iBL9bL z5b^c`&Mo8K>uGivZ{l3?NL9Ksvp_gYilH>G1Nq+fBzF!aiKBhbA;Qg@Dk3XH(#G2u zd(fSH4xN;AMIJPtd(s}h+Z+vXN3pjXHXqgE{eHQ4w9p0WF(&kAWDqtUvx6+msmozm z8v6VzCVjD?TZg*R%J)y1O>0btJed)Ey9w17H;I-H7F3$+B!1U?5hce>$uA^I@@Pr4 z2upUMitk&*W6pKY`r;%$H*ikDj}fb*b_qS^hksb+LRtGP#ioty%#AIQ7|uA31FJs@ zrD?52_Xg(S@L_2UcXjSkRMN@)4c$O;_#(&ohd5Ds;udoKD)Ic_dvTq! z%myyEpuBva*)=J`&PIzG>|^2F2$o`YS7&oM8C4FkkUCA!*84tA;D5~IWqtaZ?B8^8cp(7n1=Mu z+2YmZPJDl@f!11gBCnsgwBZoknt7+T{53PyPcj1<6x8P*c8*Lz`t%6l^I)jh@va88 zS7OB8pKFU|-{PHD)G2X$Yl_&L$!CqxHezvv9DI)jNa_z>7l&OfuzJD9BBQh~lI?a* z=pwsPJd7+x|9e_uXxe!kyT_S@7TK!~k=cmteG<{z4v3zI3wh4@1*=00XicIm?0UV% zUQ=t@xu3l)oZT=F??$@5vUp_t2nIz?w94!)=0x-Sb-fuaFyk38cLaazb0bH5L4TiC zd|nd;2KbV%qDbc-+TQT&q54|8yWUT!qww+g`nf6wQUF3!B%BpnD zd?NlUcrKQG)F!D?_Lxkn$D{=knj zL+nj6q@_F)k8}1%#xd@E``5z0WD+7a8In@>J22AhipsCN#g z%p8%tb~3K+>%;amd(h`HX$+6fi52_gzPhIGSTs&P^Ur|Ei$ER+iWQ9ZCSe&Y_-^mnBUt^-FkO>&NVMJ$l(Ej6J&rs5b3NpC`=YXF)3Djk?f|;e53QDzv^rOFW9pLdD;Ie9k@N_kG0{>?~HNhO^!2)+<9Q7;MP7BWD^{XG}^v&FSZP zLu&C*CZoZobfMIq7M@&=9}UdNkFlju`g^haKYwy8ccF;Jso2~(h+@WE#k{`Z7+sTz z{E-)!6`KGr_c(MUSP&0*dqWXkLB>X0C5V%TgI~Fg=KhfgQLUZA#;p?}yt^ zIqLbf14lpkh-|L{RF34`@k&S<=AL2!dl$N6e?d$b!vFm@Z}BQjpPgrWFvxPGNKBN$ zx|Nsl#JXH$X7qv~?*Masmm&J^VvMizq|qj0vHsmIb_jXX6Ps8p_%Rp9^ZL@s67GCx zP8OR#YqQV8oCa^|COR_@h|Uu>wEdcfm|x_N`G ziiP#K4pFn?vly$MDZH4q9RK?pp6whWy0LR;Xwwg@?q9=s0DIDuk)=Ie@|b4w6pG(= zFwY0uLc$iSK!0G~;l#c>TzRVs@BNNy`E;^D_6M18{5KerT57y_hNjPTjHsv9o5gd zjEVBA#M!D|G;>27J1V@nqv|V}BNGmDv%XZkbE`OWDGD9TfHzj|BH4OsAM`a#U^ZTr z)=zVmgzvnIvpG8C-YZVL+y4;Vwshi;-f+qDLCkqxc?a7#i>|Wklz6!O0(M7RkgNPp z5%9DUnNrrIGf;^V=GdcfA!n&RYw*u99XDPj;7GnE#V4wuZ2l}fVTSA4npXCjJi@P4 zTJ)y>TXb)D%Wfzg!uzh2H1;Zd+pMwGlU=j7jtj5Ge)#HWM5WGMgqyb|ZhW($wZk8Y zk^^m$_J`yAlEUoh!5>`_srjF{y`~o#56>zpZyzX8;CJVVX}=^YXK%7U>V}Yfe(dKe zxKpu17D`XwNVfJVLBTI`k>5v}KBGk9W28cgd;Y;A%s^Pr`i+Q=pGZERDx$xt(*&yu z#4^A2+?c<3Ui=Z3%J%eB?jMFt{(caO*Y zLE1cn?oV3}Y(?;HJ;?j})6&rXxZOGsL9w&M9o~2BsF{jSO8Z3x_o`Y=%n|Zsa*Qp4SlX=j_ZqhU4o@X!-Gvczkdnrj+QA?eYr5yy`%x+In1> zDCnol4`xhmLc#LkbTfw?;pWW8{@_mvANkC))D=4HtZ{tAJk)0f$oj%(9$zWMm`}q5 z6=gnm+t8U)Z}Fe3FS|Kisp~@x!EJKrKypt?h;6fYwOYx5V zs`$!Gkm3p2)M1${DYxLeNT@gOnR<}=9yc1W(3hmr-1t4uuH-U*Ip6-e_MoKy2UOc#=lx7RoTNMOW#bEY)F{vyBmSADzsJQd$~16HIPCPk zVRVKxNh%KGUyvo$NIR2FbRM2_7qh>3hY4`^>@Zz8ewsYeax6^P`!$Ev@@aOe!;@LJv1OTCB`&R26z>?n`S-v&3jc zMc&a_(vCYS>?M|=bCEaT-0u$Nd~_m3#Z!Lf!2qDR**#+-TUwL8GIBpAu zImywMQxRh8t8KU}tJ5Ax^DZ$K|SPn95xK zCnkF#qn3@YbQB$vC&7K+P1wrmQQeYLkS>s>{?`@ARP`L*y^tm6cV@KwL>4M%D3i=$ zS+Z{)CQi5=fz+JO@JX^2i|?=ts*5~HFSsOh++r~=^ODF~WG^nQ-H4aV>xAq1J0fV` z7R>UK!uoRU!lAvkad(CNv*rEZ$3E}3vLV>yWeA%EZd5)r1g?t1klfIpY|hEk!T~Bg zE3Fo@<29%?U}n*j5$)o|HW@NGX)F4EA0-kG@5Sw*0@4+~#A%r`h*DQUFBd5+4!wX) zwPO%JQ4y!CwUIQyg62v8LgIr45z@gqr`8r!s{}wUg0p3Fq-e(|56l=^fby{8xVN%5 z`fJ?9rOs`L3L1&5rO&vdl#P!SFWDh63RB-2QkVXmd7d~0^G4dx>W(K!Pu+xw?OJrG za}~VVQ76s*t>dxr@U+n(wICw(^-);Qj;I}04y3Kr0rO)QxbvI~3@{OKk>}pR# zi@0m&&mGH8-RN$%Cmd48;B@<9%o#c#=k*6-=tW}2Q-6$E9gQziVTe|^47-6pB}ucj z@$y|B9v)N|^V5g1^XesbXIluDW<^}n+KLp#pXjmXEj0X&KvlX6rMSLFyBh zCI+$gWq3TzkPe<%j=y+!nXM!Hh<}k2XFHb zyt5GF&V?hntqAK5Rig8K3<9sDp=JDaG%T3Lo;Z8{{yNb1N_MlfEyUpx2Z~&G2tJw2 z-j?Bf&scu<&+0*EF4~i_f;RVrU1)?=FEVXYhSFq9+WpIlt`8rKhc7dsAj@p^`Z;1? z_<1~2`HX#&PDmQ8ixDtQj>=vah@LmDh$}kBp=9Ah->#k)XKr6cp0f+jSO1AWGR%?Z zzFx0tEs{CojE15B>{0H)zD|3jH+kXkXf0-+NuhC|Au6=^9Gv|P?mR^o?{U9vrr7J04U3yfSQnoxNxRKH-IdIU+PX!& z$ve&OhIFy-zA`;X)x?qU9q8QK4&}#ca2=ySAC_}x;r$m8?OKV`UzO>FIrrN}cc7$E zg;wnHp{Ag&v}&I={V{VUkG?fnx7k^QT;GJE?iVrn-}Rz>9@{Z7n6rZ7xad?$L%#oI zy#Bfl-fK0mcE%mV+#Qam^|ILHSONX%qw#gO9ZEeeqJg<_5qrjBw09pYUeS-4{mknq zHp1~`fz0O~jmzvNa(Neun;e7MkG{;jtHU+R6WAw}E3q{Kd5!*qnB9S5!aR1&I>_=&V7nM? zGLUp0{zRx^cTss!1-ZFeqH&$7Xzu5NM%8QL>(i^^pNttU`OT4h`g0oV2CR@YB_77J zssbp#yHoU)xmioi_G93!HDd5d&RY9bz_-PK5)SF(c6uq#q->~rgEscHea9i)Zd9sV zAtr_0Lf%1J_l59gqgA-+mXNgE<3KD^G)KnDfD%lV(2a%iqu-pWD@t zonk|W+2b~)8+WSbcc+M07h-*62oFOX?7mG=bUk zy=l_XPFUt`!Z1F6R8Ibit#iGxF>*FkN55kCNI%THY>Uk4?8RK?14EN_7?bpl8CmK$ zzR|_&%70z?j;K!FX2D{(InRPNDpCHYo>zOlQ=>a_JO`MRg!c3nNe|-*B2g|F$|Eu* z@0^Z^OF5bNdpWqkZtXd-p}J1oH2)0eDRwlfW3rf+)(q>D=9F_@3xm(`?rM-TmFIYn zd*&`|FxKFlu^YW-Cd|z@-O1;hBYA9GhNaI`=<7CR+Mf_1$+*f)#BZJO35^u43tTB- ziXnxR-I2uj+LIZ3RUZ$vq(2^(5{}K%giA)$nW*QtRnwg|q&bn%?3p4{tsY^~{CQ+< zkkf6>!uajsjz}kvSPA*bX;7V~OFCz2p`63-*edoR2GyW~V#ui}S6?Wog_oFWHFJSh22S#pirLEjYKL4Bdwe$Tc z_L@59agJiSIu`9t~rUkYJsL@DdzP+)#Z ze9N$N!xIT-BHiaS<&>RMx+*HLQi!~sr9TCrOIegowq%ELab?-9_J)1 zEh$+i75!HTI@G|k`@5+y2JWAJNzDWY0TX_#d;^p}_8;#gaXay*T{E$oq=qCqYzGtdyj3`b*ix_MR|+fHQS z@k14M{8_{N@HTi9afj{tE0KHp6q041iZ@HEB!9`;XKJ;3hd5Y80X$-O6^UZ;oJq$A0Xzge=gqqW+%}F zGr#Z6@`$*iPs@uG#LRk5ClzmkettiseV0bjiYV^YOCfAkx%jX9VK}FcfV!$KG;iu+ z@GmpkR3=54H3E+xakq$flvfUe`$#&Ju;mXb3Jh^~!xtQFzQ7q4Uz}BLMrG_Hqy!Vn z&NU*k{U9?oE17X-0^hFolzpHcfqlla6N(+e2JbjW&W_ejBT{-Z6+ZmA{=J|V$vz3k zy_v=o^q(!MypF;a59UY?@}ap;8*r%5l(MC4N$+hrD%bU-<#9bJp;U%?4dUEcs0{^| zctG*3D?;~gfnw)yTt6}j4`s$f`I`<(H~j}Mea`9buST6mKZ?qC$B>=+TOdp^+#yti^hjKe(NuNk@E?Y0QvA z;?K(Nq?4yjdR1}4i2oVdU5dU;tdT@Idr*&u@d#3hXGfVMSufp%-W&KcHJ<&-yG~;0 z`b~Io{V~t*_>2^+4)3Bbh?%sE`3goTy-uyXl|xrm8mfpYBM)f1#b2pT9_qzjGd)UAs~+a}7WG9m1A+ z&LK>HBkB5ho0z(l?@Mj{sM4lIcz)W0X3M_RtNfr?9C?#7g}$V#qf2_NBQX5E8%$mq z&;TQS*vBbA`KUP^4qS>JTLwX6swr*0BTGU4d@sB2NOQW&P?A|8I`YQjx;XwLl?o2l`y~K}bL$cr5n|xiiid(5x zJa6nx_m2FO?9NuDo9APM!KB-$*EI)wDE$<6K1FqaE2brE6=e^u!!_rj@SMl}uAAJG z9VAaaK3(XF|La1BUve~ddL7=E2Z+QR?!&}u(k#wxmMb;Dtcklqj{GdkD97_}MpVXU zhQ^*12===xs+Xi<)#L(-$aYynd8flFK0d>!A3W#IqxJUCEFLSA;j2!~Sf8#OJ0Q_O}hf>gK_c{An}rY-u1C zwdIK=wR{dA;*C=<#}!jO+TL$CQaOXYJzS0sq!?gowxQ&Jo(gU7I*rj0I-*;iF5Rr$ zjaSWE#1(#z1h2RVZT0zLHTS>c_~(y&cTZ%n^WoKrD&*Fl7bVs~RMGW2I=WU%G|qPj zF|WoiDm6~L(y)O+FGsSvn}0@>Om3BU`t(HhxiE1(YMvMzb6EVm%&fqm*YHdED=u@+f4)pJDqh_d%QCnh z>Qu+yb$6uv4#m0DC)jZ3hp;;@fm(S5wq4Z3UYR(oc~gVvWBH;*W0*KDD@)tj<*4Dg zjX-cC>NPviZnM#6es@Ju*sn$pM(xC!E-NLIE{_y@FJ++Ou3C}Y;A>(6XQO;&6|PPy znkGuz46(t8SpvLc{;BSat2<>Wbs{qY)jx~DH#(ut<#dB>o)pm@iGX;0+B(RcW@iwB zdvjiMu_H+}Z^ix?E7D?r`7W<0p>`Tnrq+$L7VHw)JH2Qlchtsm_oucy=yrw+J(abm z>Rv=yHG+nYuwu7kATtNt=!mHoIo$Q52OAo2_)Z4gCP7A|#*T~vHEH~hG|W0{OAV@W^zdvvR{i#%iMI`C zyZug7Pk8{vYkWWB{BLf-8OR!`k#b%LOiW6V$bQlB9}c4DWk1f-+tc-=)6iIbQ%rec zMQbk|fkprELY4ROW5e7?VN9}^zn(dfV|*$5dz_g3S(0UFcT< zbYyHE0*--VZ#dJoQAzk28pyNL5-7ZhM&h0VT)1%)IXQa}bUPj4`*WbUGz3)_uCt@r zf?hs9k0;G-sN?U(+q=gx-Rdur6&;u>#D9Ke1zNOPiDXWH76Uj#-9z^m)|g)whZC|< zX{bu&$8}KUnu=bRbA{Ke`yyaN6y`~95fRBn!gl6iq|Iy>{f?`nn6ur9pL!v)CJ^!q ze5q*IKy>z=j@8D4$oj7)uG+3fbUOQ@f3gSfPM)MseyiBc{H_-bW@1@nu#lP2oeD=s z6*dO_69?vpW6JUV>_4m&mv3yv>qi|z(f7x;ekJHSZ9g2>_r!!=@9<^OR&?xM%JYRXR8KpL zm8lQ#^MwTUU%5+W_8ca>n~GnqL0zNEam{KXb`PbNodgP7MwWeNE!83Bz0l{beD(V z9y0`NRYqXSt?8KND}kyvd70hq)d*_3kMVJwXRb6N!_Z1`#le!& zZ&{LAlnH9ItZ2;*3L$m{R=>FAZm7^^XQv`~tvq!6Zes|0)v{`4WB%1loVU4!n~lS9 zF}M_qvY+F2=W5g~{v{sreCK6g96u}M;60@~-&f9{?T9_Tp0}ez#hWoTx;OW8`jEeD zU!L^`koG`d3jE}YYenwVr`(H{>{$!9E*F_GB~LdB77Noa_wi%0DxL2r5rsQBt9`a*6X?Thn?HJ>Zd$n$mQ_!aW zzP}Oc)0~-eVnXY`2f^TpJ#KY3rzPpK^ep!)!Z?Gv_gGho=1eE*nRRj5VD93EC#Q2e{JUWJl4T`Dr;#f-Yqzh>~o^ zUoz(+YnizSx@}7)8J8uyZXV}+e4Qw$9U#_w<=~{p8u8{sUD2EFdGOoxN383jNYZcK ziS8P5baf&#@;?3%d8?Y3o6ydC;h&D9kyO{}(e+Pm)Hg|rTtm4F zwmy)WKQcf6YOH8kehVA5D{)|Ntq7~Thn8J8nfsI^(Fnc>S({Sq;$5@UVjDO(KE=?m zHF)r35KgSl$JiN5Fm0zHYF$2I=@`M^UoR-zTH#wvAT@nh2H9d&sAUJyJX0Iq>kh%0 zpe!+tdGpq_0WceMLkt+JMXg@bk^W(p2*~Zknkkxi{??t$CjG>zHHWbD9&@tTwHQ3- zBw`+dG!y<}OTb<%oe&{r_8unes%kLc$QGgB9$I9x=N%IN%Mvfn?Gjpi2QOc2D`uO> zVfA~76YXok@p zAIj!kmHW3A;GqkJrq@81KmS#UU1(`R4NiY~gOeK-$nElb=!Pg#7k0q!>TKsTq(5!w zc0p*EHH+QR1L#$gDr)L7#OtX+WZyeUG+c8f`QM)~>&yr&m~Bl7zdJE`c~9)G_9nZv z{}B3S757ud;Kam7+{^8OeHZnxYQimad&mB4)rC+_sYUz6t|+TmDnXB~G{;evBC}@; zb31h!_?USddG=RdzvoPfy*zCgyB!^c7Jm8ie!{zMGW>Q~NRE!#E)t_M&~bKM5#?_e z!=_Y;s2BfZ7+KQqZZ_D~_!C|=J!oe@lBnf*++jH*3cu?}DtC9`_jFatv2>vI%l2T# zK2!2lu%UxH=b>SSBE8-vPx*-wA(&~Do6KB@ju|3P&W@Vp{$gnARq>Xy();z8%U5qh z`4e0u7lwIJNs$?4`6Nr0D7esm!QbIWXN!J6u0qJKEaVj{(#q2N7~baq-UuoB;Qkb4 z?b#UkP>-I)m*eCHdAjv)7e2N8#XNTSuI_sX7eilT?jjZXv@Zk$^em|UU?0l4o{KK? zyHV1B0J1%KmNUf8G~fwy2c};|@6P70=q4mr zUCCYbFfx~(5MNH)(S|%TGBK9Nq-bfHXsbaEPG6aK^%sX4Ea~S(Jg zT3_7BMC}sxKRbs%zJk=|r(oBHJj`Xr%Y*E*qP%DUGL)HfwEu?0S8oISc}_KaN1D)` zz8N9w>yc<>N=rs-Ql2I6=`R`5o^2-7?#vu}X$#sNYDnp^R-_tlN531>nMdS7pRLU3 zq~m2QxZ8(DZs}XqR=f@l@5OQp`Wu> zVc{NQT05!-ofz~E>0`7>O~#e%s&3=NMME-lbSA}TEx5SXn52L8qK)a3Ij=GT`z&{3 z?_3+CZ(NRtJNM%0s)fu7nFGzgOZkld0QQ2)6Zt)TX_j< z4}A*p);>#7j>oo8T|l<7}MrkE0`O_CX1 zDa84v_c0y>>_q3-8JM)K1;#z*qnoZTOowLUsO>eZRgJ=0YxY*oyNv+370hET zhOE+SjC&Y`jqNdLeP}}0=I=**(lo5+p5yYcT)cGHfnN0v^yJ@O&KNsUIy3b)r}%LG zQP5WfPdakm0|^5i==Kv&Dn7jmwak8BUm;J~@8*l=E7GuF-**J%RY+tnoWzDm=IVwr zB1V*Ep z=Wt`HI0shiiCZO|BE(mR8u`98x9=6i_0p$>+kfDf%@Z8&(}U_RDNwb>>o3U}*IWgGsEH3>8Y_{AfrXP2vT_>v$W3pM~#s^Tu;7{l|zF2Zz=d~!F z;ZH~BCP;tLE9oYMZ_lNePoHB9DvK}3Yenr8RNAYj+HS|^Y z6I*uWV&kqC;CT;Sec1&&`156-qeTtOo371%f$#UNsVqtb=av-Vk*5tswpL=op3}_y zieBAF)U3T=innEx}JbLVrCF?=t-FAjXb#FuMP_iPxAI`dW7 zeoYjUPjlvBS}*MXoh+uwY10g+Pr@MQpV<0Fj=H(4GBd9l=68FNL#8%W%C|CC*n*}V zl;X2FGwSkOsqFqOTr8HN?ab`_k)4g~H}5fYup)Cg%8|-hW6P6r^v&S77;flK6+J6O z-+vE;dN1~Lr5_c(nx>-b@c^$<=nn+ys5NH zY42Uy(!Q_btW?O}d(Vu=-ZLd6Gg~F0Xc-wz$%@E|P&9}_h^$J#^ZWbH>-GG>(|z~( zT-SLX$NN1>qEcHbv3kbmpdu?;(>E)iH#1lNH|Jke`=6LR%#kYMU8&LcA>wU!V)+R< z@~yuC>#nN16%fm)L_o zt>{YLIE;ieL1V9*QC-7&7^Ifs{SyUBVy;N}=9lPGtV09Kry^y27N#thr9#;f!+%7mux zOcH^kRmtSPZ`io%Iq$>0Y4_9Dc&ou@!OiUB>U@U-4tdzJ(w{PZn2L}&doZ~}i2|PX zDtx2IGnwDr=qTolENQy64K18wLao6(Kj-)9 zg!jhut}PL39nJ>C`SIP;cn?}WPZfz_X7qM<5!&x573^D)kCWY2A>m{Wz9b~0pd%E$ zW?ex9&*&S2;;^jx2n4${@+(i^HnVXK*qBhpgCrzP<2yp546PY*6wBXzMG&(`Yu9WL zo6<{=a;6GFnSLUz;3^`Gq-emdo{_v*+#D6|n#dvb-qU zZxG~bSK?GfUyS7)wz`oLi z@7hBGmM67{SaVA{s5exi>CL&A0qbz%ks}_es3T?mVl3}dBeF-fi#z=PBat)2R~;Rk zy{871W!`)z`GZrFf-vU=d+)v|a=*7m%y#R-HxLvu85KOjLMMt zz{xp$W~jlRghYgOlp`?P5|;1HxwHQUm72lq9(N(TX-|-KzaN^H>5y9TYPi?w(bF1F zQk=FQy(a6Bn;UZpB^xndFuR_&JJNWYPH0M7)0uC!q;T^cHgq`AY-4M_LpI@UrZIhP zb*0^ptRW@65O?QphS51YRJ}WlY5O;zZj>+fxlcry>QIy?JjLg)MET_b(9gJmgN$^x{H^??-u;=?95RO)k$Zrh7Ro;mo?-;uO0 zoJRKtaVY(94_#)A=QE2c2DIHof8SV?i`JE3 z2RpX?wdQf&{1DDY=VID}4T##`DxUc`lkbULSgpBTL=Etw+3VvWmnjXUe$J#Xbq?AW z1WD%vk^P60>`>@Uk!1tu^1nRJv@o~NIDjrzoX3H~_py1e8nxRy2PF4=j5VV*Y2f)t z5x?U(R<}yi*s02e!*=N7!1Hs+;9R=#NPm30!#%MWdm483vp8Is4P={Bf6mN!Nx5UN z$3(a}YLVWjpFEEUX6_;LpQ}fpRAvAcWoc2V<2NiTeumIr=G4RRH+DAPM~1NmS=2tk z$}vxo_q9~)zGXHxfO~+b48#c`jr)TBN7l=l-Fh7^e{;hHy{aXtui0UU^ZhnSUK^JGK`_ zTgy>OUl%d$qYCZLl%?2Q53wb_4X#K3aOXHp7_p<^>3LaN%z3kqMy>c$qf7<-9Ev~F zjEB>^(4s-+ztOgh0qkS1PW50>_YIiX5!(}ucZk06YW@1MQ^F*p6P_a)L zvqqKRQ_?uhceTW{*4IeS2<2Xu4KyN{>oFvdP9h4EWDkqwI=!j-`e__`vi<+_Oas=h z#=B$6s5y2`jIUr`X#I2yo?9$Z!qiElvpnlFq6-t8Kw#KSUY+>COL9$qgN$*$xg+8+5>4Ozt6S1$6&=@ zqRs2WkUO0V4~G>=x>^|%$4!9cRaGk9J_mzOdSd7Yc5l2ircW#XVg&c2mU!CG8XwLx zXS!1;_b{Zcc49zt0JR@XLz~g+bv1$>_UF#E@yPL|)cCNz+|@q;gMm#K<&u8+bBT$eLo;KsgDf2w@xJD z`eFmRw9AnqxJzuV+=a}TYjb$R9LcD@Zq$9NEGZ|uila(BNNmw3zvQXH=C%tnp1eu+ zgegr3uj_k7n=q$lQ^nW@wUZ9RErt1&Ox-ir+GER!75Duv9u zQ2O}utYnJIGiG~u&?evAV%@hAe389`6{`X%Yq=^ZEltPaz;M!v)FXT5;w~Q>N`hxW z>UMU}HBCX(Ne7xUP#WXn@(`oxLN>9MFb+S0QGX2SQOZG-<>U$ro~evjau6F2Y!nOC zeW}3sBF~kju>ZFsWq;CRuSO8Y-}s5wUyZ2th!)nmD$q_fLpqV|gIV#<*^6`*?$S93 z{Ov*6F1z8jKOO4xd(gV6=h+K(1lI<7lX3kW5tF_h3rEONmHjKRYuOQ=KQtq}-Cn$X z7lVn@6seP+w}TuknSXChgI?;B*=s90^h28lZq_EtUGCJ(%*T1(U6><$5z93^s6S^% zHikcj)m2Y=Y3NV)mR-cz-+olO`zmKg7oz8jCj94K0`;NMc+{sH`;|_i(mn_-e`}z_ zJjuUDA7PfxU(WBT(ZuuI@3EJm*ZEeIe=nE!acwwQ=151Rd%)>_Ce*iE(CZ~O*s^~w zbYHlUgX|3Sy`B!K+7}|QYos{LoQ0`t%SCK*Z;_~(g0EZJM0o5U@!;k%C_UFiq3&X+ zR!fTyTYBSs(gg3G+=@}zjt)JvUYOW5xbeMfTom=Q_t zxzG{6eIg<~6=oL(!T-3MI3~G@V!!D;|Gp^}*r#A(vpyb=UM?miO@i+UN7|S9hk1d0 z5i-|=mS2$}r`Iu%XP3!1b_86X+b-6Om8LCoa-lHU6;eZ$XpHGOBu`d^+3-#ref@&x z42_6*Zh{0oTUs>W6HYxFj1wE3sYvP@@8aDMpQcY;KSd#27}3{1Jbzoi8?QM(e6t{w z_E;Z={%2#7{oR9mLU(cZlOZ{)8<9=Nb!N;kcSTx@hBh*v(ZiVT=NOZgt`9SgXW+n& zt+@VzXLU`HaG5_H^2|vYeSJF=D|chlw{k3B!I`qHN?4rv6^COuKOW9`TxKed2xDB( zXdqJG1u+GG5pq$U{D!Z?ucQ{-ap^+)%hcGH@eP`Tv}kpl2`&El7w5lS5$pOGQM%?= zwC#_T{E6hfs;UxI^}8hcF7Tv>!DkR>mWuWTu5@K|HZrBRp`P>Hd#_wZUR^FiD}KOz z_BQ1HQOAo9jZm1glidZu@F|q0u%cP8*=dBQkIRv?J{3E@#i4d$Ez%TjAu(?^Rl>4IJaXN<&J$#j&y&|7RcoF#H4Ir@;SN#dMjq(*G4;%-Mb5)%6gL>-(`D*Zo^6Ms?o?5M#`{9Q8 z=vpuNuPg83O%+AJr3@jtmWqD%mn2W@$A~=#o$QWuN#*`WXg>M}t%!l7@uM2c`x(<-PdoB0Z{hc=6*<*-kmsRyP!k69~1k22|N5}8dhNKNZT zKQCMpgOk+o`hz~z$#8b>q%34MGKXr<3$f;(plmZm8ey;t36DV^=P)nv;aY4~9YD96 zx=>^DF?c)G34Jpk$%h&2y!F;aeedw13D4idFkcB5>m)+6q8&joqr^GSMq%7-vE<(D z3gLCq2(ON2OOyxgmIP&*!RV;9@YX8l`DSk#JSPFiX5M73jVDc#I|;901vvhG0O|b5 zU{_8p=kydwY&XQ}yh==Y(w$;d6?mW5j`b6jXyAQmJmzf2h$168)OryS`uRAvO_N#9 z7ob~v6uAZFB>7bi|A7kp-a3F~UcvN}dF@YTCn7O!2*o*cBYln2=xZNH?>-x|v+}LT z7Z^h4wf)TiTaa?EtH#Mtc$wCN0Ijz;{#;4$u$>7hnDWRz)cybERTlBes; zUfvMrNQY#(_fyk~8I9f834I1vo_&RvygH4!Q;T!AWN5-y1)Bd?AG7NP9mu@G{lrh) zpY2E6w@w!2nugesD&c;-0ZKx3=*fu!$gUX086$J*9ae#y_TF$?rbqG5&%np72d1uH z3U&5;tQ+x1B;Ms&DKkVqRLeq>y+tv{KL5{|EZW`w`5zs`UYXQSSbe8Ka;oAVG6uXu z-W-Rb+_`ErSw@<~_kCzl7%f(>aTEvT&!Z_nTXOSeyok4iIk$uNWQl}^MEVj%gxUoV*(@5u8g<{n>ogyglmn2{+&H{OGE0ru2j|I z1p?>DlEJ?Hus;43PiA)@xg-PAR=$O5qYCv&SOa}#EKVBjM$!3q5qQdt-Jdq}!ul2G z9n5IWyk7K7>IF80EWoNjH+o>rcf721P}URlfIVosXQLrA%#EJ0oB5q}B-HKryXM7C zz*c?_9{PZSIn3;^o5uag^RVZv;97Rnw>9k(;fF2hLDw_*aiWy}Y(@ik$Gy_xp(v4d zr@CmT|DVHUT~()Nb-g%md|ag1{)6K|Gur2BF77-~rirJ%W1Q?=bgg9{bwm@h*ebCy z+JSQad`0lMGiX`ei#n&7N;Wy~#i>Rm+LRtyv^;A&Vs>hf|2S>Q@AD}r`}7C-{hX*H zS(T35)2E2DUX+!sMFTpm*ty2eVG9+CYvukDyTopqti`XhYb6tJIn(;qC0L%cO%yh9 zAGodczU#!_Y)6~6 zENwfU0!Kc%YVx^Ay80B)j8$lyvkVzNJ|r>rK9AeYoDY~AFXHo#!~PF*PyQV*Ov^lt z@N`S@&*r5tII$dyem@pU&Obz0@nVE&{E{@ueG*9q+*vd>LUv>f{IBi_P_7t^4MSql z{@qAKJ+Q*I4M(6d)wJmRbbZ>qz*StDZj8_?7W6-V*p+eM41*q343`te>BZuP{9cr; zo62t5dqTl_A9jql!lA3_;^~xhEUDgzE*sLtmhQD;n`2M9nE4WuBbNw`MjHwau7$C* z1>Pz3B-?8(7@(|;t2sAtwD~AL%xn>ZHLGymWgBkI3x#93^cK&alX;e|Eoo5Da@H2lEmi?Rr>MCi!@udLxX$wDMPJk zs&yy!mveUZw=)&K=QL!YG0od$M%&vPa3jKuv<7(6oVh-D8_v6|B{Oi@&H)#Bj+1?B z7SwGPVQkltFgF{-%!b#TZw@9|i7~=+*r{sYm$UJ9+|zlC;dA;>dAEW1^qyHcu0Od$ zm50OJdG9oB$MjPtk+d!jTP)wBMYjUSGx)nM*PTAJ9ut!}WA@~}4LN8n6h+gy8}wO; zWF-G%i&>C}JAhZknRv?HzoeBJ(0h3RRm1f-XTq$i{F_J%Y({*-S_F;MWIt9NyuTme z^P&xO(!b%z@ZosD`R4`pcMzOYh+#5gv4y)Eb{lWQQBPplzbBZsw-Q@^^N?F3hyi1+ zlyxnVeb;M{_^uZ@@7MraZ4a0{P(8 zac^ub46a$zr}3&HNHvG^DdrUSN{QAj(&D_~Hf%hhO)JM$38_(|5LKf~FNX)9&15Sx zEY#?@^FJI-W@kKiO}}=nL5lTzjIGzBrchbB%g&AWw@nbTL7TK^2Z=8u^s#KAB_%)J zC3>s&#w%tQw-v|ctJejQueO&Yyxf`gIQON$nIpxJdA?*88$eZa?M2@lD~kL&L&(?M z#G;gX-p%wD4-HDOsB@4Q6O}4aJYKB2ENcg`>hCQntE{6NS0>F0+EY7iQ=lQwYPnFl6XDAm>jSlJvR%x4<3Ao|QO$ z+@G=@&%`4cU4B;v(&o!P%t98>iw~h|u3HhK$~}s*6YMQjphM5bGdHe96kO4ygVpPi z-m66Xik73PDyDeSWJ^iBkGJj?gYWs?w2J)~sU_SkO|YP&#mZzZHyPs=Zk6oG$`{sF zk5PJQzWDJ^1`CE&V~L7|*nG)axIK9U<&WE#wIsugK_?Ms9wWKVyF`zHDWa|6noucq z!CEk1>E&m(TTG;dNh1cFYKyP zrS*y3Dezo39MtCLf4VKbjkd?q<);x-Z$%XYYEXRZB(^he@4(kb7}%#l2n$+_qxbNtyj0l`+vloD4aj6Ox8^}Q;+ z_VZ+aYF`-KV7{@fExDVPqe@Xqa{rJX4cPjUb1)5svAf(TWB+6H>t<2Z9A1y%E87wB zN`{6;zDGj*Yj~ejr0V2)v`p?w!76I>{AHW)=^aSx`(F~*PsR(6s=nk~vR*tddM{=u zhSHErI{Z8BM#mgJV$HXq81}`AXRCkEa^DWC`}ok$FeyIAFG9$rVf?ImfWMC{G3v}L zC=6&qQmi!W{Y?-*DbrE!XNXF2tB1b(erLCuCceLeCAOh@BRac z#|`MZrwM2MS`fX{nz9!;QD@d(-oNwQ?5HFC;knYK9;Q^RVN3mXFTn6DMGEIm-Nzjv zB7UtaMdq-F!sV7on&iU1Hyx^1?JH!$>?ml86OFrKOZna8CGIV5^taZXcC7DOG&aze z6i1s(GIoI98dHE!qUiJ@~s#OTT$BeE-&Y+n8 z#jYSZ%FQ^!jIP%h&G{eis6{AmFejZfZ%VJdfZgZyY4FKVDoM-YS-1;1Eb*nlvLf{C ze*nZBoR6;EC~Yg}OW;HEWc8?$U1_s#@w}(H3xy4yfpt^Q!Jt%=Je9rBy5|7KK!rxG z3S^Jh9jxAD!0-2H&RB03MKad(UO5`_`;VUU{?}f)zNQ zJj0r<&Qr%O_6x-DeEn+iCvh>6*+0_k20mDdC3>Eudu2a%nOsK1Q+s-K=>pPbU%>|U z4Sm|0F0{MOhMNgzG*2`P4G76jt_%zQWC&T)~159OZ&85ogc(Shvo$M~Yl#map!O}GC7`wfW zUd83Pe_>K%Ky>Vrh>ALe`%BqBmShQA$sUwOE0gg(d3@=?&kV@n#kqHq9p7T0pim(G z7EUa9-IrbSgDn1^@p0cJ0Z8V~-2SO3W!KV^f&KA)QEz1JEJzxKoo~!(*jk=B8uUTp05wXR^$p!F>0{&SI*9FQ z7+p0Ow{kw?SxY`nWV+$EKJ(~D@5H*prBFI<#@S+Pa@BeXlmCXZ7?ag}PikH>4x(-dn%wrG?_@hZ11!Pa6Nj+Eb{=wnOyUmKJalS5gIfDe ziNkFhtjsFI*diZcy=nq7lE1)r`)Ki|y*uW~ZbtL(CLB8R7JH>O;MK=6%zpYCGN#EG z)%O!-1-{1VG;QkGuS^!HsltVS2Ygl-P~)G)qHBOQB^;Hdj*n7eSw|02J;t-swlo~L zYfEt&yidIrkIvr~^tK@zleZP3W?2QMZ2AwWJ^|e8Y300s1jO0Ch)jNnV@vm8+uLza zl)iwA?~Cv*ClYTh_M*?rTS(e67tY)Zu<`SN(-V>GI*wNhTOF6Gkipjyn zxWn!-xu?4H!*eq4745NPwH2)_i^N|gJyg}{aK_miDl&hBG5g>B2D2M@U@qilTG82; zU70m<87Iwj>E6B{@IQYKr5<7WG@t zi}nYb^GEb4#x&su|C!%0a$l0@(w95Q{iUeKBwaB_O@U6f*7BWhx9HmZ7M~N;Xi2dZ z^)qSZ>=@4nwir;kdo|vU?M}JBdhng|4HP)1v~5p3f)3~4>3`q7e;|;l2uS3lkrzDT}oR7O8jZxisxwYnI!(C_a>eC zW=wn-Crab^J=Rvm{eg#))+-fa&$vKxxe+ZgpQ#{i0-vX3zf0VgXmJLA0Buiug`s1t zMT}f38X^jy9PT0pl(3uN?gzZ?tycJ~B@HvLJVUIaK6(5n3-yriF#D)Y&2rVEQRz9P zt?WoYpbPgG3sAYkmcAss#=mD-s9b7Jx;IO(BV7K7`97=3TM0*DGQ`<(ORA=KDe{jC`uF+XwZ

>Bn?@?$qzFpW>uq4a02WDzuw4Bwj5`wgD7SI>`jF=MoGx&x`#9>A54KP2uxE5)pa zCGZQ*D(K`cOH4&N3VIzV95OLZlpX2{=h9a0-}j;&R!>CYP6d+kbtSiTK`1imMDbBy z_ULgoW9%_ZIHXMLm$}mQN1Jex=cB$SdePaG!)X1&4CFcuIwIdF+5WUAP5Qul!RJfF z>uhHVA80}71G0P*2+RP2}JztuBCWMA(-o>P|VHBFqEQGhu5z?y{CEE?9)(_VZ7?H#MuV6Y@phkL+ zGO(8Y;a!v2)o%0<>$8KYg6Cj`TO^n_^%Q@%`K-672QIu$#-DRM+YWI=!JX^4Zs|w` zXZB(L=<_0@nK>h8_hS2m2_nSWpWcFJQnFH<=XGWmyb0}`+Y1eCvSi)GlD3)1VVbWR zjhLoSt~0o!`Q-~AFxH!x8( z8mZgL@tMzQ-A_-zBE?VGoZX$4q`rXX{7$^{R-*r2KgOe_O4Qxcf;4%4rzw#kOC1NA zy+jqew`O3?B6E7>DGRBcyYM92p8eG9ZH~!Aoa{|8r2820Yit~{9+!%z%@#t{CJR#k zio`PgCgC5x4a@%1W}fdR9Mqp6xf;t%QT=Ebei&Lr%p-4p9fj3@Cy3}`ZNmLt-%y zNAxjcdaP$bRUuXwaDYFT16w&4;e(H!oA7(?4Afri20xW8*z0{53je8Urnm+ib)060VnEH`-+eUJf zs%b!HHgsa1X}&O6Wk;50y3)FLXGP8h1Df{dFXl<)#o_BcC~C` zoNYK)`wIOvZ$k2Z2PW}uKvOCU0Z-R+7c&fIx14Cv{5WJ(g`)hNKh2wZ7dgdbRNk00UNejo92gRp(4{>czTY>C>143r_2}BIclK8!2 zCt9R5{>=8rU-E%ZKRK&x~WX`aJ1+%&YNVUhA=dNvtZ zYs|^^4|gd4R$xU)u^8g*&bfYn8swELM)Cf7k7j?Gf7%#7d-kBs>=ydi%$~!Z7sXFY zD_XHChCx|n#H}L3VSNN{@CmE(* zg9#hPpe6Yq+PdBhFl_mSjym4IIXM?~S^SlE#orNdAX&0y+iUO>f%v{o`7?eq)7yy@ z`*<;z`6sgl%xQ(bG4;`?!WZt0|4_=r2XPYvm%bE38y{h2?ggy+(jhT?nudm1cQI<= zHW;7zC}!msFnVN*&=*;VjarQ8`p;sa^eg0eIg;+BQTY30pQv4B&lT=yNG?{3 z6Qj(?{^4j8T$UGR*37DS*PR{f9U{cS7<;!V(#8O7R9!a1+(L5-eq)F3RsnQo3$t0J znE%rh&Yk@Z1W!!F)+Py!Thonnk8Q`7+qXsRxZRS~o9gj4tQ#uVPcO_{$a4zSHW9dZ zns~vvx2NV^M3&-Dk)snz5i3p=ger^r%_zY3N0mf!{rQK*mZ7#~XDsX>oC;z>E)cH%1vQ;B7W>i0# z$y9S? zJm^PN7y5nR6=LqW)230{)JH*{9E)8jU%bSWt1@Kd)sFo=Ut-o>c?#85r|GM!&^xpN z#Y$4NDXmEyI}M57onrCcAVECd8Y{Gt*)dv~DcVx=1J+t-!|9b9Dc7*)*fNlxRd$q^ z_W~1~6nRh2UJ2C-ywO>K+K=-QvhFi?2Lv89ZH98q7i8~Hzz_{DFv$+L)j8WfH;~57 zm!rLL%Jh);B~we}=-DxCdR*U|_U__4urtqtmhXVdwJM3%jxEC6^%UBlpD);Y@S2zr zJqsPH9{P85-zoAAJ{1b98?h>sS^suB#Du8d7!_zj`=aEb_4hA=7V}K`WiN_&&Yp$O zYJ?OwI+<_?h0!KdcEF8ZjG2RO%pcqzsYdG0h-$0-$udNSf-?Hk{`1}xc7{Dr2ElZh z@29VSx{y%e-_c()B!i|oQ5^f2O}E@Um%8~Y?fHa{YtoP(`3F~ZpP?c0 zC=$BVBZ=qy&u3*|m5vhKGx>`37k?w9=qRM_d_f;~89Jx43jcm|A*VpTqwh$@?pp4( z=Pr~iV{TyGTX))AAtiqGy^Ra)CKMUpPkcLmfj!e3aP4;h<@QveDD7Ps&G~@3kp`q{ zbQn8#_|dg&De51sV5pz6z3|O z7yiQ7l@H)iSAvjjxIbE2^7Nh7>YG1px}a^~q96qoY-NM+z#Z0{J z?PG8^z7pR!2QpAQ58v-`PvD3Rg-^`Jf@ML>x$e|! zgQe3u(Kh`g3b(6MxTh=5gq_8q);l7quar1xwgo0>GT1WkqS$G)4tKZZiKC{QBpw4# zz|6)O7ft&@ND1}?jD#?-L_uy(+B&g6q`ZdV>Ds=e>cg4L#C%Djg(@nqDbv}Y4Rc-7641J_CwjDU zfBAJel+qi~Ke1LQx7iRcAyCu8J7zCk>J#u2^LE%_6VF7QlxAQ~h&uc9dQ$DT&djHAUQUp@B}9pqpsTxk_^y&-3(e z8lK2|wvpd!N-%yq2Tt3f;d5m)CNnQ!{Hbu<`SS>`6Fn(&qd7XQuj84Wgcf;>?#qb}1Hp6kj`vF9Yt|FFi=(u-&sQ-n$94kC2@LyVbP&iwHs zJj+T$*vT@SG~EEhZ$t54y8~6PTn3#Cf4qp`{rjP9P|4c_?JDLyWJY6nn-iH_aA)tp zK%7#NP(^tUn)q=j4t#f{F-yJae9I<;?mUZ@IqV*r;V2w-7vhx?=Lw}!MAq*Dj18*C zx)*W*bGNvQqX(05Wuq_c@rxEBHjjOct~5>NsTk!K&plmsCkJ(eAX0 z?r5vmK}|bXgf{L+#FPxQ`F2Qtcio4plT(3L*TkBy+`A9ShUb^{__yJnh~4`TXDS9^ zz_AK3z_1=>{P{IoszPOUF08{nse4a%Y`STQip>G^*=Z8ab~VRCZ)O-AQpTL>Fl;y2 zESBvzqK~J0;Y{u?5y~tERnHLEg`5)IHmOtPj~>YT->$P-miEZ;4s=6b%Cyj+A5Zq6 z#%BO&&i{j|taQ$<32{_wt7OaASLj(0B`#a-5Tz~6c(P-p7>_PR=espwT*q`t-Qw@! z@AtU@dDBY^r0y!C&UcfD+`3WR^)Nxl}zxOzDxVJr;S((5*Bjs%2kAV#_0#rCHDg>uX3? z;rk_f#G}*CphT^L_dv$9@7x;op}J}YhZG(N7C;rU8Gdi<|eTsW&w^Qjz>*FEvjS(}R2mto8Nzhc-CL;7CX zgqRQyw5(Zz0c3Q@56r?_Bi+I~5hO@5T4X5U+Aw=rHwO8=$2=VD9x3nPR{F9USbzx#C(ikAH=Kf)>PEHCp6Tw z=wck_7;83R%3>?J6~%pnRRQGJdyqs*%8O*ef~fzt%)%7`#^eHTYCHd{P|3DV^2<$+ z(h@m?c-vLzgxHf}iwkYCohm+eaECEgP}K70Skn=UcbER)qt9LZ`hFVrX}rtxEk)nv zXcWJzfzq>ADDGswwM8sHzrSGgyiZUyIt%~ZA7RQ&%-BtHdCz6W48UHL6LbUf-t(^K zp(k?&->@@9hqn2HY}#+)@&Eh}bNSuhSA+VM?qWuqgi^EE`6aU*Z^~`y;V&gpOInV= z@cupCv9n8>k*WFsFI9z^P*eb zFGKlhx^UL8p~c+6ZCSNVT*%O%m09c{y0TGh?b?Ns`HWKKrX&WAQlf6gO)#)|h8xSh zNug^kcD7&Sce)Rax2{L%=t`{lXHVZvwhOTt@m#fe(a z&e#u%&PUiE>R#kas8! zH28iy&g@t$-WyxeoA|%XK~li(*it--+>Ub|ggFzR;EK~>6x=8iYi`$J|MAVJdG`WW zxo7+arlfT4J!)nT#<5vFXs-23SX)eimo&RBKmEsi4_&&S=S2_dm!YteSqbWP1jRjw zIHgN-`ukCB$S(*@H(JU3Q4Fksagr^Onh(wQXu`g3zEs_2$L~(gM+B}yQ0x+Xem)c1 z0v4g0YClX{YKDXhhtT)a3go&qV(DIg`ctWkLvv+mw=J`UlvSbhq8fkr=cic`iauv! zFzwf847-%Wd0_ULb$tp=&b!2)K7lj!AJO&E8AM)W-`Han%KBWw?lc>EBqdEl%7%&f zNe=W|+JMr1Hwxzr=4Y=xgkj%OU^U)=^x6`!nlmjEn8|6|nt=(jdolV(4enJ=Lw&s> z7O!u`=xZZT(d3I}-Eyo9=ghXWIfhL-2WfEu3yu09`QTwZNGryzx#k${dL88_ZlU7t ze^9woC8U}2a<`iAW(BfX9ngz5sOO`8QGa3L;Z4`7_G5CnFD2HSNv14|!n`T$uFy~@ z@@_hc0nDAR`|+O`U$q+#w%vp)?~eS>oD67tSB&UqQq-odTCnbB1?paC(BhRsm`!dL z8@zA9xQ{3AvJ1q@y``{yU_;BUe-yJf)+0r=C+Ybpaqh(eH~&RJojZ~%zC9J=(w88p zxf{I*7{`LG7 zu*1(OL(Jc(LG~UUxN%!W)Xn;X7m^yZ>zeSrLY5xP|B0ijIy7$KH;m}hj3Kt{1@@Gu zc~{dUi)C&A=gOHcy-6rPy^S|Fu0VfCL!o-e4JiG(g$JqA5k2g)STno;!9yhY;-`uJ zJ5zD!${09g{1R3}UqQ_&h=yV?&QF&@aqr%AzH%EPBYueSEB#2RM>y}hJW+CXrl^Zm zBx8%YD4Y=~%yqa&B(()c=bRQj*L9_c8O*;)vFA*L5|#Zuh=zsi9-4dwf$dJ}Z6`H7!F7nE62G~aY6|L%n}zE}3$nL-%gQLt_##g;Y$Vr!px|{Cpx6W!(7v80ba)ns_m$>ep|R;$aNX)o$i>5t*+I#k=-ll(6* zb4FrA_}!K6Ma7BkBxurBLz=d7ujsEKA(ub}B4rt*6*J3frW?7o+E76bXxtdkD?Vdh z&kCX1-Y)c(9jpDFB(!u-6Ewyi$1Oc6?ySCt>9is=@;+Wh1aG_<0|R)tLT;$U&Mo%0tR*t7pKhE~~9`?(YTa#wDkif189K3EE$r}q2gQ#D74B?EXquL*SkPOZyBjvNDRrf2f2qLyjHhTYE60y=FLKX)gS}_ZAZWcW z)qlB#Oz+2-{?(2&K719`r*`4|J9X0By<8kVwHqT%bjWMrOL6w&af}@zM@RU(TUnw& zbGKSj$UYCMF4ZQl4tt7SY(tkOt5bBP9?h(OECQZf6+2B;2|$LNR}v2M{zaV5)^jAH9hJk%A{2kj`&s0r6SE{kJZ z3@LHZZ)BMV!u8oLY`%RAkB7U!RKops)fmJcTY(RoAANMlqGosX(R zAV0fx?D?#!{{?F%_vCJ(1AQ6(l^JjBG)nZLj=u!kq9r&uaU+I@2lCG~0g4-<@bl3m zB>K+6mH2RMEGk3Es9^eX#|1X}C6F=>r8(W_!1*uHmOqB8h7E8Y^#po3 zSF!bV3%q9+Hzt!Dnu%X{~+U+*90jJ_&aGWiw`gsag&#Zr-;dP2B(GLt&gi<(AUlu+$S z3^wy%j?7Du^yns}JzZ$^c=qCq?1}mP2^uOU?C~bZmjL_vsZr{U&teeIgLW`SEH_P_ zj&1scR?g0?(rsoAGJ9xGF&DN~o>C4z$AsLS!ll}rB0h(Sf#oGaH{X@^j5#IV=KT;_ zW6Vfm5iQ9~a3@P;H?fF)L`qfObRu}Jc*9;fm3)8tc{xr~h8yPxcTTv$W)=Kb~it-l>ErTsT`y6iNaR1^e6nU<+w>x_gtR1nXOo1u~F=OVY z8Ai@$2H_Pua@^(&WBy)W$#S5Kg15{gdJM@*OFFB43(6j~nC9X@Qx(7C5w+lB*aqfA z`cVRXfP?8K*xU}M*eK@m*hiwTl|L;ga6#S_8ECInA_~z#-lIUM4`=RdcV9%h)Cf}6 mqr`K}=gj6oBC|gTiPCY7<0Ug1QkwGRc zDuQYg1^p}<+P0C3AP{WPqD@ed2vaClh(rYG&UbiM-Q9ENaPB$ZcMfx-?ePAC9W}B3 z*g%+ziYE%;wjkV*ZweDZnC~r|D`d}RdJDN|{(U-orWnn=FZN{nqB+-Z*_;TrHY9>l z@V`%8>fWwoY{6rW-`b5B^WtJFV$7>LQ;0FweA|l{^U#4bV$2Pr?8{uNiZMrhtjBzN z6MHwW-@XSmo4Y#LmwDq8#+WZOvL5qv$U8O9R@l3FbRX+654~nR=1Wt&FZ1#e^O$Wm z7mRmo{`Zk~<}n|6!&x;S9cInuXYZNEJl@7LG}o4x$9!lVW6aH6{FeDtKkwZ<@|nGx zpMPOJ=E)i6F+Zx>Q*-A;68mL-ca!y)@BiYSnk#YcjyX5Oddy4nJZJNrS)PwMvxI$_ z2Wxrn<`rf3Zoc@JyJJ57o9APWA7jntHDio1|M|ce^UHeHY#y6t9&`H(-l=)nSLQLN zx_KAosq@TZo~YUjb4wlbm``5ijG0^R@(j)8PyCkoM-OM-+*slKm>;KkhUS$&*_XNW zku{sYx3YKhhH=h@dANx+o8Mkz@8(6#%wyg;z+E<%SF?BXgF$}Fd^yA3&H2;JV@_86 Zzne2x*q8a?E8ds+`cvjHubpIH=6_axjGX`g literal 0 HcmV?d00001 diff --git a/source/tests/pt/water_tensor/dipole/global_system/set.000/coord.npy b/source/tests/pt/water_tensor/dipole/global_system/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..4f6c37e77a3ecd7729cce35d5982e87f7777cf45 GIT binary patch literal 184448 zcmbSS_ghZ?|8J+g)82az?Ydu&XH&}FgpA01TcII)6cSRB5fzb;jP{VMN)kzmhLvQr zwD3Kjf8pCt-S>5MU8mPM=lOih6O(36oHCc6Zwp_f@v@LjOE((3>KMB(A7^Z#W4!#o zjhiB`mULV4JHgUGG&~b2bG_}y#uJix<5nr;c0q?$a z;^Z1d+NPR^>h6A6NotaHO&b>22~prGN%FoJh-1d?WMU?Qr&B#4z0HlLN%){Yzye2) zj3>R}_v|Q?Y5ng?1XhG#%ppZu8<&HK5l1xBSbBTE4eHBhA^FvRh%#$}-M{DT%A1wI z^FdTtePVv|Jg_mX3Sn78EKOA-}j9o z$L20>HMO8)K#WRvAI9%61KP;O(aiGa5QsCTD+`UtJhvGUwMtZJVNdMgBdnI30;dg@ zw7yE5$~=OgTIo*P=gCsgr)aEEGNk`91gPp<81_HT$H%Y2bf9HE&c8{4h4(1NS$o1d z{3-NBMQC04Vf=7E!GtR`sYoXS|K)YE2kB}w>+~_q>)*$$lPu_3uL;G7#j_&{5+vrQ zNmswU-*R6){s3WCq>PFxD*NDh4BmVN=cv6y$)#lFh z_3u+&lT0$ibfqZZ%D0knHnCV&CQakN4DlW+XCQ3jUpRCaky^3>#R;m@l1FOfqNGY^ zF6xs;jvak?u1JqU^+`3`n4(o95w(AehfKRE74JHWfmiE`K1bS;anWXshIz6pUrG@Z z6N6p9bJ5sw8U3Nb7%D2pM$sIkzDUNw{?q9ECqwRoSFp}TfO1R(>GHNLnC}!NiE%14 zr|m3oL7Z9=|DYge6;EFw8zFa_QSd&9C&ZtL9v2Z(xwW6oSbq(I39f8%_&b(rbQrS@ zuCe9Cs#vh-AWn!|vj?3oSYSscmL{8F+L>**BFdxx{Ffo_(;67P_alER16)W;#{2C= zzqhE+A$xBYs3MK%EBX{=k<2Twv4KXmD*bsX%G}Gcm}AosyvxpH>(}CL zKOeRSoW$@FORQORjR^+Dvj{^w3OD$GbunY$+hs+=rah1n6lJSt7*hVUPtfp@#&X>< zc<)cZ33Ut1X{g8VJ(uxHxR2GB)ZvNXPDHl6#!6>RXuljw$!}glsA?WWC8VgP^b@Yj zxZsnrAvw#hMQ*)3sUNo@*YhXgW~D%efev*3=32~c*Q5uN^k`H6Z=8S3-6vN|suFHP z@_KtZqijS6%DZqQPn+%^ccd-{Ta2H+04m!;u%yfew_YEBSMh$hFAs+9)}`30z;N~G zJ1nyX9lfdmE%g!PnvN&)N!s|b;u&g=`%=9nkS=}@9)G^z_rPN~-A;$h^iG(U8 z8EDw(ioI@@v~lPn-f54cpqU;tb7eSkj4kMeq%(EYCSy3(m0B+ZP+QkQEZp4yqcTY< zejm^KH}(^D`u;;ovNmtpy3d#rsz~+?p=?)I0u$j06S$mr#}np%M%elQRGMc}NLXps=1kwANHZca|EJ zpY6xl!W{G#DA8o$H|X2nfEqVL{D@E>6N?M1Hjsyjp~}Sj?amwgPZv)F^eFvEB^!~H z@%Zy2(EX3LIW0|`!S)$Sv(m@5dv@8TgHviRb}CgMJn57e_hYaPrDNYw#eqpwQ z3aOe7;QmQ|nrJCa!|iR@xAiaL+;nMGq%et2_ar60O0@lWih!&s^x|{@ChF#6ZM-vy zZ@rGI?IoD{br-hX(SywC5)8c!g~38&+{|x6_|_08R(U~2<|?`$xl@JJe<+Mr!}+=1 z)U+!RN&iV<>O4=9F7$?1#{`6X>|~)~;?zESDmr)EVN3d@snJ9Y@#&Y?*!yy{{#i6m z_?l3WloWY)?&kKEKIM(7)9aRSOv-npo-jF5$le8yr$Q{dshBBF{fHu73d`3}gkniM z4rzt3zW2IpsJR2Tb~Q7lX^m`Qv^&ppegiX|Ylad3E{`0$IJVTgnQib`!TvjQ38N0` z9!{zms7uen;fdmq~>u(1nSMh@ea*=&^lnLr^&HPLB03t0*R~Z?I`kCiu>hDk(&7&3F-Er*XmeNSoTcyMY90?_mYpYY`^j*@0O;gX#?=wmk5s(cRKu4o((#0 z<*vh%3R2dy_rsS^v(kfRJh{dSl)f;9U+)pFu13K%vKUkK4~>pG#P?qcTRH6~Vzgu^ zCC`y63bv!WOpX$2EXiLX8ZuL~=~k#6F-IT3Uy39&MJRmpbe_%CaddarFb3SAaxY&kN=G)S#(I%{zmV~9k zGGrNAkB|p<5t5`p{{r75+BXrO&i}=o*k%;!k0GC9nHX3%idtbYIuM?XYZlLNphSw+ z*~cSs(1xVMyl83b1xVUB(&Pm^YHcjVZdqgMJ2{CKl&9fg-3i2=aG=+7#At2dO{j?3 z)2$4C64-nUj&Ho_b+0tH8`j{${2atJjwSyAD+~#i;f}U4@f}%((1a7{t5TuwYvX`{ zo9wc%B_$4C#jT~E*|VQU)P@sKx~Ic7UUnq47BhNfn8SAL(fx|OJ=&C(8~29v&SnD7qk=sHB+fXVJZF=m?KQugZ#r}$^P9GX7ekTH5sc@ z{$kCNIFA-KHd&I~)x_EK9uv0Wzf6cG+QEH-7;OKP;K$sF$U3PH;gdHZtJlx?FA2dV z$`kdIOK(u+|<7?Rk!o3U!`vr6z>Ef53g~L0%b2 zVMpp?X!;lNEN@Q4kw1qacH|SD^nAopzfA6&dm(cEEq<9Fgz>&sI6L!`=KZmBaDqIE zF6?A^n|z$(H;GqpW&dStvNE8U;2fNCID&p*B`*Du|ANR`3l0#XYovZ240xnLuXG3j`E$rmIcX( zihKw=t=-V5ser)rCamYhqjz&KIu_ZGmVF8;ANph6FpsW`y@+uo)9~_-DLv`qFqoY+ zweGd0;>SjiwzHs@!H%SQeJPgRaHT&doT+J(IA&fi-{hq4nVv?yaloT;!{W`0VYis}v*r#y{ideUZ8Q+0qH7BZ(kF+Wz(XhN6%j-yV$ zJ8U4+p3WB7(2FZ6Z0!;?nmQWE#*dajV#z3bU7pN>HQm2HM zmXD?5U-XHeenNi#T$XnB5Ecex;e}HU8`_qEqUwu?+G$a|OfLyJ`?J75KL&zbVrWRN z!t$cExId~71G9UuPTPd&nGe_*hqq{IR^#Q~3c|n7icsS5v7v$(+)YkU%YP1(% z$qHW_v?*g#M3l+UV*xIt3gFTd9a=DbGkSt@*{de*Of?MgwAGa;OpwH@Ijn2%L#y2X z;(~lG6c&4vj-EKZ5sroTD|^=X!JR@3-yvz^ZFZUY(q@wZs9iq6UhK4{y{`2z`}3IB zy-W~4?Y$^0ez72oj#wcL$B5zW}|ZfrUz~z&ZCa4;$CQt5_G>*nB#!2aWBc17PY>@ zj{UD8FkY8V-mApAz#fbg*wgaT&-keR3%}>@8$A=EuY6 zd47P|b*e(>NfKihM5r-E9InsAu|h_OraW{;n0qYyIZvJv9*EQ8^&cVI;znm)$&k{y zQ5?>0 zp+-r~6>c9s7ADo(pLo@Ox=^*R8=ZI0f*0ydhYvsCIj^{c2N?l$y)lKkbsWK}bSLs~ z1n*hfAk*B`kI7m_6m>3xy)^j)t#9gdr)4Z+RfXwb$NzS^8-1=%#FQ%<)L7_3K`&3? zsgV`=j&Y*gq>VT@s!D5~%FsNEe4g26TMD?PO;Q&Nc%I)l3}P-p#+Kvk{js978;-Q}?+r{; zGN5PciS9XPAxy!M3UWN?gm)#{k0xQT-iGw~6luLu8sZ8q$ltOIB)9vC2s((sfop(SYX6NkRRmWo$M# zXM;konDo)FFJV-3&C%eyt=;GoB&@X+0 zh^TR-Q=83U$O^1jY(%c{HQ4eZ8;ew2Nh0q(o8g)OVMigl^?3_B=6w$C#S-*k=rmJ9 z3{E`w1t&2MOGhZt+;|1ro$WyFGsaRvq#hl6Wk7i!bjjwAEVZuCrwsv_D4hP>BVd&& z6>aA5jr%-SA*M+a))zrCL9uk#o2R&39Swoy4^Z^_7SjJrg|lrn!Vf%v#)9Lh_?d-> znaXt6A{#$dhM~Prgm&z?0{Z|#n*T$KJXs+;szqtBvMfEgGoFq9mw`q*Sy~x)ljYw% z44aJt^wcPnX|?BIgKi4Due*@dEZdFBurfBfr=VEr(?Q(7G|c*&3Yhuxop_k;i)a-B z+w4tlS-a50=a02Mfs~Q90f+aSg72X-mG2dy+a%2V-;}ZRVKLg9HkL_Um0-ReGIZ8W zkhfum7-V*(qU_{!I34a`i*t){=+Gui@ch7%deRa7a4goX6hMTtE*?(Nr6(Z+IC;ky zhhyY9&9D!aw}Ff>cL^bX2^#Zz<)vm zzA1ZR|0_LO@$dt>9Q|=|svWgoYry=P{}AwZEUC4HBhF5pRK#4UEhYgi2?{hTj!1Rs zUX0tONd`&g^tI+Eg3Gk%v8Vz0X}m@r_r7*Xm{H5nAMCLs}%{CxNhHuUJwh+Hz%);vcWr-3x6E=AIZTNwHB360B+ zngj#^>()mubmr9)TW6WdP~LrsO#E`;~Na{m)JTXZ0G+#xJT%S6Bb zBgj=OLCP@?{F`7&@lP(o&N%{x3v6ju)d^^R@x}73=G42S46O}DBp72u^h*QYV~uIj z5>87qp8@$jcGR-aj&^_k%XDvKBSuJw3{)quZ3`>#Pe+`-t*&6bx+VDURX;{zT|K-N zvzf#84EQyH-fR&^rNALvwkPUnsbbNiWe^*3rgd7%bo7c9_Eh+x_N_9t%ojtRrV>`G z>C^0!f%vb%4`vd|w13$Meq=pC?{hgS+4~lP91m}*(k6vJqp-2K3I7Y#?6->+xi$MS z2Qf_~DQVK$*7LkMpI>r=JCmTY8k=&&h9)0(rm*9endD6anpp!5pB!iY zn_cMbn5k^*iDHZq5W&j4D0Z zBM#uh$pVZ7&STM)nYg?=4{up{Nl9P?`cCFSX=XG+J>{Y6S_Zz##ps*n1}Eq1Fdv+Z z;NzlLzO5e0TNo*9Uj*?&YfhW@qxIsGA=D9uqGm4=UJ!%L1{N5OzR#3iN>Xj_bZ(jb zWUIYZDJ*Okx~A2#fEERcA!P)=aiNxJJuneChBHBar1kJ0UKzwde4ht3o_!6gS4ZLM z{U7hPxev`hHHvDh1H1=|9O>5-5jwg)jv03Plf1eBh3pY!FI2{$;N*MWZQE9lrVU0| zTX~$RR^DW7-V$(|^^MoEI3F_~j$tnzUB-`Pr}1#|KGrUB6L)@ALiLJuNypTDZ1sPQ z?Ho?~v{fJK0zJ?@uS4%!e=+~9PZ7Jtn!9FWlrO2mcTaPArSlSFK7Pi*P&3ZAXvVMW zPY}xKy}_0h7(OpR3l1#94jWIpeQgLrPLWV@ol1eWDg8_u&85z4gOYAhZkX+U#`(=?Lh$Zb~#x_Zq>?uQz=+VGLAwE?wH6QUi%oqz#1 zbAEk}L%&ybVOxG6UMZPS+5kVbd^HB&W;d$#9YW@`6-s4`&;f^0K{!D1h^WBxRJ@nl2d%Egr#1QY`&! zCP7>EBx#%J2@H=>){mIlN=vGfv!K04jOg3b7~an*ZghZq z21W!kip2h!Q#$80G>7jkq1W%>w(%N_u8C30;1k5lB_Q;IAk8xW0j7BcPOi%2=Jys# z_l2o|oxs$+e;}YThBj!PhVSiqTo#fg<&|3ydeMOMjht!1lmc|8t5Y6)Y1W(qq@>!@ zrekjOL9PVqpORpG$b#N(RG{ns9fOv#2f030qA6FeB3s{tR>+Hzy~=X<_2$A|L7vLG zUC}Rl3hNfjQcRaC_#GZYaAYjGmhD5k%Ub5M%ZTJmkHEb6H&24o-*Y(r+Pt@v$!Hi- z+I?-}eUZhyBYdSBcM4J5*B5M&)GoAr|I9s0I`maX zfgaea5}%v_&EBfX;Z1!iZC9k1l4EJ&dwCLRG^J&m(z*ZHk?s$>kX_+bG)Q<*v4ak^ zR2E_2A9oE)uVI^C44&S9iRVkQajy0hmR!rjdCt4K+cXcCd3A6!R;KNW6*%zX3(Sgz z=|g-TR2;rxWS<@tmEVDt)F2+FsgnQePi*|)MVOHUv42BMVewT|MX6KDhq2ga8wYKF z0W@#h#S={52eV3bxZF9(8UzmFV(?cs>Q(D@Iye>0mY$fuEfmqG9BIm*b!eLIiIRnu z)V6Q|7XDg{1;#!c#*il-JIrif)UfS|x^(28CNsIy#e(={DS2Wz^O>p4d<|nEF6)a$ zCg0dM`#9tr6u@B1OU8dA6YAq+lhox5Y@d7|CU@#!t-2P$^W8A>>ocsJaS}V1 zPlNZipYS*pjJAbt7%9Am`DJCew{{GdX8hvr!bW5(IQ^PO?+swq=~&Q6lv*#>*qM^(CbV$g0?{{+>H{& zIB!hQ230o4u#}0A*~v&2`XwEed9q}dRn9!5(ou5#FNTZScy8I6L_g;%lZ^BnsI}^2af}W*E!+d|av4ol2yi%#Hk;cQBaS~BYN~DMTA63t|LkNHtQWwp|ghyc%L0bpdYA6B`!%N z=V$(8sV5C-+;nR;Qff(&TR-uh-qxno+qs$Ru#uUcl%qQ*ohf$lHkRvPL=P$znD4r5 zTu?ATcA7GCQptq&o5w6_)s*6Nx(CAJNGBzAXg{*4~1t{9%MlNn>?$D`6+Y`A-re7|6JVoIjz+by~&*?$sgc zm=!b0vqGtbI>HXfvoW2UP@-spcSF{^_8XE|_Z~G1bO{xf%+I4` z*IvTh<{(p1;zhqr>K35Fk{-`}*xAv)RRIv(=8Q?Un@AEto{tQx3AN z4NuvUUPlsb=ls}ru55nNRUG?y2Zh10ETA_VQ|EHJtE*DU!-yRCCcj4ebQLO@BmiaZ z%r)oBQO3Yy_GU^e28NC4La_|a*_EI?-F-XnbEC?mF9L~E+3IrH|8C;rufgYRI#y_%aZ6)Ip;^U%Gaab zPKkspFXLE2BmU!4punh4n789ALgS>!Ch!bvp1zY;P4;kTkz;D+Ynj@`iAZnbgIdE> z7CB^u{0)cL2v^k!b^?vZg}xFUgpul?R@n6 zV-|)wy4@GAJHo=}-@yE1`6b(Z7x1M1+(yDeiPB8|HrCznft_#XqrN^)^EDQQ_B2_l zTP4KhU|zG@vH=9id`JFOJ9?HEi||)U^j?YEJ!5Bbet|5FSlLpU*eUoQGoYdsYUF#@ zg%0akvflFsB&g)W`On$B?%T4osn3au^ez-{I_5?B8Jgs`(UFX8vepRunsw^ej!r(Al#gr zQJW=5Ki|znY`_;34D{mEa4HtPS0{6SZTietj<#TV+N5bou@g(-B4J8C{d)BBeKkb# zlaRQ^j=nl;(rv{YT)%5f{z)n%yX7qG#Xae{w=P}g{GWo>fR?doLnbV}Gh#HErVa%!(;!zZ8!|A_ zp^l&C^if=gT6tC^I$52z&N8DfwrQBH>q(ctI8a4cDgtLrrqmgFG~b{EOWrc#>Al38 z`7;pl@ETql^};TjyEQkmP^MP`A4^j%)02Y-m1^X*Cl|lJe8b##QXC)Uw7c#>D75KQ z_spwU%<-nccz%kCnCkIzRx{pT6{6ecEm&OP17z5KMC2Zu;wr^PgkSFB<^P=p^^#SH z+NWJ?vSSr?)f`7r=T&B0wjOh|R%7S670BqHjMvA_Y1_iXD6F@Dk*Y13`2Gil9?s{# zV^8zuC{V4UAJrC4!@USSIuqnY#c^wJpA z%tkta>9s~dF(;lm+^%95lK}Nc#uTN%M+r6@F9@=ubCm*QdOHOA@#f^OBuSBbH6Rh& zg^RQgmtJ_|4)r5iAO)qS0+=buPw|bbaQI_4ywlWCBcVdQnxAp|m>1%|h>`ldKFqS@ zG7R~86kj|Ct42)8Wt}(aAKMMl5>2`l;6)2cz-$ zt`S9e+HkYJ6?c?OX}rBF8BOK*@s(vb6|)A``$J&5#Sv@O8E(&9i+qQpa9*(lo+?ey zs2@kS`IcbOr5CVIbRdWNIGmdF8&TfQPBp(pOK_GK=VlzkQQ>F?M%Q7_We zOoH7<0rVExa~^OJF6?!oQi1VQzB>s=Klo6&38U%ONmw<&dEj-^NU$ao!P=$hiIpWI zEh5k020T=kru?gcw)`pZHnj~<;{Ei z8lJlLl)+P^e;8sz1Lx5*SBbWF7PH`Ud$7zyo$$^9SB6qKKS7kzIUiN=;vUU@&ryD-az z^0$UDS9u<}HhNQz&0{v)<4voB-D%39U99YzHN|EPvhKwV7<_k@`SI%5`SCp{JhqA5 zd?bxI>Ma=AB8tgMDl~b&FZCzNlfzL7%3C>+JayGMj3!4i-~H+PA3?I-CQ6<+gy@g2 zG$~d}(}fGY2)HXiH`gdoypkM6)k<^O2_7BFR3(WW!>DVvp^u+cX-4E2dN{?8ayXuT z{Hg#+R;_}7c?-M!r4ZrQgJHZz6(9E2A=Gdgq#nw_Ks*f#+xGBo)Foh#!*RAlNQmc_ zcL<+dgoOT zeNQEux}t!6=&#}OqD9PgvpR$}`O@5pN0|MpsqlR^g(O}KGV#6kXmN0%as7AjNzQ}* zo_Pcjq0^|8ain~e2Fyyn!oAA@^fjpz#g9H=<11;(*gwF!^95;fx*Vn3e`D%09f%Sx2|=m!c4Kce%Z9JSpLr5kwyv@Ui%^3sSxbw?2p zwgOX^Poe#nWy$XG9?(ZG3ZAiB`O>5v;z;H?e87SB z-pgQrbjHvF!(Rvwc@0&Lml{ub2UELN*zGZ=G}})|f0U0`cJ3q@uF2jxA4ia(1PyCX zDET^^2&ESa)Kwe6?s{Y*NI;NM@0rsNer5WgqequZooKayKJ_;klJ;gTYW%K2ueRya z#~w{;Xg!Z8iCmAV9>x^B1van{_do$1Ep*5+aFcs^>DYGq%FINrx`@WIPA5L-6`Y4ZNmJ;4+=`PQL$#EFzP zOo2hvR9~j)WPN0s#JP-p}FpGnW0TX zCHd}QIsHsLM`O}b3xv;;L5}hkc*uTax}rmD2ggmD`!%8RTmjW2 zjs1I^X<+&guA~#Z&e+lYw`1tY-FB9-+MJ@@1?cM1378x40*U&E@Z8e|*QLAha>rUs zd$9^*E8k&G{W(1TT8l_|eVo3pOFPxyIUgByi(WCP`gzu}wwRUe> ze04M827jR=*@tGT_(EpfQEZgtBO{q=4yzr2q3l=Kc7K8lhdrME?dS5*96lb@rQaO? z`+JTbqGL>H;%7bT6dz`>94?FIiPL}5NoayV7cK=9Dyz95nJ6!3#JI%TdVMT@1Ax zXHVcKcaLku)sd=*Ey_XKIZHBF9$>YTe9*Z>i2`b7K`VA5gmUD`#QPUyQyZWlqelrDgAgdJ!LY6@r99$vpmp^) zS@?uWELn!~(sZ8Ie3tJC&u8IN$+*R0gZn7pNDvYwlcbi2%xwR2pus==N%OXaYE zL&oG+Oq5u4oITy{!s%J_86B%ZWwkIfUvroFEG-ea356xYLy#i|IkiqhsvsE@9N}ZNZ^OflR?y z0B>$D;YHqB!d9x&0)N|AIH&x4VfM8*d=H%$X9)tFgc{ z5&i*zbp2@o=2`qil7=GrOPk{K!Y?>;*^mwvnjuL~lzJRxX^M&|X0GJ=K2Hs3fb$>E zaaj2pH_uOBe2bJ<&#-8rJ{jHphE1Gjb;oW8+Dtr&XVZwt7Ag8Ye>Fb0+vBF7JYDP3q(;|a^gbqXT4O}}v-oL3 zq#dQbmZrdte;9V=k;dpqIRDvrHS$8)Y1b}1zo)q1KV*Zv3x${jG-P=oebedhR%Ee6FjXiIqk zI${Ly!EPzif?Ba5{vA7&;fc9B`N-u(2b*%m2f_=haCmtZJAaFZni_* z7Cs6Wks|?48&fbE{QT<#? z+Wl|~EX)l#-9VoX^Bicj^R@ePdmHlCb0@8K!P50XTxTrEk*>Kql*SYd@+JnTkb9E_ zZT665Ygcf6g)`<hA6p3v&KK)@ z(1$_KdK~rrh5o_>7^&6a!a`wMxNAGt|LDZ}MFTh!egz&XT6BJk4eb%lN9b2+T4Ur& z+l5PE^-zmmel(=5X${ciErXzu4LyG^Luxhq(X45~b(I9^F7?bg=w1@?YIzvPN!6~Tm8dJ*CccWPG=5345{wsbp&q6XWGYfNdIjT zDpo9I5vmRp^1+h)o*ZHpQ)Ma4T8q9dmt!luWvEWqQu}1niCpl~0R+RXiwB$bvGj1Y+jG zK#K1&r=Ez@aC7vc7xhJWA{>UPf|ucGeFIz19Yp7%Sp4?ShSU@%tjxNC-#?V7IrS2@ zza2n~f(V^Ik%WZKVJ>T?OXn^n<7%-0wKol8T&V$DI>O}-1V2J!kvflm^cLECM)6`( zHhZFR4Hk+v>}0+M)+d}o==>Y(T(=VrM8{(7Sy|R}-2ewJ-NxpJnZQw~SC`y^8G z2djN1v)NvpZ=2YIW%GscZeA_&&L4qApBDmsn;?~yf(wD4*{}Yu_^@jyjJLeT*DZSJ zaaE^l+dJ@H#T`dSP3V2f8*EuW0ZU_KY5cU+m?^4E?Z+KR!#Ek1T;FGKx(n?~T!uM` z+BBSJO!iVENRP6kz4Er4PyG==sZJC!YD7<sBMq;H zA2=Nuh$B0ASorz{GR_}GT(u5jPF~0N;G0PL=Lh{&C5SM7h-$-N?45HK=^INqj9>s; z?{;?RmnG%cp2gRvOWB~5D`kbH!20qB_P*AP3K!<%$y{er&HTf2$=rv-pF#dB&#_he zPvg6xEk(9u0DeLk{SvbUsS{73dZ zsRG7apYnS`BK!L6Hr6gNr6yNCoY!nak(CXdR*|4l{%QWneS zbHamphSWFQ&6>~KVF{NFQ*xGKE=kXL?xv|8eyQfvy(g9x9d&0}Vs;euLbCYDtk1l0 zp}KUsyoaZ;su-3@Pnem_R2H$e0^Q2WY@UfD@5`Jw*eCLh4JrP?+sqo?rGMPDU+u;D zjx?rMElauhgJok3S=+r4@N=CAk!9*M|Jx99Z*`z-vLT(kK86lDe?S0FmL^>8#-9VS zZ1nykoc#M7&2nd$__1n?pL!cXN8?IzqaIc=f)nw~qrFJw+?^33`Eju7N*O;2mjiGohn>qYfmo{BhB}r|rf9k+x!Ip{A z*4evYIm3iqxqp(G3V+1vBF4_P`tvkje}MS!n@sLXKAS%B6KlegnT(7a+%=DRc)!E$D+KUujUE0>6eueEbptmH-H3nSI5fVeaG5k;I=${Z(wAMprK8p)V}1@g zcHiM_B~Q}(EU{4g7yjAl()vz!1Sbm7*Rk@{yGaF4)3~{wt4aHNxZJeuHH_!_fr}j8 zA+zEj*ezKyKk*3qTZfUz5~&&?KBQeN8!0kd^DvSd7+vb9D|rVajb zed_58reJNmBJ$ce-Bwf@HM5uF?rTo(`D;Kk{&JmaZf7S6D$ul$X1GbY(6KxV`tk77j)(&Rd(X~%^iR=E_) ztQFG@-?81C9rW|tKWGYlW*sU%xVMAjOEa%BF-`|<{v<|)qXS6($;jTbA2T`s=eCVK zDY|l9{4fzN$9f(r!JrQnn(R@^A^aS2A@57+>|$~uW>p$4uev3i3;shZ#r?_;0}xprF6%m5|!CdZr> zm77xlyx4JmIqIL_LMo>k5O(Y&Oow`4Bvp@wxL9bU)q$TXxxUdge0bW&_2BB^BlHWW z!xK2HQHNKvg{f@JcI4=Bx$oTrn6cvuPS|Ksc7zQHWagp&lNK#&F{H}}UZeY$G|ek? zrOj80akhUcVr^_lRBD<%I`rilU39PTPW?|6RdPo05o zsplwY6eDq&6)5dE4_gU7S`(23wceRb=DQ(fMc#nKox5z^N*yY#NW{pTMJ)02IP&ka zB*EH4?4_S94Mu3uno=1yZLtg;@V2KVm*%m)TLN@Kr5_6x*Tep=1HGN}9z%1R@o1+h zi4^r9xa1!8?s6l6`cO6@Dg_^^q$$#AWATh@N3pb1hN^Qdn98lIcwH?>rouYZ7Oq6k zkEl?-nK_-GHCts{MLKtryN8@FsB+STJnACw{v@bH#e(=8C&Bk_API3;Xx-$~ zxc$b9JX?w&AQOfqP5(#HdHCi0wqdwEq`hdTUD|s+_j#$zvS(JvmhDHf$tW|Vl&q9Q zLuSZ`WECP&RFqX#W~IE>`v>5op6C0$@AEp&#{V24aW5!< z{Fiq4I35?D?wo>$m#dK7ZG>a%lMwQ~tJwOh2O4jmg_k%fO7y)^#~qEltO)!)_8&j@ zeQ1HvBseIfU>cu!hS#q`e*8lGz7#?UTf0&5qT#|gTp2S8tm)JmS#dtt3ajrM(TP$2 zBuzKZirVuD(A?&Q2g@Fb<7>8a=cGYQo_bRl=jLKpP+v@T{UfX-=fxCX2a+lLfL@Qa zV932jNlgdh^M{IR46Gf7J6e!Y$nvveCeek)Kjx`I7X!L?Z=rp*Fk!ua9XFQDQhggX}HcojADb?-pvoyo*)j0^a%fp<&z9`*v1v&{OxZ64z4?+*X z`1@6CRk!4>$7_)-=R_^a2XG^LmT*1hNB>%r@oMoKVOL;JSEm+Yt%T>| z4N4fDDm=HOV9P8QTGQ|>u&?3)ocVAIvjRE)iHQuHedj40HfqxH?JLB|^-q}~{D%X7 zrWOTUYvlKKF|HQ7QQ^S7B3$YWGYPiz*Fy@?8|t7lz=aB!!8BUwf=?zJVfDkRdo<~n^)KQ4Y9VfL$78fn2eSwdkvCF>yz5_M!L}OC{dLGLp&f(kufn;*5dUTw z(A0$|M9p&#q;=z-%Bp7ZD!~~qRvFW{rCo)6>OINjiu^$7vktVycB=?I8zQE1_HB19 z%{M-|R&xKd8I9BXCb^w+1yf(&7408~3Y*KtxV+3*Ts9RFvlY+LWy2RyFsK7H`|eBf z47*Z1XCF49C&cUw4Kn%C36ETB?oGBs#`g)X^f#n!Us^HdLj#ugu_UQ)N;F;d4Jx;4 zP)yk;T=vuu_sefXtL7ojEJ_vGP502hxB&N&T$H$_6cy>$;C?!mnV~A7*^m!hABSa= zYQ^)H`}iKS7=!W@5gJ#9h(^#OnuyKnau^cmPf{mC5hve^=OMi)sB{Mk&dH)8YN*J| z?@CK{j>3!j_2Tk1ZJM{*60>Hm6a7Z((ctrIpb=_ALnD>ww`&SM?lmXH@!d%6U?!q8 zTsSLJqWN)a(XMMR9`X$Q+`9(c)bJ27v%)03jy0gA$3@XJtWYeiZo%Vu$)f$9CIUk@ z2d0ndA$}e*hwQbPV*WB&G%j~RyTOhC)AzY>G4Us7UGC5tq~P=4-V~I55O<~>Lj8DW zI%t}P-wvPQYpq2On|tD|>32MPWKQF56YTi>(>+Uz@~ibAow)-|+l^`JR_=ih;vU@w z1G>4l7P$jA!y#9lQmt;IbVwUU2duz4BR{&{ph8MxH=!iXpEADwz}zEqu_n7ONxgMK z=Xn==8zoCuZ3iP^oIc*o)g+}J>hMyS2G>Yt{r_1~q0CF@I|NYj1Z^r^Rl|-QAF9de zN%G2-*skS7L)T_u-hdx?$FrjHx99M1%L^n4E5q_UgUB6b#DekkNQavvCv+JJqpAK~f`JzVs*Lz>=ge7Vz~zgHmH7Ckz$+=HI<^`^3? z-AI4GExk`4KmmECROsqTj(_%JKxw#G{KHvHdzS~(jmi?`2P?#{NvkkS%}kW9TO)?9 zZWQm5T9NjN$;mBEp3n|vxk=WJ+ z2i^x#p^hDWH>wbux-e7ZXEQig(MrY(KacJ9JlCEwbvcw9})CbN17DY8spx{ zBN*V~NL$X!!TQ2A?Dyh+`T{xJDcR1?^B!c}BNZ7>OGSRLE!{EQ2l7}c@-sbX-HEdp z{_4sdjW*#oNrompPRFvj1)RgU(XVam zab!vXiul=FqLYq|4hIm}FObB7LJ@j>GiFxGGxPXUGP2))XjM=kH@U~+#k%c?Ui<|M z{55I)eG7^-H=;$(?5jF%OIO)Zu)|%K=9s%sxU&TrhV~@W1DAo_9z^T@p9Nlq*DpUx zKgQhu#tKwzbEl_P_n@D$6w~Ay@TBxICR7E(Ij9kZPQ~c?Z8M@4+{3qNMih1MI&K^O zhR@orv~Oe)qH@~cn`%x`xASmNQ-R)jx>BvX4;~E2g7ZrcQoX$jPnREu>tB27sB%L8 z$=h+dUl-`)IEX&aH{fn<7x<2;mUIm}id~QFP@r-_Ebd&({V!8AyRSpdb3M_^VhC;> z+=vT%Hi_b%Cb0XHfb-YCT**A7Mww1QwB}i+7`ae|>aPV;=-HDJ&(Zp{!pEQD#A7k; z_Z8f)7=_snBgDsRcd)o@4D`SLp8+bsj`=3oJ#ea+_hc;EqiyJC_AeMqhoXY#sqLZz z?J|q;c9aX%KbEIc{G3XD(~3QEm!Z@!5R)pU$v5*f&JOBV8r-+%D4#Gmh= zRy1%Zd$xu~VSJ4vDHOJ2=ZuN4>@cK5DeEzIc{dV|eM#^4L8J`tPLKI#_NQYV3Yk~m z=FyKrw%voSsS#y#u^@FnKIf0^K{qFw)AK7uSm0wrZ;u&L7pHN|7l&eMm+d%F*%SZ$ zn~VF$4zRmt5ynQZ!nh^NP*w5>8`9kAOr8on^}nIxxT{J>G3YyA9S2~*i7{PEBxJLWMx>g6fyS%oG1gh=w17 z>5n1wvB-zomuy7vhb&kuu0+4f31XjSF+R36!d!8OIDRG>-}&E-?^-A6nR`NFviSyT zTrW%3n>vdHS@j4o$q!6ky++*eIfa-xFC=|4bSXNg3wH^9FbHOprKf`gNI`L&KIz3J zir4;teX}tIT$81FrbQUJ-;5H&KVspda-JbL&_-n$b~$7tQm#alC411Tb>5WxW3TYq z?M-uE22+HTAzJ3SQS3P{syOIRTVI?K-}_oputFc||2a~W_p~Rqq5vwtrV6=frZn#4 zIw9%t5Z4!mVZ|3K@$~mCWL)Tmbv;LkRfV5mx2!Lc7yW`qy{rh2e2cPp74rXhE$~=T z9VV>#h`1GHlBsHZZ|6B~?jC2_obwKuZ%nBDhz04FJVE~?d$J$pOVb@{@!C%owwE%w zFMJ)>O-ztBpU*-MO7T%PTTFVGgF2&gSS>gk54k2T9xvguKrEVPt750qIlO+i9u@OP zi7vU1@X*JZ)})R>X>x+-BCIJSbv~+fUrYX2d634QPIX0aCDfYzLTrEj##O*1X*ljGt#QLvjYgsNnD|``Y6-JU{+PR|0$ra(l ze@QOOCj}ah(ZGty4z`K<^&4SeZ4bM&{XKH#K* z0&Q5U0sW)$^azTyIOeyoNUMeGbZvS!@|Q>$e-OU$#x%O&1Wu$CLE)Ajee=&j-$4g3 zmV4N<-rdF;c8g>W;4`C4FkP!vqyBOE7;+_u_MUA;h|hjz^27M|p^Eo4wPMW573x^8P9wjXV1hBI>&Q~E`(~T?;xmx!Wv`0K zT4vZgTtb!G&5#qVPXTf_(EHJNC`EUpeV0!{CBzzklFcYjwHnU#lQ5=u0Xjcbu%qIi zh|h4u*;Q4TrqLm0p4A)LzS{G7|Djj2LI_^!8quA4x>_%^f+0cV& zRVa&_jREUisXkMSzHVHL<+q6@ab~(OgL93Ejud-FiTt$JLUYu8_Ms@!x~!SF{`v$v z7`srp<3Oy@e1&ppH7a=-kAGI%#EMJ{%5F}?>&|h)-O!DGbN6QG>NjG_8)MpW!+~Vw zHw#lob-LWggbG8)h+}<}sk6?Lj;N-I$<=cF^ZX8*argMV?MEqcA7I|E0;>Ca(%yB< z)z;<0+$E6OL$3$6i@iv(Ri&VP6R$4#$xfHD|MwSVippmlLz}b|jXdv2egoAhjST3+ zXcKB3twfKdm`C2`MgOK~keZw=X{%e&qHi%Mdb&gMkUNR$lOwQy_gbMo*@o&>51?7d zh|rfMP>+kle1)@^H75sSZD*m~G#~YerLdDfh)$L>b?RMRc#Yi(~e`ylNJ=Fsu!hg%EHN7U9tZ6 zGm$fG8F%lpBnHFYiU zXd%Hhox75|ij)R*r-l{IBFx2r8DMKNoN+`_*)bYZYt8AU!d^+r+uP#)j#)J}!t;wqTAcCOHJ~R(Hnc9ii6Ghg>lIC<{C{@R!&hIBi9l45?0Vnav z@DDrZ&fwy(SRC_z4T+=@Po}bqXeiGY-X@8n|J>+&1^>>sZxLGIHuRCX1zoRrk;$2G zuU<*$l79;8dzjF&_#DWVbKv;Zge>b0An#`>+%A8Cq7IpXW+d3eE6>R{!$(u%BN=2_?qPK}b)S$5x z#{x>QGg*;dcWR9CjUjb&3|eY5=;en_ zeEazbI}P+Hh0lLhZf9^eM2Gs;*5g@pHTFN~0q<}fYIw>}xQi}aLYd?18z<~d`atTD z7PaR842XXcOr7$kl7wAO)ILr^-{-oDAYTjGc&H!EUgRrYO!FlR*CnD)!!5kXY7{pg zZxjc2mg4b^A>tBEk`z67ith_$am!tv#$8X5{K=6empklys)`pMTa@Tcsti3CuwGKw z;~lo={KEMTV`{k40aLyoZ&;;G?&&S~;;O*iVCKNa%Fy2v?Ex`+im_+#W5kS)y!vdz zIT$HihhYf`n}lm{bGnA?_lqz;#uC=a*Kx~#1WYwO&^zKK_o4^EIz9jfN3&oU8Axlb z{?92JBKldszO+sBAA4WTh}T`7z^1-})1Nb<2eyLz4=wT3$~7AK(Djc1-`W$0Si1(Y6t z0ll|!E&6f@*cze_9UhCwB*Avc=fTxL9mG`cd*>-BNL~`*U$})?G=j3EeS0 zn0>FivoPs|2THgHnB28@5%)xL;oWYFweNkQTX7z<267Gm z0`mkj@8P|p26|2nrkB06Y4;Ro8dTMvT+>ZSLE4j!>G)AWfj)IvY(=AH?8Qw7TcLe5 zOk{B1FD$<%4ts&I7&tXyT_M;>jRp z#+G`}dqpqixcB161?~!}x>H2yA)M)Jz@0+}+OTU4UIi-9HD*wLFJ2?GfXDC$6#<*W+@Icywteke?c;rcNSBmcSDL_A-WqjJ^L)Du~IXjQX zkA9q6N=(Gxludj;R-$g^HR5yeerWIi0$V#RI&Nn{QPZvHwz2^!thJ=WAGIm_xi)Qx zWT&x$1G)Znrm~6xl=N_?KEG{f2H!&b#(PnGoj<#K3h>yE+4Pi)%#$s^kE;iB=7V|a1A9~QKbW4m3*n|HfxKm{v3H#fpP`dKDXne9xEY;ZnukiaKzRgd>>K}zv zeY@zE|69B~9gX$BG}!~b2!A8xM2d7D?&YmT`KE0`%AhA~M{UBZmo`NYNQ08veCY3x zL~)RNHGnHLv#p1=TbgZ~& zfL`zCiCxMOn7P$~ZfCdRgj!#GJ8nWRzsXQW(iXhh?MzR-Wyxq+hluViP5wg)F>tv% zu*9ocREs3&RzJ_7?ORZAvIGoN+OL({XO?X zjuc~YH#-WSszn!e_+s+q_r@Wzc1SGt9@fq5ExlnzmsTaH9p`2jyw}D|MJ=e}lih z7F9-?(D#Nm3<eJMeczK1!8Oz~=B;=v}WvaQ;m+N+m#7+8sgP9cb^#O^}mu2cG*;Qp9%LTs{l+ z@pg1EY6o6E2%)x2S9-||Tjvvy!U;c`o_YxDUJs?8+_SuRd^5uRlHm~b1xdOa#lVQ8 zSX1>J0ea<<4Ht8n;r{{exz|OrO{2v5FS9f;N+LJtlqhsL4uke{6946+MWrLp*nh5* z*zeb+{mC8Tu}1*raL#l8yaiN!@)JEzMniPU;naD z4>6}-t3iF_<>4J~igefXrj_|EsNuQL<|#h3Xm>MK2XsJj*dSVdwgx?gG2IHZC6n3~#Brv1 z`kotU%xJ_<9|Ib!W=!4reEg`#DKVlt1JCFZ?tUqgTs)Bvo8Nh8-drtSEh)ye^a5Cn z+l;@R|HQGdD@gCQ7}fdxR)4L3q%#3Y{19- z+al+-B~5-2h4A8Ml7ERZkd$iE?Uo|3R!$Akr@PaHLLCg5E{|Z&$lLc;i7r0`dsvj{ z$*P?&Ed&Jyb|WjSLQ20dDo9YE<;=;IM7b zARZcapk>-9p?~4MIB+gT;{587xb(#kpM29K1NZKf#7UXs`!WZS)4Lp}okFO8RvKpQ zxy7FaPf{74j@G-EA%A8NtxL^8;i7sx&sL)LTm$rTs>0GTJ_qo7aKs+=Z@YA%zc>Gh zlKNvM?FT-ZHCLLxzf64Rx*j#ANzLs+I8Ldd?_UvQED3}&~mm+=pgD~|Q zN~bPp(Zu2me45{jBKfR+r{s;;(5XTbiu{o5@J-xS?@nj3W#Mtr65}5#QXKcsVk3Uz zIrD`9W7X(WH1l7_o#_*I&-70<;jSAyahKJha*Q+;IVvy%$Krv4I*d^1LY9ADV@0wO zNuSlBUeqdl&VLTvbakV+c>RPZJUmq#*lZ}i%zq?ev=;|PFX|LK2fEVN**wP!8jU7f zA6i)V1Cl>$@$QQiIdQISImrzTv7_-i{5kH<9)#;9#t4nCf`)}RT_d(rv{7}K>v-?pl^@mD8n$2j;ePh=frNb@YQbI{Z=CxH*t`N%ASM5<{^^T z!7D|?pJVu^^XH0#&H<6Q=Z&zA|Hz!Q9jRC)3*RSyk-_)R?mG?fHL)F1^PK6&AaCl) z^VY6QG|5TMoh}zlL*O`NI=9o4p6q0QN)691E4YvJX>4GRa&HQHCr!nRL%0LrMW#G6 z`(ybl@UXc9y*uDSr*kYR@zW|vU?*sfvIVuhS`xTujSHoyyU?5WVIp(dJDg*0y=^cv z{hlxJfjevq%=BpTvkGire}11O9dQ5L01qQs8sM6W4cBETkr|V%`6n^+?;BJwgEBKd z8b{9bq_um4DZKA>?j{AWqo*&8D`noZmle4<4<{>jsttP2e8CcT+Eu1b;|C>U&n5|t zH|D-4&zs`Q9qF~I0y`rXKxcOmN)&a;QH*C+GaZ|jsnSKCJ~*v?3kA9+w7_XAX!1qz zCYjmKAvt{a>=4Do?z>fEQ=%o!9(oMDdbMNIKrN~%J%;2dHF!~?PVILMS?PHa z3s2~f#%WI|d{00|4tv(>y5R4W6G+d#CJf@sBt27Bqi4$&as1mKiTV9hTqYURtWOgn zYXv)LyihrU-{nIEX+)32zU?+xaL9w22adtDgg)>v4kH=Tr}%-r#TmIr!g#ALwLhqn z_SrfLTT*8U{!{PJbH#Tf5N9bI4 z^f79}@vmv{)2xTmZf(S}%PeWnPt?X6VLJ0)PHD|hf6@zzweB?6Wf}%u(xK`a4{G(_ zh#Ab_9grg0J31a??(scQ*MV$aHR00_~es|Kd*^P}k+mU<2j%KRtVumam>W^G_&YFjU z&71J^WCijZ;^F7715c~(aGn*74NpyBd%O(uN{*nU!v}rK`To4}Dvp}(#4?ZlaI9wE zRNE>%J9L9{rKhlQJA}pT0uNou&Og_USRdLSk2Kxr*^m{;YTAb6pSJX|Y&-PA+(`dC zd&lYqqjyCwnzJmB;&05vEguKE_s5Iuu5LtJpS=j`+JUhVBu0 zUgm$m`0A~aGbU?9@tZ6ZZwsLVQ(uVM!Mia0tf11eOrbZZ1X4dFq%uL1?u8A9=Obr) z`(euGy?Ge@#uq0#wWvu-7gJVR;7^hXol2?w^icVB5uR<%2TxX`ulCnO0!D{x; z^xbq!Ozv+%M=u4@uszbKzGy^=gZ$|3-`mWtIa2P6{~}3Wx?fDIF2Vd{J@nTtmYBb-!teV%ad*^e(J}ozVy@j3BePmi zZT3hK{6~>u4@gs}hRoH8r({Xb`~y->4Hp*9f3S2LvuPK2K9~I+DnWYmD8q>K&b-Cp z%iZb86Hgks_%$qk-xa+#rQq_YLdbk;5Ys1K!jPJaJYVttTu9AD^qOf zeF5u?XJcMUhnTcE9bPgk`L35Owmp4BXP%aJXoV!W4F%_Kxlf zxt)F(H6c-$J=I~a1@Adg{Vj^`8q%^a<8WYVk%;)AOb&w#uz#DrB!eBtYY!hs+ukNg zMZOjl_S?qinTbMkk}Us!&S2=*(V`DCwXSh>2z;_fgf#S}lj?sEv23>pD08K^o8RGO zgQ76m+9_fb$^!q4h?aDj+5^GAH}lskO$;fr#>$UtuWa@o30 zcS?(~_QMrOA2}p?+|0+EZm+O?s16;T-W}K4+ELg|lO{gy6jGYi2xp#Z#2*vnbbX4> zl~%Mc=>gX3e}`0-4UIU;T+Zm{IC-r*9r#^{s~X=RV;GMek->EEx+1wA-GwDGK@^=< z2m2{;2u~YAX96AwpRg-pl`S(vE|0|bgdT|9)Rp;AC2V51^)%=1Wc5Rr&R_V9!S?P{ z`jO96qrbz@-;P@O&NnGjmLA1A(TvtRNZ4}^TC;UYt*QdKhnsPpT{)SXj`Mf76sP$a ze<^pLME*=LjZ3hEpPs9Mp&cgq+dlQ(Y>kLMDEMZ3?lU#S5e%s6F(Obowa+7 zU0V|%4h*5Q+-*O)X9Wg7U4#4G4kK{ce#yaIQ!!`41sr+0L`<9Uf41@@F6Q`4lAmiy zDyRp2-lal;?!NQlI*BAHQHoIyUF@q9uNNgd{K#Lr;3r4v6zrdly;FYSXJ`X5f-B)SYcHmJ>csnBFAxxY464WKA;n$HNw(aL?X5!> z*}K#H{3`rHcs^8PM3>Xk@qk?gE7a{s_FO4em99gCgP<2<^y$-r9XRl;FNO8*K{tl% zW%eq7eM()avvMA;{<{YqHSTk7o5km^T(l(q!r38#?C^ZWGb+xZ4rb$t?Fmse--VVd z-^JHC$tgE9ZQY>LT=;;zChf-@v#!50#&L(F!Lcv0!u(R?TM)almfLp|ayR z5_}m(DSeAVJreMuvmSev@P3|wy7Y9PDW&p0tg89ewA<61!f)GB=sj(k@ywL8m@!D( zd<>2Cf}P%0RAPAzK7WYJx_MKC@lkvl#-5a&ck%q}DnuSBg8Z!GXpESG&TZ##l070d zlb54q(h(Rcbf*x_R5U#MfpzcL19OaLLA&MY?CCDFZSFSM_W2FT#}BZ*5KP#pXYT)xnj(afp`=>g4U$@V5{~F+|2i<532`ppL+r3 zJ(~=VwZmYv!iMj7QCJ(g7Cl#bP!x06VO91pzidyN19a$y$pB${&I9v$8ItxSS24K$ zz1aHDn3;Sx(Xe(ZD%$$Nu%%cO&iX4J$ob&1`4#bEQM@=78iUHeapL{vHc_qfMs)lM zlF0vm_nziJu`h0UQRPiOd*9-DL_tm9&4=>zzP}CTXurbZd9he`*9Kia{KLJ&EBVei z5L%oyZsgwg!iO!ea8u&*v=#m9{}1otY@rkFN%MHV@YBf!A08S|9sij}+!y@+x$f|? zM5w3glIs3GBp|nYO z;E9X3XQF=fXsjPB!PHVuoakzegD2-=?~3I-yBG&4yI1(w>_?j;`y+qQLrgdz=vVL> zSZDpkZ=N@d4GzX#?BnxL8&0geh~X{gF*5io2Cpr^$y2*9w!Q&{xfSU5%#L=pYty!U zsyv^tBD;Ocd?#S1QVPEt!_Dbi&zE9H1J4?U>_pz3G(Iz%k!MN*=Q`ZM9O+2CJks$= zW-sg;nYGcj$7GiZ@$c6wT<=bJYiY|o@CD|YJyA2gLX?j^jVW_d(6l&&{XjP{s`(Ut zO^m|bq5O>8e+&(4^`JT57H0wiNL%F?vikEbu#f)Ky675)M#^ENLof2Q+QTkUNBUSE zLO*pwgzx zX@V++PkSlsGfiRf;3vv9+f(?OUKlm87L6n9$#Aj@9u1MA`U(@WwveW!(|WU?ZZQrG z>OybQXW=_D{13c2hq%}c8T-c~C_ zj{Pcxlo?r>76|{wqoO0sp32j&iqomx`3%SNP+nztuD^uZou>q7s=8927o0a-_%2b~ zYfbGr!4!15kH~iOrrnwPIJo#BqAi|@t}>RKZ9YV2a8K?}%Rx7y2@|H>6zS|bnz*SK z_3qt`UhwnCY{Ed=C~HpA_KLL2hG-~14;v>oAuCRi>~oca^%U|hx^Alp|m6EJ+2tl;`cK4oci#tJ*Bfyub7OG zDhcv-C1Li{2(0hbEk^|3$x(UKT)W; zIkHEr8=Z3g4?m7OV);@N(rWj{%B2=C+iF42Ja%DJL@z3uu0)aV*W;IkAI;I}O7or% zWA1A(MXpdH^_7d5Y5q@=+Pz+g)uk}di4m)|pBHyO)*w2gPGWq=0+xT-ojbfpjJ~Ie z{mDd@7nBjv!v+rNp46}*UW|T2kX7!>zQN-d_&Huub@v#)tvZT;!}((4)MH?21>$?| zEBeY@4_bd>W~&AbI_ZH?+Q9aBi!F`Z+0f?eY9`Y7=4~KN3oT!Q|DfN~+(Q(I;F&u-7(Rt9Q|o7XsVT+>^;88BAl%5Q>dT#n>ucZt~D(Jm&P;r`SS zZPExd#W{H`T99B&I94dKCw8TTx$;!;(ULju`DiLOA@j>t6tv431q%!*zut=O7%f8W z7Dw7Pqz4uA{uJH9UbI$2m!w8{Qp9dwGMU8gn~@=O+|Q53%I8SdL>bdlIe%hyL}+a1 zetC_AyQ!}v&zXfC#vVSeo{yotcQ>k~s*Q3cm# zc4+NNWbXbOZpL?DT5}#|?yScsC*B1*HwI}j_Viz~J2mYu$KNm`lK#(|9f$RZ8tF#X zx2?&WnJV>`*_b`mm0sM@qBBP8kljo~F8VZcVLayW|LN38B^uGb2J+VTv0y*X;8x9o zhg>$^_;sP}fdlbAtOny}s?nk1c`3%4E0^l+6YE%!Psj*OJ2t-e1HGL4z1PG4GF_YQJCkD2-D zN$;2a#9GQ_<~V>Z>*WQec<)7mw<_H;7<)C2=S35CXj13=`YVkWIX9D$BB?@0@*l5G zHDLynvc`laY*M1aa0{BY)QfUrG-!g94OvL6sQd60XkWNnGOOK%5z6KVRue0t3I`I7MA=b`^af%5L{6pG%*p}JPl#nRt`Q@${DWC>wSHDoU@hZihOaW_72( z74__EFhkxUTgrUVfC}y2*xf^iJ=w9a_R%J5cBkLe+lZsQ?@8vHHC=sn2yz`-)H$5H z_t~9jaJA$eOZ>Tf_!mF+v%mYNDXo%j#He%=+GgucA0~Q1x;h-M11DpZ=R`OcMdJAf zH>jj~qVIT~?W|jkA04&0v^O(Qiuu`o#*g5?)1@eW&Jfsa?!>R!hCl~rBY0hoM>jri z`QN&VQ*tNa(Ec0uxC?(VbpyH_dX3qAD)F8jB)cSf6q}SRR6O12P^db2uHGy{oZ0QX z-;zp-V}&Hjg6>V{9dos(uza^M9XoiEJDiEoE;6Aar8L+szJvBFUm*Wz4bJS*#gmqI zXbn$AZMp|SV}9^1uIb!848p*bx6!2^@7Ri;1T))A6#HI7hRRR`-Y!GQ=T}&n8-q*d z1gd$qHTwHTq$!yrQLmT>VAD5jr{<7$7>_u4v0&%e;2uXT}Md@03$}Ag)!JegJ z_B75pUd2K8O@mN7q)bchTJrnRn!Zg^qOlg6@$0iWb?9kQ@BI;IndMHtO)Au~c_(}N zr$`)CZiu1YRnT3iEXED95+mcEVSJ&onEq7}{(afQvoA@EiTWd^?$_sTPpRnq?8ZKw z(~{ytR-*KrJdU2s7AEzXC|WHgc{cq3ifbFF=cVq*{WoT_w zcaoC|gnCg|`clA|;A=J9ll_CSO;)r)!wSL7L@wH8NtuatXnUA~7t^`3_TLjE?k>T& zx8{7OeFH0J?xS`afa)m0J2zG7sksk!@KNh~zh)Sz41k7zC_TJjf+hDO;P+dZxma&R z$%bM03*J>6P$G(S7NUND3RQ;KacBN9++;&&PiA-O-TDO8alL57M;CHo#%^r}@Bd18 zi+Q?IWNF;V&a_(Csj`RffeL?qc{kabdYm05MPb)!MZmhmz_yk%Vuwc|!`5wA-fiPu zzppllPX`r*;fD2M)lf%vFTKEOH$q9P3z-LggVV78*x_nTA9KGVSHT#kJeYZ}evjmq z!Dx7H1MT@$IPC1tnZ_DSsr!bynO3;b7DRznvXnGVg=)PAkhQrw^*Jp|vdg?le!UVo zoZxP%VFGHpS4n(VtP_*AWnzD#RFRV0T@gBeI(oXFk({?n5`W*^7L#Yb;~umvnf2Df zVBYcJqhU|4kBt_wv%ll1vKjqO^QH8myKrfl2K~rzCFdJUP|2OFcb*=!Dn1_LwwutW zrD_y)s~2gH2%z7FhID3oFimF$`0ZC2+V(G$c7I_WhVLhlKdtHFnL5e#YfcmiXR4ca zSlEsbv^&X~j8^QGJm~u#r4KW4K|zK#A9#%EnY%E-YwQWSM!GtBts9C*W2k~`rX47JQ@tm`=O&Hf4$EBIOP zpNBZ>b_=o{n|YrUcb_cOsN9va_;(?^qg|i7eyNb=bNx~FA8kFd2<5UjaOAiFvt<9l zzWxO7V*h_{kI?_?9Vo|}kdpl_46Mi$IyH8*KrIFTcdn|$_)!fzCem*=if(nxVli`^ zIo_Oo$ZgEB*;4lrcg62ha@5=2m|mOcBWvtCtg63*1@gD>M%9IyhG(J2iYqX3wWYj8 zm9SIF#Hz<`r1$r|P&AB1;$|5dq4!PLneD>z;P-g5c&ga#y9vqM*-89lLSIf8kg29Q z&HQdk%RH=UIy>J-W}8u;9i|k~=tN=5_&M5~fz(gT75I6O>#_`39R)p%;<;Vy6{rk# zqk#)gz@u^{EJr-$os`)aSsIU+!-Z%vK8OPDQtp3sA5xXP_p*Bd3@`Eyl@@h!@3_Fe zhc{42FsHi{v$1_tCtN&L$e3M&X;wMt{Qn&Kn;X2$_TcznS=v%oA_`XJp!obbp>sh& zypLUr@Qj(l(^NsMV}GFQ-j8C0?g`PrbB&gzu6W9OyH+a-nma23(`~&m@ryTQbRC1- zi4o|vtRD?e)}nO=LwO(1dvV3M2L=9#lq@>@OZ-UEB;&`@;=7DQ+<&|s_p}A~Fg}TU zRr`@OTN+d2TE(95Oz5kP$NM9Su=uHr6AyX6;|(cNP`EhtUUk|q`%_ow=zVYcE%097gfzW$lZ;%2-YnV6)v{)4S2Jz3*BI^Z_h`)W$XuJ50{1`6~@}4y~&PedFv6NITm5R128gjA~xxD#h;Ri z_&Ie8M&y@cM4mrA@6iL3I`82AH3@0lS-=b`=cd-6OeIxJ@!pJO!=)&trJX%$8+a$r z7v8<6Os+$YqW463$}w%n#S{b5@t38DoJ=wLt~KeLRio#7c8EA51A02>JDQ$1N)A4E zp{2Tekk^<9H$L+@oKC}y_vdhmS-_PA{8_NyibbhUk@;>rhUKfGXCv>Rmp_5lqamED ze&8O&atzD0g!1rn=oZ8L#IOX!@XXuM`3^o!+KhJ*rAW1Y2+6V}3^^9Td+qJ0b8-v} zIBPi*U`=Lj2e9R_1mn41YHqdy=Tn`j`(!VwOAEjdvjCdc;6#aLV{qJ)-LsD+G;hs3 ztYLSC%B^4U4;v&r*ya8pRfZh&kBikc7jWTOBaE#_NLD2F6jSCNMQknSy8gXH=j|J) zaPgr(wHL&y_+99J(T5#-?4(H#VOKpNu8{Y2FN|V-NrJa^s-(a^@g>sM?2|L3x(Bk9 z`|<%)J2;!=oMo@^WsLPQqVbPE;`<5Sts9;y+M?{Kk-Li-YIVYhXE9G4`qJk>8TLDH zRvqd~mt$SX)5%awa55w01=5*&@j`ZxE!lSZP{)96fn$g1(vFg2qQ~`8JlNYRI$p*| z^2_c+db1tu-o*-ojvV}2byN(KG&2vfM=~j2hIC6i(5CUdXoBfa%nfgZ_L^a0+pv!~ z)l-*la_%+J`zyi*=B39YTV{y|&MC~Doq$}ltf>5V3~SU5 z;(7E_@$K0eL>|sVl=M0{T3iv4?7XxaI~OU>{DkG?8vGeN5VLJtg*l&hr!gtvU`&}lQ?%E! zzb{jhpK;6qXUS3A!NbT=?MLA=yOQMoM!ekApS!=mp=EIrH+zMMEm=XdVL?60qIj2e zbQp!tZ^Mh6JYn+Cn@%OZ!T4#Ol5U@+5LOvPIjgpc)z@?|!H=EWiEc%2PdOk%eGs+i zKZV>)6;bY=jy*FfasS1YA~nrieB6BrA9L4>!`0ac4t|Zm;cDc$+!WK&JCQ4|M*qq? zM2}&Wxaev`DL(xCD7w%46fJ1^g6nwHR*QhO#`I_P3nY)IKqTjbMiX+e-n|nY&70t| zgf&LxEqp#;*6->-x=^A@bHcXpyDf-{T&3_N_>KrZphmwx0`52Nh(%{~*njv=Osud( zn|BvVW8cl!qaUEju9PKxb;zgnA0}=!Cj<6f-824(%lB-_I^rSR{yoCrxtvW$WMavk zY7DE;CL8TWY~Ij>SJoPoH|mJ+SvFcSK;jMyM+;%EA0xU=3`5vK861u8DQu3}0O_$} z=vU@BE)*l|@&gfjqneirWaH)D5t3r5S{SPp;+Yn+eR90FYKcE-CLO}%vfDT(C!uT2 zQ?aG%HEeS4!`+|ZLhfAvyMa|{|LZ2nzj9By=xRW1P8t%0(E+qyiCz8t{%Jp>6PRYP zRoGabg=eMuRhJp{VzX&3!Ut6qbsFrDm|MIRe|fi$=1_T>u}2jS++&;bUXH4GU(H@r-8qfsvLP;}L!W)Dwtx-}A4A869u2=2l3*bNm=ekQ9L(AHC&Tg17Gn)jBp zRhRed8r+ktG}Wa1!9H|R^7=}#e-Le7XH2=SUKDzM5c#ra{=2CM{Z8md1DD%Sx5I&S zm)_%l6rFcG)@>WcZIO|^Ws|IoQn;_Q-pz3VALp^}xNp{2dhGFnEvq#?AV zkP6YD@}BRX{quZ0-1qNyUFUfm-*1!UPP9J!fmOQCvF|8*24}F>{_H#a>YWJtnhIFC zKSq3G6BO6SU~}thSSZWTCC-Cby3}J0=QIDs?!(b5M$}x%Ufsm&xLBh_|Mxv-eB$?1 zw;mMoio3(ijeT_|1*4-~X^fN!{fIutoi97W2|ez2 zJY)hpR%%hrorvlY%@Lhf!6?ix;{XU3VgA=$z$#EKTm7GhMNBSP~) zUm7d%qS4V+%(YRc7^xwYwy|71PM4*V2VE$?GgYkAQ>8JzE3j>L0iLar(3iN+$ZFDt8)cDHQho$BlAtVE*5o@k}%Jy9a?1)anb!Kc6q;nQ}}ktf=S7|$5tQ>Q!94N za-T5Ym@*Qa>Ed@&s#JC$|3owX8Mqs^%7Dzr+taImnMkT&M(izjTC?^5l*a~=2JbBF z!U{2w?-82LtghL zP$wCfG6r=T^V!4TmA~*`IKD27LypH`F|2I?>;{LS$GO?4{4;|8HoOPA5zd~l%_yJh zMlW8+Lao9XbLyN)`;iv;y9}cK>qcSMCT&_VqCYKi)AO>mLol^3esN z--MsSAl?kQm2ZXixkS;4jri0%QE1D*6rX~DA5U!QLPRH?=4r!jnj0x`FJ_JBG-TBD zAme0Z`jBVD%$(0yczFv3l{AYf^)j^i%p5!!=ZkR;Kj9XbhOw`{!2hEmB0KfyWb{Ym zsu4EKQzCcmHasxn97-1p3K=sVU-NrV>27uvFWrpf=_b^Fl`okt6u{)3RQKA0f}=ZN zpU{hbe(<2@GRpL8K6CIEuwVG-2hOPX;9a99ee6CQ<9p45xy(Y;hR%eCswXtU1a{pG z$8wE0p3_c4^QQ{PsCiJDN+@&W%Hgfun=J1q;Pt?tIQxcs4^2~G+dmG$E2L?{*=+1g zID#dqzwqeu0R;Blik-<#aILtH2{}BY;u+(Qi$8?UW+S@d-jy!y4;QDeTT*x+cfFq0 ziqfDSRJ=X{>joXi>O(d(b$JRThl;tg(}U~=?8aE@M7XF`AuDb$cI(#**O}~h91wz& zk9%XzuUaIIRzo-0u24U*3wM@Y!J5v&$Qrc?84iyz?8Qt>Gd_#D`r9z77>~xom&F&; zzI3R8`|He}4QlB{M|S2R^6E!%;8B0lS(Jw0)qSa?ZV;`rNyRzOfxMIUp>1aI5t1zEuPQ)$Qa3UhL^M+NDc{xAXz`HYber-prjHEy|6sR+?P6iFq8Xbao#>3l zUSTw}1&!=lIC?vn8Hc=h}u+$!-9S3kRwSNt5|HvExj9O*y? zTi#2O)(+r~mmrIx>&#m1OG=Tego3?<^j&>uvf5VhrOAmd%<-jBqf3SUR6mmK|63d_ ze}=gH454zq3$(&2Ih)Acc=I=6t>t(0^9U23je3+kbs*`T(xk#1W#+s2Q@flAjbgV~ zcc&qwr6x67^_Hghl-V;ka4~ zO_M5^WpP-T2Khkc;~RKJJ{N8ai-eEbV=P>Bjo;mdFdsaS=C!RC*^(g0PZ~*1vVTPG z6(`uYdQv;{Y>YW?DrH=Xob02xSv{1Zqw=BEl+Jul52`YMf|RO{c+sv#mmd8P*LKU& z{zu*DYe=2=?No!{mB##8)q-8{L(IEiLU&5#NN2}KG;YwO2bUG;*y-!2@nN^_krsp> zk>UPzI8>sEvPN=Nx6d4Q%a5cD+tsPyegvQUd}&(m87TDFg4{8(v~bL9Jl?nf>J6IY z5@C&{#<5U_6tx&?(a+M)u$<^i7G7FZRPh@hxfk<$j2s=f{2HS&{3v|b2TZ-hj0;Uw zn&SOdl8zC);~ zcfN4m-JLEqKEv7@4j9J#&S#UpqwKFX2DDky@q4epEAKou^p#ih^i{)~)cyRU))^;xU*>LMO)*k1b?d@&w zYQNxf)!0Nb-{A=F6{HHfX7m=LJl4ZE7={0o3%u7XHbLHqR$Pj8r1W4bm}PdMXP+FY zpil~3i(8P_YDRyfJ*lg56xOEelj{;U>JhyY9an8={$N+STQw0=FKf`UI0f1}BS>hP z2^#xOho-A<5^tG5ae9w5C10r)5$@i!ZlMQll2}nP=j5ydJm_Y%8+p9$MfMp!yf-uF zT^a8VbJ@M7nu|f~h5g<38i8HULiblaHZmvsRc9H>uFBE*kxekD?Lv7vhY{>0PyJmL zDQPb6k+WM-1v~11j*2kU53>d`xc_-Qt>`cCWi-goh6*4RD zPNSVJixY#m%Rj0&&ApW&GPia@bK-M|@H@zm@*uy;2RO+bq(8-W^l^3tZY$?9dz#3> zK@GP~MDe^!kJ_B?hznBNVJp*>uEaKoPnR~aXTge2TRPH+p33xKnkG%UZ9`Ra_?*sj zvXDDH=|H0nedTA`{$b|SLg|=c=|?&}nM3kA7g6foG?H`Ddd)|;>l;LI_0O3_v;+Rm z7jbx7K4La6#ra8B`14W>rJ#K%dy|2jKIW8qrVuq{Kd_i*j;(>YaO?9At2*r{P4+y} zqUFi-suInsdL%wPJcT}?ZD{d}5i8ytN3Y>3)U5VP94pv?#v^CNddqB4G9e!2Ga7_T zn?9N|c4BP(O|eIFwYWTsxr-VSX7Y?-R)inna4-h08qT?7-VOaX5Aq`h;cd4-GTN$4 zvX)-LiFfFy`J6E6jh1t{rI{T&sq4_aHj z>Uq-vkCx`3P|cUD#Y|k>T7`=K{i&|9C$2r?Ufre!v?^9&YUDOJ4CFnp@*h}qq~KE! zGjUt)G4EWD{(aRUt?3WNstH$1)Ij^euEu<6yK&2d@48!spEvL=SVt`(72i>(0c%L+(dG@U9~oXgRpCrQJwyk8&4GgHH~xGO&0v||pg4ULc;jhhSlK>d*()jj3Dk9|4DpVB7Z z{l8FKRu7MXmh_CdS=-L#%S}#vG7F`tg_}N<$tUwPto(Zes58Q+K0W&Kp&_M+Xs!*W>2Pf)1 zR*Fogvj3|0?nFpM)W8+ z2l?Fl7+92qWm9zE@$DMsQ6!f1?iBA_UqB%u5-(dUaB;*Dto0#kYM+d}2OcP;;nWi1 z$83j5JfroaC#}pn^X`iSIDijBI74|vno=EhA%87F+9Vrrg*3SL!;+g;kk)*F2(bbu3Pe0nO%Q=z5vIU1v zY!T(}_>5|PK(f>ICTG`w2qT#q=ppRs`8u8< zYONCiBV5QZ%Y_~`S<|0isXp_Y1*MHMXU5?wp8!K=3gpgrx>NF_kI+Ci!|GlnfaZt{MlK8zg z-Z~ER^x27i%$Q!En}zgMc`)bhxZ4pY#F^Ydmor8*DRwGGHfG{qoHEVc5RC?%siJh+GdhFNvaLv&!%W&yqBg7UBCL6W>2| zp>_m{dfPhq1-4=NBLtF@>++Dsho z+EqM@>P~y(N+joGWSDswh1KT+nAcb{K_L$hj#rosTe&n*Dp9+QbKEeSSX~ zlcwf(EcxdHZ*_HA)6s&`JrZPnV^>p-F*3%AZY8|M!=qzyih1&_d~Z^bx5wiVx+EXRzA$FI*#2~;Bzg8< zF`sqbJP9?<-GJs@hUB-#k{w}<@XXVv|72X~_p3kbeEYw1!iGv_mEr1k6VfYpqJa7A z1nuID_Vv5DM`HuK0in3;bAvOAZ}{%%PitNELX*0(aWCn>*y(5 zw#>$vCC@SW$_nf};01*{4^jAJH=N&&!M}dZuvVOdCf-x7jJSjcvkKVP%KNgN`%yCX z4z_e%%}mMrs2^R9Yc&bTpB{ndMP~G3VhXaxOoF+VBQ02)fk&LNc9*xMK0%vNR^~)? zi4q$9W)M=69mqJwo6nA05N_Z}&+fX>zV!kcwn-S*ojcKO3&q>012_=ahA)RVN#-=1 zLNIq|4s6R1zH^t0n9egu=g;1|r~AarcX!!+=}j|_7K?EknX$+7+tga_Ii3x|?PMo# zx`H;`v4{82@1k)}cb?;KM8`uDjQgTbGoNu!Wls^VY}2O`oGDF;e1wayt!dFSWio8w z%+a_rBG28JR&RSE`OBWVpX{D6di1B@Zvda&(|eGwtE2e(vlorlmKF&c%*gJJ7e!2( zCK~TM&~0sZYDkI@Klpwx7iuZ)DV&4aFBiV|NhEzU9$;++A+p$AIOZJ3A(KRZq;OrSXCj6%(Tetk`CsprAubb(V>X4Ec9HmRD9w4 z$5o!m%s<;lIELxcM8u;1jR~Sn&X?MJULbCBx;P{~j2=}rVw&wbkrvN96809VtgDjf zH$E4;QrOFp79pk=FrUU%LWcV01^bgaMZx|6GH1v0utPE;N#ha*`RBuO3Gd>&WMk}u zr%*ncQ!xI>Z4_T%eqD$$W$7zpPs2<0IXRM&zBYoRS~#<C9x;{;v1nhPX=|&XSX&}htc_E;%bvFjo-ukJIN<#4054IFZHNI_8-=S zTalc*9MzV6fEV+7&nwoTa4eq`7jm3@fjV!oIE#v9QDvgXHI8Y3|N zX!1#kMxR$?Kll*geoK`GZ<3{?6><1IrrrAwpN~Skr=j0WIZ0sYPUg2Af}{C=1*EcD z?B@c|V(ItnAhx1EoqI&zz*gMcX~@HFvOYOy||7%XOFMHA)t!!rM z8<6wdL3DHXS>E?J(Vwz@oDD3*n!r>j-L|I(uKeuY$9`V!4{hdW-?c^iu;z^?*)yNA zlx87n=6R?*(V(o}{UCQR6$T2bH1joZ>_`dLJuxIDqbQ8mjT2c??0J6>39D_YVt}SA z`xXwuFX$3`r0i&qwi)f*ERS9u(iB{VsOC za;K^xsTe+)ohyYs=;nlM?rZ0w??@lgy?IKgMRI4C@2L0GzDUZO*THq40wuZa5jk)E z@0V`CsA;CuuB<_OcrGkI(~unbJo|g6I!#frpu3w4=*%ul3VGIp3_3Fr)6avv?D-ya z`6#}=@gVsIQ!+EiMdnCP(v3NX%u%uEF{~WTcTPbmXAj0Kzl>I^V{jWW1xLp)qvN0s zN#DGQsP_%npJ-0*%-DZiCr#-)6lwN5?k#rk47|*c@~BZswP=9F8v@mc{2ug>heV1j7|9+`Foj#Z-rgM=7_#qGcbLBf4ni6f#1Bd zF^m}wt0{iS+hR+v>q24nr9b4`1(mPYpwmAS#MZU9#pg6*n%SW)N``(FBVAO;qG705 zYu-;}+=<1cc|Mrd@l%{Ty`BHoits!3M67vz09OwOK=rl>w3A#>T3|?*p47vDGa}z2 zEXdWq6S)TlqvNI;P8sNVg zL!wRSd#MGvY=4Wvq(u?cF7&hDHu9M#WfSZ~y7jFX-;X&!W*&6)MF{px4B{n!5U60U(4^V&g7tV@IOi5{koY8Bf7i-h36J2S{fGja%gdruX8`8YY z`C=!t`!+NGtH4FctM z^PoEt4|soepaMltIy`b2j8`XMQluhXFjy!|ho404SQR?);4R;%7w~K2k+H`t_JWX-EiaHl7s>@QMFAkScVW&qUC$wSR@%w0MKPBvj>(N!c zTM|){C^l`irO!&fBHWLizMJ)FVfU*A(J2meqUn?5!^iGaWM)qZgEtAyJasxVs5dpv zPY~J7wq*QWNoevJPT{#B(hnF5B%eUXvTV_Alw<_0-%(U@P-k>A+ymJn%cI0qhxSd@!Y^rG$^400v_JhM8b|jM_WxC z4d>m$k%BVxe_~9y^%j?mzhQID7JS~k)MJtxE_dH1yk}^T%fULK^rut2m@7|>ay$cuwl3K_%nbk=)&!_td^%h6nuPJF*o#67Y)%vX@3OKt`1kyWQf3%jz*>#67$@kBBw@sTh~ zxhIxQG8Szw98p}DCRXJpi=_EG#llWYI_+KrU+<9+McwI*ZYAHB&2f{@1$&Fj@Rr}7 za^b-+P`!it{{m5-I}0Y7<&cxrMe8p;#Od5Y$*X~Qd)Jquei@O;5))cb$=o_mW7_{y zn@oQXqf6~pShMQNBBZ%zMBx=~g7 zaxC=Lp)k$?OFtl*e8!!&r)yAwRsj3xoyoMSC++w%fMT2+sbfwbx?pNd4pH62FdaX- zpkYUu{bman?$-@TG@#G1R|K9r1Qa%l(Ip2`TI013bfbUp4Wke|=_mnYq5l`4B`UNTQrmhWl@QgP$XIT105XDx>>qH)VdQTolE+Bp~9Ub|Ri zrh8I<=8@Ii;Cb_I1qw9jPEX|d+*$IId5o@fKjpbN*IR{lHQb%Wtux*-EC$+hgv(xati zM&vNth`Jr_M&HVq8TnL&>dN)W-o%!c{Ij6llXyP1^&)}`eaK_74ILQx1l!MglItB$ znr?m>p#!vmR zU-q3i=DiJ0wO!Eq=a4vjgV{7n(>SkOF3vCLho{lbl>es&V=wl^JvsK9KmCR=5mOLy ziWyk0zv0Sk>RtO=IIonCdR006oc9mujR(;3`J3oHz8ROczCf2Dov6t=EH-C3QkxTd zT281iyR;X5(ENpIVaG-52s1j+xf|qi;D_Q_|D(I6Qg+^ zeRBm8Ps!5c3(~xUVLzMmKa34kB~5=lo|klB?%!0Q*3FV)ytoT=^q`2kpiTq-{=|f; zVu>#2&E!9C!*`#P_{p82!(aE})ATsJI$}&aGScziu_BCiV>Uw0D1-%?;H&mq&atmX zjOIwFb@>7B5#6D8$sJRr3ehWfKWFWk_dQ`B{D^ngE2FSdgLel8c}P9JmGf7#*yCnT z^-a?e6Ccf4HglTaa|^uY4(nZSu$Xd?Qjx3J_~Lt?;$K4Sh^dLWGMa zj&-QAt5b%4y)8!MQ=SiJGmEBg9*TID|8w(aEaE)W#tID_VuyGzXK?$Sdo9A;`;yN# z&SNy#VfHr8!5K2^(?mvcw`i_dp=3pdRb`SZH@}IcnLX(J$_O#Bs#apY--7)2e3e+s ze8i_37Z_d{D<-LwqoA@~gy^{ysF#-EsnamT@;)W_eRpwJTb|jFGPEygbHS+q8CuJK z3;q5{JYQ}_Huu^tpS7o&8{e=ds0-!3Fr{tz6_6ikK+0Dg==ZKlxXxT4W?bEa*sbSr zvPmLNXJ5ung|ql;dcwQ=nFNfR&V7fc$?&dL#(r z*Pvn0hxSTwp3dv1SeoEUb(%ZjwMYZ++#PAj);ai?kRh@?zYG6DBigsBQ5^jBLZ~Nj z4)?AB`(F$Y&V9+&C_Tvfy3(9YGPJw?5H3~srsu(YM`9k-?rcGm7s*nWQ`hjS_goRu zcfMqZR~1%PR*SBA`wAS4`OXo$MaaFk6gOmkAkNBMTpYm6AN~ZMT6Re?w`M4m+(y!y z;rn=ZKLnjhJ~Z!UDLMi}eWZ@3(7-i6-a96}dWS`1G`-;)R<3=U|Hk9K_fF|eJ z6?vZg4VQ0sqXz3zaZ3FWl%>q+#8)MxLfkra=eyB67$zh;%Kg zDY^^yt_sxEZ#LGikML1((IkzMX#93)&YR+`Kpu*dvH$cY@zixFWc<~!iZcj>dkuJ| zAwljg6-xA60KWi5q^9dp^K%7SVf+Ju%x{TS_=$nLKVZNIJ8H~Tp=IULw0XKU{ZM`i zqn90=y;Gx6thfa2j<$T=TypY24}p zlUJ%j(|)X&Xm>@_O?Du&+{aLy(i7P|`MGiM6aKURAvUx+^ZVjG@_a&3d?OSGve21(#% zHM)D~2l{VGfzMuVifhmmzdTp+&(WW*?hO}j+K=MFTpt>B=ajh04i&BDH&|_CNK+ql zLzjD<+_5qvsYQFm+@L0GHC3m*mwVH#tLyRQx&|dZVCI_E2B-&i=X{+j`Sl*aB4t%l zHc_C}%Yr2??1{|f+*{nfSh3yGi3*tOzfSkLBx9f>b>n}ZxMj&%98by`=uJB(SkeE_ ztHBlybZwdo=}+uMyZ^kx?4VSH%Bz#(oTr$1I~OxkwWu@T4bS^hP`Cdda<)|=&ysUb z30e3N_6HB;l_+}kX`FI+i0A@U3Uk|ulwX_;Rr00Kw+Er((wjz1lF;jyh1lH9mRggC z)1(`RQ2Zp3S>PV@Vw@5!ymk)yN^W$Xcf&Hv641NCk6wINqn^VT;NZmb*v@moAwQh( zTlP9~y6ch@vw-F#C8G77K7C!dox`%+8IX6RlwT)sA@q}Y*knT=kO-YiW@5=Po>x@c z(=!x`thiQmyKYA{)$haxnP;fVGA4~| z(I(w;xPFSpP>*|fzW*93-_Av<+Fe-0KY=;Vw#?^T#gCP`w7BjH^Cf;@zm^sC(8)p6 z8!7r2#{HM;xw!sao>oM#i~fG6@a=IJlg=y9t=uj=hd&LS4_dT$v_76h#USNLfzTbk zU3?qA7r$Feg+_@oOmC(mT(U(}=1dSlC6_@F128#n0(xBZCHvC3*v`y5{|6)atj%1a zt39z?&4-;y-N@~Apm1rvC@O7@X#F&kg6%%lLN85)JPehEdyAczyC@5aDXyTQN^tvI zfN$f6;cT)c?vL7or-$s}ukjf7nWUq-wu(E)Hn_|#4yW&Lk!loz4-bvV z=f95_sKgl69v)Cy2ktDy*KfYXDc&vIs39>96B#@xAW9y=9)*M^K&R3oD1U4I=dkP;t^-c zu9S$!cy-H2qEuu8dB1P)UKt~~PO3a(E``aEJc;YMVTk`5$G(@3C~2uh=%6hAJ%5MX z{z^3WjDhWzxA63oCX<`Y0voDHij(Wbl^e#ib_4eSrPPrAP@iItXw#YxoKfA?gL^Z_ zaXji6mO5F{)~V-_>=BPPT~oRpbsQgka*!zX0V~JEf@b!?!FCy%|1_Grh6A9=okRJm z3_LU%g|Z%(P~TF31gS)Ge`&?%b!q zyhx5@%l*aMj|UKQ;46O5HkC+uoyM$Id8*GiB?c{y6xrXi;KyEP|MOdf(p&E4SJ~60 zskOrWWeR&4JSfOhpJG~u;$=@qEbPPV)Eook?)%O&25bKPjAh@M8pbr}QXcm;^*f4C z6st=Xv90(v^A*e=SW&!>>+zIY^h?lz*lCvo|Q?rswnYCOBX z7cJuCdNNOIkht~Ij*k9wr}{xT;s?7OGOoH%+3~$%;dDcq@Mol<$5h)@F7<`FR_DrFnRVs#JF4+BNcSX+Gi3n4#?nWpc#z|UWS&Ci=zA$ z|4xmqarbRMqUExrd_N8CZrsc9{)dB_7n%9)N9GpF%$?i@zh`dZCG%TmTEE8rCE4Pb z7|3^oAJAY%VA+4p6xr`7?EjWZzRZwClbbJf%>3vR`b`Iq<_D9?#00T!oe>tF@}~OT zPw?F|Lb#Y6gl+T%Z1;I0=1DkvTh6--@9jPc8W%aE{1RiCmouSR2iN`I0r$D5^O=3= zYCmvdMOV66D37r`3%Zori>ADNhK*6&F(1s?ze`033uxi&tsBiM{DfMgX0%P*!sm}! zl5c&OxxO|Azh-~-$;|l#liD~mJdPHw^#+i;lPUyfw_fg5M(>Bpn5`~H0mYtJ?!8TX zo1jUF%!Zs6UW;zkUX=b!owWA-LEIG=^0sJ4_0;#cGN=z7xljgq?y-E)R-w#WO{lrl z2rF-Ok~e&cJ#V|xu~Xa|DoYn$vAU9?k8i}iqCjzC6vWb36UDaBjbg!4Gx2#$PmJmA zLeHXKplozsEP853&Bl#T{HBb7Z+ek_$``EBn}t{XW?=LFM>ulb1Jf2qAZuGKzY~<1 z2kLN>ej;FnM&T>SQu0?Nid}zFa*m%%6$jhcWwR6UVLoK( z1zm`@@y4OTtk;#FW+!l%u`JV1#JDI5fIzCyKijOYGWbT;+Tj|l8=&9`OONRbj<}NMnhk;QA*czx$(jGfF z8_aiKSqE}?m5c?={e`l;7yHysz~WYwI8kax#>~HUUhtGVF~1SD%ZQ!q_k=9(D6e#} z;f#qQ)?9fDmHHgWWU-e(?WFO;Fs zx#L9UqxpE8#rbFb8=_2VG1O+(!f&n_O`oGkzlRx7344Qk=W5WM|J3N~dF}@f)1W3@ z8_vdCQrN3hWF7aU{Xg94XigedjPj=Oe~d`>W-e|F^`eHC_mDeiJA5xcfYOBm41PTw zmA^_6viTa6j%8v-bs-k;+2*Cxb@*=o3+c7pd5?V_S$z~J%C0NjKXL}=kNrj7TLaQk z)x`NH$M8*0g=Uz&5RcAhV0E-19W34tgW@OGk_F*i;n$r-bOY?cz?wZpg_U z65|p!35OXMV3aYKJ>a9!Q8d!KPu3JTUYvlIZ<|D4zc47hb>=ysZsF0n+7x0wScG+y ziRP{96x`BJ+-orr-QSq-Gi!$TRfn(Q-MYh2k?}x?nJliq$%V%8Dfn4$hBoaSOlAMW zrU_b**<;VUpY9aDp&6^sjYYqgHk7dsE1 zugn82l?>gZfEv3}_Pfsd8A4W<3EZ;PfbW_y8)U0HALzpL%L(7Nkca-5yPJI`?%~Rl;{}ZQ_ab154)|n z!}FPUtnM#PU~lMkDE0n;8|qOoDj@hvUchfXpZgV8z{TVb?p<5RY{0R+pX5G7Ngg(+ z#-VfW74FGbqta~;4EF9u%eg$*e^>_d<gb#%#$~?75hJ)fe~xxbY|QT4M4eB1ei{H;zH`XJ76EJ`>NjCgWe9K4kcAuvqPU6^8YEK5#Um z0QV6v8sv;JLw$NO)&M3SWH6%Mo(B9k41aq$;y|%3Ik7waw*Eal>~BuH7j&VjinBPw zOwkvve-PDLgI9?gh0rpk0jmv#(~7O4CYT-X8=^(i))F!7t_gYdFU&u)sVB{g94v01 zwgYv7lVk>7!j_lij)DPB}II#e+{<|b4n&Xh>uYraC*@$8B-h^^+X^ z*}jLJMFVN;=g%5zNs&m*Qr7GSF7Qn{WEqMo0D6(5q?bj4jJpNl-enc z;m2O0CC8Pb*O%j-$ulgiu_Vv@BJ4~4hHsv|Xk%dmf{I(<+Z>O)Tvv(CSY?vR+KOcg zpM4e;e?#$_BpCkOExva*#=i6MqJ@1X%6vzx>=Pt*ujBbL@7`V&$>GT}Me?`pN*mj2 zuVv7azFn z5NQe5vDUIx;yc(|cvouUci}%QS5{-Uls!6AlxW&gBT^leCq8k{ajCK@Rj>>6n8HGg z-_5)IzI+efJ`b^@tf`~WmR8R8K_#D6Qu~@wtZQFNU)`SuhxDXV8~c;xK`)B=q(vz< zo)p{Imof$iO7_gtr;Y@B?sm2vl)`#!TO0@~2a@+7() z>CXIj7wW~c!-EGkX{?eLO)|KK5vCS&@rFHj_o@)%7|PCT_8@lEqRi~&oEa4qpr=dA z{8r-Ee4a`DW5>7Ne{hkz2AzqrR6L(^65aP>f4mZfy&jH;8xL^1QGvGRY(f8=y&^r+ zjOMVP+$AqYWc2GrVUPBqgS}jX+5K41U`M~5Hj0HeRLK3EKCLTt6Jov+*=_4ZtL&mg z4Z8?5rtvfX=M&`dd+_>{dbG!ICiOG3RepWPa*Ina`{qgS|Be^gNqaHugc_+-sfzXN zCH3IE>G~ukiD5`2?94Ujfr1%bi_xb&%rYuB*P@BT%_-xy5!D^GrxL!m*F7+!r~^Ie zR_IzxJWq7{l?$E!nT}lvf|h%;zwP-vR8Jm3Q+;k=QE5EyW==wj`B4$u7T`>uGZ;7a z5_iDUamg0C>PW$d@6KxE<(4%1*xZ{@M=s795ywIy`v9_{{7QM zo&~=nPq*Rlq3&Wk_nPdp+F)?+hd3DNLU+^^X`Iw=q0sXQ-pVH7-gi%2$*#nu^?Ptj z<%(F??*n_SBA8MC5TCM4@S$d>FyUF&W+CNdPXB%99q`EG2)t!TxoyW`cwrY-C>zlN zn?&qgR*EjQ@8G;}1MdDcg8CQ!{@$L1v`s;%Re6jQ|J|H*?1>ROFCn_B5R-b1L}KS{ zOyBpMon%XK^K}N=Uf#t|lYXe+Tt>Q|1N~c?%>0-)q6hEWMQsjRwuXr1dEBj-xDyMF zy=m-3qIY&{;5oZL&2tH)=vADH&T%7$JTICtXBR@lo}#(8DmhGvlni@W1CKE>bS=oS zAoN=mq?sYLcs@ehqY*`&+9qtm{kg+pObdLsNOsgXqlbkZMeZpP{ldRVhLSJ!;hay1)%Ak) zj|Y*)6mvTA#avV`3Z%{vjeulFD^o(e&jzcdK>_wFRmaIrb}Voz$lFVa%HIl%clas?=lo zcSI$$ayP+}F1E?jAB!9x$B8A(i+KRwDWeNB?q(zNTOMR%dW#<$A7d$d6Qk3Zw^kqp z|D9*xxrMpRmu&IFposTP%kb)?0j3zG;`VSax_)mee*0TNwT~b5dKizYJX=^_6U?a^ zj+RN&@m?xaSQ{x&M)N4d+eV2O#a(H`ZGEH$Ul5B{YEj`cJ{LFG&{1Z3SL?(xTi$~n zZDZ$YTnx$t#Gui8kYGp3_RmFXRYD$nm^e*M+Wk zImvgtgD8y)AVX%F%o^T`=STIqBi#$f7AcYW9}Th_Y=xssT9K4!OQF_w_%t{U?SAIe zWgK&tdt||f@3NlKFWAqy8P})iv750N^M3I=BWwfbKA1p+x5a^i839yITD-4m>|YLhbmoFky|(uQ!`cQPylB^E1pBkvme!RE-8mI zbzeXCreD3;S5^0s`yS3zc#2u%XByD3R-62WzsKXPa+D=h$@cdZ*!63L!%rh-$PL5x zHZPhrUjcru_S}ydLM3~vgtxOF9;))3@Pokkp1O44b`fXSXF@4Xi&CGSMTl2#?6lJ- z!}hl@^P7n6cfxT->J>Xr%Z2e2A83@`L9_Z_5w~d;vp#-7a<)Mj8+b96SA`tg8#w>u zMKgD_=#qsbU=Hr0u9}5N8QGULbFVtQha+-PM?YR zA4g{yR^`@pVY<7!Q%bt`8j~)0I#*rH;fA{HW2f{3jk*oY{ih>3xLA|{A{ z@y+-9eXesUviI|>HRl-jJx|ez&dztFu#s+%cGsg7Q<(Wa%bz)Sf<|j<(Ghklk4f`k zS6CMcThC5n7d{*5zLXr8XF^eGUi3`bO5*xgkB;O^DBVUwC_HkcCCp=ZqILs@D^f8k zPL{kHZ{zWzR3rp7VAZY@xT}+kjsFy=spnI?v0$$7{=?WcpdCZ{{(<3;Q&9f<9DmhS z$obGFTo-n=koyeTP>dB%Q@Hd7od36qJxEs1-@;Jm5ro+GcU-korVPD`)>>DUge>ZGG zWRIO<(F1cb5t+EC^g)EIF`=R{JJ8Q9LF8w%!<4y0(QaErn!76X)-b1w$@P-2Rm`Ed z?@8wKcZi)i|6rNliMKB+c@N>w-p)^0!gqtS7S3ex`6pHu7GR}aAni@LCTXwRjdcFq ztN1-Gjn!C>nuqGNOzM&39^cRN4ocB@o>jQb zy@yL)L>^;aGWV=M+DNhn=HeXBWJ9#qh#w)G#k>(Jq}MzbftNR;?Dz?>$iV`Mr`F-* zI%l!m?uHneo&&>2T~N4WF{*3Xai=f=eHYGx&Ghcne~B}+p7Oi04BSC7qW+h|h1(7d zco*B!^KI)T-4?qsyWW`8J8nqstjiTvvo|9!eJq|U-4kn$?Lyf}C*(C32&3n@SZ_NW zJJqC7p1)OO2YJz}mkqc#Q~~q1^Y?1YSG2$XEisg_r}cOT7-&E@z6=kzGxT`0D}1@L zIV?PveY-!!mXp=&3|R`-+&V~Em?PyV^9qL6;?TaKIQ4-&0)CCCdD9i!7V~E@W;%O* zb*Oe2JH0r&9Dh=Wp35A z?hEI;`ukCjb)GnoHw>#BCP32cft1-BF;jO9vckuqCSW3b9SMbp>zVljnlG=1?)-WC z+TNW)>#R`59qx3wk36?Kdi+*eQ?|1ViUcODhoU+I0Km0wm$7Z79@q65fyny51 zttj^o^M7`&7Za82>B>xF+H@*kqPEtSYHmx@1K&XQV_VbLr`w?U`82l7F(UW4RLrTr zkAa-YUw0@EJ1R2~Y{JjHvkQ67qK`w3?VNEO4_Oh;_uV>7YuJqwkGt~y_yP{nRm`IS z?2_R32X7X?J9y$q%Vnq*Kf;wC3*o+15sG21)ZoLj#MTfj?c+i<+(Gu>v-FpV%nX>7 z$&N*D+K>=RdQFS)DA|Ry*!SDKX*Wtg29fm0C`ylAjrO`)1T0mf`nQ`UuY()Wl&D9! zr)CL@*W3dxk)rmxv69$TDPq#So4gB|IMEo~SpRC3W!t>>+IKJA1Znztw%yua5h3nE&-&TZ`-R3N*KFQK!#6{O(I=hUn z_xr`}sbWN(vcu$6y7a8u0iog+goOD9B&i)B8MxMlcNDfXw@ZoW_*5R!k$gBL^F_qsD2C+XNUBbG<33S z>ZCgSTszTG(@XNlQJyl5e__*_M-rb!A7MA=Gn#S-i+Md%Y0en-p3X9&OwI(EJIPSb zotk98@57NPe-RyJMfS-G6gE1H_9a}ypcTCTUEYrz`MlG2_*t0V_UAc69xfg)MT|-U z&u6TmlUj4~4&!4WRdHIFjMb!(2TdUtm?aLR>QK^;MKB%gL`s}lGj8NR z^P3ITOPSJY<)v`op6#U(+BEEFBBr|hll=GXq9}X+2C?x6MW4^wSo7vJVl(E6{(goc zLiH_r8`q0LYLCV6(Xq^wzbl5NI-ZS1%$`ixu*#aI>63k$%C20!G7s8KodpL5>d$5eI( zAH3^HedoW1w4FOiZ#E$Py`xKE=-OGGwx(6TkL8Wd5=o?HZ*;VbdRD%7wR3 z*Oww+<&R>dAxZkml#4A!JB8f*B%$cITXdgyNGw$yEJ?`Kf&Dg5y6Jlhf#Zn#?~Wv; zUWe?PDp zgJ{_`=)xq&3*zaFT9GFI0l!{2QbnOMHje(wvkxztcVe43zV8>hd0CLzDCS>QZ^ig? z8nmU0C#fXv#uT0{9`o>|G2WBWGfSP)%{17rK=kDxb2Un($!;UNbwB!3$|h6#QyxJj zQht>1&x36B?P%ne{t`(HXMtX_^Y70FvAQUf@3oHfJFfcT-X~2sL(D!7XvZ=O<{fbN zyS|$mPF#x8kS_H6 z<4nn9onv_Y!;Ct&3>0Qnr_kds-`yvElT=`&H49IdIVBc8g%7HI39MdLf;Wqw5ab6?kHr7-%H)7 z#wZ0*RWronq7YK10+jr{DawC1@?F!BrmfJ%$SN5+9_mcLv(F3rB2_XNW=e1Bw2^b- zCwKO%ps02g^v*7W`BvlvOfJg#QoR;OS->+=VI%aBhob! z;Lna*sV^BX$iU`pp>%nPHT7>R#TwZl@}0@~#dV2jtE^_$w#vYPFMe7O{NNaNPme}*Uup6F8|vl z217p~7ZRJ_c>lLWl$&HB&*7Hn74=JE7QF@wk`?ii^Hk$Y%{N zyxWW7{kep!?~FkE0Y}8T`q9--Q5ZdV2xp;tk==1sQuDebxjsW3>z1gI>60rGqn+tu z#Wm*t{C6y5bD)qn^4>iYZY}4nb?fs+)wWi?>yJz@DMqS=KIBiGpaZdWJ4Jf zWVl}zh@;Q=oP0@^;&%CzOmU|eQM4B^DS7o(w%hy^gt#aHS6=3<s#g$wn-DL8?v$dm%ALj@x+&5|Z(;wFKUI%ZrVq~D(b*UVBWF_@ z)Di@PDd3rEG4I>|U!9>=K2Bpf;>Jc~a?S?5Sbo+|r<+Zr2>Okc)&~L;VxFi1vJYO0*58Strklo;+iJ z83+n_^g8t6H9OjVJwsR(GygE5L#X^bBPm>T3#LEypg25HY|y@iTUWP;#OIB0J+e)5 zggdGyWxpbEOn}%I-+*a1|6(7cE)9AmMcX3{=`r)4y_fyuu7x38oMcXsZ(m~ga!Y#3 zU5*KAZ_sOSi74{kg`C!WgakB63TAD`zHZqFzELW6IXiN3mR%TMMc`)}T6-x1EsW3lS0+nxk24UT(3@(qH0dOF zrMk}VLpe!5vD-Kgx8F*{#jYDA4aQGlnj0n3zt~?|W&H^!W-Jm3HxijO`3(VmVkOt! z{1Csl+JvUrY!!Z?)>vYoT-sT1rX(v(108e6iL}F4Fx2OZL}e1YYhG8v_}{zIk0r|N;d8k++sg(!#>i2Q6!VP3q%muH4SFTmkY(#7 z{yaUzBWASBm|cLiyK6WrXhEq{p29uuAA6)1Lxr83(Y6}I_Ek*H1Z{Qy%KfZmxPLs7 zYL#Sh<3P1|tfoP?RfCu{Q6|dNm=UtNlUb1tm~xo+5eHN#e|Zz!*1J-@$4@v!yvNBQ z?(~cuty25{VU?T%*%vd5^9!HY87?Gn7n}Ro(R%Das3aOv z|6COEo%O0`Uy?S=7pIbV_Ww4L%5E#N(~H0RO_hjijDg0YE;LsA6ABVz5Ixt0Hfvtx z?x!A#!jhTseGwm1zezlH??cqRt9URjN4#Az8Wp`SqTyE$G2Yx$%>AfMr%p;y$f7kR z{=T~OCQy}vU2G-wbJXca_YYVxXfHy!8@DrkyO=V*7=8Ku_O|-F(DmDh7kzyxT((%a zF1Nz$Ia1`xJN)Jz!1Wqc8Zy(3E-ro`)_j$tm<|*ArQ}MN5AQ^O8O|*lccFJDW+MHu zG3m59P#yPGU+r?GlnCC3vFAK=nThBU)P>?k6Kx)`s`P}t3GLeGNh4>+NuK<@CXtJB zqdYmzlP^yaFQ0OL?~x~6+_8`pj9n|kD($eTe#KgBE?NTegWEY@zs4-2+@uH{GiV)O; zGj3NyC~MT#w)ILvz&dAhnhugvKlqmn{)n&&%^;56d37C0}thhWN$^Pz5Nxz0d+_> z2&7N!)g0yXikbZ`%<_DPd*Uo!ybPw35>xT8CIx$ARcY{*p``_9Qc+&4OWQ~E5%IIK zvG=kJJ;-w*2M+_9p=Coq`B~g;lnp&ev?t{r7Buj_8TDUmOCLE`;$Cq8OUGOaE$(E_ z-;ixMkuXIJ?P5t8&r7i^#I5A$#EY1BW-;P>on-sP{1f}B; zXUz^PW@3U#GvY5;P|At}*lj3F$yw4gdEH+8c5Yz@WIM9L62xa^p6x$=4%JWoqNmjv zBp#NciJ31&fMqdy&I=}`8y+~(i!;9?ds2sb7tU(VfZt7jO8eX&4tWc4IIb7w?hC{= zg8(Y#`&-h(uCTZo&Y1~9?71ZvpcYJejwZA*q(yRXa4=RkS<+{Z;`5UgJH)qmb|LDH zkW@xZhut2|?|*j1^g>N6Ojv|{XB)*nji2IM(q8=0v4KyN4nCexht+QG?Tv0nij!b& zjTg;7s6^3+8-&JP7n;Q9Pi6fm%;Q|3f$m1UjrV~6$WQ1NIfK0{)3~?w9J;>yQQUMF zdoFOVk~xOyC!V9eF%pmMJ!t%|`GxL20oBWIB+6m{v)Luu?TbfTB+o7#Em zpBVelp2qG{rUl8VVq8ZT&M72g@%jC@G}?u($>hR$?FMEmSdp3GPS{R3jT7H0nb$l5 zHKyutY`l)oy*I*ir5k28y@HiPHx%!7!Pbmo+*@6Y3YpQ+G}(jpEf-;TbrsqtlrgvI z77X`J!_2NN+@*7;E@!sl>~RGwZR6+6h$84FtQL-X{?sy){m+`9$(O=O)*uUmox0J? zlYMDoVJZ4-`05dwdH&ND)_Kv9 zcdOsF7Gc;9H;VBIg~?_98MZpnP>n`0aNBWIJ6hA=u`1N!<%z4-Bau<6Lq?xl#H-?N zXdR zv5k@?Sx)E`!I^%afY5%?PsLMtCn`QVNXQ@dqr>W9Lf_e&R`>CtH5DhtkqQS2%OY}^ znIgUn45X&o;iC7_D!Ap!3d_eQMbV5h+@7+p^zzQ`V*Z+Dw9YXQ9u{(RzpbmtYvZnH zuXa=o$``s-`ZSq&274oeM7BvMCf<~#a_-9Le{I39n=17EmNL0Ck>0u+_qGJya zW}uDKj>3LLC-iSBBVKOLb94T>8 zoFU?Cz47C}$>Phur^5XGU(tD@cj>;$0;uOjP>k7e_;YS_KR-kEPP_&GWvSQ_)|+!m z7x076xg%Wk$STbV{dmTxkz`LxHaVl4!cA zt$xaLqdSl>VSdTLCdAHcz!c+^FmVc_42?$^bYKj|ZtY7=7XPrIED0+^h}2*8g!Ib+ z$n;R6ywK@*=HUfRGtTku(n7YyNSt4-PBpFO)I+s{v#LaGygT^eCQUg9yy)!%4SLu5 z56XP@?0E4O`+Bj<=(!B%_3vZ0`D;vdm8C&9>-oH-NFTRrG8eF!8F&)XX}>A--`o&k z;XNs;TatM9@v&HPB!aBY>Z3BylYCr1LM^2y)HjEa%g=xKXEP07R=AMmzTeoX>44O# z;mj$&gHm@p$bL3Or1~{xl2~DW(ITiPH( z8$M$=P|KxlBIVCF%x|?1=na_Yp)B`&mNmJ7*Vv8U+G>m@^c zTqtCkFI`BBD4lmbko2C}(z0XQLp4gDWBurStf^C?`rr>}VrIjIEY4YR|6OnBE)4!E zO{3@;$_B~PwISOPdy1bE5431l*g|-&XhXw$88Rq21ewk3PM@B+XL3i`g;r)O(0tCKZy!{Q zlyDuYGjQg8M+M5aGB11{Q2IO#tFCL(k2kBap?sEb=J?5Iq$fx82@cL4pWa%8Wd41{9|TeZmkU6+j>SEHeU*vZEp}%`b+YlW(l4o$WhP- zE0VjROA8E5sNVrS8Y^W&w|=sp?Vkgs{kEc*AWJghJnsJDOmyQc?s49?z0Jyj?nDXI zuD7Q1@+D|d_MuA_$FRJ59d6HffNpn=V)dd}l*!-0o&S#WyK4vJ_0J$-x&iguaRtFM z8qrP7g5KvHM?HUz<9Y98aQOoKqCeuyQzPoNbCx1uEM&Z&XEjlahPEk!OnaOIx-a>m)86S^AR~<$_ z2hr;VZ_%8kOXcZZ>DJhP$ezodBYu_)&AE$>WJ(FVV_lLw2-A1;#)8hBxHm)#mzGb$ z@VY&agbc*a?jcABUI0(Or-+t1E}4JB9^vk77*_LGa&wplw)c2|XKt!uz~5n*FOvwr z?@gGl@ByQiFN2J1Ene|VV8-S{cv18d^LD+)`A8j#n5NFZ?*eh;uO%tg>yVS{Mj>g@ zq^`0u6kG)173xmL78%IDc?2b29GGog!pyW(Tvl+P_B9zuJ9!z|vzY0g{~u&@1b2}5 zPP6Vme7PymU%ndK4(>qL15xP6K8?nOWpLZE8dV3%QMT-8g*^O*A2hpl#f7qmTp}~Pc6yIeL z%K1gnZJiH&U*w5$nG~LpC{UW~eBm3Bi}Cq?&@1haMDbb%7M#!`iTr6%Gitan`E(Q~ zRtp-q=CF8^SdRMR{uYLMbnHaI{vg|Z@wx@ z9_!Iw>sh4^TV9B7z3iC?HB20N=SGWnzmhnux1_%N1L%kKCUNMY1LaM3r4q9QQC+D` zUzdX z?}S&I0ab@7VQY&&-cB*1xZ-G(B#e}(ChL-Kb{@R4M~J9j%G6_4DZKVf5@##eN3NfW zLZ-Du$WO_K<>p$bUff(-k$)ON2QFgHTJGviEJn=rN5~s( zL|uQ%Af%%SiwA45N4#9bs6K;GbEG#6bgr0PimX}(&St-W>7du#pR{I{_iL<^J%c1~ zGYX!42S+Dt4ItDaJ<++%juZET1c~7*Md(InPGGESxHVu@bzMjlJ9l+ia6@5DRk~7cyEGZ@O z9y1TWqt#f4-Y}2uV)<{B4pXIVy{qwJf*c*nGNZIz@^I4^^!$VdnjicYW%}W?!l*a; zHr^L^U3*c%qc)+}Y)<{$Z*!jB7!UOfnJ3x^Jw7|OZ?Wba*(3ZN6N4k#%iv(bS?KU9 zqB=5xy%r64aB8ykZ0*y->vSp}W z0-uM5Y+~17hGg8TKH}Yn5%_&5H55~JiwO_+!({Iy$)TugV(6-H7=3QWURNuc^ne+$ zfpR2&*qIJP9bd0}K~9D-?|cJj`>btvFoM}V2Rx~|dkS_;bs>XtKjw!`LjepZl%1=$ zU-*lNvOxM*&CD)zIQO z+GOljID|m{tYwTy!g2Ns78=`AfmtTn&z~0yk6F;s^7Z%^x?5x$`H;#pAF6o!PwXjI zp~)E%n)&jx(3}4Y)iDlKB|eFm!^)I2wg#T}?xKp%OUl=8VODB2x=&=rWcyuyhMz!U zVIZvu_$u7ZHo%Fw9Hg;H7_Z_^L>CPzJ#t%|NzGt()MqUCDFqH%v)wG1RKzaI6J!JYe|=V*@q0GwSR5t)wI>P>=Q#R`^}lN#cbw63B7xM8ujxw zA!c_0@*@kdqGC97^N(Wj$MbN?-iy`zp4}5^L}ec{k=p(fhk15(lRL{TJa60bPLU!* zm>H+phV{s!09c7_dd(eJHYxg{pA007-&M5Pcf+3T4kqbR410!# z06%PJkIcZ185r4WO8T38Df?71dW7(qvPn>DU=o7++tEYSE_BPV9uM+tsiWGSRQ`F)wC5`jlfT~ILl z8XWouQd^u4POo_jpP*2(8DNLn9h_VF8%(P|&4b#i?Ra;qjr-0Ik!PC>r^$gfCOuE19fBdT6uO#f+K6>?g3^z5oReR-#b<;=8PvYvUR{enc7>+amO+KRnH zbMVsImUIrS$K8Y5n8)r&-G1*yIM4PUzJ7sq7ng9C*#b>7|KLySFgP3y#PjGE7?YNU zTYkQ9UQ>eDINmWmi(%&5Jv_>Jh?2mmh*Hi*htG8!9qx_ka_)%D=b7!Po!B>dHlAK} zrLa%yFk-qrR(x@%-6dzSHo}9m2O-p)D8Zc<%oJcRLdvS;ShSIMz>=QKUh2lPpd;uO zr$h)xXlRQwV%f{9bX|)yl^+XLwVwE1Z$n{{AShoMfQ9x3WRfILh0QhS zec6~g0^2Zud=vBA?Z}LoEIGICz+lR6A){}^yxt8W!|t;%YjUJTX=P&aovt{w#*9KP zs*1e#?7DxYCl&>I(K%*XH$Ki6-tFAwoZ`>aQ4jI+unko#|0}ukwFGJ5zeKuEckz4B zO+?KaEpB}%FWn@4gL$~$M8!uH+B9jKSWquZV|yu4pQZWY?f4IPvHLH&UQ0Npov9pmTwp!|U)*sN&q%v#5J9mRVHJIG3Ek9P9=7{Yn}`UfhH6*dQ`p9mgIG zQ`B06(j3>}MW!~^ca&`0ioV?O-u>w@9vdCRb#Fm`+GS|UsRUGdcSuf5?JLG^d5G_+eMRv1 zF{RgT)v*_2wHUlPLqxkZ;(T4WcwP8j+?Medf1ia*w#~D|mi7C^wKsXf(A^wSC8s2D zif8dO*j+N+=O{cfn0;=zRy^^~gEr5DhRyv|`ckU|8qv=Xnxspu+Fg+l*NIM{Lw}cB z;(7l^*wf#Zwp{Uphr&s$xb8?7x-{@Rum&MTHr!8ahB`aNYR8&V+4cv_#eN6bh}n3~ z9khAbkMUv5eC*HcO|rUDWVB>CF83z-J;)t)J${Js^E&ay-4gE1ruSvPX`P2)9>GP? z`-%z`ndwo>a^~*M_opujvb68vBW(2drl&bZbfoGN8ZDWNSXY34lYihtDW8v+>ll<; zi*RXKQrmt8)w_5HwLyc*lQj^!G+1)}&dWGF7lIv_-@p zAGGa1hn&m)F#1HajhXpgvR!Ej^K{m?=u>f~Eq$r&LF09-srwU8+EJW}!?J_8TQpcW zoy|e#qMVSZu^WZ%=s8G#Z!Tsq8=}#zLn!rX!<+%QGD&b{@NF)5< zlepLJNPjLVQ0LZ2(L=DON!pLzJh7z#Qg4KmI-1mwTEqO*R8)H@Q&{ymVPcZM^Rc9Ms;(hInU+Mbx;Mg)m_MVo&|ocJj6Uj zW7=P~m-Dlyh4fEnTBCIV;lb_VS+6eCxMVZ#)~ylu8Usi%PlwWW2Ejw0&(js^B$d)3 z8t+Mw=X_>&-4B7bQ5_WbWa7;0eBRr7k!fWPo($N+F6ba?FkFq#+Y4ZIy(?{)!wf0; zWE3%P;CxGicu=zgOLu(4-oihU*D2dE&QhLYwY8}CUFI@6nv$x6A*IyXQ+293jp?FA zGmd%FHSVeRXm_FKMi^%!Y=3&p9qhDtm-L1ub0;f*qzUTDT)B1J|B}ux`OYlU9g3lx zso5fi4cDV?O56`y-WY1J%#3o5`_ZL)7X|e{fmI(z;p8nfk?fO?(KgncUzZjOa?j!T zod2-7>a0j@o`imP?5XJ(?h2H{KQfQIqjzTwtai3pL=Qj?%(#FmSzmc7O7HzjY z&?8fg+7fd)ugTxlogJ8zR)dFwTOnKQfE;${?4R)q9qHZSb5Ngj?tDbovjZ_j%AH20 zFT?fShBSuH<(t^Y@G#nvCcO$Hk7>IxX_Fouc+C8f{T0|a!juk=FsBIn&wTDSr)=&~ zIdYG=?S?b;{%k^9e|E=)cSD#dz6S9V`=f^Wps{J}M_gcuxfkc5uJ;~XK6#xv1^hWu z&_MO>YrN+Tpz$2p3tsVpx%z&zZAhCi5Aj5gy&Y(Htw?T;CU|?b1Hr=$=;?oBp(j3K zd5J3J%b3xZo1IwNdP5k$=eg&5&f)cL5ndj~UD)Axod8mZef8CC(ff;bUUyshBBjVMI zOK5-G43iDV#Ev1EnA86rj;%Z;>AAQ;a`^CBd|IX|wuw|B^C$z`i{^{+vIS!6;JXOL zrqF+!5mVbKg|y@0urITutEY6JsbYZG@tX8`QHt32$PeG0O=x(3d8(Rs9y?Z=QRcZH zP|mF3IlCjtdB{-Mvn>3(bzY2f^5tEAAT?ZlA+|m1$}{!u%&%94?o}UJKD;a0eDWgY z_2-4|4okXpMvz?KI?;5@jw*%(lA7EXk?=~FPHjvS2RXAn&9M)rSFaMfu@yYqSHZHb zMq=b+&iE^i#rgezV5Sft+56}t($^`FT&3-$W!)PPWYC0*dv=C)`*aV_yPMOy)tnW* z#ku=YU1%RWhb5l%m^s&ohVsvIZSXVf+T0}iM`iNo<`iD08$$ZtIgI)53}hYGic_iC zxVq;U##e2FFWN-t<_mZ^ZYgH;FA)7?D&dfyi0fGj+~YZf*`}U!Z&-i4uG}i-hdA<# zX9ZHyt_ou}S9&pv)xcTF66F`=;?!;}n*HXns2coB4COgQuPQn8wUxm$X2I2LT_7H$ z5%n=tp!m8}#DqoBMD{GmM;~KmQ!r`qT&U4L4Nl$I1D@U`wD*7-9Q*xI{D|)(lIAtS zV?8@6%(`D%!JV6=sa=KAke9;P`=R7{OqbI2&FToB7$ElgpBA-4O>x0`DxdGJq2)vb zJsi)>qqwtBXTD0nv~2FTo`Uq$7<%~U7~&SZ!g~`%a(SwOgxfM?VWL1i%B3;tYaNa^ z>XVD7J~l;VKt9@n0@Vxfk^9;s|LD>YyCS&tNk+^&bJ8iUz(F~7I_B+U22cb=ER>^= z;vJZ>sXx86(4myed%%SNvg@Xe!26%X*jZ}y*jm8k2;VD@8_`m&k7DN!S2*+cc0?Te ze$)S=JllsJ_0lGn6>7XYbs=lcFhtq6!QrbnZA!?2t2Fa3*_Uej@ifM4dWu60Qgqn- z6uym@Bb(oHBsW_Pzo%J;I2=7M5c65&&Y2_**{l$a9a8K`kc1BTZ36c}HnjipaoBbD z#9tF_x_{&(*42i>`=~Wd*>elazAK=vekyi;J`c@r-Jq(n8rNFxVMdBRV*0yc?v3j> zS=b99_8^TG1Nt(@hj}iXf94&7IORlFm-V8Vmi&G5^dxtgy-?jdOpMd(Dh7lX;disM zM0VnGp?zgJT&Ea`+1e|`+NtjZwzuQOCr?UDnWi@Tur~E$_vgji6ENq8IyEeFr*h*hINaTg+FG@#caKAoVnt7CFR>!qnqZMq?aJ>r zWq$vz6&F<8c<#Zj(L2s`%~TbBQ<*!g$hr6b?umxqzSJ+&jZ)4N?%KY_``xFY%3P+A zUF&f3^gcYVSER4Q?w}v%i64&S%wWSS$g4}y;BCxs@M*+aQ#tn6Cox;U6Ze<}ZS~+d z%qIqs{T3;4g8Q-Y1AMvvcv9>!FU8%&9&}`)su*~?8XFP|@pLh$ElHI^Qu0vIBbp8` zFrZ$KDquI3vjTPOds8yUqVx>B4RE6mwQ{i3yo`7acgl`cz-3O(P90)Ha@y&*=};m5 z*E1N^XBVzG#fw-+AF4Tjia8~1;)|>c>GG_2(pX;k?LNF%bqkiaGb!mHAW|sEki%S9mBb+<#syzqAe|ujR>D_MhbU z&4tLI z;F+D$zQu={R|U~Wy$j5I4x;z5b~JqJeb~LfZ;-u?|<5&$w|$ z33CVuxz2CVu*!^lR+r-@e@||CsZsTx5~!7RK<=0|J;*6Saijv(5Amcrj{vN-&4B^W z!lwLL2D>9Um|M_=`qaAOKbI}AOjO33PcA~UItfN9%4lC&F9|DU-@<5TjF@#=EZnvV z7Sk>9J}?1qwi$^0R~9(4bQ_-U=~;R$t`Brg)?n4`4I*rt27Pl5C&M2{MJRKfbQVU? zh3&T`pVu;YapLRFTUe_%94ZICh|iq&Rw=b$E(W`>qlaVf z7Dw{F`x8!S^P&CKgQQ~=X!+0{2n^%F8JGhp}6jXKOa*y%P3W4`>4(Ugd*Jgbj?8Av18 z71^)Yn9N;0$<8nl<_(6_KcgqJ!7Aa#GolD5TiTn~g#5Y8qdvns6SqQtx&?UGPZ!8sQV3{q}bYuU;a9xUsG^dVIc~TF! zFFJ=?k;gIxsxHZDo28=JoiW)2G8Q6rBf%o9K!-W=V%8=&9 zT>M#cO>{r#L%R$8DNla4=z77Q`+%HVu{32a9rIyq_)MP{Oo6)!#K<5!deza5ral=Z z1~_-28CyeWdYCHiUbdtI6$xU3dNn*FqLFuds94wb1^r68;fJlWu*klS!T!c@to(`6 za%qtn`wnSUs`NL}AoP3L3oKaHg!DBfl5x2=Fz=%^XG1*b^6K}v{?~%ORM=8??x_aB zg>JGl@kdWSM_-Y}2a{~{;^zV$Ti|#=1$1uRgzcomV$Fo(d=_UP#GqY>V;*c{(sd{u zN|;o``m*)lWQ8cuwxlrY3=#4!f@FM^X^B%hO2+jkvolIm zx#bv+Rz^_ydL#O^emhEX|A}*R#z<=ZK8B@6he$r!muJNv6s(r?1(3A?5H?n0Sz1a*1FJFL#*Sd|n`H5=Lil{6IO zM$^!->bS)`<~5tus8$|`{Qh1HXJ*|+tC!+$xD7UQ-g=dxHZ5Movv_yzoXW{iLI}@2 zo_kV9kv4a-G-;t6d*(Es;9Ra0&5)O+CCn+Rv6H58XFgzG!E@#isnJp9-R7s(No*d3 zQryV~u`J#|_~!H=&0Ps%V@H7KbB(C$S6l4aZbsb?)FTXvK#DPGMYqGU_dBuWg)JSK z--KoRy>R#9Se%Zl#&jiJWNsdXaG6GAb*~fK7CFOuX9dRAX<)!RTQTi_tiB$9F-)aL z=_p@$`qW2>%6wmiwu_fAp6~~I2XDiYQ6oa@D@RBkyh_LUsUs!zgLaFKyUVd`fzc)N zds>o7n!m)okY)rr*^_;tIc$bWk@j!SBDGu<-u#RYXymToLVr^2NXPvLru2TdALX9e z$M-T9_AmOg=Wi(%%`_sKtU%MhR!OeaGM_||-N5BJVv4UTozBpw@V9Pac79iy6&6G? zXKYFM`cVA0ZnTBDi4(Y6=EFO%xb>dwZS$m}S9Lu1;2Ej8H0AEU4u|W9P+-Z9wdiVQ zi8H5kGS7>D)#CJ3C0d`h7aJaZ!>awV^p%)3$SnQ-HR@z_Bmqi^j&$3{lcbKFL~ri& zJ&g&Z3jx*n>76(#@K{2ckrQ!2$XSy{)gSu#?!8$sC;{6SnkCla#IWDAH{1;V& z7b53p8GI}aX;8QEIGb)c{u}hJc#7G# z_cDu3ljA(9QwV0G0DW*j^dvcGaLaIFTGFa9F_jxoc)-OME$+lCL$ z>`ZFcqLJ<`yibb7F`i+`TT9c13NIAQD#L>Iqxh>j97BFR#KsPuTW_>y&iDh2G1!dg zJKW=56@!(WTNPK|;L-9JY&zydr6G@TZPRj`(9$8j+Y8|vYfh>ZM9UZN#Iw!%v?52) z+?06eJ+h$6MJ_a9&@aU7Goi99ds^}N8~ezdDebfq1-B@YriTT+yk<_Cd%e&yb2JL^ zAGW;gjlCA@;kI-%?#=51t@#ot_m9HO@bjGOF%t$JhPWjC6wk)E3u_Bm^seUp&d~;m zt^OeFS;_ORi!BIV`Vw9;nK;)^nw)aqk4bbq8dHLac{B+qOp-DDH`-adzXEitA{PmnSa$2s5!q88W&M?D&tA^`-}?u9 zonLTy;RT1a%GCKphOQemiugZ`NXk&9o3XBBa`z9Po%AVM%Z2Whzs8uAD$K|Wrpxu* z9SaW^Dy0cHdFU+uU3yy57PkSnp0gt@b*fm;`|$#oV<=RdkFMXD!7%s=&Y8ubb-z0% zea?k(V=S!FlyJrMA@oxu^!VgZbaeB@NnZCeJRh5WIy1TNUmuD!`HP+GBdd9#icimEM0(3c zNuMrm_=N~0a|>BK%sj%+bp)iNY^Cmh7-TNm*m=S!($`$?d zCvfFrJ?_jkqk)%9uwJnhg{@|^P42O<@u`F*`-!@)GDiA{3S>1q)3&~K@SOGmTl+9S z_tZ1!uDXTwKg`IK=kUAU{DW=AWLTARZgZd}vq)EBL^Y^m-B0d%PDN63G%Z%pMzj$l)}1R9o#%I7J9j}HCgu(_E$BHS7V-sv zX9uYx>oD0~gPHm+G(+|~db`dJ~s1^cDZyuWY2tFI1d z|54!Gi*ps{YZd9xPB}3|li6H1Wy!B{Xko=hMKV3vosPLBF@LXK;&pY6XmVMImfKZ6 zPkqyb&iO1HEYU2Or@sRb!Yq3_sn(bF zafi`7kv%06XBwip2BY67uz!)WGY8}G^sEt;TdT4+%a8VG`_T2jGL*J<2#sJy$>6R% zX!7y^W;eLdFLP&FHl2H7y=~tYRzK_z{63jo_n__*kxr0-Rq@AY4(EM z6TSLvWM@nb^bjjv-`a>7Q&mXsY)^^|G9V2tQ|esFZp@RGWPVDAISCeIyUmn_&v2kW z*Ud@O?l|^H`cea*`|JCq!DoUmZQ5>12@|j5S%MR-)6PNOwFnr;KE|3sdok_BaM)kE ziM6kCQb`<)+i7GjcdgJ z@^5-4jmi057Yb5x;`f^!Wk|Zx`_mp+A)b(Czz6GOnR0)e;s!p4&0s8_-jW4=lp;mS3l=Z^CUv>D_TSCTs zU+>>^4+EwO`ZjnH&mf~=lh=g?amK^eKNFtQr08Ch6z$m?fy27&#E6!olo{+7b(W^< zlaGrqRSWWA@B6Z|^TmJl+VtmDBhD4r2)$4z+P7m5K1C*@W4t{b;C%lKul4NrwI*ff z!#I+67LLpv$zQ(%N%=~6rd*CbhthG|4;ZUZ!+zWiSla5r_bYbc@mXZ?%On&JcnH}w zW$2@M7@PPtG4o$3;*;YsWy(b8Gsk3w$!g?;%}4GzbL#L(#yh_OyeD!amjj#d*V~Z> zpKzmn>wTd5sV|i-x25DQzWAEqK&#pP;?iRYwxy;+{#_GXjs=Qc(!9H~`GAh%NbleF z1$brMj+o;)Vk_V0q;%3?G0B^*O*k)BWN*Pnp0$YJjpB;JHLTt4PAY%dV~`dA-7A5R zzMw+h@hV8^VvY7G`egBGGM-HH!^_JC+{Kn5WJ7s{4QtVe~XpFcZml<=P>c{Msa`LMTyF^QVb2$WmxZ5 zvfpfUz>h(x1;l?8+iU?N~K1<$(zrk zXYK5|^@G;pQ)Q_HDZ3!51qio^mJmGv9@)h8^B$vvX8w z5V9JkVqVoM@km{tTspnEKek7#S*l9?cdFq_@;ebbPM^M=n1a|jy#I>q`acE$mfRUi z`&3#X)Gt6|RUj!9s?ro}MYX%1XxY~8vuW&G*f*t!5BCGa(2C!18gxM%GBYSF?qo*N z3O&hxI_)CNpdZ!oZbx*nL7J|F#-A}O7L=zF3gX-tIJ8FAh~Eestce0d)$o z_-Z|bUUX{}Gs>Qbd?!`vRO-UKFMC`#r$EPhb&S#thy9*hPkP{a}LptlGSS>{Ar*@=*~Wt?cqwWcv$I+$l)ETZ)tsifV3 zM!&YAbt_FI85{c2Zr+Peczaw@YU4y#vYg1U=c$6!b8qlZK8N3L3iLPk9t=Yd<5NHv z`th5ejW5oj>kS>!*j0`3qd42@lLQLvghhlj#oDJ}!o4c|w@jJzZ!d(IX)mHno>Uld z9*vFqbgM9seng%|-+D(X%k}2&P9d`A?!|;sTe`*^vgZfV;jzP(KJgxN%=$yvRO3m% zv-PReb{2E*b8+OEI(arqFz{CiT55XGd;a{4liLSMSD^tN(HIo6T{IY3(8+h)w~W;h z+wMBkhvHP`ndXQub~coDpJ&kbv|Pent2Q{FHlV*dzA<0qI7T%nko|9Yq|ph; zX_#@Ay$5F2C*V}2G_HEQl?>k!i@WT3iHleyf*THCwsO8G7#EpusLy*)>wXBbnTcih z+-T9`kqGJH2jyBT3Yt6(2GtTc-UKtn)M=SUg1Eo-rr6x1NXu#mi=(%_gz?HA6mHu? zOtJbd24-!=m_{G&^8FRVa*v{4?QqUJ8N)Px2Q2kux#w_KUa`YF3oF0eC zhs@~uk#-zu9)MX=s&uWa0dLdv5tsA<5C3JLZ1y0?xPC{z@->un<2>%m_ptrB4Q8|M zAXLf}Ll5$~zbAX^#swqAt~Uh)zeK8@K9ct6Q{u`A7_vZU!!H_J-(Fq;_x8UfziCZHDYfb)|*XA@ILGf_tq= zJX2u*d--zqO|bK5@&arKoW*X}rI=RDzOsT75~nMc%pzd!UWvbGTR8O_-`!@c3Zu-m6#G@GCQIGoS*O2|HMKV^1|9`%v=Gk<6I~@i);MQ*1!r z65Z&usXGde*;ALX9u&z8_@l-!9vL$U=(oIaedlJ|87@&Q|3!kdgnT)-8Ek%4{n6q^kYKjLOaHP z?}Y3$ZPDo?Pwo%j!Z&n}nA)!vM$1(xwIB0S?>9owBF-A1%>sV$9`b%DI)TF)^C$Z_0ix_l9i{1~6hvDfJ zl6u~o+&y*)$DV78=_7ndcY7tiJc}1PXL{2P`!`5@7$H)(1k#nabqJ~{k<4HDT@2`~W?VW}XCiM~g2FG?BIWddC|Sk)otSzd5)y?+ zrZT;%aKYj;yTtQT8sxt7i?B-jCo;y!k*l=|Jqvh;fv4=~SDpqfv1O-dl_|YT??MMk zKf<8inL0LK#JBJDknN&Ji){J1&S#BpBiPv;ehgPTpD+_#onnr8qb8rI`Helc{NQuT zGauTe$BxF6aiK;L&4v_oY$HP zsT*(c`1xz`@<;$)EtDb|ziP4QS3h_w+{4R1+2Y?OL8^v|B-_mWn%Df?zw-y5H+^Jg zm<#2dRibM-@??G@1&j_O^Zh2GQYQ{mcX-qFPbtFU6T7?@2#VEA5UU?MW8t(e)aR8N zy^ZXKdF+P$T+^K_&_O|Nd-XWaHj<@il{~Q|&`u=2Acb+?W%Ucy*z0fR4wb3L0t@iZ2ql@ri zj#l5k_B7GTQ>0&0qeET#(s!F@ddi(thm@n_&2~8cdWNBG#Yl=ii3cx#qvTNq zb{}rQ?lHTuds{gsos=c3nDtQm_7PUk|KL(v7Ub%TC}F4*$=$tzAkN}SID>52SiUQnukO$e*CqZ|EYXl`}{J6f(_)>eVt8kgCM#mhDsjI$@sC4Et`$;#tVHqNHoMgyp{BPvyJ%Rl-H=1_;J?br( zQUAe)(y0lz%`cBW;s?@4HHotJ|4)lK7od##9iiMm3A26T&ZKiy7 z=W|K-_j;uDQG-;VOBvosbMa`lp&mD8booL z_VlV_9|kM}b)RlW-*fsvUZW@W=C)}fX%V%HD>DU{;0%7c<+Y`&@OuI#p z(hH>YFH@18nv5k+`PU2d$Zz;CF~~#_iG!?2cD}ZF8e=Ge7v$DU-130D({>6kc?Xg?4*VW z@vkSf>owwFaaRmfFGHTg4s>4^h|zzmc?Yu(ruZyOZ+~T%a3p7hDzSZiFDy}E4rtm3 z{8`fvi&wFiWASqYRt(4FRo%&>GYlQw*(1BujUJ1=u<5EvOJ;F)b@?1Df8LY4rEKWA zQX677^r5@j&eUY}5p|{>^lX_04PXBO_t!g+MD(R@-WHtqTLqOZYZ04g2*a4e%tKp& z_rep$kIcsRg8~y3-sA0EK}jx#$XfmY`z?sZ?iz;A^E%*A=Ffd$D|8-+!-Y|w@LuaY zR#qJ6^VMriDn5?B?>1r7-N%r=a);SP<~%p&{p$GBBJ2{g82k4kTe;aHZoCz3HD)h$ ztDP9_WI{2!nS=M0`+-V^)WJV)pH$5L9`3qbVh-@^1W1i|hhy^>VXC_-9`A0zgr_5! z?bQz|mnyL7P9j!mnz1MI6qfnsqE>YP9__l0%}Z`!LB}M>IUa(`qXOiW*xY*$hTe3h z&a^V$W1%Vd>~ag?#QWPp`+fgKJwm&*I*BFQgn?frntA^JUHL$PiDHfT>{N`q>-vyu zP!|l*c#l;Dy=k!CQL(k`0^etCXwY9pp1=1-_L^wyI;>4mts$H--vj8Y(W#~;v0(fP z1U1W()6RChbts3QpCSdldX28XUSU;)CXG7XiU&sdIM!~6`F#vH(|l5l4D`mjnMSns zSEuAC=bciV%qdr|QN*b{k*sZ=?Q6E1_kYe?!~rFH(I?)Se8*@MILz!S`m-x#&c_zX z&W3A94l5OQC4)slP7xMl$O*&2`jWWn7kuvL%x>LkPdIF;EwsQqaRyhp#q+QpXStJ{tC~ z94cK7h-mo-s4Qp4&BJ8{*%mj^N9_jIL~UZ`c%=v)os06zigOp1hgp1DXK zzE>;=u7ykaDDh^dwZ#2d4Ya*7MQWdG;=`qSG~^{Si=aF6(@*#+w>t}~!Dd($vq8k# zb_&@JPjvJvzGl#J8t3%9|GzI?^Xwo-amFZm)nWKfNkjdr-ZaAWFji^TqlEqMsjZHP zDUzj!)@o$@Mjwx4z9U82g#NDTgXW@mcC8yzTkRuU7dcqlqE8NP)i`k>78_dBDex8d z7|ylAaeO!~UhyK8CS^*k-^A{XzI0|)sIuTDI{yb}^r- zGed!-OMl~?ekb=AyHG`KIduQZ)0xrikeWVQbWYgl)3k-X(mBh;T3Rk5PTm($Cqsq9 zm>fyRPzi#VgOPOp8T-O?aHZ6QKD7QoosdQ3d|Udy?+wf&`(yLi1$YI}+S!SlH(3Y^5_{g)jP0Vf?GUqvV z^8PB#F9u0z9Wd39r_bLw+poj@rVBnaI-ea}>hAPW2%6mM98U3b`th*=2&6~yg%E;Pu_ivK^)#h3xj z=)(88RjFshFLt5krE*`+K?Zq_ub`2Vj|4vV52<#g0}l7uO>qom%nXQZJIgs6jc}!o8aZ{y9JNTGLeZLwH=6r-(fuo3sTaV06MOts(lfWFyPCone$g-esAsXCU zGNU2dx@1wOMYnlg`CQ7BhCMide?cBJ>JIaPRIXr5Ezf*}ExA{wAhKUS`u*z(M(C`= z%)WQ9PNo7aM|j^@l8*_&SJCt~6qkvFg$wUHUy9q`C_AR=EJ#-y7W?3=tHwnXg0Hs*{i znx+MtO=&2~Ixph7O%_u(&&@ycJ}U?=`byMa_C34yhO<}9l>Sw<@cc&^zfB&ZNiGX> zi+OHR{T!9!V_@XV%wN|^j2nL*%XgRKijFy!mRL~q*Jl_pYdkLV*?jPVr?}weipi&3 z$!_p4+%9I8PM!mOO_+rzJq+pVG8ihR3*B{Qq&yX54^;Q*EZfRmc&+X8iU4tNH=5KoBVEoIk?5Au%XWRy) z94yD8Prs3Gtxhi2G^u=lz6ji}L!A=#L^Rh+_B8h(w+V)v|NAafvaRTX$`N=hPRHyF zL+Y-(i5XJqNY^o;!)=-Hn|~84=`)^X?ZV(&?nwM7OT&8ZfyVsaa7wJkXR~ySyFCS) ze_iDc*=;;mipK@ZJIFis5slJ&pn4`9eQF+JO2{epG31=RGRpuO^FOg#d#vr=^M ziutn)u8#;Lh_;$nwt1Q{6{I)A|d-X8(uO{{WB1N|*-Q=u@DfOzC zq5*a1F=Lz-wV8j1u&zSGoERZ{&xpF#8H)0KdxY0>Tbhu6RJ?p%BksI4p!Hr)@}mYj z)73{pTu`v4ecWMQTCzjz{GvyLnRQgOG*M{CxRAp3CF1aiB8;(V5Vp6E3j0$R&_{W= zXeo`qHfQI3^z*6^n|AU&r72f9-sZWq{U-#e6p9skN;Gd$9lqw=6up*yLfHB4RL&d$ zpQ1WkNz3EJ71zx$;4+jTS6nH@sa3EzS# z?>3`>eS#5jaeU8Zr}y7{tk}5_`r|#2AD@c>d?$Ob?7gTgd<=Qs>pb1Vo|_5!P;Hh_ z*WU^Ftum?OXroS*LyeAjZ;Dn^FO}(uRt>I z3t#PT!L^+U(C*?-6I<)>%>O8Kp9r#=p+FCVV-R%vx1`wChfd!82zT#;V#$F3_MbMR z?%pQu(L0i-{c9Yz?J6c*=>p}USrYF&&4LU=U6`$%Ev}zED=If@BmDYViOsINxHo*b zc-iv=R&uU9ylR2(?8{eh{ncsMOgt=Vzh%RA-*XJ&GtVnCTX?lr;@riaq;^jYcA^RM zIA3*Sh!#FfdWZoF?AaCY6tjB&M8_XTYCHN0S3MtNjzLcv!9BC+zm1p>u?z=49+R9d z`~|O%TbMDBEk+h8kcVz0CRi_&v|cc0?{S{6TP8;ydW!7H(?qZ9%(dI)jULe_#gOh= zRJ2x+J7*uD66(rqu|{MBzhfSi3w@fULOE|bpwGVBZ8F6ukNbudf zH+uZ36rXucQp^0_L5aN>IL`i24_R>k22Y29T!s$B&>24k)efTmcm4`b8GV{xz&q@> zE|9S|Bq{Fn*Pe62YTnPuMV7LkQyFn%SKx_WEoXol#U6|QFtt{i6t7C-Q>GsR=e@;t zKYipR9g}or59dc_&3gwGOD61SLDtXDu#NmxFv(AWOg71|2W3D0d+19?_C|>Eic9Qw z@Fb_%e43oF8Hp`kq*Y!aTvKczf3y=#k2PueI=-9ES0cH1BU(`UO?XWl)<^p7TAgl||Zh2>|+Q*kYo0BCox#w=b4zzW3PQi(y z9%K{kO>{ynOSgFPjDh`2c}MG z#g_UT7^Jxw!-l>`n#Dctg)*avc#iL=e{@IYl6*{==<&pyG^Po8wtM($LMo^Fj&Tjwq8GsgPapC+w})q zV*W^O&%c5Z7R|Uh=9E~yJ_}DLRbx_-yW~avS!}%BLO8IMNrzj_*Y#?@dCJr0~_fLhR+d!qsF~ zXlVWvXZ~FmcI?3UdhrK*@?%A)v?<*<)6Ds3bv|D(Ymm>zhkx)MTIV*5W^BcvTp*?Q zBa|FDfU)PE2%Wt(xE#L`#Y?J@HPasXuErEo^Z^wMLpV!qO}SQ882WSw;`uq)@N^FI zgLFt$&4cc3_zy3c&lIu9mPBSEl%MI)^T+J%jQ)+Kg}rGZe?PC6zeLDrOZIqrlB?Gj z&h2=S-$X0AblDw$rYy&sxMeWne5_h-BA*GEM_oS#g|;gYQR!x zG`-N0(BC9w9M65hvtn=3c*(rD6EWDi?Hi8t3!uv2U=6lR5OKjTs6*HF%z_Bb}oJh~b+9hX! z!T>~_*@JHqg<$@%H@QabMBJ*EqGl-b zM|u7ryV;AbE|8U&dq%@VE^DbcxVr$YaTe5A zu0Sc?rg-=x8fKF;X!gFZ!ukAcT87rf$Z}nY>%N#tzHcCkS`t82~$E%4A+) z<{VY(UdCCoH2$-mR*CvRJ$l_WU8KIQ5<9M1l6?4c5glTNPn-kkw`ZJ~m~caK^{b8K zVqa(Kl|58w8|)X6)y|aHTw1WTpiuJ6(2|~X28jE~cd&X-fe1Q~C{#8)!q<^z;=l6V z`KNk5#|@Wk?!?Q`d|w%{>tr{&K1YTWXI~IL-@DU{>JHc>bV_y)Xhi7D4=C+rNH12& zkfTR44lmZFcgo*TQq8`mf1Gh_lc$4Gv6A%%<7aAtgle*EQn~V5d6f4GGP^R9|E=Y3B5#G${U#-6px-YEB z=({4_ew56<7gxGmCP!AB{Zu<@LGpDvboFKmuIal9Z>xFYfL%ShuGlEn^qG^-yEUZC z>=jB%XT()qDbftTBerRG=UF@`Wvm`z;#`?6B?jS>ZV2EGj+Lp@e#fEno@iZQ)E?iBZrUbw7@`t znK!%fmRSQgdf$YnZzgW-G^Eu}tMPa|`xx!jNTa9_cjy0tyLuEZ9~Km8ro_CkSom4` z(lkfTU+OQy-^8JmH=7XP5P*vHa@1q`Wc=mM+BgeM3hAMaFQGHA#7Buz4NS>$>T3+% z?@jRm>hvt{4RbLhB)hd2*^U2zQ`;R#?-=`cmbPLCJ9`b*=HjPoJNAdm(-nTcd5l$J zzOWkQmi#X_4t_6Hh-G*F6JEcEF$3bNP#jnyf&=}i8|MJ5txTz^rV7*j=fLQrCEb4a z3s&m>uu?Xl0EauAFA0EDRSeA5+(F{$h2oOsSY$nZ2)hqsM5E(Yq!nGkrD;!v510Al zKB!PoSXXLW<5;k$L6y2LRwDaSdGU3zEDg|Y#QGb%*#YTKxfvUUOH4Y#6`42vvPMj5 zTg`nCK6hC7h%G;?VST$5$tp%X5AcIEzb8LyT9dllZ=rjG8E>DpXexKMuA6VcA4O(7 zx?9ryhT(8Lq)%}?SDfn^kHRN*bab#OeYohu^Z5@Fy)D+^ZK&-Y5VzD20q7He`sqvL4j8BGjxD-i$EJV%6 zY)Jp=%CpaM6x+l=md^;r3%+97gWveIb{{%YIVZiS3BNycRz>+YhTiE$nIaC`v`y&2 zTp#M!lb>On|1mh)hb**SLgBj(?Of2GUh&y#)tDVv`?wF?OwprR_p)%~6}uwV8ByDr zy;vD)Pj5dcQ{$Zt?1jIDzI&vZvB@kP=eO8DNP+Y)3O>)z!}PZlUB_+AtnsH*&L>ZQ zdm1f&1ITNzprBzTP*e}5?T;MUmBe4uj6Y)S4{aLOZb0{KD}+p=GCgCq$A?jWME`5b zGjw9?}c z_qPM6aHA>pGro=^T?f#{z7CWcmIT|eLuuKIQjD1~9TSe^F#|FKr^Of?^|^vc>?o^P z7K6fsbliKYO+{CZLZ5qhE7!6gW$gj9Br((0LY~(DO^4shCWw*JlzHvA@YgPY!F4Gb zHBkz?6pynD;tRawwlR0-DxY(G>C*{;xua&G*Sfwme%M5)<|m-qe<012>WR+<5qR}& zCi?vwjMX{%y9 z_}Y=P{y<7Mp9CwN%}Bp*#_qOiF{Ac4($4w9YHqohYOw?VML!T%?W#rKzJPW$EHUhCQE=yr`F8J;l_5nhhex-J=#)EvBXwV>&qhnP*oU1$?q@{MPf;+aEO zl=BGXizAtpqzl;}{8^AT0eyU3Ff8gG^tASHUz=;PaToEV{R&hEufqwCGJFbTF5t>2 z{FXfemGC?8Qrm$btC!-$aQ4sa-ihkKFfobmchUyQXm;s_sNUSy9-Il|sR7iUnC2V( z`Y^l>dsFQ$xxz~MGw^>jkcuA;6mp+AZ+EZ~;p{G*Y1^OQcdv0aL6s8nN0Z#rDhy_( zzDz+sifL^TTm4UAQ-60_yXce{U!KjgeD04F_e5uQAx>C1Qe01E>T2l3yv`-4@1;+F zR3_o#?P#p!oQ!{rG)%e;#d{OxfXDMQtK=zOX=u~ydrwizZ0&yv7X10vhC8G0;!?mB z@v3JJ3Yz;(5)fpFo6I`)*gajMusu)Q>SasEDjkLXY+rJy=p`oJ=tGV-{VDM50Wqb> zm*?!R)HQJjpZ#s7no8H*#Xd=tc~Z3lN^I@|2am zT;zP|M#1l;XtwQ1@$7jcO6{eno9THmtyPOoI7w0Tu%48@vK5<;%Ft0hzgMX?p<<^D zS>5bOD%;qlSYa*Rj?RO@sK@9#|53j8fU~&q@;WBc0a4dng1gDQd&`-LA%(qgc1$Tc zs>Z@c#~%-7*YY#QA7w{?%5Thid&WM7{*kzEnfWH0WwmVfUAq{YZ``>{OtsE_&?t5j1>Z zkw|r|67uZ((0rUOve#%}^&?;QyOaBS@(d_N$MqMC60)Tv_!`B@S1&{|SZqf84feuI5WnPkVT zPW0i-{QXOog=_4Tsl%}yrqR3k{y9SUzv(Mx@dY&}M_K6Fo)b2l9kll^l)O&ZDE>XH z7k!7yl53F``9{Qx)1f?n>#jm6U3*}hw+fAwHlkoRGiFaL!cfkjhwkFMZpsqecXgnm zXT8Wf%MJ53^Vw|`cT?8+@ENopou0z}DR*xg`Q3~D?bV|5FWjlAA7_sHC`b-j7}3r6 zzTCGyE_pG*k?*gr< zXkA@7KEHP2?T+=}psB=I}QjO@ppM11>E5p~*>?iX*y z=?mLMReT?kW@b>g728GAw(hik2s1l2rV8Wwf9N>hle88%iffgs?00E^UkL9yqdcf> z<3}j`tVZ)28#24|3vJ7?FQ3t~y%n0q*Wq9VQfXF@MHzp*c31J?bEk^Ilcakn@X zIX`Yng>C5n{pFHQ6NWJ+#UvNx}$E3H;a5;N8uh5~zV z?4K7EcvWWLo0PS<@b8__uUUnqZ_34Xg>LZPwisR`nk5^JK8Z6P2e3Q92qXPt5&fng zpHoL6hqmLh{9xLysE4iW$hn>D!~9P@TAtEV7&IGVvpx5ncfa=SwCan-v3eBLSTCu6 zR4T57CbAc58m|5<6$2+SBec#82Wqwo9p-Wk?YE9SBM-!%h+1*U(2c%CRB^xIfOu(S zNwe0x!}bFz_@nDa2?Z_CH&H>;qZ00v??*|pA0{Sqme+!rElyS9%*|K0YrYMiV|a%& zNeb3{2KVh>kC`LPxc6d3ACzn1mFtO3Bh|?BMKpGoXwt`*j`VL$I!rjLe&&ul^{L&8 zN6gk~)3KyEYotgcwioYld()BG&DdJdgAVZ7_NvlPysR|g=NU7dr8xtBdjdu{&cTmm ztN30r2N_!jVO+dDnxAY#)}eW*pYs|W|NMM6cQL|@&E+s?ek3tX7>qAtm}NLpN+LRS z@xX2~!qzuoMdcL?9laZ^F5l5T^BHtvQ}IpjFN!KMxsPl{#@v7Iw96DN106}-T$RiN zd_>|oGa?@|QZfw{xr%0-mE6yY-BSqEHzb`2nOM`DgfdeD3VV70`@C=CL_j@WNLDkC zRsk0We!w}!{rGs=9>1Bj=4UzxryscCm0v0RFXo~6@i<&R_Z+h?S0i^M=k>I*VL0Rl z_b??GHJ;!w--$veY{W|CXe?)zw$JWJNENwa{t;%j_DIFN$*wf+6Fa{Zmmp)LGmV|- zM#JtV^E1JZcFgr9m4C}{qt^}Y_e+yeWv*nv)fqodPp`|t!Y3y`rc#$V^Vy9#L zdmZvx>%p$Lo%q*9l{8+kQ)xm4ly_^;+gYD+VQmf6D)ea2YAH(N%=e;S#@z2=CZ78U z?wlF3@0UHg&trtrIe|6rHRxJ&f^T?&vTuHClJC2Jz3J=F=LONuT4MN5=FFz^d?(UR zxW90tQ6-Z^wZbhNFL);AAMq6L9~ASP+D)w6>LSq`_zWNZ$wB9oG_`--D{TPWpv9yVYV|t|I+IE1VJxsc32^b|f^j_uZb3k86gtt`hyo;hxE>pLkZ^ zj}F}~Vn6DAJX=4K`tuyO&#D3_M0n7mgXiI(SOkxEt1!KvF}uqP+0!=^E4*y+ZzcCY zJHxTVw;$GjdV;Q>eCUy6HZ)rGvG_07Swel`EFT1=%s@I5wFN;+W_bH7Q+(%)cEs{2 z2-x^o#4l2yQ4@4=(&($umeHaarptL}WJM_?PorMPAb+F>y(m?pNX2b9=hlmc+*PEz zB@+_ zEuAU)oMb0(_6pH?nX@rR`(xyHOImXHDJDGE#-3smddtq6r;;`iA8toKvfsje^-#?5 zU4V{+hwMXkz^&s>SgLm);-)$}KSbdN|J$&oE5>Alh7XeBZ0j$WTyP`Fb~!rdz+JGp zf%J*lgmd`*S;u!b-*o=WT&N+5G(U{?OmA{=O%x^;oWryV+ctA^CDXR(+tvk|*raG&N-AKI9;8SvMj&E0Ltdh2Eo8B;AG>Y3(ArGXCQ>gYh{ zF1XTU&j0MqH=qMi5}qN~GP^SwZx1QarGu{#S{RSo)>iCa_Y4mexCb&$g|xf8#MlGf z$oO44j&*jTm#sW0Yn();>3BlpmM^1NLE^dp(d=^DKVUAW!Bq zN=S9%InLGX^6 zgB_d=)}S(V2|wG45%yDux(95;ajj!wdWH>6;mgbP3$?;H-JBZkZHBg%flv){C6^XU z@;dumtexM?nQ2E#dfg_5y?TlIF8XvgtXS-qZ^hjO*O2H^fcN)Z=@zH6ZmOi?z#R#V zvCU#8?I}$EXTy2<0-wvwP|taSr}#iMBSENban4> zOfYT5VI}rZ=w`veRf=M7bN{!qOvHUUj44JM)K=FG`cF1MW??HbqsztqZz)(1e?o-q zFA~w=NeFryA(E>&9}$y=zE-IsY*?_6>zj$%Lw=~6JOo?n1mEjc;PJ~q^z1c=?22Y# z#B4L>18{$Bt}^-FRTIaLR)}}$x)h$2lAnI63l6?kq3ur(N&Gk`;orucs4IQowoeh~ z7N_$5{fm$Z?E!7(vaS2tAL=)Cc<#aMmVWxQ-0d^URYqav_FnW$u@ioK24Y;0A{AWw zfRX)_G0~d$^xv}}f37=B+MdI2@)mrH_2(IQCG!9-An;{5jwW*c=bZ&LkA8+@#lFz4 zaHFt_r&vF90;F~u(~8m|n9y65wk>m@vAfs8nwcxzxu>*o$#lHv(t~z&u_5iLRVX`X zL>1F*scbcS?!qjnX|*-o*>D%~Kds2t#*Rk3o{G;!Yw)o$1~=j&u;=`A?h}uJgVJiO zF4_;t%1tO-bO)Z9lOl$pG#vT5h zr%~tp4KM9~@jY_`4u?I0WLG0J0@bO(C42U~h!kvwOK)Cc>RJIXT4l#;j1YK+vq_9hP#k) z4`$=^J%ax-q-f9`Pmy`|0s?+EVBJeQ-?ZeDXzk0arSOYl%fmGBzj@NZ5^w4`ph=9- z+KESwUd)F|5+lx?MZpk9k}}dIjc7kCxn;|4J6n2M?#DY;YdorE{?7^Y-M567YrNs0&n$uQrRsT`mB8(3r@v~$Jh7{+Qm|Y7&eMqfAp!jciC05 znJMDWd28-Q>=LoE&Xn!uFUoxR=h0pivTD0fYo;WK`GrQAD?i2QHC=~CHe8P~#A8@lM z6~P0T6Pw+g>Jx)N)D&E6<`39w}$B=$X(8R9Y=#5znzMc6gaY^@~S6e?~ zNXkwYYXs8BD}UkWy;-C*IMKFoukk%tMi?-s+3?>{NoY=p5`BgM0lCizv@D)f6m zF)2hWc4n4S-gt2t$Jw=3!mq&H45xh=;8co8XI&_1iaft;=F64=uPI8n{>;t>%Lt;uJ?;w`* z*WN)3gZjyl{7glf_31r^ySwsErx6>{-=e&SD}@Rb(pcSsEo95@j|;dmM3&wKcA+;F zHz6~)4q=PfsgZV`^B^kBwpXR~Q3wBzqw@^txqaJsJK8&G4=U}wf7fvmlCt;A-ut$9 z3S~=)WMwxrkfMQ-ky&O!D5InjlE!oXpZE9urgD9+>pYL+^AXn{X^ZUdjl!&~Nc^W8 zz`H1cvlF)qJD1C%W$JVwSd6%Me(~xza|*dTd|13BHB)>__~H|GpdHhS5sy zGCf54&M=<&?!!0ENR^lQKyyS85=w6HygL|cboC@XEUjog`x6W&iULDF3tH=`OW*7V z2={^3v>{T1o||sOUGE5SzO4&zE`9E&CC$*~bjnp}y4NagZ50 zR^-}lO0mZRsC5WuGs1LfM#%u`#_y|`k9N!{;T>F-JF{i_(27z6QZA{MjF}orsV! zC^~b47`M0>CT?-;b?-|fkIT_nr`_Cp9!y;;)#%r<3_M9=C)pS2|B2-=%$Hph+9niW z;{&}vM^Vha)VDFKkW+LM5oX<}fxF=Cg_R;S)q&)WU1i@eRLMnHc1L6p4}Czlb%U!$Wv? z+-X7s$LUe;{GK!?L!BOd?na(=*35^uBehuOZTn;}2Sw0WOMeQJy@9T0dy{931r@$7 zX6CLh_1#l}qk5as^!O1P#@)i{fn(vh^(mA-KgPZ(yJ1pwnSEg9q|ZKrvqj8tFY3j; zf=kRfQ>GbHwMjet4(EHj(A+a}6c&5 zUg##6isAouVdNPt*jio}?TULb%Wjv@Y10xb$DM)qbqNBd#$&C5allmj=`h)N2>0~| z77S+J`miPY5%JYW?6cCRmnQzy)%~`J9brbf^IRxeN)|(&m@*gEkN1H?#jp~d*KMAH zt{(DuFS`#{Hrt}uFJFX>x`4NzmNTnf8`jrluyF0ZLqqnLeRHQ5rqbm4 zQ4!_b!#pgJq05F*LbI#~&1#Rp>nvUJ)+O?r${dF?hU~}nq}XP@pL;0NwW1(e994+b zNj)hv)s(y1AJ8q#j{FapQFH)1T*kPN!Y(~ZE$j=w&S3PLunY6!w6Xfj67*tcMEi{) zXs;ZEwOc2_?#Uf!zH_7X7hj2C9v^Uw9n1r?^2MT#YcSs9OM6e5!tTugo+Gwl<)9WU zY;l0MlN8A^u@wrm%z1~{%`97 zEXnNIF7#Uc2$p6PA*s-oMh`iI=w)Z|YNa*R7o5Pyeie9ry9U{Hb73yg#boztc+cC7 z!7>4OCffnc;Ul=`*c1D1K16Bvi&zr56}2g^5VW%t177aJ^b;4+UzK@tTed>$)m)rm zuJqE@aop-=Z|-028(d|Uc*a1!r+Cw}$t&@@(v3ESJJEjs8Qg7hr3fuSmvvX6l>Lzb zdmJh6+;|M$&bhSXpRiLqR>V&#MEloH^z7UvLfP5*Vbv4t{B>HQTD(~#zb?h%cY^Lk zzYw#T6R}EJLe58%MOt|U`$*i#AX}4e&y#}YQf(vzo6yRW+!eAnL=AN5;`jqXgMJB5 zIbGVZjU583N>H`Kibj@8QS|tGIP^`O{y6@_`3WC6!|_zi9_Yif`992#cVJGxJM)5w zmfw-(UQ=%xAU}le8wHVZm8Qs2bf6iZ2GIe{Yr^WFJLR#Dv*qBt{H zA9O8v9HL0FC%O0g%7NOH>M*TDYo_+BBxJ|Mb9d zaWd`(t{q85>31cuG(HBp9m3%%N#;D!i6kFb%+8-rg#KP{(f3A}3 z+VCE;S8d~+NJDutM*A2|u}Nc} zn55nxZOaV(-+h0I4IcezGrt2LKP`gQK~E~W!HkV(+!=QrMpuH4;BV4*>^h)CeH|6? zG=yEHUAj`b;U{t7$OnA)??!j}{1TVO@*VM?G5sBwhTyil(9hJRkYQ&aKR21#c!tz! z^8lwm%kjN>4oVO9qrdzf`H;UAhqw17i}p4c+AqT1yTd7ZtT7TdNx`>_zmd;8F*m7| zzkM3iuG=coN3&b@Wjp?ERHYf;n|QwJK~^W#=pN5Jl_uEHrg|xgN&bw)W>?Y~atql@ znxVoyuZew`5&DPSV@XOR8lEC>q&!(pRHeoy4dy8HrgI}(#gUt)Sftg59)I`3)Wny< zTO)*go;?)GmF#EZPTQr=fe5|ZjqewqFh_bUHjQPbk^Wtn{MAL6%pAC?-{XE^p;&!l zD^7+n`$Kjv^I~i;VFu5@9zGVTF505qL5>Pf{zQxE6aTDcDLTxIdY?u0lAb$jF>rG= z`uC4Pz@;wY$YE7Uw&!`MZ2Kr_a+oFx{g)s>ZI9&HW<#-q9Y(<(jhGV0&i2t&Vs1zq zq|%h>ZMqwbr%I93G!xGIyHI4f|p~y8A*f4ZIf(+zHBdGv|r%s{pvpmmhUSjjZZD>1Ei~Q1;sJqvdrq7ON zHqUG9G1sE~pO?WwyMaBA^2{Af!r-m8w3->-^Vm79YG6zUNBNV|)@$swaHS`aUSw_l z5O*%E<98-^SS>Utvg>Z{RbW zD{^>rLadRBht%yZR97f3EFNvclPC?c?9R-;g=a8>9kBCH@UG2Cnx?1f(@_;OI%fO} zL4LXv&2L_xdS&`sV?kj-J*g-u9MQ?WNv_j@rdiL0>+3;ep~f>;l@r`+?MJ)!rQ!YY zL+B{Gj)8nP*z#@_I^Ji(^k^Eql#>xt%q$B<9WrWRhVok(3N|yL`H~ZGR8?UQq8!zH zOU2b#S<3a0rG)J~M;@1pVI`kn6nj-NE$a%l70YpE{)TW&J%@lH9+GWE72@;eZJ6*+ zUG&|QEXvQFMDCk~V(K78?ujkOBFFA1*t`lhO+-$o#-sO;&2W$GPbcQ6;JVxa^kwec z)HWTmxG<3WIWp`|G$-F((vrD6KffoZMZ+I@iJwN1V&T6ns9s}^wXRao$~cC&QV)Jt za3<-e}H>B|$P2nXE@&m$*{l72Zuu z)TXf$EvWC_FBqMtL5rNcXx7ngSon7i=S7#|s)-Z6d+o)S%8jUVbAzVGI5;N?9MgFO zw*{advMTt>?A6T3LFCh43vv+!oT=$e0r7p2@gN2s-t8DY_cp3d$3kNbb5Iat7uwe zO?zJ@VQaus=7kv3*elEhGEIh6`C~k&UW~NOs>tZ^mAy!#p%-C;+QawpurdZEnign{ z%D~)+8(8#g6l64GA=HY|^Mf?-2Q?S-{qx*3=Ew$cSAgI6 zGKyHS-idOelhN+a`>r|6=!hMO!TSXb>SUym#RPoOcBXOCZlths8~!WI#Af#nWPR(+ z?72c%X~>ZNjG>gftO%L^eMQr@0D89ERwO0eM>>CpJ1zE$Gv8lh!WKKao^(B+-0%wT zuboMKjS5}tY6m;5g&32g&+N+y?DCm`8&;a6cJikPxzQgMlQe1AHyK)7S%UGLTMNIa zOyMtXVNWkDTH5gq3p!rFM7KmFvuD=BP+8=SP{Eo+T}qrcFaN^&=Yp82IiioM&{h|8 zDD1DK=%F*2z4fEaqQ0WF!-DSr>_g8cZW9%PyS{xS;?JGCuo<9^>(^X`e(rrF+U*x> zwyerCuzi8fOegH*4o0DQZyKqsK>p@kcrPDJKka{VCRK@UT^L9o_GbOY+2p`;=5pwVd($lC>6jyBf}t)Fsua5r zdeRB2e7wjO0o>>7k4JMph~Po$%)RcLxXSD$L+XTWTlC%xFu zd8%={xKrPU9=XX<{M-$kKk=vPp1*N^_IXGqZ4!@u?-%uXFVQ6+RG_gAGh=6^@LnuFl+snss2b9U8Eqkm{|Y)c{XM=uTflwc zU@{0(pw>P^a67Uu$>ey!`M5W<&$Z#$CmrZF&cgKrQZ(__SX2huWADnYq-1Ey{huoQ zyKE7VyFi0(&#gr8#@qR|M|x3T-_MZyDqV0#_anS||3lLj8S4435+858#jMK;R5$J& zioKZi_Cb~Zy)=s{w?J|Mm&EkD5n`)sFzt^$FK&hZ6ywJ9Czl?&c>dgl-Y0%RJ%-_7 z1N$Y23#asI#+2dchdT-g=0M9hT@`WPyOP+qj4>_&96{WDixbA43{P ztR_fI3}ngokv!R6SuH;P(4;SirRkI~ET~%EmA>V0r~C451f+%s^l5b#y<;c9fAZk~ zo3fq4S}_F%148n9nC}xcW8RAc*FV4^zXxsVYX{X~Es(6{pZm%k;_ZrWIDN^Gu8nk{ z1nC&8iBhF=zV>9XG#*KfMs)Uz4SkDSfD?U{Xe+ZU)~5N0!Tc6I%w95?y&J?N$6gej z-@$CnTyf#B1Eu|PpfTJLSig6vB=+pCuqt_#sSi&tA+PaH~%PX45UfD{~JW_QlygH7>wq=Q^6%!>f?P9+e=@dRflsg za~HEWi~BkuUNqz1Wwe}jroYTQ9C14j(ly+}b?-;kj+t1}FCI@U*e&o&n?Cp*z$~6; z#}{^|z>qW8RBcO*cjd_2X%^o7$V3n3I}Nt%4|%=AXkM&JzcYKIWkELPayPX?Argk= z5u#&853*i!1}Vu$MW8z~TU6I!eS)Qk;aHqCy$w^lR>**sTTQzC# z%o;HxQi`;hf4DiR6iWA9Y4*%(crBBP6@C0^?c7+b3%rVBYIZbQAxoHsFM{b*8TO{~ zZq+gpQvGYNTJE#tPrydh*2|IQOJj0)qe&YT4CwV13wk`PJI%4RrE5Ld;qlsl%5+u8 zW~436DCcgEw+9u@XV$q#0h-8@!ep3{qML?A4c_FXnu=7#Xk06LitXwraOK(<=6Kx4 zQ2s{5eu%;PRoQT4*5$rEw-7#}4h1mbeGEG;mHFJe{KXB>Wo!rj;5C^ZF z;$1H@AH4Lj`M@sh&rv4jBXW4Q_6+xoI)vT(Cz54N+p(ED3JJwiMaJ5AjJtYA{4LM+ z@7?<}cAEv^{?S>?wDF*t)nPp62*%GPeaK+fa17~`@VC{L4(e!fKWdMdmryFC0#zyb zXrKr@YAIH6=cs+OoXA@9U39cZ@vdEhHG6&vjh9JS`fn7LCUrwh!ft$CAO}U|GEuj{ z4Y|~vj7`5{Psu=pjP6R;O&gH>Xe4(2w4|%ATDkM1552e#K;~J_LAmfdvkD%{QK&i` z$Zx+|d|!MW3N!EEz&0yHZtp>NvR`B2T=w{}OMY8jInU<@W7!LPD(F868+K`tbi5-? zIlUJ2-H5!-FfVcZIxHT@UhUI<)PLD~v?ypZ|G=604{YJNnF&<{dQfcl5^P?~Y#~$b z@Og|u@t&dlPKd#CnMIgx83v7u3*gpfj)acIn4EABG0gMW;ZiIaxz!S@6`S#Ct_gc) zwXwJGF_x_uBu3PX#=&0^nB-dvJyC-pkCSk(TQknY*C6rOa?B1c#{-i;7`9Z0Tojb4 z%Zdx)(RWSaZZIi@{+2wJ>rQif>yyRcXkl^7j_{71-U*y#SYk;j(=Nh%{|(+jbEYlz zD!RVigPa9r@C{go^yPr*_$D~rn1;_gywJY<0Z#G$$}oIBta$b^^I{=}^^AlLcM9&B zyvESDC`dTp_+jQPBp+CX)tS+l#XI@s#}n{;J5ag$ag_I&jftG^ypn$rsiltW zHuhqVpD%ObY^m!8?&JJk%M7ppI(ONde%7#Se%@Z(PE({-so5gFX9{M-s8LwjMIn3k zAo8jjG2W|FVs~MIXgYnCUDkq{_0Nd+X18Fb;z2J13x!_q{aC2rN6Uk|QGb(Zcv##6 zl`x8 z$Kwrc>gDTAr$!J=cCpP?6&O6%o7T$JRq9Xk# z4$V9#E?wYE{em{wo_{Ry;h9gH-3Mgm?-BCbt5CULm9DAt_u+E`f?nuRSh52t$-T#v zDrI^ZrBAmNtB^G2l(^s-gW1e8bSSzmx$S-kbzjc_|DFlGQ|U+vy8_is+fZtqEOuQk z;Vf(z9R2?a4V@g^Oj-mV^RJTg&JVEst&Pa2a>NQ(E$ok)C3H;|U}mTTz8@Rt+gfje z6*v8OUiC-p=W}gZk0B^O^hab=Xi$lRF838&akaycZ2FDB-?1wsxl6e7#QewFT_eQ& z7n(FRV=u;UY?5dnlBXF)*Klc_o){n%z`cr>ocD_r_ci*HU;oe8eISN8z|Nd^c*kAO zV#znz3ej|`H|NtMMA;xYydY<{r3j*ylP@ocpKpJ#_Ty>4RcFn{cbV(oRRA$31z zPyX9oC~KQJ85miS%R27xq;l?Xcn`AqcoJ{IJ;Vg(VUlm-)21gp= zX-hBk?P%3xeTe_u=-nzlgE;%rZ?Zcz$2!oCDqqf|a@S;X8U|@7(9nBj=)ds@_ho*< z#<&VTdy*ig>(GqhweaZs2mki*oWFN1WcSKYh3-U{c}Uald$P1^@G+bY@FV5i_mWHI z)sT4=ObPqVg?sZ?yxQ(TPgji*?uqx{?VO3`u7cduddY`FA6`Xr4aqL6B zi>2s#%uwWxzKp#Od(y!oUG!a1h`n8Uk!pto^JEV3-~IoapwXOsssZj{aGu6tAyH5mQXiz&nNX^I9~hyjARWu0r*{PIUAN#>R&=7?^ww zE%8TT9^*xQG%um)&^DAb2hsuUQ~a$y!~6p$@(xK6Lx!x!V|!J)KYxcLH)IN~vis%a z;1~`!Rz^&X?J-53$*8GZv`c#~I#Xe@z&QUlEU>U0w*olmwhK zWWL21BU;d}2;b&^fwB&39YLhh7b~ne!7dm3u)k3H&o5bhM8}VY^MC=Z+r`CpV zQ0?9y-#;0X|B%1v`hEj~tX*l|2q}^*S3$$~@8}zxi=O}7F=$#n297XOEQ?Xc^uN?DxGyL3Me4^q`B z#>0`qmhVNEIbCR8c@UNO+=kZ=V_F($Np^e}=$OG-u!p)7821KqW7t2sqZd`%h4OtO z5b@*oV)&y`_^Z7FmKQhSU7IbAP9BXH<4$7;_ouFNPi~+6CsEp1gI3d~S>5%$s;V9VLqYjRZnMpcZecBOaU_T#|mQ;5D}$M^ix_K}4$SBZOzZo+5O zBe*<{5o_`X1+0|4j({<%Bx|nMNS+<2Kx-=nT=Ly4ifS)n-NK8Kn|(P0(7}$bHbC#C zK25lvF1$LeajDvfY+i8g{be_-;OwdGf9$R@y@f7s47h{bg2NMEVc;uE8Y0z|?ryvc z3!^TmOR^)msScE_`cEW&@u7R`-Dzx_fc)299WpSi+r> z#@p`n?h1cPMqCmh6Az)mvjFcyuW|m7*(-(V(5;sj%S!j6;btZ@KORNHt#@L|P+sAQ7bZEV94QU96GEHryVJ9;Y@7zMVv3)G+G45cBdm}doxc%Q@BjFr*R|qAmrl? zk@RVZNKdff98`|T+jC#cT5m#US2v0sF~LIfKT|q#MMXGsKRj4Qg?8;qL|bA%TK=Lt z)$8p?fBQkyHeQbG)}DeFyRmw89V`lrz9OopHhhhi3ggL&^sbu=+8R#<#8h*}QpT0@ zaT*x^@Uq1JPEp>gL%lG<$xo=P*d>nZ^+9QCj6~vo3Fp`PQ=CpX@*^)}_fco^S{sMZ z)hW=t5=5_*_A`g(4TgAV(AyDpqN2|$)IB$)Q$?~k-ued#$GcM7p4Xz~Y7&BZ7pA-} z3m=zc!^m5U^Q@(q_C1Qv8t!VN7#sR4(u^f*QC$^4fsZwL9vcVlAdn`z&WdjBMWQ^P z;mff%0Ks7~=JJG3dIu(S(e&%Fl za35=S=%ahoA}qgq1$NAI^Gxx>@QbDJe;Wcv=_rg|Q_QX*GfZkGa+sn+_3ND34bzX3 z*k@VupC`$k@StiZ?oyiC(@CRPR1}4Zl-Pb^n%!bV$NiJoz1t?nG4I@D-f)TZoG5W8 z`>!Ys{0ko+H@Y=Ng1WWx)R^W?sS#;niN6AU({(28yKXclIRcyFH0gglns$eISXrS= zlRccd{}qeFOPTG;j;>w(BP4n3gK4s|pf{f%O1`{hS0Cr1egCW$mzmwCddq{nGka3! znBVM+^Pq>kKf7+~iIwJ{09hmM5xo|29hJ~OpUG|mP12b7l)E`cF#WLtO)xFRmzl}1 znQlxEyH;b?h<2QJIf;RJ^=RoXMJr!##FbWQdOA^-p1)5+d5|aH)1FDLnm#}VGqJs2 zWQy@o#Yk&lKguE(QNX>PdeNa8@-F z#Va&%nmGsanlvao-W!kW{-ETQBE3|R!$IGAOf1x=>wf;w>so;>Ja>{Z$;3xhSNhl| z9KXynFs#FgBK|vt4825LwG5!J(o3RO@lLEVm8aK(UP`>@ZO6m;U1@E2sc2M>gVM=b zylm## i2ILVOY8dRyM){{>BG^3%+lIg$2k~U{6(URl#G+(y>SK&gJ`}CmfUKI%H zaG|DjHyX!p^m85(8fJeJN6sw4%)8at=bO*}?M_(tE`?h$XECJvz&zv?GBsIU6eIp3eVX+i`R*bF!LmW6Rq%p2<4Wmm~?|BF>(!kpetFTiTcUQ}UdKt3D4;uiB*{wkW%HMPO;l^uZ#!=fN_%>|*l zn-RA)9!^Cgv1I=;Ead!8Ui>|DS;@S-!%8Tsy@T72UAZS2j1;>X_#bztz+qp-guTxA z%X8IDqr1?j3Imw_`GsYib#)!ho`N3@xEi8Ft6m$?AcRuQ|k-J|J{S{rPq-5BaOMe?qt}Mjo~MD!uWbUO5ZUjDaRXb zmG6<}9|x)8+4!(Qiq>rygOnu%%ZNhWkEEhxY66~RJ;k2D`#3e?1TIeCnF6k%+$0LR ziybhxn;qrz-_L$?f{^l~am%BSZ4(B=YZjD|xeM{`fn>+L)w4Hta;8Dhv1CE#q!W1; zGo0#syHK4%G>*odhT^3+$o+Fz5YLNO1~=fn)^qWoIs>OoA7h~VD~ZzgyArj~YY2I! zE)OjXLZ>%POug55<)BrSu~mvr8f~vIj9dgXHIF!)~kr9jw|W z8r2@)*NIRVD!GauX*F=@F$XbAJ;d97caZ9=gI@uE;qN(85|Hwhc^?W?s&7@`J>whx zy#9vyzA+Nd2lw%JpDD>k*;4kzFR=Y!O4cUE)TQJpbL4wcPZLi%!*dB$uO^W@`Yev~ zzq3;Yn}vlpyFd$)q4c{`WL_^u#L`RLsgA?*57i=U)eU~%b8p+hkQr5H;XivlsvB>L z51ccUZ{_odc?>2UdxcCm=N4vizBl8Bn8&kug?xd5<)2b5Rz0iT2i=LICKqU<}366p2b4bGmy_WUCt8k#Oo=8=z&=Wb9K)$KkT>Y ztd*0z;|ylbU|B31*H>s?XhPgtJ@konDTuFqg5($v@#6J6@$c(KN#*1RBFb736?4By zRy_`sSZ&b2g%7hu?4mn(`@0{N^*DtY|MIXd#hX6Xr6c@PI(wamFdsJw^KX1aO@t!% z#8vPkrwT6dyx%Z0#XkcX`qeH+Xi-A&g3}n)&Y9EhX$UFK!t$T$3~IlGw?_`+%WPvh zZ*d<>DTdJIB;29my&96kQAogEg#C~AiWeHn8C?>XC9zgiRWr#_2a^Hu0dvl_$* zPyA_9BK>kZgeFxn`+)hqftt+gl%`v%?$mXZ3_V`_6R8Sr^r2LZ25?sBuBj_6kElZ3 zM|s+MQH9*Py~5Qmij-E&PQ{kH*!xJ8jaCdFTabdXzw$5 zl5LqRe&q*{mZvl=@NDJ&hcC~g^=X*bKv5a*OC>KnC{4?XYU|mBzJc8|Up$zN=)u{X z0J6+9qk!l@nq>5ynf#X#!hWlVhpO?|;~D0p@GMxr5;Ds!;B0yWETkHdZ`zfvS)W3; zx4$9jElpO|TR`zVtL>#kDPdWN8*EK-qd;pzlHp!p&ga4q@=raB^RL|KZnYq1sX|y! zIEXN3Cn_GGMxTq$<9L`KJ?_w=%sVIWVXGtUPwm77hi#bBtB761dNhGEM#^EFKg(68 zFFFfxSm7DuV@xTy#{o=#5H1pP&B#Q54=z03C`tr6N$lF5zBkGR<%O$Cnp-S$Enb!Yt1VqAXtf(T8U;Z%b`+HImSAwoO2{o6gH=uaaHGZxrcT>&_`n*x z-cW#*KLhAC_uF(rt|Drupo_id;wiHhw&*h(S2*I>^v!&?{fd<}FY&&FyQ0g!U{2Uw z7rVKD*dA z=IM;E6)6ZAUWEK(laO5-gT*}y;aB3%j-zb6hPbk*Ia>ymyQbXzaNJtT^H&(_k|q$hC`kT z5z^Ix*50rXqS>7sBmBr%5+x?`OlWBq?**>^C&GUBq&#s*BB{NJXOHx;%-gGA=IW=& z5AKFu;~q;+rx&4XRjOEaOrAN5sp898X?ouI3%mN&iOR=Hyzh{s-+|A>wP&BOzPlQY z({iG>Z>31%y&8RG=1J6U1=^abL&te8RF?J)Kh3s@Y<3H-H2#P+2F2plpi~SvQi7J7 zBP0iy59)0E2BqisahF>c-5=!O;lxdtaoY`J8c)J~`g-=|sKZF9456Qi43AI5@x^Xf zJ1CeO)(RA+Pr|yUK-zj_CpwqgAoX94xYk>d&K&v=?~infOWC^IHC+Z*i}^zDM_2A< z3}8l>D}{b(M!ysKi#7*$mMJtDalnw@vo)Cyao=ZZ_e zm|1yum}I)-Bt9(7#>5|51ulKAF*hz97lM|Ei_FL9JYJ5q7qscibp<4iZ^p1A+|Tv; z$=&)gSf6I5Oi35K34Vadi+a$*D$bKHe}4nyC53Z5-4l_`m zEhw?(3!3#;b2cE9WOv9>KZ{6o;~h~?=|Rl+k;aJ&QdGFc2R8No7^bU8HGZ=oYxztJ z&Qzt5C)gQu;1h1xICAGmiP_#CapQwEbxN%0tJHUP%3F~$_mA2OW$4WlC0dXc!euA-iCBGjW zcwYRFcSKz{XE;>+&mXX)zYL9t8X$5{zDJX2gI|}k0mapw@SLYi@>8N<`pj5T-PDJq zlOoY4ZdAdRt-aZ2%^CE2PyPFq4Wh)zPLch%4P|rfsrpJU_$MjzIqXO;H$M{ZZggUn zy&2D~z3371C@*GkcWQ<^Jy<>qE6$kHrDT8Fmva~iO`NZhlcnZers7p^H@d@aliowd zi?+4yB>kOpXQdfpp}sFIgbyvX?n(dSzZsUg(Vr4e8f4=|?)!*VL|M`^cAB|rR-#cV z8H<**!E@|w?4Hfe=!b7H_Qo@K3@t%ylsu_Ec?7ja&d-lI3h%k~s5z%XD=U)Wn)(8n zAGlZeZ3|x2+EVBVPcqte9kmO+$t1y#PvBxb~={}T=ew8?D~$Ar5LuDwAD%b;`yNm?-jwoR-9qZ``254Ct!Qz3297F)i$w{!C^^0dnZ2&!M^h5! zm9NBq!){=u`V|BpK8p>9PeI{;5#5Q+#;`+wA+OB2rn#KcsaB$!wd}thl7Z&6ZU1wM zlFW>=;>WKuDE!)$%4RFV!Qvq7Iq&T{c7u2todoSGSz_+L7%|XkD-QhahFtqMBHWwz zH_l(gsct&L?EGP<_zlC2g=3)FQ_%9{dDwd14@0+v(E8FvaQsMc^>(F}EEQ_gc`Vs} z^_Q4jsYBO$9nGJ2MIGxix=@IFp=9OODDkE62#n(GIeYt(y*_85rrRh6Wou&j>=joUGpb9P79O2{{Z*_Q0Bmpfulm%6Z1#NCp*RaleK7-hPVYD<+41~iO& zy~VQ5^i;);4E7I&j^zZL^I3ol&)Mj^c{FF~+;QbZApZF8;7t1(gipJJW7@r`PlqxKqvGGjO_T$G$Zu zN*G;)t6DbnUDclCZ#uD4(~G9wbEeVh<~)0`qIJ(KNqfq4_@|viP|r5#{B#foW;u{E zse}Kh+l*I-6dG- z($4E{$eI)kNeVOLIvYi8XHQI-&;A{qff#wZ59*w?C~kizKFF6rTUwn0Mt;KejE`9G zw>#bE{#!;tAqv05i^7{mWNs2Gnv^C8$pY^1f4d_V^;1MzST}l-7A^+HIFQ}vSz<@I z7p^MfL-VLmusyKMwSl?sZE-zElOAYq!pSm4)K4~}dS=4TDk~6cK4{YVfPS#(!+Vi_%rP}# zZtPAUGC5O)7qc!vX&=b>hdgBl#UMeUNV5K)LO|NFMnttQ7dIn=h2xfb4DY&DOrPGP zpxE>u%5~gD`>aMWTzH7q{ud-I-A$o$FiYr9%@Y*J>;&foNmxcII)VmBCLTPBCvt~y zp<znU^94nEV=_QY-|s?Cb#$q2j-fc6XU{VwX?oXo zQ|x-;KfT|&rWEMMcO|`~ zyO2a2z->J_3U04Q`Lc7^u}+U_R#jkMxjab?i(#)qJ5KTUJkRhj2HfXtCbK<@I7~Sp z&5CAR@up9%x$sOjq+7%Lk^bIXIQDfX<)?o1_DdemDGze~#hwPv)TXt}8G2CaPuZ%LV>92T((_8BJmCWQnai&Hj^&jgz8~aJ3xO$*I`Z_Yh<`qj+%I zS*Vzeho1ZsG~4UZP>WmW{QCu=jqFDhz;+|(ol z&QNAgOuGm=bOASt4Qb$4J=~VxjliKYXulsJp&WKpZ7_oQ*I03B?{=gm^51<#wZC4- zap;cj&HE2#`z3gi@3%QvkkT8!589K2J3O;4havh{U+R~rMSfAyBIo7-*IN)A{ zJw=mY^!PT$sBkZ5_*OjMxf6bCO(|<(JTm+5hVflzN?#F!rf)0ogU>4KE*X&i=0HwQ zd}toCM9SC+e`>NfxpDv9G}n)On3cRb))AA3??Yoh1vVZ?-{qtR8M)B$4NWH1WJw_c;{4GbC(RmnMrAPj2n6tCK4AWm)PcBzGpO~<2h{ScaJipr;G3@Yi@oV5GsB_Qxx4sokiDqW>yKbb}U`NistKeLq zNhRaE(X6WP2XP?EoPZzO3EeqGRb~4}Rl+cbY z#>JS;s4iDPW&qE-SIonfqZ7o@!ZQ3k;3%H>IpP*`1Ae9Ji$Bs!F~Z0Oljo^Q>*Lld(=qA>wOQ@i|9RF&!2~&9|=Id6%??agQFsDB`t^R^Kn?gzI z+h-zu57E4-PGWX%RrC%UM1yO$iX#RkVvB+o?bmvRpWy+bYU2ecH(Ww0Wm4FA`J8h1_lZ^C_Hq*BEU2OHk2Yr?i75BoZ-$tkW1 zv3t@nzuA~Z-zdTIkeBG;XHKZA;qHAsqE2mNmO1DG^Wn9B>_W{+zyIUtECZ^}zAj8i zH%NDPDJ5~w+KK^Uw_^9$-HH-|fQ4d#0iq~if<-DeA_gLgC}IE#7GfacyZ`UU`7jK_ zz4v#|*?X<$p|h(#;+Ic69On(9nJI6@*}(@yU7{N8o7N~Ed{IEbY9)&MY=tQygM|EE z9oko7eNnt2iB`P4Q&! z@Ll!{6P(NK=)Vf)MQv8$*S#C{ZG3^0Ys=uoU5z`-TafBX2)N;fH~hPe_jE+;>?kyP ze1?a%JSGqCMN!+kkijnIZff{aQJOscWVga-K94J|)uEoJl&IJGMELK$E$MF^Bet5c zFY-t4pl>ZZga$vyes6x8pSZq2tgo*W=Sv#-d0|hB6I7rp{SS}JyHOXJByoD|Z?v_T z67e42E^>yo4v7f9>eWZ1VgfU~x~ zG<$?GP5v587izpo?XNpInX)TBX_&-rp&Q?8-FW7-S&SMap&t$ow0g_h!kO70vD^Fv z_Kjv<)xfv7V0IFtNsSz}D>1J7A$W}7K62?7>|XZ^<0hwI-$f-Li#r|( z2=?ktY1)RAV7?pE@&o9rBKOJ~7Q<1I*`>2gsQ<17_|P>2=YFY^_lDlcd{~BbcMCel zXEY1NOfi%*B24SYIo%~9g!dQr15Ux;^1gTq2eR&r%`l z0wem)&%%*vKM-JWA61>DShmTPvYi@mZfqtJ`Te3Ua}goSb8%z61GTStBc54CqqAEY`ACuJ9gBVCHXBaffn-$c8)!Zyu?V@@;Rq|`3cBB-G+V3 z&T?ly4LT8np%DB6R;^~Vq;EbR|9TJKP<93IUO?wbGtND=r?kwo==J&^bgu9W{mU;g zCxvr-YU2Vc<^F}`z5WmhW7%OIMyCr@D1L2;BG7~Gbj`avwrJ_mTNPQFKi3Eo-ZjBWh-tFBov8 z0^{caQ+|AdJnvD1rZ%zr;+61K;y!!YSDgJ-D&i06lhfih9N1wCzZ~wE&YFhvq(L4| z-c4bGC8lig^;tKG2GNn?Hr zPa{ljBuTn(7UY&rCw|-fE{d8eO-Js2#xNBxVf64F{{GdYO&gp@>(~$M3;8bAUeyfx5uPl%-Lk~^R{f$2g|&qvUTEOsjp1V1yHfPf`5>`fdKjBy%5cT- zb&+9f4hn~4!)1S8aqrq`?9*sL*MsU5ZEKA2lip#>5KFpWYk{v_<>>7rHF{bogGu5M zQm0zcp`XR5Z+e0uw{+=A*%`#Ye25=E&FE;ybCxCiO0T*+7)4Y>17^(F@{CKIxT`7O=DHn;&i}iRn+b+D7Y;h=Al}cljDarmLY-3z# zy7~|14K-kqT1rb)dg4Cf!h?M|3wcT%UnrJsm9kQ5~v1R(G=_V z*fK_iUkl#fFZ}@LXEX6AejK(uYe0PcK-ko6M#RnU7%|%tXGXEpNK%dWNiJZDNMQ70 zSt?Rcp|W8^i(Sts(@{H3@=DhfDgV^zoC9ZJ^pns-t5I^-bAvGPJPyNp6m9=;Sq%TT z05Wwa{r4U@BC0Q66}8Ga&g`*+6RAubb%px+9+Z1+~3Tg=KJXtKl-p{Cyor$ zq`T})k&EXZK09NlurK}I>KzDvV?r(+YP3V6C%tG4px}2hyxST;H)OqOS&k8f>FC+}a4|0ht)vMVfTX>acg< zact)~`>mN(=sROK#%BNI`N(I)8UKNn(P4CJm!bVvx{%Aib6D!Y{7imbR&R+x-3*{=fOyB$Ov*U}8V!MNZ#|rF$~P??^{# zbGd~4-q}K#-LTJQ?ZBJs6GdICKV|biH6_*<%ieV&?7RUf*lR%PQ6r9bccv-TrQ+}g zCFV}v!{IO2G5fnaU66f-FcWqmOz2LjRj1HC;wGNPIMC?&2H~C?38@MhT5z#QbQ(lr z_`iBgS+P!h>>ULe1y$NzVNMy`v2M1uq$O7Nlr_kTY%*P`beAc;=kFffCIh;cX+s9x zGobgwj|R^7rrKeLQKKA0Pp_Mk&)EVfT6nOFBnKl4*5Tx&$C#aZ3EszcvD^A0R;%nm zcfBDvz`yJ3?q(F8a}I7x-XfBDv`$W^{{NnQ{*WexyJWz^{u6iIRoJPji2WaUceq)F zoD9u5Pre0>`8=gJLdV3!a+@P{^>6g)jtVMV@=9kVjxNf z3vsu^mOGv8iP3+e6Dm+!|^3% zRQ#Cx4v&agefp$f*ajC359BX)qOdYKDm-R{Sa<&H?8t|0kT)I~zsBT0e2#Pg_lG`0 z_rybdn$*A<{>S0~-!D!q|Ayg)r^I?sL&`S#g>p-MJYpVdQU6)ks-(&8wQdw}GY0dR z{Sa;eQn63KUOmp-IXY0$@5jthHKX@hHk82ZmWO4Iw6}?K0DC{6Qek)^$#)9f3SU5CdLewrqdfa6EZ&nsXO06;W{&8pBPiyOKDo1c|^3$ zv?cG$>>|vYDIDzesDN5IPiii%YI@Md-^nnWw2!?JuB3b6Br=^M*lEhyoY(tsdq^%0 zz2WT3*sZX=tBMN$htNB66r;uwQXbT!MzRs7HuZoc_W~|E=fh{|F4&DMM}WyA>`qBV zlI9_7;w-fFibQ5OkAuQQN17$K5>XL-aLUP#MoYzF$;Nr;8D~qAD>>tl=SrvddDEgY ze{?$gQ|ucj8v0EF)1Tb)JJ5r)d=_A@btZKu*N5QLyFlp8GHW_l$wo5dbTXZn%qbIL3{G!vr&9WKAx1A(&Rs%u)d=P!5+z? zGs}q%ef1|TX=P-lT9Qsk4@w`KETr7LsX?kQsi`wVHPcx546vem%sqPg-*NH!AM@tr z{i(gmN^)1nfV1ld#XW~I4CwYtJoifzqh?=3W65^$@nO5EU!Gn%o+?ueuy zU51{Vk|sHI)#8aAzfgJo9ad+DiJ8XDn4Q&?E}A$~Ejuw{Mp)48Ja_uKy%y_#=};tR z;&af5;Y+fF!QA~QVgFyn%u42H72tXAb2!=mhUDkj-B3Dl6mL$h=RD9gank<|cFkOd zcp5A=4}OJgqXFpcR4qa$UxM~7ADZ~t3m;AFn04$;bCchQPnJ&nUD<=ats98qsS_|{ z#u6bl(vWIi1|sVtclSdzs6|T+^8MZmuLb&)S3eHNf{A?gcA*#F(~x93kWROBAd$Zd zK7{t6Qs!9~PL0RW;$ZPN_Eq2(QHK$8;>6NzF5>QiHlB&)ib_qz;;UO9;ZLrqMCQ;R z(L%ZQ?+CC2ERJ%G$6KEU7ZYNFT8!;oKc4rR|KiU+~movM3; z#C};t*H#>d&#%{5`9hth$yvgDnG_j5Riugbs+d*y8g2ZHuAHa^tgXP@3QLM_DZy!_ zN=#5OBZaHA><@YbKRbO2U?0Hm>`uIhh{G>i{@hk-(C?jFk=X~NJ>e^w2XDr^Z=qy= zPjb`y@&H$WiroKqo6BSd4}7F=j_D1|I!qj^+4uV zZi|I>%vLKLNIBh#MO36IlH(-goMDcTt@>10cNI5oOn_XaKIv>ZjU5l2&|?qx0Vj*r&oCtH6KCh6(5&CWe~)sJK0BDQ8q|0O)Xd%^=8$@; z(L&CtO}#LHskEdRlY;0(yO`h)cC=MUR+v;$qwc;pw*VjS1v~d-=$V} zSd+&zJYW!)`i~e>rLJ61W>+ms$_+~DP5}e zryscogb_QUiatna->_foD0ZgkIqXTj_YlX}|ASt+Crz8A&CJjUo&$g#bNXbqHVzjqyU^sBiWL7S0?VTA zp=N~w$sC>y17;_s{!*g0!vpcu>ow|q)yT;>fxVa8g|Cq{tykF%R<&B%g>tsUh!Kl5NVCa@=JeYu_JqjN4`xB9UmPt$&nwc<-mMsfGMuE$o1TG$_fz@%Uj%73@4~n)YSc4jY;o}5D1?pHrr(;XMcGnE(Y^T} zo*!`KJP7}H42)><6*H>puSiLzR`e*Fg6(`ec0q8#867>qqu^njc1wXf=9WE=#UqagttI7m<5Wp4s9FVmkXQ=32h(VAH`&`1h~J z0xfl1+{R39x%aSL)(>$`I^-wQfJi+Xq|9@mP3*1HZK#(N!-^WbK>`heyn&7W|Qo4LsUWf&2Z{$+&-% zD7!vbTq(YTZikr#yU12Fe7nS1XiG}^H&4_i@cd(d9kuekT6McI-j&9oDO8tEP8ooE zekr&;P>VjSG=tH9;gD+MPUz=Ow6s3N2D z9UZ6Df~41$;$iV4TyvUQ{5vWS`?#~XYkG*dr+5{5bBm$hx&;5mTSMj6Rjlbh6jwrm z;P8(#kQf1!uGrNXONNStxd-_zA7RY_~#4un`) z(&`p<`tCaeqXTD2cBK^xQ_E^ZM%apX=e0z@yDB`Hr74EYP{#L{A7QzCub6xGkMP>l zRm3FT6{8w`;98R>xjxZR6!JV}lJsGrWOE#5S{9P~2M*$O=0Pl7*&eIW>#}c>s{fevxq-Csx)kTcbrrifI;Q@WZmFEwG|KX&)O^S{2EV+dRC5nv#{a; z!;B~`_AzP~%M{I<_6|iiq(~!5hEyiKgyk0*>a$3jjP})G{o^+XpD9hAXPd+dcJW#b zeImXLc_Z}7CA4H?cYHokB5tk@Vv$y`{OOxy*3nl%JG`2!`R@xb&ICSLp{_USK|1a6k2+pdcAXz5Jh zSfoWhsnX;edah`EqYBNCXp)c49vH0>l5#IyQLP$*u}j>Fa#!pZVQ(^#dNe}vd)9g3 zG3B?opWJ{JGkYq3w^Qt8_R-!_bK2i*hZN2`96#?w;Tk+2?y(bUb2Z4cs}0$For%^O zbsD_UgN{1v#gk=bq{FW-o@w_u~DPu~;E2y)8u(ZbMy^s6#xt-PM@_KCst!d%HK=BwBA;#kVpDbw!gB9G zTEUxAos%KA{~8SHc;@54{4DQNP;q3x2>WZ@y_R7M_pc?(ibTEHI;>+yk4w0g7-$=V z=g$<$>4YUUT;G zJOD}P#R=v!)EA(6r!V#1l+9iJIJn+t_Zp>QV9F#6&3b?X?CCkCoP^#<`EZTbqdbKY zyjb`i%O;r7;@tCa;LMl$CMELdSAbNVHolAN)7g3N#Ke6^prfeI`BNt}z1xZkZv`qp zRVp@(K8!Mt@8VBRanOu`(Xe^EPpp^HDSW`6^{>;FV6lIr*z|1^>~03*2*O@diEe%^v-`EWTqHVZRIwJqO3GLEj4NOjYy&L zZ-Dq!8_Vb0e&~GqS@?Kt#x@@nY%6UR^20MQv+q#UTeS*ZKN~!9Ga=o8j~JQS1HYMX z^7ElIeLEkD<`m9vP5rMsMt3C8+0ndhtGKN5V1$Xtdr(-H}Hu8q^e&*V0Xcrk%-%D0h-Dd-= zK5H|dAb|AR-@%=IQNiQgS-sbe!OYW~xS6@#i^}jS*NE9c&h&cX2&^{hk7HiDvBbp! zPfpK8r2HYgmrTRYGwkhnITz>3okQYj$j#t z@Sc5J{AWz}`vA9L=@|F=3uLsp`=hl9!xJAP(BcQaZ`GpDZR|>ZdR7>JGp1bbI~`=k z%u-2L8k#LdZx-|xJHy?1rgellxM>*G#g;Tn(xGf!$j^Isx^(3dmZj~%{1=aKH(?HQ z3WMO~{s0eNCPH@IG;FB(ib}Qr;Gf(BCKoQimuIjo(_%3rsR$t_>yg-zfLTdLpnU2E zs;B&i<4DB$684hMPDZl)Y&>G$n9w|hFCStdpYK9<{5B)+oC_(R;;d-z9vE)wOwBPp z=zI5Nm>=&)TbsOS+DdEoer&_!bMoYrzf@d&xF4J6e@A=yZ;8*!Baq?_@ynA>#LgN2 ziEqcb-#r(!>i#_uJzy*DpA4d!*mSX$nHc?&1L!vQ-@pGEj(2MvaF;W3i-%g^MIUu6 zA7nved{&}-v;nf&mDW@xM_mpVb1&7JGF=s@X74HJ7#k7Y{D$%wW%$!2mwS_j6gas` zA{+czWIkmlOaGB#%uzX*+%}{DlifuFYTW5cLX~81mKo`OcA?8zal$=9i<)=#pbsw= zGVhcB9b*@!`YIkScVpkiH^?d;5K(f{^kl*p ztb9}-qn$XD1GtPz` zE-4mOhP#<#bsniVRK&!&$#@%b8qS0GO!E0GOg^7R_JyrTtV|bYxl0-xHVfx%I4b$) z2DZ=Igr^>V#LlbCX3KRHVVRy7XK&0spnH-&=jWrkzz#q6-4BXbuZ%s3-rT`$6^l88 zIp=98y9|9$yHuAn`Ygrsx9wtFt~R|+*1*akLnZyrbfwF7*+|Bin5=sVpQHKO!T{iBQ? z5ULJp6kKe9hq|kUYOyYPup?#oI|YPrrgw{%1|{0QgL1SB*)>bk*nZ!+d*6+!*%=*s zsskHK9mr(j4Iro$wb`0fx8y3;WpIZ+l^rjfeH>EJj37;Y+8b+xthgXLbNjJ~{i((I z0Z>h;9UeGH;Y)ZZB|bPSB+}~iasETRJ>rB9GREwbeuHrn`=arQDs4IZ6vvYvi*wo& zFyi!0{JDEWd`w&lDfZ;-oVc8sIX0Nka1S2t4@A%zMeagM(fawV*e^A>Xys%XatQ81 zzgui24=#Q~e$_KHZPi|bGrH6HHe-s(=FYdX z2N~$P(m{TP2i_;zscOQVB!AARR3Z5K4xDL_<$l){9FgCT`yFx={j&;z9^0@e^#c-P ztDv$(k(_t$!6*5*FpO5A(TnF{GM~|V+bhr+E$%OQ+fm2hoS(C=W_Q@#Leb2Q>ywvq5S02_lmv5LXz>{i{~4DV{f_@8LW#GABHH96+b)1F1yYwjcznrs|hM6 znU}-toNcx*kR1CE**w#pCd2;4wJBKoNS@;BqD4sBaa``xiUY$WVq4ZO{4vs^TAuS4 zNlO#G)aN{e2N`*)kwS(gwH~q{ORay9O4Fdw&(?I-hxf&aMEjX(*~VPu-RlO^xmh;k z=$;0n_F!_ndK!O@>}F0Y`wNC;F?;7QrmOFT?B3IOwt5W?d$H@>n)iiEE+RbaAIe@S zP~`6%^y8WC#ka=HJIaRo0Tq&+Cr^tvhKY5>7f^Sl1?A;w5}&cBp}}YOwRShe&Y0u) z`*n}xo7EFBeP}Ea%C1QQ4DX8?#Q2ZF!0y%PGx;F0#@8dTPzC+Dzp8qr z5sJ%t!)uQg-TnQJnQz?0O}D2rrzi1jR+qDLe1F$rhi#!1rRdpEWAYX#?$@9XGrg#5 zY$xuN>a%018(nUdAwzc~GUHC)tCjU=$nQ$2Z@Q80P%UJS9*&R0|ATq43LMk6BI40h zgq?JRs+9z`cYL89&hF*~5()|Hg2=oUeC*wi{k>nshU6P~@S5m*i5Jp_Z^fASf9T0~ zcp24fNUf8mO@7(P;QUvQc%Dm~yA6{eRMnAdV@3viChqoQ znb^%80^j=WxTbswrs*am&$+q?`+H~_ZcR(q@8`SqLF`XszD2@Pn0GbB^+EU1hrLky zE(gGGTr;e7M&rL9J+ON18ASM9Md$u;@Seh5ZHF54m+6mN+xEcxc`45!y`eYA6XEYY z>5cw=W-%{BDDO*l?qK(EhbgAN@+8CFxp>8X_(AF2sNl{RJT_;aq?ITAJ-iJmRuU>_ z2K^z+VcaP@gY}1{sK(@iWHt9b6Wm)6(3lsrzU~5cDR-fR!G}dwfxV#C>)71cgND3F z7Z#aMF#n!Ab@bg|s5$61&YoxX#nS(|Bm(ildKPrXsMC?>s%XvXiM1Q_>14=obRRMj zONQyt7k^pyDBeP|xi;NPXoS_&M{pcvMru(~G;Ygz=7--9lR6D(o~y1Hm27|#=uyz6 zl%Nj2Pna@w6q%B?5uoXy|gzJ62@;zN<;OGI3-8BJC85NpD3 z@y}Di`pN;4!(VD)Te$Soe|P{8I!N= zMw}Q}D_R!o(y={;=)KU7rrrIEy3=te(GBL$N(-VN?uA!bFZ#Mtj>dgnh?K5BCE*qs zyjOVzo6p&zbml{GOY;+YACeQsbI(ew44)(5#VE0bUFAD7CW(TRgC#x563t!nM91yR zLj9LHvS*)>gk0v%=L89vMx?{2rT`aJ*tM5Y$Xvgp=<}){sh6L|c=o#89HL2)^VE69 zSA&$n7No`e!@U!vX^p7{X~$^de&ty-^s%JGi%pQd@BnkUHyB_010Rm$Ae{4uX^B4?rx1{ip`0~tqDF!=3g&O6D{ zTng~4kjRBEkE%5t^%YvWH7JI!g>>5rTfV>isdXXtiAhNdzX zd|Oow!u{l_m!~?#h1X)B<9mF4&z?N955lxPK;p6Kni!>!CZy|P#s1tlvCrY0SgSZO zsI*EKmmhmlT>Tx~`ofulFAm&&e$KotdCt6aBbOA;`0if`JJb1mc5A`YpAtMRipK7L z%=n8{LZ!VwOdH-J@1+;Ec=V*h+{aYepu%}?UrJQr_v5jy)T-Wx_C{)v)K5jaF`e_z zL9Zk?uf>Qp%QCTIaYtd1$5mzw&crXZz=BG(tzvWSV`06siJ!#|?8k~12AnIt&e`U1 z_FeENyd9^cJ;?ZfJ$1Dm$Wzha-jF9vjN6AbMrO3?peJn&_z%}NvKM%lI`yb03TX_a zZk$btF&;nzGySMM!${p4(B! zh?Kw!&zhl-a026x{)Vi56(*+~z_l>;Jr=*mL7!8QeZj6c=PwAj{0r~Sr=WJvXMAsv zrt_TBEOVD9&1X{NRGAKyn~vmre6d8UG6%NR-6^zdv*fbVEmSTrrvvCGcJw)qHN`v= zljis8UllTXydC%E_oiXY8FlhKfaKBunq(|RZ$p+KrJ@iS>r5zjaSzPkEbxKF+$-{5 zz&Qcl;f&QJ?~){Z?2#p6pSsdsaTepOip7*FJ9>D3CzeP}7xK3PsCuD2olDV#;U#8^ z*gDa`zE?!nDpg9zG9nXsHT3`QCp+Hn<8Ey!e7QGMozTD@j4b3jdyS|yLb*Ny%T}%aX6y&23sP`nMr6!4Sy`D_iA&xm}$kc zI_6ULG9?=?Tl%&|pLQkL(dyizu=4dK|B~*s$^1AL7fVQyowSqp6vFur--(%1?x+yO z&Y1^Dj6aTkb9TY|(FG*gALZF;I6nG5MW4-P6z`aitcMLyn`B2+xkjaA3h3KSN?9dc13PL7RKe?7SlVVMda+wC>blo z|F7r7+fC~+HR6IOa2qRmkdX#IEjMI62*EiiLF?p3U_pTudz`$;f7~F1{20o9?@;<| zs6t^bMUvUW)nL0{g$~$0kc_iGEF3=R(aQH9f}+p3qjgy<{Fe&2=C+9|JPWE^|5ME9 zUMtp5XFuYFao9Xi3QZGLVfEFDu7}9btrQ=g=X52zmz*K^(G6eA+~@%Fkk{H9A-uW? zt4ChIj6y=surIiMHWhp7yW{4$7P!hk!0$s%xU=H5c-LY_`hj2Y*yEb0zN<&Co7-^J z+!0gQnfq+{3{2qMLeB~>+N!+~(FbJMM-tM@rfJ0G-;VH1D4cy`0Ki z{UNrrug#rovM%DdoE?3L;`6Pa4|l!1;ooHwhTRRvoQ?gk@XP?-A*rJH$t?8o+6+0X zN9=j$oHaW!vYBbTrcgqG`Xf+!;UiL+{~vSO3e#*>VczF<9Nhf}>pYI2ZD}WCAOGTc z)>_E-=bj?p1@6r>rc^H(n$o#aq*vS1-$~4!oYh_IGuI>c7M|%3yc5)1;zm78ld-rx z6(f1p{oN=XIn3PLf5DL~@26qk!6@i32Z<84BT7pe@kd*+@b_kBSli-m_&qH5;`~^J z4Gi;hxo=pEfMe{`obeDxA3eZh`2+k+$VBe@dzkWl12SI^gJUCi!^ba&?_Ym*NcdBk z)fUX)_wO6tg}42T#s?)g8lK8b>N$fjKvTl^BNv)yF#_{UT-bZ#N9Ok;VYeX*5dq)P zI>b@Hsu*1^bfGno$3@AYLRe0Ji;iNoAg}&jB5Y6^rgC<0`|nB-JaG>@(){S&rOhJZ z`xSic?@5FDDbXbMwM5S8iR34Sl&aGmrPF+2lftg)d+M;R_Q9q`6W+ylA={*zm=WSg z_096MXX$xNy>3GHIzGc~eHGkpXNs&E=IGz^qXUanaIM{%Ecs{;o?bR8F&{yh;AlJm?a4oL%Ws(-ARtN)`74J2>OHT#O7Y!{TX6 z#K+?ElEjp9JSsIoA~m6-VV5NEs1)6CX4gkqFX65D8e`=Dpv~N**denMZ_EtXLFhzp zPPXBQr3H=i@uu|muVFe!pYo;5DSrMN#7(*+l1?UbUpEJ}&!mO5*acIaOnCOXA&v*+ zBBbvb7)wR)=b}uUzxV)NZOgDfTn+kTt{`jdV9tD3iNdPa{CDIzIRD)a@ec4ux*x4l zP{yCK{&3h5L|#9qU>q#5T{}|9ZDIyTurFdg+QqxcHgq0+pmZ}sc-+#UU7@ZBi6HvY z%2|VoBXHOjLarK`r0%&B7&DMO_1od$mID=5d4{*dN@PaW;KG#{F=}jr$X?%q?gkUZ z@4AknN3WW&_t-4S&<(9(jq|FYmDbgT%|%K$lNcqkZmbf|eNE6|6Ij@JLeUjKEW66LA15i;rgFWI3HP!vk&}ec!oN& zcN1ZgNi^YPGh&}EgYNe}RGux1E#n@GVW*hebcB5yMjjY5L4{Ud^TqSqrJ_faF6G&& zP>6Iha|T@~F-MC+F36BQlrjxt*egU&rzUX&QITOU%FAg?63!g0)>QOZ?yHQOg#2(s`GPDOJH#(v&2knWOC_ z)syN+HVCaLad6KKpoY$q;#FrT9(#VrlyVbFTQdwZ_~$qTJ5l5hUCiZM^AzF?Fchy z$LMcG=pA(&!#U?3I_x2CXl=&soLcVVzQovDvdla@0`pCOaF1P|NveGAh^b>|pfatA z;X8d#O?vj+g|2@%gC}cM>4c3vDXCRpK(;bD4Gp00C(gic*JjSb`jY2pJ$f?s5IlW6 z=!bzSg?-wI$wvHK>19II-{N3(;|88&FpqXa3>@B7pmBvdU2$2B{XXnbd;14dmK3u; zvq|EykXhMT+*|QV6F$qG>5($;h2(Wa@mC4|J=ldNcTcG0JY0^Q|q zeqe2?*qEz9(cvHQR{jlV6F~O{e?o=kXZ*)b*2^k&(AsvBXFj08*Y}H0d-ia)unW2S zWQbGFN7;$;5$X$~CBwp#u*pM_uCL_)%7d=d?CwZz_0F`Tw>h2uXu)$hJ(_vcly-BM z$#jGR4L3iC-?6Ph$=uCr*t-+P3eA!oPaK)sd>Q$+*9+^-ZozTQ6dYW09_@7}VA~Xi zRqP`U-*g{_Jg2_%Y(G*;bV%AV3x9&XLGG9w4Xru^+it(`d^qn!t{j0`qAdL~`3mcj zks`~W7_YsUWxih`Hl^ml$D$p62P=er!x^k=*ckM1xh^_gHe!I1X7RBJkAS!uum_L(%CyuCi_FumhXjJ zDMQNZ_P)^K!gL7!J=;fq3p6xs7gi_xv0LziSWu|Od7TKnY)lvSFN|>5yC3`pC}M?9 znmBYz4}VVi(D?B0?4}LyE$!xXD5o5eLng9kS(g^|;XZrmC|D^u)6I@?un#h# z`6a&O{WuyY##mFUGjm*ya2L*6kD{jaBuBn?1>M_*r^_qN zk!H?2h%uiE@%Y>g_?HbsySfYle^eklGZe=@6BN_9x3F>(R{Zx%BwkC0-P>HuaqW(_ zsYTGpEkUluK)ej)`PPnGn0-TnTTfmJ`>}51_-Y@9J(Y;h+jtg~n1;E%6mVsQ56$In zb>&4re%}kKD?ALJ0=`Qa1k%b!N1^>OhPsC^c9IV{nN+q(I+{G_N>L^#f%Uu*(b&f-yckC6b6exH= zf7m=(ihpu?w5NU&vVu0^a<~df^J2hs#TabQ(xCvUR_u6t8*?~2=xhHM2l`z`UU66Q z;l9VC#}D9pBS)O#UEX8PANyYYC(`9DXsvm4;Z@%g;^iV2(i)m5Vy6bsmgP;7A%#A) z?ssqID<+GHHthCP_M$l-r-&b)oM^+~e9=DtJ}lNoiIuB&h^6hfvA@AoGRGlL4C4H% zpKg{|zm8}B>D`5QHx+un;unhZi$z%%9h(39JI4L&E6Vvi9Hps9G5npd(m|Tuw5rmQ zCPnI>{{@xI4tjJ~o3<6GvF9jKn9a?Dtpc+EANWhue;?!S(`l%6Rtx#>1t^R-flJ%w z;~wt=58f}p)$CcYmG^}D`==OL6^5FD9(Zb#g%r8#60fMW5DSZilhF{7F={^?lR2IS^N<_sh?4dP}tWU%X9`&zhNIm_GlZtvE@uh^illXR~59mon6$4 zDQL}fFP8nu?Am2JQ2%F!P&l8%Ge*wKc(U*Op*zoIUZH0lyQ(yT*u5!5(+_o}`MLqf zm~#rzE9~gF-g~$$u3(Nd&-zwy?l>k5r|%e1gE8;pe7@rS{Y^;L?M+FQAMt|ElzT3Z zpa~fY)Y-|qpG|#8I(jsEn%s_p z=xoWs*;;ha;R{R#pOGx?ZcQIHF=Kl8gCgP?PxORNERbbCR#GLphu8D+SC&FHy~M>* zdGa`_L1*JXiP%083K)4ud^>Yj$oCB)&sEVPX=|Mr^C_4eRr>6k^rTRO59}WbLHzJQ zD$DxEOwKvz^3RD-_!sH#?UA@Q9NUlF$0}(X^gg@-i;wc2{?J#EX=Z{;x;Id7X$6z< z(dkDQ>8q<>|dD``i#H4C2L#c7=D-`Y(__L5{mUN!XM52rs5u()(57cfPPlsT4u*)Xq|49Td$b>;@|fF_6^;KMFA^#9 z>}X9z8U}4I5L0Tb=<=rod>^kT{+H+J7+_9=cI)7ovNSD|v7#(%O%!>&!aH?M>U=7L zniEpw-EtCf4tEey*PVEQ9^2tI}UY4PL?FC}v z!!=0sd51sG+a#0cu0@S%7aIE7f^tvkkaeRW-Rsn$b9si8Ue2?YW;=RT$n3eZ=9Hc3 zNH%+q;^KTC8p+O6`$2rZahK3E?u

Qiyzhb}9_aLB#D?1SVHvMD7_Vb?!oJYaWh` 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

Hg>!N%nHiHD{I8hu zNSjW7F-@E+9}?wtvuNq+-$XI*gnx5GEQa*@8r z|09!_GoofDEKfYg_#s(x9@iB%M^q>+jCY>-dQ`>lnTEW(IKhm)@#^%L z^I@L14TWxBDHgvj#!kEQBH{5BSpNJc!p9n2%7)IOr}ZK#jpMlo)I#p~IqXv1jI+C$ zMU-2McG->4jMQi6#;-_?V<*Y8b;v(DlNp(eUMNq%^adk#=?Y2@bwx_5JEB(fizDBv z&;ic?R4tT8)_tydYopLV+$)YUGiP#xCH$HlX|&@#q|e9m?V+G{_UC4?`?)^lGwO1j zOotYQ!t=efP#8)Q{uTGJVZEIwJ@(wiIHa9ty2)bjX`{&8&i&|9Z&ADEl~|v>PMoMX zCwfy&aQR8R#2|lzq&!v|^P`GH&i+E42XOYn_6!QF3)s^+pCTg4u>aOc1ev(gXDvC* zT<^sNe>G~aQ^s3ns~_b)z`1x5%1haWp*5Y#9F;KMS%9m|FtPZC-LnBaySTv~sVQx6 zX8xa1z7h2#4PnQlQOrwQ2G$YKS@zXd1gwXDiWl9}P$0Ele-xR0M*HebfKnX{=Dx$d zxf(Fk^~0U932f7K!?({AU=^l7)vkKf?9&D}6$!}>8dB+-A!cmb(X!QwWU{XjzB3ol F{{X1TbpikY literal 0 HcmV?d00001 diff --git a/source/tests/pt/water_tensor/dipole/global_system/set.000/dipole.npy b/source/tests/pt/water_tensor/dipole/global_system/set.000/dipole.npy new file mode 100644 index 0000000000000000000000000000000000000000..c16efad029725ca9cffa1de92abc0fe0ded3e7d8 GIT binary patch literal 1088 zcmbV|>oc4O9EDd59a|(S8_`HyDmJc7!lGi|-$^v?8+Y5V=(1}Nccmm%w{9wuMcg_G zi%5*=BpH{gMr-%&)Q2>aQbNMCok_~bAZj`&B7<)Kg`RKcne*j2_2;5Qq33lt1)NKK znLIBkm+xi8_fB@>J6Z9QvvNPlm1ZVn<;vv$abIbAo?OfGQl!~(Z9nFD+{wz--pQ)i z>i>poA}*tET5BxyR4Ef$~djYjHjBU`t{!&F2X z4lQ)6AL_YMoJtO7#W)>TRHzf}9N^j$LXxKg>ObD&V_$R}^~Lwnox@d_dS*cFLjxqa z?TV+88KzNbf$Z*b1UCfJtU)8BPr_ib{~;w-a&eUFg0+c@Y}x7qy58>$)0dB#-zMd7 z@8V*mwUtC|N6?j@j`EJJA$Rk9Yzb`f`vD8h;}T;m-}gmpk&<;336O7+4UZfFo5?wa zoe}~wEMpwCjw@QnY^`Hz5C=j*1tN8_ZNaXo((p1d{Y=erFwv0et(fk}Eb9Rl~* zJ!secg>DI>=xu)~j)YFq&3qC3Gh)F@y1^!R+u@(NBJ9e8$h6T6f1gnxdDMtqUR6=} z;uRRWyhC+eY1sRu2{C?!Z1Q{@Y|OS9%+^u0%~|-|6=Rg$(L4|SRsC6-14HaB+cO0iU7ogoB)IQvX$o`BX7E zPY1#BL=M88Z+PFd_Q1q!IgGCs(@1R$9KR1ipuCoSrSygF?hcjqj#HscI_fv{C}6IM zjA?-KQ~Y45|Awi`+>b1MFU*?;Y5d>UgR^xaLWW*yzCE3XwNO1g(r&|;4)W`}hTBJ* zX(cKG!kjoX9cW-TuU%HZ7<9zS3_~r8e#p3Q4PKb7y0qjR>UWh$@yn+e=}8ziE3q~9 zh*69rK{Camw%i69x@Cm$S_AxWZx1@BC8+NeA$z-%GCX9Ep0b6-a6An@@J2Nw!&e_# zvUQpi>u`gB4TxMxk^ltOAD06=>_kS zC1d(^DP9-6rYk0HX86!SyMwW@IET+Aui?!ySaH0>Vghb*(_*^k_6h{x{y5ooT)E R;(c#sgH8rUqPTZO_%E95vK#;a literal 0 HcmV?d00001 diff --git a/source/tests/pt/water_tensor/dipole/global_system/type.raw b/source/tests/pt/water_tensor/dipole/global_system/type.raw new file mode 100644 index 0000000000..6c71c85e58 --- /dev/null +++ b/source/tests/pt/water_tensor/dipole/global_system/type.raw @@ -0,0 +1 @@ +0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 diff --git a/source/tests/pt/water_tensor/dipole/global_system/type_map.raw b/source/tests/pt/water_tensor/dipole/global_system/type_map.raw new file mode 100644 index 0000000000..e900768b1d --- /dev/null +++ b/source/tests/pt/water_tensor/dipole/global_system/type_map.raw @@ -0,0 +1,2 @@ +O +H diff --git a/source/tests/pt/water_tensor/polar/atomic_system/set.000/atomic_polarizability.npy b/source/tests/pt/water_tensor/polar/atomic_system/set.000/atomic_polarizability.npy new file mode 100644 index 0000000000000000000000000000000000000000..2aa2cdd4f24b43064889b2de8c0860da82fd3a19 GIT binary patch literal 829568 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

)q{C_R-gUTfz5wn0Klh@A9*L=2P&a`S6DF zsFyE?p5FY2b<6s?=J$^M%cno|Ir2PP*B_+EyHFHY`jKZ^k1oB(`66B<-#K*OsQSoX z#qaQj;~nwviTf1Y7i)fzzJYvkUU`)34j0K!4n)0QII-(|lKnYgT?+FB^YMqOhxg;% zm7n7S;>Z&Bt;w9adHgtM%;)oS`|%w+*89TwT1P*+>+2(3(do!j#N+4N zFX5#DIQ{L&FI7(;=Fc0R8yI=C`%`o;^WhEktMx-iZeCP8_7J-3W(^+&-+jDzUYl@o zKAz3K2hwHF!c!kXx3*Hm?>1}s1iWM4Z0o+LH|Kko?Z@DLWPQJ3ePK}_STFp*J-=Ob z%mw=F^W87;Y>nD;Y}D_2!FYoAs!@yYzGr<$ykqLy$+us^54%2rs-rBz<1V5rZ~MKb zzjDR>M$ymr{3H9bpFs75PWY{P{+HhCTj1TN#=Y=u_U-v7aPhPMtIz*C#fw%gd^;a` z^4ouKJ7TO*U+%j<7Jh0J{$r7SYt_$xCC`2W^)n1yPl9Vtak%1 zo(3o0fyd7m52dT_+qaB7Wd197qD?>cXzZiUrTD{q{s6{zdYAU14?Rp>`BKy!)E9A^ z`pQ`F^qSk8Z@+{qnl*WF`05M)zDr%t^E%%3IptmUobY_Sjd--DK8NUR9t7X3_sV^v z*MeU}Pl~QB-+n3YeOk!}%5$)8xu3H9fgPf-O&#fVd=Bn}?oIDlKD?nm!aQ;aJ%R0jkykOypfg%5MDPAFd6SL`7?bQozR0>N94KS zuAe>o+j{=r>3%igbBDpPpI6d9oj@No02fbze;*kKqo2>0r?_8_IU?{-=PLXt@EeM` zoSe@C!FwyN@4&)UVtzPKTdA2roE6zS4=hkn)q4olmEohp0Mn4_)4z z?^WPG`GIvpPij9R)dS}%`7pKW=N|!|{REyjZ=Iy`F!@k^M%>?>^GF>!>MnYh^W87y zyX~lJsh)Q#`a`vjMlD=&8r*n>cltFQ87xjb92X8HXSEF_xhx*f( z<2+{7&#S)BsHI7w6Sav{gCZg2y#q4S(Hn9D#qk;rqwDn|yKJeniR_=d3%d ztSb_4Zyov!`PQ=gbGUc;;)de3=Sr_pM|iy`uX+%ELyDKnywhiy8#6=SdA@U~dR>=3 zqFce+pufQ{&;3MGk$1YkRQ>?@^c&JWwdf~*EqGp`<7wWrMlBq<>3IR@UmtQW^X-@C zq$*yFkyoB9*7J%d?T4SfbW-Qz{Z>6X-}_}BPQ71?_-bqx=0>r;LGah^*P{-|T-<#7 zC0$gfvTvw;zwM*1{^ce@?}JKpyEWqbrr#;}!)8srCHk?^Ym+bM zi}TifdA_%OUWKD>R?_Y5`YlWEaj&4eEb1H0_b!V+><1s^JYV~<3~w{wyj|xV*c*C! z`N-t!3y==B%e{HcpVvAfbg)m&gSt=AI#}k7=EJ?ZH}2!OQye$+-rg&kYtf;8^bj4} z+pPO@>Js_ZCH}f;UWU&*$^AJ=9j_1meb0I~&uc?JkZ)Z|CmXfr+_&UTMB{$$z(5tB$-_(9IX|9`~u^^@FbwE;$hE%j)N#&uL$|8|ac>4PH$= zX*YaD>G|-6d1%#rri16TpRdko z3%+W(pnu@&y^_^OTA#B)pT;yi+Pm<*SJ}TtZMU!1 z?%_}o?#*|X^`>;cIqrR~F)En%-F+<&Qi2ks7>gf6QOYd<9{mi%FmyX4} zEXB)qtcU(>;^RnK-*5Fvd9Lk!^q_Bf4lKT}ty=yS*NXC*8{})}$=6!-v#h`{xc$2|X&_v|N7Le=#Aa(wddIUg(T+qRFObk2)0 zKR|IMUp$n){|on>1TGX#U!(7^@Az+?-a0Azg7fM0^!snaZ=PWu(JZ{p5c{`_YmGPb zE*}kC{Ol=s{@)32w3yp|5+9Tkq0hFyPW5!0!j$@4(;7hc~1XvLB^& zdh6~-1Sfia>>>1Q#_?C+8}sEUnn%>C)lYvHoH36d>LT@^LBxIe2;L3eM)m1doh1(X zb4YKq#Xh{n*O#fEykDdv>hKP)hCWvB*E;LZcfa&Lb%|@2A}+YjD&GqGbS|NLSS!-O z-6dbk7Y{X$!@TDa???2POINfN{o3O5$1BGd|6aWRS-z_7-Kbq(^}KB9R}#KKiTR_FF~k{REwB zH+XsJEvI?EZlZI}w=SK(o^h`A<>A92eX{)nUWd;abKMgDAYVLG-MkfZ9)+iti3@Ad zwH+rhyAK` zc=#ZU@a{Hh`4=x2{9%~a={|yd`=xit^F}9QJ_eiFR(7UwnJx;)QU z_+rdFn)ih{)6H7zS@-%v&!+v$w=VOd;D@66<&60m;&_`n^K!&Vy#u$=-Q?pB~y?O!h?=0@1sY#Uc?h$1fS&7&&z+K z$37gO9=k!D-;2Jt&G(1!wYNQxGhuz<72liaSGLcz=g`qt8ud)oYxBKds+TmQPu~4(%&)}1x9Pl-bWOUMF9i*ZbOmkG8+k1>)ge);EC1IB%Ypclo`NRi~%^$}aq%=Qoaq-d8$a z_q7d!ua1D@KcTNJAC6L=O_Tc9em?hd)M2d)(|voXmwL?E^FHJ}=&} zqt!Qe1RLQWRnRy4=kTQY@S*)}?3=3ZZ&i*Xx<7sH*%|vWN8Lzs?DD-|;x`7|mk)>s zE7W7J!2h<%`)?QZV@?3m0I^UnKsBH{~>-nYv`pK zHThSixy9cr@NoEZCsO=17Ed4Jk@wO{I9)%(_NQUU83)KzJZ^>AJkLwy~~R8yK_?ErybAfcjy?;d#*P8 z$7`I&eD_PdyXz@aQBNV?=N;}=#)aaCCnK-6KWP?T!=FPwWR2Q5*L`cozvAER53pM3 zzv28Xb0727^|Y>j=!fK6af>-c1z$Pyxzp^Q@$n&e;e7be`Y87=pmT_IYG0eR{JmF+ z|1X8_gZ4Myeo4n_{&0zTFpu$xUPgaoo%w$1uaB)yA-~SIF6qm=(YNeAFWs-9O8v5) z7f}2@!>_H8Pv^@k#iRACBR9TfZpsSlweP_UIO-Pjmgew*$#=i>zBX&|G%H2>^dda( zD*kMq8+eg?@?~)FzM}q=eCJSod4tORLgopD8#n2P?N|7kzbGJ z|JCzuJg-f4p;h)_9Q|^Kym$#8y7ic^qWPDt`uVTq*-xN%w+D~=i2m0Vbb1@q#|PwL zvz2@+?uQ?kaAZClrFUshExq{$^RKQtzl8s7q8FH;KJk9|>&d?&AD*{AfOz*+;>Jnt z(JbrqJhxTi{9*pyQSe5-{ZhTyI`c_*!%6bxCGh$H{`WF|O!oPm#SbT6f2HDC7k;A@ z^e540qk3zfzP!8C`wIEW5;~)N`IqXjyZnXf386zoXKp@f+CI0Q9}xbd&qkx?|DAYV z`^t?{Cm$ispCj+z`P((9-Y)pa8ceicIWP(l0Vj5rF!J+;P!cZ z&Ig>!n{f`sbH7>a3;FD=eg5C6udp9FbMpc6QJ!PIdBZr1b>Be`wpOWrmM>2cFK?V= z{oxsO9N)&zVT=6uHs@}det;Et?0oT1`(oYKZP)ubZ_DJ-?&EMj<<%k{e~iDEFCJ=c zh5MJ^CC@(|I#=bHF;9y3cqrx&WHsRZ@Fdrr# z?p6Q)0N;j5c$!6U#0>TRekGi71b%lo@)YHj`Ocy04Cbj%5Fb8?^CtfW&-*>aeL6_q za*MipzIRzT=!x@1`{z>+e}#V6FQRW*b-Weo(9_IO&Bq@~&(Qa~#C@7_Uju%=jhZ@o z@KM&^2(RSB8;WPmTKwoF`SB_0TPIkTbr2tet3IKx=)>rjef|jJ*-s$f>mGAbR_RaK zi2h08`2lmxmy7BQv*_mS1D@|3n#XosRl2-;#Jm0QYki+fbe0Ro``pKTI7<9Qm%9IW z=;Ng;y3U{XyA4AIp>dlchO08oR`6GPgd#=o}}*~ z-+n2tbp2}zz2OJ=Z_Y3ep;^OwSkKJ8dj-Bb-}|M$fIT(+HuPzjpII-`lluPcH-8Tv zc^jQdzBups=IE=VULxK38F=kZ_<}1?Y&z~2)!p;)^VX{guU~+_84BF{&7FrWeCLQ4 z)4{{Zmn7f)Qr_6HZ%x5}jsBEd=mG4HJzJ!2UJTvd-kpCT-@B}Mxa*HiJ(hcQm-=g; z{dqO~RHU;%4*tluF5RORew1s0YkzJh@XuGOv-bG9fUbESeZzXCxl^tBdA?O&_up1s z^^4GT;a|i3+DjeisqY*2>ty8B-tVvb-+rEdrha(q4~NjRjRoJUy1I3IGvJXC*J-(D z`SiWw*;+L^8~jh!i0>ECe|aA1ZR--L170iQee%Ua@lD;z{kz~k*CXMvJJgZqLU$;Bu)ZM4lt@%vwaPdiQBwPW8h{OBLSFHYh|l#f4D+>3dlj)&BfZ_?jo z{roa@S-)SSLhq~5&q*fbVT`Zukb=UczNRu<;&j= z-bVO%6ny^(zITc~?R@*Cc?WIYuQy`9=m!H|c^=2G`;C0R&;{g+hsy8!)J>*>UsFH% zP~dg^n7?;^{?G{C;!Gf#J%1AlONj=2+Z1F3ZDKH{n}5`>Ga&Mr*~`> z{{2GeIK;z6otFCYDZg**Z@zOVe!5w!PWLGCN8#Z)c(o@{uN1Gbf=|jK{+0ROFX6v7 zyv$X6haS4W3x9`>aaGK9H_s*>G2ebk@7k#0qtI{M3tw;bUs@k^5?{bs*I{FB%-3O= z=lN%zljVA`-s97}OVjA-JLD<*$uBSY9+OI8mmu;!f)+BLn72d=BmoK?q8T+OAbouaxbmqIdO7dj; zd-0yx*XJTU+yUz<;idA$dGTy7)cB&JuU-rthjA9V#K2eT-|%_Nr;lBPki z_`^>41vn4Gf0X^q$8QMNwkzSG@yc@$!NK^Zw8Q^PzHK+?E4|MAhkWty*X%TA@A}uM z|KX0OgPx?A@Rr2kZ zeTfwpPVp{X4<0~uO!K1eR`Azf+0T495Nb zp68#*U%7{0(hb*1`1~`K=AI3RkHg@?3D$K6KEG8z&ozAAe_Q8o5WGC`KI-SH19hyc zga=*c9-ap;=kxv6`)Xg*QTof*h!^*%OSY-sjpCoOp{gUU#lb|EKFeSE%dF z7Wu=PKU^VBPQXX4Q-{cR4&?*m`FU4D_eI}LF&^>!4SX%lSMiSJ%TvCh7iAyb4V)_< zrM~-qi+sXIcn@}c&b}_zKF>eX`}B?4{{O;(k79k=w{I{{Uw?0gSy1L<9%h{udIC>dWUy;8~arV7cP_UH){7es88=9b=zrpoqYF8^Q87L z2mChp=~VEDo)4xxZGt&xx0(B}P_p`CTfZUvyHGU8VjdoQ2ENTc)2~K74;>YCm3+LY zVwUG+AEp2BF>zxb@w{1^SF`^J{r2cq@|{D!U*fIz!e0*+*UjapXI#97&fIe@*UucQaV4peRTk|^SoTss$)99-5 z=~BcyHEZ!_o5b}+^8HPC-xq3dM)-5sZ%_9#-@2sV7zB@I-LCh~btLm2j)OY42k~Rh zcMeq#Y1Hyh9wTqQO}%RjTsRK zd2|ocoWp!|2gSp#`KBTqf75s!{4qeMaR*#=jh|29i;z!8qR>$gtgdFGH`_up0>;S0{)yK(-^JDK00kBxmg;W`;Q?R+{t@rPY}hZjQMpgj5> zdWI&xB9q*!p_oG=9wFbmEPsF={Ms?_xmRP zE#JDt*9>@v&Y}DAT-yWGAM8^x?0GooHbz6=@jTDm*ZsGJCz`eP;f#3=_`#3hFXEgQ z?b|!V!BKdCd^k$;Mh7wH1)Ys~ZNB!=ZM;SQ*EIKRKRyZh_<8Yco#;n_zu_G_0$%Lm z6ZHW9(G7g17MK&0?|zAI-&1?8m*T;C=<JpPqG>^f}-kuuflnzH?|ETXp_R%r(0U@AMXWny2n_z{g^<2>)N; z{N_7{(&t5gvg1SeIf%aw-&FblX1IR`(WU2Gmvm0peO%;y&ZkR}e&Y$B?+ojCn|&LhpQG)% zx#vBGj!1DdpN>epVBh#5_R0LO^2xUIY4Wev0(Z$@HQ#ERRg z(Cdo$qIyg|yrFp74IZsvmtuUY+m#Ql8@ZdH2xsmO@`q z5x>}_PxdRTi^)UBk()Z#QS{b&~u8dR~DY1 z2KU_sZ!~J^mvO(WYnBc@-}|NcB0c!`4e;(5zM&6``iXk3MB|3q6<4enal7S6q3^b79~e^S#T`bv0}0LRUinc!h{KNm@KPty`R0oo+Mfabj1TQE0uTRbCH&W_eNO#e zqqD^KF(1F7d(;hGxpkA8w>L~Zr~!X}8ocN^U?b#X`PQYnR>wM<$Q#`^qBmWmGMKOvs~ApVFC;nm&)Pwk8OMbDEf{JQ_P;#oWVz33Z_ zeg^Rxt{+dwyRE-B&-;)s9_l^bBCj8JpOEzq(Z8&D2{++APTP-@c{g77*Zt<6&!366 z3ExKg06dRwh<)pYkG^=CS@8TJe3a`=R`x^S2e7M*BDe8+|10P;4x=(lE<6E`P>uuuW zL)XvYfAi_*rQ6+#dV=u#T*cf}`qnz3heG!i@0{u_`QnEAPsDS~qtknApM&VDQQmcc zbGT`KxY%F&`9!^c{eM;Y#{j>fN%}=+3w3|)S)-P&=xh-WdXJxqOMgmzcwxZ(Yh$nzekSrrl?VE?@*6dtkf^ewwgux#-`| zw=VbTO2_d4|H(P{-wk}0V*VHBZ_YYd{9^OPL-j8-sf$m9ZdZBa2spPL_dRdqKewn*0V8hBp)B8_se<8+xW=M!gspP5I&tn{Pj3Gq8H(# z^6`h}0mMgN4IbP0R{PVc&D-nToeiCo{iXEx^8DXF_7}F5_5g+KQ-}PBC zZq&Q*5qgJwbqCd58|d_Q^{<`_Bi^)8yU&RC+x^Svzw+gk;%V%!am7BBp`&-*Px^o( z=Ht-Of5e=JeETKe+6Fq;^YGNKqxU+G4y+9yyc|FG9Cy`Y^7U6L&*%pqrTa5Q{xwBE zPOnnDc$xm?Io5fQKKpz;fOKDbYVn1$_@>^Yf8*oms}ZkpA%0Fj3;rDWaFqCsMy-0! z3jSqlFx8vrcc%N>DE(IMr@nWC*Z^)bT z@ix+(*vIVcBAvzw^5tXHC3~#*7In<`qF=?n{CN_z-UU}Mi3OKk~)2C-% z!@6_${CxLI`_s412EFbrpEJHT@f|6|$uW4SIrLHa;-T~kIu(qdp@k@dEX~a+#$c3u5|vo(Fbhb9@S}Q;N9}YL&d>HZTuwPz!~FT`j-2l zBNd){i}&~txYs=R1e06Ln)Afa2q8#!>J%J@li)yk8g1!_ilo70>VX-RS)~ z8Ms6BmUiG_`Voy6siWuHuW#;rP|pK62_NG*U@z0})}l`PF#48tF7KzU!@94=_e*}T zcgY_|;d2MzrZ;1LvFd@>`FXzgOM1{oEg!aN^YiGwhO4C?-)6c!PvD5_4#dO5=gv`nW_!QHzj*%gD@E%(<9o*ZsJ8R5z)|=h;5(8p zZfH(%lRD7l3Y|5dcep%0xZTOok9xEHd+n2D-DS4@QvPfH@Fo0uACf;FA%70Oig#-> z-m|QIH?Du#mqI+wQs5-*gY(W;tOIa83_q!Se3bYX>n3N5bjr75E`W5{1MAp|-mTZ! z&v)^Y%7-`fF1s&a#eFC6h8sn^j(u>4!0$J~Sr@9c)$M=s)%Bzwa=+_w`1VJ{y>sxs zUGvxIc^|NE9|j)D7B{3*Xw=ldtXnp|01r26)irOK2PU4JPGGS z#Jr7g?mF*qgZlG2e8*Ypmi|89Idq*B{`xEL?}sr5LB8b=>5tCzt2bIc3KcfX`(>+*SDBkww7-v)3;kA0aB{o#&0VtKLX{N}q~ z;&YquY_G;SbAF{hqDJj`F!)QsZ?BN2<-1?buk`+Y5d5Ly{~>Tw2Y%pG@Y#whM|hv} z=>dcft#f$CJYv*aU6+t9WxbN`*&^{|4&0b8Zdl(WK6@^3pMA}Ylk6+1_;Nbt;m9X8 zU)-?1(0rh9#ANjGx}Q&ew*{PW(eV*~)qJ{2_d(gmTy>*4>-FHLI>sTv+f!%9zc*k0 zW!$TK^J#@X9v=H4{k(1JLZ1hJqx1JUzF7I{=8hZI<+*QJ_4MV?N4Xxse6ULA_Z1em{57<`%lM-_qCVRO*RSxtT_X6csyUO+rs4}N<*e2J)^^M2*y=T&!T*3aIrv-G{r!jHP% zGH>3{{=Mi}^4%}bqc=`h|HCKV7xMi!I+WAoXXE(cj-=HQS&yxFKTO^DPUv2R8(TH{ zyaJvIU4ZhpeD_N_y=E<)>$>9{eDz)Ot8Vb|o?{G;TC~6U)+L;0AL+yJg;Rm+)i=;* z&dm+_+#UoEXMfUc@lbsWJ>M5_;VpD(OYrmNqb7=U&NIaOdFB=6iyQ8vci)qEi*bA% zZV)dTF~?YW^xk5BSn=NZ@=D>Tr@U92<~MksXYfsJ)WW?lMW4CWx4&fd#gdO%tA^i* z{s7^=2jGoXjem;Yub6uy+@Ej1pkjTAEfoU)xGcH5AC^gi&1Y?{4-vk_BO1}FgT-S7+$o&D-SEl%W%Q_qT&GF~gsk`Lk=k2$wymHxka^?{) zkXLR|cX$Jy^drvYQFIac?w8_)bzfJCeKv%XUIY*C;(7A4&`+t~EgugcpUnaB;yvoe zV-Xjm18dgu>s|7G@jg$IXXaa%^4%tSn-9T%??(Ky4o|>i6F&5H&vWCRF=JBL5E=OC;V>adme z?NjuM`TT9!hroSKj$a==bFFb-t2+Vm>{9^qyVwHgUd~4~riAspoOh zU-=IFz%sZuU!BOh66+k$Pnzezf2iv@IQES)e~OMHAKnnIZAX6Wy0Li~#XZN3*Q~EB z`uPw(B+fhYu!wqZ`{cdr)Eh>Kd-sa`+dbFp6n>(2;+_eI3J|S;P`v^&o^rP%fK7w`T6Cd{^xwWjqrE}UDQMJWzPZMpg&*_^S{1N{5uKn`+ame z`Swfwl3ny%oA5Im%%$9jescT3=w0|uG2j9n6 zbO9YszH=yky+Pf4B77=5*HQ2AuFowvpLgyweTn(@%XM+#Sob%aaol6x(g6Rf@!&%h zM-PKj^5H1)Hm#aDSiDDL)F18>{|5Hogtxb!`6xV3zPKSg-;Mge;=_jZHnAV7Lob5o z@547vG6yW*ekq?H5Dzxp=Ub#Ne~6BuQN!m%y$^jU`sRFb*=^32VQu}?-+PL-}|NdS1;x* za}GV%k#%`K^Wn<*vhdXbc<+2Xn|*FwZ*bjQ{DA$;cKyE4yTRMsgpbO%F8ORWneTN! z`VAFF=9r(_Lx*ymxo7j}L~j@OQRQJo^+|NDIu-L$;4KcLN0~!k(}_4D9CEpc-zo3} z`FK&`h@Rsd^~n+9-7$FG@O$9rv*?=7GG`=To+5wSLFf*p1G^IIvcHt;D}Ik%uT)(< zAMO?Z-l{!kN&4#B#QS|;#p{RkE>F8|NuN=^_e*_;E$YV`Mf%Zy$a}m|q?_uxZ-qXb z@4N0my)xgt0P)z3TDqHA`WvRA-&{JrzIp3PbExOYV~!IK^W87yk$Y-=T|q}wf^QNZ z-0^x`UkB$e1Yeac&MR(gc|XAMCyDpR;0LXPn0352&qy52hv$`lHLb%d(ntLQ_~||B z${p_08TRLM;>KnCQu4(O>#^N;?fJyc^R>?1IpEU0U9i6o_#@x_(t3<*Pji1}(cOH) zoT7pC=hnx<3(pj-zg1_uU#hQKKfgEnqvVS?1Mm5iIC7QycQoqIdbji8Ug;0_)Yh4Z zcbkoTS^Q$7cHfHUizrVyOFb;#{gMu^<2r+Rd33LHMYw*;^E z1^rjPcUk(i@WtZ2{iNU*5a&?1-|zIO>we6;$+s@+CUu|ghQ3gLzQH@Z+ehX3#nLyd zR?>y#<88##?BaR*)>yZ!Jf&GH?p-ynVLzKg(nsZs8`cAO4%V~t4u`0JHER1xsXz8M zxPKnJk?&oW&(aosYP-l9&FZ@4$7Wvv^XK%)Qy?S&U!G-~<(FWlY~+9ulDfa*7f0O-{x6 zog0u)d)-_pDj1aNd-2}8@7#0GJ(vB;w=U_vnzj8WsUO3iZJ?WK*5>cwgCkE7{>rDT z6pn8b_s$g8x3rI^(8(WICs5Rnx#06yY{u(3XDHFtnIr^oVQI~Li zLORpg%6$Xb)+IisgT7{-dc&&w_Rt4-UiJfc;9>M&pL5^x;a>IK959Dyx#*pGTxlO) zqMzJ%|F3;-VqXfhg4`ino$S6|led#^(G>be%X&_#HsBi)MLwTlIRF!TcX@S*n0 ze&zOS{4o0Vq=V>JTF)To?~Ykp7&( z%D#1J0z4nYeqE}Myoc}H5cg?-{*E?&W>bZK?X1@?U+{eVq2l3Dzn*aLZT9azIIv;g zM|2JI;QuT5wdKRT;@O+w59K*o^kGxK>v5k(*~b&^Ct}^Lnte?3Mfp@G_(I{RtL%s8 zA|6!CBaL@i@&9F7J+^ckZSea!_TysFpWnkDU=Kd{Bj;g7@7Vypd->wL@t=6m)96i} zhAu_EY+Zlv`X6}Y67O5Sb!lJT!q?n`ADl1t!&V$=)Y2QR*-x6dID=j`-}@!~M&Ei2 z$H4-RO+IG5_Oo%w>zgvWl7TK4M@fG(f^bqu!=Bt~lZfv~% zEOY>#i=g$nf8}EMXiJCw0sETo9P;3xKb$pAM@RFZh!5^p^esDH1W%wmEnog6yxtC; zS3VWX{`~Bp>y_^YzepXO`+qj>6G~a%yKgfL3 z^UyufhXa4H=Y9y{|CPv7#FOT`U(&hmfing}N8`RC&0X3@-*6i~yLcXWr8;dsor82H zjp%o<&K&-?QoZXf`u|zKYxFxjDp_^r@`EzJQFy-=itf`WbH1Ck=c)+*571{i&i>{* zhvGGTzaFrzQS`2p?2q->TgD|txN-+QY`%DC9Uk>#c&OWf+m)XjbA7qUhd<^oqrb_A zd(ER!C&v$J6&~?9_sDt74bOiliYp(XKhL)=#rXs3#qScwr@(Vjw^?SzkmFkE)pt&DyHV?WZLlwwtruk?-%)KzI6$=za>t5gFoP4fxqDXc&__o#Ch>>S9r(s#d+zQ zelXsE*NykL5`WXB9<=2?XZr+@kL8OSs(UwU>J8|jHsN<)P+#bpH>Iw(#P8u#eBAQY zm))0b|4HiN_&KhEtGf0wZ2RE zi{yKkt8d`Aw<16S7X>EBx+ z&g5H{b^qcCSJ6kkApWfs?W6sQ?uJfE=kObReDdaBevia^uJTTuVjfY?{2KMiC3xO1 zS$DqsWgUZb@<)0aa+# zCyVAJ>=F0wx;|#U7axRtd5Yp$v$ijk>m}l;XTwj~x_NNF&tcknrF{*=`|k2Sy;r0^ zUn4$xZr~zy^*Qk32zg7s{n9?ge$l5q1`qHhy!EkK@4+tj>m%cN^0HQ)?fp`|<$QO> zeM8P8;i-@2(0Q(f^}U*}k}qypABA2t@P@wsl=J90BflqJ48con!w=`{3-CM}#q(`G z?>%%HZ@}}%YTdu*)G3GYEq;-9KCJl-_l?qjSePG8-^l^GhFj#(L&S?q^b_U7^LnR_ z)yf~g#~*K@QXl%a)GM2{bX0THF&~(}r{6qZUa30KuZef-)|VIQ8{XivY5$vX&sA~V zkH4P}9|{j0pwAd0|DQnb`+24K-X<5L?;J|E)vU$aZn|DcU&XrNOpEJxY-XWhp&3j;cnFsw!mto%1 z^F_u&hi9K@_(XWakHH<+(fQ`Xy{NWonf|DbeK?~|3y)oXeFz-$#Y6Kp zx@X($({%XIS?6lpt9R{V&m9TfqQ`F!0aC|0a`Kb$z*VX<^w7_bIiF;mH}H4J7dNE0=`+t@oV?^y?%5M`u03>y zui!sc;W_5;BWl%oc1oW_{Q9xzPmw-l*ZBziq4o14%->iH-%9mE=8+3-FGCm+-+dPjw63QNH;O-Y)hM_)V}whcm8#!qK+5%N%(5G$Y<}!uhMZ|bHQeaFZuFHy+76i zjDweMQ$N30NuSpXzZ=E#8FW%(#FKpQvh@J+8GQtfm_zrp$@|=5zTP**x)j%$#Z%?u z+2phCcsT0%SLJnz2k1wh?7lCp=Y4p&eD9a^UG78NwGR<-ViTQI5B=0^;C*;R&TYQ^ z(tdPZuj9U4B!1k$Ut=Hr#soh|i>MbSAt3!hEl?sasAI<)!&>ukKBwRvvYeI?)5-!$490L$4BV z{gnFHOr?DrOIvr)dK&fipB?^R)WMJXm!*%jKie7dmifTN()YFMYX4SjgS)$*G| zq0pO%57z1F9bO`i?4T#zNb9qtb?n;@75siV_+Ie>J>KKb$vbaTZ~lb7y?pme^)JuE zUah9y{gD~i#9#AhE~oo`-8Y~-U=F@2pC9bQfA-_SYcs#heMFvjxL(9Bx^BLV9?HD$ z3GQRQJVod1AoyFw$F(?*s!JSH%C`nRcbPajfG=^rciH)s=d=k2pD)6Vlhl(t)QRp_ zt|zHJ^aLN6eCu-l^j{qo_IHbPlb@4!Ef@90w%~(D%^y0hz~AJ<8^TLn`Y0c`eiyi1 zx;^tcAM)oHqwc49+4)-ndel%a)(0+Bf zN3-;43{lsbi8_$#WJmK=g|C+I6Uet;ig&Hrz5wE%)&o}v_u3zE6kjaQ&w5z&{^eVj z`VBka-Z^;AvGC=w&O!cVTm1Yp^hKN0#q#xW*w5U!*KtAm@^$jf4mj&M@9~$!kB8{7 z^VJ=!k5XUu@0eq|LcIIiB0qM|tA7&niIiVX5?}Jg4e1)X;HZttxKDmQ+u*5Y?YzZ2 z$G<#$_|`d(`Ocxe44y+kUr*o`(^#E_F*jYN$IS|;on=e zbsg4=qJu^EmalHE`_rhcAGhAbby@X`dS1X?`iJI12dnyYJ{+ab9l9^@(m>??*{ zwf>#Bf6@u8)5p@Pv#rZ@BIu~?Jc!GTK(0r8Q-?I7Y@W;}- z-GM)xiGDrJgP9<2&i8(aw?2km?WBEdsYBc=`utwkAbPJ^<9&Fkd~sg&#vZ;X^XP7N z{JGI-_w0KRyuEmVTb#>$abAAW#`7Ns?(pXpf7p$BpZ@*==k-n{y-vP$sovU*bI3g_ ziX-=n@rd(d;r%JcMfi_=bqCc`j`UID-LFO6MEb90t@T_W-yDrN^3I{~_fOKrdF72i z_`L$hPML2C-cxgUT zSGt8E{7_fvA8pk3Z$r0DzsGtpADj(uNJrj8=dfGJ@8#;9`7yq_+j)n5i zd^%X=jr-<%!LuXQ)l`~GlKT6$bn0ZC};NItvpZOirJDqP`(u>DAIx8Ii zUf^||KjZy#@Y?PNxC1ZJN{0{CXVb6f%-7qteiYt5-@B}vzDL}6 z6zj8|M*QNQ`5fw*m;Cp@CHdl^_?ZLp-fQqnkE7nIcc2TOxKNabJt)-m(BtIe0mQHM zqMqe?k?`*%H)!;Jy8Ty79rUb-;Y_&~+a8i{R751K&mm)~c;fRNdS-mTzXmC)#qqVNredHS^7n{73Q8_PX$2 z%C}$Q|9(|#-pB&@d@1I#i8t-Ko=&`2p&w@rf8TfNJUgXNviIXyuZ50%Md>@aO`iXd zJaeCTd4u^W&w^*Oo;%yRgew{~^UM8tsiSN$*Std>`2xNCHu|CYA{?2I$5#H;tXqc< z?s(Df|Aq6dqIpX_*CXf?n2)-w@MXStS@ExpAIf*3YZH#&qCV82Zt^mCVfC56<{i)1 z$Dw%~J@Sy*A|1^x{KmZ^{ZuD#h|cS`@Bzc{h^;!?{nGjC!mDkBkEr#9s*juBSOyPo zfDZ@L>daNIY{8>#g+9+d09se*4}%|&Z^vRKf0%qYO7-Of*H?MJJ|nKr6}@lWO1R{e z-+|ESNuQH1Zv5AWf}3v4;}E`|i+Acz4+D9JeHxE?1UfP5n)&dC`*aoGA4k34eWcP& zb@6prab3^8Sw(m`U!EeK&Hb*6_$%*L_C2tlp5Cc7<{vJTcdju9J)d4w`k+QF{qa2c z@e;UphQ8}w#Ci8+E06gMAI^O1`Y#TRG!M-0f-}Bye=_*K;d$BcQD0S>W52+<^5H1O z>wbmKl{oML_2paiU7O#yW*(8>!yLNG;EU5d&&q>6H*lorgKd$IT&8cY&pK{IUo(19 z@NquA*SBdHXuz#|EywOaId%b#u@AeO%}_LRWg0 zbC^#rs{7Rq+^6%kNT1Lq?^L5!{cjEb+gbSECF&3P;)ZmQ&Dy-Rcdh7R@gWx`>Gw4+MtYZRRafSOh6>|mCJY0Mb<8}Io&>POe z&)g$_u@BQMdCV9*+p{8H{(Lw}Iw9k~A@J;%=4qIN)o>pN`*zy>i10l5-evK(`%&ko zUypcrj<~l6|MY(FZ2Ei8>7&XQH-sqhlM5cUk;MzB;V*C{6kq zhV9qLdpll)_dB64Qhj37`E^=e)I79&L$;_h?BdV6g8tn8H(S=T75Ws5{X4S7L-lcV z;IHpRJwf>PWaYZJ_>38RdbW$}wc@Sv#Y6e_bjYiQz`4(ZA9VkU`*uPC*mqUMUe+2mp z=F3ym-{ASmYxdQE*O+6@SrdPoTlOO|J}jD-oA3S7{FD7k^}QM5`BUn~(|rC8^Ouis zq4lLYkNNTx>DjvR&gwn(e6QP;>R*jo_wXfr;Q~6h#k78v>dW4b{KNiG_AQfMs*T^= zT=YTU69`Vphc~1jvR-t6y4NLuqj-@074Q@-+T(K^SA>O;HK z`I%jQkb=b(4g^N4xlk%J&_G=D(X}$B+#Z4wVRc%Sck9cMMqGhaN^eLA2naQTpf)i@#DwegN+jb8)RxWPR7@L(CWF<&WY% zjZyg9t9kX})*a`VHZVtIPr1cHYAKWxucW(4Q+GUFPTc?w53W z&077VTV45rc*>kkE<9zDgtrYc-=G!mn`j31l#5=u=`knMDNBv*&-P=MZHA}x# zzPMq2!gIe=r(Ad61A6*>>MJw&IBb!BU4ReHmwze$XyHRK74;wC#s&DNe)s|0|Kl{H z|LIRB&?zpX<7?Gq1^Dp!Od$A7pI|wJw74i1@aIbjjUf@3Wfx%}}hi%l-c`O#!)x{sIrOi`>=Y7A< zc)y4TBk1*dp8pMgnj`+d1~=x*zf^DNM}FnHmiwf1?)t$$UH$tzOMmg_+sq-ILC2jh z&a1DjUCH-kDfoBs?$bqazm2Zw1^TE@>U-__DeYSh`&&O`C_sEvcBJkuew8D#|rt(MfUjz=b34qG2x6xt#kJ^=jvVOA@r*> zYV=^%f5BHxu-<(2%Kz?=h;e=EUFzZ^^oPF7{2TkGz8CA&`}HyNvh%%Ps!JZg?>;Q< z1B0)|ht9g@8^!a_;OnUu<=ZdMAHwG)?v?p8-Lrl38}3J>&+RJrE8l*p&#TA#bOwKv z@#t&#Zx0(HS~a{{=mgY{`5GVhe11f#LpE#e%XQ-21o!Jbe4~5#^GsUL=J-*>v*mlg zg!dY?d=8(6-k*1j{G@H%TM2i~qqAAz=lSw4*GuqMj(y@CgOBR^{qnpD_?^dT^Azb| z_l@Vl=Z}J~);rJ(y?}jV>1W}-<-;4oK?iZKl&9Q5S2SCR&#|9M3BJ^G1=MGiFCI$A z(5%rtq5ImQ9&|hU4COanIG5Y8e|pFAdQCT*QddAD4w+H zG>I1<#PfWGhc>~t=Q(G)fhW*s^YhC^=k+pkA@kiY>4m~yH1c5QnXc0+{++N6hj@6r zcurlm{Sq(T{589|t z9vTEZMo;<$deLd(XFlBfo5O|l!+%4*HKW|K!6F{GSrZ@O-Cm+6U8laB4@Zeldt2d) z#rc|m$FM)!zHyb`1L|Tk$h4{7?$GX@NTOOw98|FB&=lzi(Ofy~4c>>dPD8 z*>j$sQWRJE_#Cdlf84@PUi*?%f)@?`r3eB+-BWt)HPpM@(arsH=Hkv7q!3UN%E{w`1mgG)DU{9JLbFbQO);$ z2_LrV-yRBI7tkqh!{cs}SN5qtJd3)fbZ|rXKIGdk@$=SIj-h}32Hbv=KE!5i{*C_a z@W&Ef&bMFcYirf!p`_#bkLI({XCJGbcdCx`AMhI)hhJ>f+42m93^hhn~scz!?T zQn;T~x}!~a?jiOo-@1g4j*$;fginL|85RPs3zrNPowu2&r>M_9pUzzRnRe9e#50}7 zr)`LM-(b$jD*W2V=wH`}i~06T>*%0om^I#|pKYD_F#DdT!nu3NKF$OmoGorxmx6zd zd5dCxQ1h1dqwm*xY~?re^fTw{UsinURj!Y)9`4Uo_@m$~8h7yaEa&jFA1*#BJbU%vfP{?Ve3<9E~s-zhT{mbW$%vmoEM>UtVe5iFIA>7j-^npK0qL%!3F& z-lD#e?;L90XumR_Yd;6+i(U}-?MwVQyvFs=6=mHo#p`3iAHsizzoCBKg1>)W1Q_SI zXQztxH{X7#4%vtA{1l)0zb)o5q$iDeQpCsKnXjtkOOkJ0;@29r=Np=@rf-Pz*1(Tw z+PX`4jTQR9^2H7Nt1(wE_QCpH)&08Q!Y_lb(wvTwB3)a)cUgEm>JF~MhVI1k+Qg?# z7tfW_y*mXy$(L9D_VD62YS;1T=cxR>-ZP)GE$-bz)}MEdk9dndb@GMK=gGhP8Tn)j zU&j}o=LR3J10Rwv|I)s6`254sz13&f6620UGSlk@Hh7T{ylMV2Y|+(M}DUBxlVl|A8#Z77x#b7MqONU_h#{N>{>SkZ@*mhzD>b1=8N;HA9tB& zz8&=j)gfO+T~9pR0PCC$UPtf1AHY|6qKEh>`!&pP9#4nAU@ z^U{0n!_TaOlP-l%yYj7$^T=4Y^>5PQ04ep$4xyXX?9@iES~U-r+C?rRs`dWAT$N}On+gLt2HkAX{0 zz_aDsFU>VG-!#GJ9R%M_F>g8QNa&1ixsFun_mQt|u6L+W+aJ*J9sgH+Di2uCX!O^6 zKA7{EJP}@>M0$W`EuQTYbQ(M0jI-43+JW=MOMPcQmFUaKwl2-t=uuaG%ADv`>Urzn zTlaB1L1+1#`to&jvaLGXy5uX@uKXS;K8^%_7f#XEm zPwaC*zeA%I?{v@a7P$Tc&TGE?(&zR2HHaSLYwD6C)LDAe_qGE+i9dK=UKuljPnbx9Z0ivAP%6i>PC&%4z` z@3z8zzK_q>ApP0--evKe4RG%~y4MBkP6F4$+r&H^#g*gmAo=E-%OA>f9A6dT!FS2S zw$RBpYkY`ZuSB1+&D{2U{SM}vgr_E{tKK#~3_YlD|1;w#zYF;O=Hq*XgZ3g{)cHEW z=N`r%!1c_h5AJQKV04)YdJdy8-upL>(#L z`=xW&!dGORdvl-sYo0nv7r(;|c&O`9cd`CH+kTm+#vhM7{|-K!la=D&f%$FMCE(Y- z0-xlIhmP~;mgy56g6ADVx7N4+P5AmqXS2jT%*Tr=9-6CpRx+ks|odj1jYUX1HZgkyGJb1piVO@&u*=N+tT_>3(PV_7LzgqvR z_j{|zf6DrfJkdk(!sqZFy!G$lJ7$>6*$2mu5g)&Iy{xES*{bubNS{Rca`f>!Zn#ey zUglV>`p`+gYrNAJ*w1`%L-@h-mxn6Fxv|Kz6h{u|qg;&pMZYZjm+yXAZzEm#efpGl zg4dRx?^~}2Ux3~4(eiw-Z0As)+jaAc#uG)lJU*o?XW!Z{hCC)8->dlEV}8n*>#+1W6m)Jq{6y@JVqeEC`i%0$4c(7s z?Q_RF=Dr8~Uj9GloiRs1I?~0c`(-O`K^^EGy7*c81U%Plso1C9 ze4^*(X1ibNyE&+IkFE#)LkC6v)d7EeT|8gqr*-yx`Lcfhy*Pj3Y39f)uN3Xak)Dmb zi+cHSa7R9#O*-pFO&f8~mbH`Fr=_N%P@D#f^sf zdE(p)aNsC9!w&2CfcJYAKIcCBn{QpZPwn6@j3ewjB)x!jDWmZ5m!mEreMdeWk@jmp zaFpukbL3f9;pIEjFXyOxjez&pi{_o>^FNdhsLi@Qg4g)07%%F6b>n_%-S%}Dg73(; zF2(6SeQLX$v(wZcw(#fdqofAetL=qP^&zxW+}aQWh)_M=Jtcotpj8{-Y)M4Not z^NYTvZg~eBnJ*ryPpTjMyL@eD@!yz7f8O^w44+%!j#2h2U;btNuI8!E^3J}8u4@MW z)Ml+Z?I3agF7NkM;zvF_ul(5lP_KBWZlGUS4PQs=NR_XB1@FC`*00j_W&5RoGb;55 z+=LJHJl|P#L#uHvg)j5P4e3%mmtu!^*7dJ%i5vD!y9=ps7coe?+%1<7m|83RT-Y@xVbijLG!6T0HK3zjk-bUYWF>sdf@Fnt; zeErMz$Cj>U&GDXj-#f%R&*gkf-DuQ2Hux)FoY(v1`!nRamwi#ti|&zUeja*D@dh*S z-1*ieAEHKWoQ|&{_iKzgN)sQLNA@K)zGse4zV}P@xn`~U-U#vgVZ;aNvm4geb9y|{7JgFq5jI4{~A8@;yX^kvmLlU&-agf`IFGQWs8U6ubVZxd2swz)E_*r-f>^N z)lT^DiO!6!=BlJ9=04%;<;cmD5lMjgVq_y+zC&-nQr`W^E5_IR$X{U`LE zy(x+VGsH#r6Ws-md>478{OI%5^;C!LME{WE9rMmZSLwXczUTC}RrV*%77rCS8nyVc z3G`{(?l)&HV564KV=H(M^^b0&7tNPf3iq~Z*Nx@t@`66|=kRLH+P+Hq`^)HvcF_0! zy3V#=&c75tUPc^LpV3xPytLltiSHeG$}oDHd_0@!Ql{n_>U zit@-~_#;kN(xvah({G_u%y++}BkYE5Tzxc?@Vp~M`E-ZA(k1#lX2B(^+_QXfUbwGO zzgqa`ga5W!zufrof3LwK58>lpr>*O0{&J&sT}bz7i+aOkrSN{_S2XZX zf6S2G*ZhFEpYI$huWVaK<2;{zIRj2S($`CeG8}qz=`r)w9i-oA!ebBPYrAe82mITy zTK903e!e-+$%XIE$8U(&Z8-m;-~4s(9g6#J9Y?r_tGS=(0uw{@q-7}+Zg}CqkTyq&XLaCJ~8e)QvQ~2zvPeK=G={h z4+Zg&Jii}00q>jomY;_2V>aBYKB;D{`tqiE0Omw}8$M>*pWFC5%!Q6abAa;24dV@b zlcF!hI8l5~hrHz$_2yaQEcl&#xL5eSOT4>j9?E{v)KlyWemeTw6i3f;FZ1DE>B#rJ zAIz1TMF%=e+;@NF6#fR#eCtx)*o^skijO12{ZZn2v&JtAUgJ~h zl;6=Ok?$O;KHR6Sw;p{C!uj8ZuN-}f%;UHdby)EO`S85_)%uZ7DgRw8^n1}~X?^bi zK0ZU#hc=4vO1^U_UbF?@bUAok>aE1{1LOJP_+dTOuMZ1;`OcyHM(uxXUdC}z>u%IK zuk)P0arm4gAB#q^|7p(rf%|&V9WJ9=n=ZohJ;w>^vRnAuzM*eB-~AHb+eQcbfOpCM zFE=XjaDDXVZ{mIXZw`t3ZR%zD@-NK=?7_cmS_crm2c83|`u`W`^@gltrcRcR2T=Za zT1-bg-P)P4@=GeW=XxfQCrcF@UBf}_rY>*v6S`SMEF z8{NNaJ(0eiFRG{Xi7WTfuRV=*3NPo2^U~Y3z<;-h-`C*XzuNR@J{v@z3z28e+ED1yI-D{tvSq}gDcJz&%YJk?X!-X&MPA>>b=gl zF6oGR=<&ATGhP&UY~sj)b(95t6gup_^`|*^=v;)5jF3p6_UiU2D zehE)>iErEJB|k)WxQvd{ab>g^533&g3|=lD&nA7m>t8qAR|H=@j9**Rc;7l%{H33f zujPw}(uo^S90$)X@?Jfo52_E}afZ6lEAq8_)Qj@%m+B3k3$t64Pc6e&@4zS8_uy6$ zPcRSexQOm3U)<1~jb`mSp7Q@C>$>n+>Y2YscX-LVF!c5LaIf6XRwo+1CAV?SCkPs%^{oN(zo^5vC&e}sP*sBdk~7{Rxp&;6PwKe<8vKNoy(GJ2z-xY6PMoC5#N zy8aM;uhg5t#qUSl$jV3AN69!sUl+iCAMx()nJ&>@c`dp1# z`qp*s%^-aa3*4_R{Nb|aB)Faep3Jvj>bvREZ|L|p&pEupx$Kgsd{?9sdsV0d22YSJ z9*W;=)zTAf_+8@uoacTWqpmkVU-PJSzUUzGz01-q_pR#!r%&>EC)me!<| z<^7JAA4XnAo>}Y{mhBvhhjPErW!DqvQ*NA9owmz6uvw`886WaBAC8h<+OwQlt1I~j=8NYI&Y35Ztr@qjt-7o7pwDt6=yXQNH+NW0S`j`8yRi}Io574TGgQvi~%e(`_{2ubH zOZu(1^j}B4&N?FM5AILdsPN^WPj8X+<*PfWzTDxS3{u};u>U;ySeN-(bG*xI#eN;C zFXyWhssHYW&}XRc`WtZj81b;<`lag*;Y%zXT)uV5&&K|SXVK5B1@9^S?>@N0^AA>{ zjwD@kzWhsx{sB6#*S=TeE8F-FMg6Pd{UT2}1J9UmzogG?!uQ^ex|VdIC%}RI&@qGi zd5@>iE9T29RX1qEr`@A2J`wxkem?8im{S})j_OSL-eu_(I>t$!gTXD%raGfmnRsO%NTK|5Ye=47ESNItO{#72i zjE~5^_b>PX>VNpD?AgaB&BCNh>03W<-IM3P(#O`Q$uo_oIG2y%hx6fi>99S|=02bM zZgC%nblK*0j)R9k;~h6IoNry?pZd=8V?FZa*e>ehZFycmkzeQ4Vjk6g&1nuGA4L42 z=Z(IHFZ3(+<#tgYdY3pk1rAw?c&|RjAJW|~#cA_Vo|E+{`!`WZhu%OBuwcEY`J3<& z%(h=XhrUmqcZklg$Y-|Yu66ynC?pUwNphY$7p?%BryJ?PWmZ^U05&4uBdx^7gm z&QTSQ{RjB#Tfv8lCw)-VPqb(McJ}KR9~hfcU?8-8t-y6Xn_>wx;#i(=m|^}EgEhm((w`guLYdKA@7-mTOJ z?7H81`pZ9IzRFx$ykVWV;|BgO?87X2-!}Qnq;-ep3BVuu@@4Ta-jB<{1L)p8iTujz z_MeNNx`$svzH=yjMiXDuN5sQv>MH~I*tl+fHTFq$%{%BT^YJ#q8`fJt#_xJ7`Y07w z_KAn1?z1<3ME90YN950KoGx5H#=V*?s*fF@`}(|+Zh1QJz4SKu@W#*mUg=wQTn9fj zYvB&h-&tig#kf~-dYgQ82i3p# zqHkIG)@0}>RcA0hyhZ={MAXTIEA!z)`EPUrPgp0U&%0RJFK_tkzrVBe7vsT~N}pc9 zS^4gl>qORL3(x-@{KZN5w0`)FDDHn7>l2R5m#1hSJU99hb;yg>Jx~v7)$||2)1NZG zSXuv(EzW!1jpE-8`n#6E5mV>`_Bp3t&`0Y2u4HKV<*9fUH0iL zIOHS8#n9tq+b`<@nAZzG{~5a85qOSv=p6j{!S&$eeDP3tqiY`5d>iYq|Lc*js&&A< z*Qfuu)hpD;TD8}k<*WMHTsPkZFMYr|zeo3Q^nM9Hoei8N-X`Dua@}11ai74)+z&mT z;>l4TB6*kVU+~!ZbSdHiT-SSMJs$lIBjm&F(DS;_QhdP(aWh|Dss0q7vr&Amrg@iF z(Bas}V!*iAd;#xW*7|a@{!i_D|9R(M&+!f~SJuV4?@jS?0N>4Z-sR8ud->vq@P^m5 z!#n)aJ{<7Yt=fLEiW>v)Y&*owd^~{g?LK&D$?q{d_8dHD-+Tt|@(^+JJ~$-b`z5|2 z`bNp8hIo%(6~(_sZ9Jm=I#HC*tfFtr7Y`N3dyW%PFY(+Beg5!Uj&rKI!zz6@`SMD| zg9GzVm33>z@Al0?rx)+B@|#)WWxjK$&)4PtFR-qd8%@0H#m|M4*Q{?wr;`tFxKB!R zH8zX;Qp8Jb5l0%eeZaJ?`_v!itKS|b*!kk2d=J{_;tTo6arzkMz`J+vXSX z-Om>{u*$dvuK4*%XUy-*w2hU%i9+od|cz!u^Hj3wgOSijF#49#y zz6Y0hzI4GKCg1y|^>p0#6goBa@2=6W(s#Z|pX`izQu@yG;SJ#^^Vna+yX^PZenr-a z>b~6~9-cu@ns2}Kes%c$FVh#b7W<<*<{$h{n-}$54*XT~)hmUcqRvWQKM?&V(u*4J zU#7l1ZvL0KQ~BO6>1Se&Dskd+<^HvQd05c%oRQ;!=XuA$S^4S?!VUJR*k)blE5(f+ z`jV^ZU?`>(6J3`h^b6(?ox-;>UYM?{vQROZ_QF^P;Rb zfsaCW-uAkJUvvFW{m=Pul)d9`ZSRpq1l(s+qH zKX4Rt!M8%UC%tGs+^cxd4xNMi4ma_&nXCT&=RXdZ?!@=Pg-iGcj-iXq7dKS@YV$dl zcxR`H3+qLGlre7#-t+_JKg>~=&Bp`ie)PcWw?mf*M*q~ePJnk{lzQe9=3L}kmvljm+Wn%`{X-um-vjd-=fNW<+28lUzxm>! zcr^Rwj}&}f`26R|zuHkJSDj*kezu3?o%!yUbjbc(Gn}&pbcNsGZ`koU^u2=bxQTwg zRU4OO`KtPN-#ioXVG!JYl|IVe&)#Ft%PQ1c^5tLhn`qcqF8Y^UHy1y1tQODs0v+>i z%+1ofp6~sV&aO{he~WlA5AS&oePP5E_VL5mSH+op_)xwD&02ZbmT@od@kZr&Zq8Ha z!-U5l#wR;poL9aaeh%ibov%>uJF5Gs5A!;8&DrQH%@QyCIi#!Ufh(rzH~)sZ-VC_i zeh#DYZrKM$I+lEKL+8r#^X9?v_Qf8n)Tiir=3w{|nZFmmkq^&%F0|%ly$C-d>APl% zahCYBXXMY{Mn8+?L1%Cm+5z9AByv4 zbT#|n#;LOXXNOFx@jdUe^MNe4tI;sutMhlxb$+jdJfvGuM+#lD;`;*eF&~Z+UvcD@ zCth%f_;CWB>aFvXIFE|+rv`e=moE1|Is`*OP@sZL#)Gl1wVM-bBgINZ{z>6!h7|k`d<$Toki*| z`Eal2Gblg4;B|4IhS{Iq&*}^xI&K#Ef#qA5_}(u0@~4&Zk84GEeT@6n_d89#HHfdz zJavhD@zDAU`Q~qs=Rf8gZsX_BsMSyPjQ99Tkze>?)St86FU7qkcw@PuKbx7A#q+`&2NB1O1J!r_ z0sWK*+_x3;Z|H}fgBSDR4f$-oQ@hVs`PFCOiAV6;E%@HY z@3QO1!s##IF`O5FR*7FbFmGs`cK8Sghy5yDoEIsU5_ zAM+CR@8$WY=*gw`>eENLfKKtc+FDxcm3&9@)%B!T>!JT#1viaUo-^%w zi_YoQ(6I@B9Vd^@$773^?y?Ubp^txBG}m&rsIK0R{6+WcZt%kPJI`|h^+|;1JCQe{ z|FW*AavoxzO0Da1Wgk-MgY)If%8$ce1N<}{bujUujav2bk%G^;`);_0`POATkDjnX zcM|ml`FeP6`a57S@N@VsB@VSg9#&_06M z&Y|>dUGV!D@!||P>5G`hroXoc4}3ZN=jGFs?;P4!#5!x^4)+bIAG=%8KZIY!sMj6- zZQ1rqb+2Zv^?gi#{1Wlu0en-lMjv3GqQLvoLFD5Bl>auE`!Z1MuLNJnJcI`RhZoqV zd*}pK&}HX)zqAhXO&@{xHp%lZkROM>KH`h{Zs(=h&Y|#8%e)Qo<96u3)Zg5w^&UKh z$GJniJV#wA-+t-yHfyb~pr=_Pjt@kil<}^0u=dxN9w6VkTqiQmro3bh9rjb^JM@e* zqTgJ4=A|ONPQG{9{X~uz=5e*o`Qp4&_;(}lu=u%W%*n~8`_kt&zwsEn`!wp3;$@Ed z5YeSLuPNqzvc(PMg-znZCC731Kj1&tg~vXPu3@}reeW|@Hs5~vewn{j{4UIe9SVP5 z;lTUm<-s2}=ts}T1Gw)~>w4&VKl=Dj(7`sC%k-?sS8y5K?;P`j{ADaW6tt!U!e}0k3Ur0Xo8a-7U|Bn*{3DymdB`nePrLSsHZ5;{wH;|xbg1} znZRb$?TGWyec?_HK&xfgy1;3;_AZE#1ke)!LREF`_+ehm^Yx6$3?<9pRV*&_cMjye$fYjk>z zT6xx;h==fQ%ei_cs}Z__G32BCOy?Kyy2b7^X#+54ZTNw_}fP@ms9oEFVKbd z-Dd;N-*W#Kez&c3@ld=*m%M(5Jn|CylQ-z}TJgSF|DgP4k$HUicpLFdNBp7g(Q?#- zyyzPAVOWS`sV;L%)1QeH9!zcFr|1Gpm}&!#@Sx8zrM ziHp-wXK>yro^(F=dHZ(iJm&Lh_xu#S!)r0WT>Xh_{Z--8SET3HSa$ z-^Lty@@L@0Mc(O-{Wsy&?vekm6zRnB#Y4Scu2)_}5Bf>O2i5iVEBC#*|Hg4eeJ%Oo zhV<|5lR6##N#xM=>7OyS%_Y=EG6; zO?6$|>p@3U=v1FM1PPdz6Q`PYQk*JypJVsB_n- zg=g2Pr@X+&VGX@dpMLsfaK|Wf_g;eQ^Tk8?u=R=K>)^jh_osjhyYM(;m2^i-Mf|++ zN51`1ylbMYxJaZ5jTZb^6i)A&N)wc&K%VlaP1&@vFAyRP#0Ts zow<^KOuqfnoSUP$N_wa7;d}62QGM&3TJOPh)G?V;RHW0(w_kpjtq--XOYiS!CBGZj zX|IFlKS$>=SxMKNPmk^XO5%X?uc))?y?)ENJQejJ_>ZDGSU$WVU7r0?J~cjqr$0{} ztWCZ(68WV1kS40E^{+oae7Jg*bd=wRe$sQ6tasD?Op=#9FXr3q@0N{c6R*^$@i%n7 zLjK`(G;8xT`utn2|KTT{kMC8S>$?xT*ylz0$Q}F~!Ux8DD0$dq(K~U3v$E`)^`i9A zSHwYd0Gr?_>mZ&`H-AW9+cntivG&3$^(?cIpEW#Rz_(QAJV4?4r#ihS` z^OaBdD19vX>O_k3UF($LVLox)A?6P`o+!>diTaED_w(Tm>61I$t8XG-Xslc0E#ck`>1Vd{UaV^z%E`Yl9nii*y3{;)dQ~>xe$Gk9qOAQ~vl) zt^T&>MR;;CaEbl+{LW|jDqf=vzjhuS+im#I_bU6&DZaeNdv-T|u65+QU+xQ~|BLy( zt5Jv5KDEt@;(I%1A0O(M`QoAKBhA|VUc!aH2S*J@K1{w$-~6}Ws2AYFeD}-q=;H67T7`n`7j`Pki3M6+Z;Oi@u!t(Kyd8_*vE$X2BY{N^qv0~>u@-)Y55J$ZRfq> zbMI8rA&!F+dokBcfA2N>c&jKM%NGxYd*7kIauELTneP$(%1y@=cu%eLNJ`@9xDFj{UwW&5PZ_KX4g8*jDZTUY4((%W0nw^EP^?KLanhFXuw& zmg(chzcOFkFz)sKI3M=>8{NNs?%gx%jJRK?=ts|oH>@wD&Pu$SrOr@@^FL5;`OpbofCoV^QQv11QKG^-R)T1B53*=ju_?j*{uSx1c zTY+!YH{ZdhZ72E^#XG)%m&?c7==1H9Pd*^douD51A@!lI@11dm`xMjiQNrhqnmPh~ zqJ#9Wjnl``AwIqezT5f%@dWwyOSq_2TR&qyo4PA;vcvnk6+D6TVq4^G`QqVsH&6X> z%l19Tws; zEWWA5{aWC>4dPF{O`h!gcPrL~KV8f*&VqOSIn)>Ws~Ww(>tEiFB3)S5eMaFQsQWkv z-e}eS?`8R__p4E>-t*diW#Fb|zBX$0fi0jff5ZE>M81-5U5eLz@~Rc%MEcC1aDN)L z>R>CpUo+?#H=&gw8%6KQFvtpQXPs&qQ4O zu96+$!h`{jGTluyR};r-f-`h)b$jaoXzC+PWJ;9EI@E-c?U^!zT>zg(}| z#8>o9!q5me{C+VWhn|@Yw-~NMf z2|k_qcpLfex^HxVy6YA2;m7#C^oEWU&6zi)QhY?yui;d&=)od-;r-!!bd&g-l+Q!ofqRf->MZ~ zKA^vR-u?h-{pU5G;aBKVX7IE59)IGk%6d}e&!_N7{ac?$=7#61r^uhJS=+x{@0#bQ z&Vmm+-XGq9GnI5_+D%a^VDk`KoO zb@Lf`!c}yn&076bbA`U6;H%6N7{6rss=C2`WnGH)ZNmOvmFs?-)6hYfuTnnnHl1IJ z^L+X$@gbUq$Nmg`Z$J1d)ybyes!*De9QX& z33<%Lm{;sR0j=wubo(WqE#@IoPne26Fx8oRo>v+A4(ZxnF}E$>eyJbGeZIGxhk*M& zC(rK$PbJ*-JL)Yj;6?N8m*y|G%{Q4pVX#lDW{Hj47G z=hR8Iz<*8rQdZ;rQaxp+5>J;8Zzz8M8Xwy=`W$xAx6RPs)wMpG{A3%x^a(spzB;V% zdZRWUYM*TP6RA(deku=&`fQdw=dn^gkT3sIzkKxF1nvbVkv|{lZLG7=&o5GU$#=j0 z$HPGW7X8*s>I`@6%Z@Kf>?eJs)9`TbfwS`EDeiYrKKx~|ZsWf^6v#e|zERg__4n=v zkNDd|fS7MxYIt&g@)P_FzJQM!4*r7vL-^nGmFi2gqHxnObb9N|gWci0 zeOGz@gZc+%D(YV3pYxS`-t*lr=`cEhW9_dgymW&<_nfR7?9XG@$9UiJ;d#BcjhZ>x zyt|iw_U`t6)<=Jwzc<1C%C}!y*IuQ(d7OCuF}(FWddg1li|`uo_^a>(`FH@~-sqF3 z4mlG(fa)_ma2yQ(UhpEmoIj-FMgMy45C7sX|GG!Kc*Y#&E%Vfs;(s@Alz7IMMR{2P zN9L<9%U7<=yL`cQFy5~Le0uh&`&}=>?+er)PBAYaUtXzu)pZ|3rF`)?-;clx^c^<> z7b=gL0x#xUm*-{U=g7KFL|oM8KL}qA>MqRreGbo^4@ar)6TWhxbN%W2Wnafx{Mt5o z*9y9}eD9b1l>6Y@vCx;>r&#$`FXoS^&n@P;!JFp8hter`oR<{pK=f^l;LFqIoxX0q zG`<(losVa;eqQlnEAX!H$7&JI?Zbbr;v2m|d_PBg%ojI=XFYH99C`m1pZjC(+rIH* zpM!}?y!hQFeE~bnZQLLaeu*x-8S}EGQ{F`1Hp{y5z03CB zRp0+c)Wh*NsO%4=ym|+o`#sOq;ymVizg#Dh{=~Zf6ZT(2r`M}gciHg!jo+8~kbH4N z_oZXsIq=5U?902we3k0{KLjTMC8i5y>g~!Tw4m}6U`aJiyInT1c6u1!{V79!t z_D}z4nXtq?%ZE3#uJAKQCpyV_e1tDWqZU8E#9WIl>S8P8WBJaZ^j)3cr*-Z=hp%~# zZ(u+49Qyl@Igj)7apc=C;oc7Qg`M!1la6M!60g>*RVQ=*>yzU7(^++TdYAgv`JZ*MBzNT?pJuX7b8BZ-zZ<6BE51q_-pA8*IkE*{g6)duJvrh_i6mI^6i&( z4&r@Z!gmgtFGN4O&)le4;`=r901Nch=fhFfnG5eef|uS2{!sneUF#{S>&?OIY!&%T z<>T3e`x@b^Z+;M8gUG+oy%y$X!voLKuab|q@%$mxnV-R9Un=%%&>XC{;Pz$i-I*dH zV2-$-FK%ca)IsIC5WZ~j`Q_uVM;+GnmaD9Pkv^4t{D$W89;>y!t-u?q-~A5V@;kNO zsb%o~DtYu;rFq)<`t@|4Ix%kpo`ZYyfcM(I#MW(`w!bR+uzc}Q^^mT4nBb+P_uq}W zxavh8`Mko{+$3+#mw$=ZupjIoI-e?*9?`NUERDC)h-Vm?kxiB|e4+jSiqW5jaTu$ni;b*2gdp_K&_qfM; zJ_0vA;p;VUZ#(oi%KOJz_i^5_d~w5i8}r!8cxQ-EqHI^M<5O~M-% z>u^+mUMb><^X-@Nlzr;|6U2iD1wNa1tZQD;^)debY5FVk#d*g=;ldO6Gpv#~-i5bm z)XZ~0ANq{xIun(q%#u!m_i5pByAs}e0%?3-jn{XdvV@8|J?I$6ff=uk1d`!pKe+FyytR0 zuJG9)e!ok-)$=&)H!@iXXM9awneTo{ztE`7M+yJV#{4qnojrKd5AE9%^>h9E`RdE+ zCu+Mds@M-!db}0x*+K9F#)tSn;E$E>{Spt|hzWXKq;#h5e2KzY(XDrc&^Q#)aHsZuo zWu1famV9_#@qHiMG+u<;KaBc`>I!Z5$)Y!0C!blOA2#1P6rTMd`cs7Op9Y>#-K!Zs zo%Xe%u89w6zI7>|YDbbz1vIlPvu*e_3@quD;>?X!dLnq z8nyWP`652*PVk5NedOa0wVng(qbljxZiL?a-yH(qJ?rv{;>AUD4&!{yS5FaNb3h$o z7To@rzRFkloAu+*mDh};clf3#|H>CPlqc?iqsEKkx_$C)7u8|=_%5xOA3&$QfLKUi><`6%9ne04qH zB>OF2qz`nSdgMGh_FK=4tX`z<|==bx{Jen@)Dh2Twp z%5wek-$}Q7r2Eo3wykRb&v$|+P<`T+-vRW#`Q9(_6w$X<{Ql}bJuB9eI*w3x;T=1U zPgcJBCEVMO_eeUMVfdyC!rKcUg1I^6i)Smv-nklz(k<&c1+$av#Sq{O+dv z62X!A)}{XWMy)=ur^L5e&zs|&dTXDe;`)Pq_4E$pd%twQ+QjkmMZEP2xVNAi?fd7{ zixzmVZ==u7hYvmfggk}Mf7$-1)LDAQSy2abe~#u~<~xU)FR}-J@zn1ZxavA{R2uL) zgGG4qB)H$Ylze(@_v=}IXkL^#OGWoDd`Fy@v7aAMpUB7e>OQ^W`Q_9}UQ$QC37_^W z{~Z6#H@pKI=xws<^m?4TZSvq*-mT3_{gplV?T_gb^Zb zGI(N@^SI}87(f4;{r}^SRQFBlnS1H{qr~ra-4`GI^XBRG4jh2*Z&QcfV4dgaN6*It zC@*Q$!oN3t9*guv%b^ouf4~>>ykmFiQ^|J@_4(Vz5#;qZsdEih_8+y6s(EhvF-!lE zFHh0D-X7=d{o*;`suPV7=l95C7JXj9hfCo8eDP5HUnlr8xSH;q1J1l}!QJQRLt)Y5Tmx^7NfoTm@b zbLifWd=p(d^@@D?mvB^vd-D}I{fd1L;Njhm{(^VwH2Cm7@g?6m)cfUmy^qLCzO6im z)AJjw7f?Lh#dmZK{cpbg60g=H?k%IQz7aTAb)=s4Yw+8j_`LFt<>PIn%Q#l6e)pQV zu|vGrVP4Un)S64Wjqdfe-vjVgzWFJ_hwYfZCjMd~>X*(_JVyW?*yj`Sm^}ji5q?Ui&nz_^YA*8=*;usDB+1_t^HZ4 z@Ua00o{zp!@=e#j$YUPkqnhs=DqdLk|1J34@qHQJ&7SMat`iwYQAf(RF7atG-z)SR z<^z-$dk%u@vuDW9jOX*!Q>=3pPdLUq^%R^`qHd@7_%1x@3;#Xpfcfs1c~9|Y0|mVv z`|$zsp@FaJ6nXNz`9=EL^1Wa3BRV=q;9K&>XQ2mmeyn$SEBY_xFOZLCbDY=by~zID zV;<}j{Tut#BOdZD5AgLOaX%l9k`BI6OOIz=@^i~nK0L2D-?t70+%(PCN0H~VF3#f=xcCnBlze%mc-;=X z+HMgq^8nxS*WmRY`Se2Q1oXbW;GX5v!Ae)ztcmaRyM7e$PI0Bl{o5_>cTj$_OJ7Gm ze#5?V;yK*6ykfmPe1QG*Ho|{H_i-KlSH5_te#0K;={P#j8=S|lLZ@ec8=c!TE(OB!~=BfL&4|00$zGtg!em@>SUgCGk{*;d=YP-Z@(Dn_rzoB~-}_~rT0U)C)WODim$rGQI_Ml$?898_ z!y(*}FV5>dIxx?KkBIeoi|Asz75N#sWX5wX;H&cS0G?y;KOg>Yi8!zf4_b)x&CpM& z9=qkAr}ejy&S}4*pB#SvjeUBl3+>ZqzCpe3Tjnl*i{Dqiyi)UB8#Vfjz!UbP(0(>+ zo!4R3v&4OyD0=_$-7nSs+u=u~zTefNylW92u}wZb3BF%0@{wIFS?iU;^S$`o=-9v; zE72EfpA>Ykfg`n#`Ocwur?%%`p@&+HeoD`^kUs1Ua|PDmqrN4sYBn z8()phVqJ>v;Y;}4og#hdJoWQ@cwYJP0drwz!1334rv{jt)pLD}`qAC^`&sXo?#n@? zcWDs3u^e@B<(-~4deiz#@~)fo_vJf>t}o+vP5%F3)LDHFwZ7X$apgAk*irQJ`FOUU z>Z9(sAF4S1c@ByDa?CH9*Vf<5hodwXs>AvF9Q^Q%`*n)``A+Cm;591gF_*!Kt#tX9 z{ap2R8a>;b=k>z3bv+l%aieI???dvme0hq_Rh#<4v&d7(8_^ef{=;$W13r0&|Ke)%Q!iBDW#rw=$E z4R2cZoB`%em&qsa96(jrTqWr{r^vhhsTTNjP~vC73b4Ds{D-dt;=&p zs6PhJ=Kdn_hSup_3f|Pbjp}{*?w3B7{jp~qztI6~5!dZ!IgJn49Q`mG``B^bN#6CI^B8=7^YJ#~2|F==O?rpX;Muh9F8)iq_&Y2R-*=1hvwS$}ujl^o zFaGkcJLL5v;M{rQ-9zffZ+WjDnKv!2zbMY<+b{8KeQ?ATaQi4{O@*Wl?}wRqDv!E^iP!om4)l=Wix%@zn%|M{eko7s6TgSd6H^tJ( z4E+wxTJPLEzMNatiNi*{P3nI6&Y|LAJNog|4>lit*{TmU=y$jjxEDN+4kzEb?0evO zD8z4Y$0x-9efRN2pR@U1_qY6VFYrJ9|5-TK{%k|=GK0Qn>|>j_zDQiZM;>Fmm~X$V zGxw^MKR;%_ZZhZF^NT)=xTy0wQprCcAMSPkm-g+Hc?{#JvwEjG@ebp+@B0|KX8&HY zeAS#R`{fPc1Ner1j_cHA8@1{W-=WL-9)HU>@S?3c%X<7d^uF33`)tuV*TL~8i}bwB zTK!Z{-1kNQ%`EfX^Q}vGzESI*?9#vcoP2nrQXcawe0^RN;g1>5?;yX45zbiT=kvS+`Ocx@MhhRa>-76?5$_h!QMN1l2S~RuMZJ6!AIN-l zSmB^9@9;=L?|`mtmU>t>bU3P`ydQjrc%6LbP(IS{pg(*N{!!L>DegDG`&YS#m+j96 z-pCi{<)iQUuebQzr|}QHfv$d!_xKC;^FDn1o=LPuh>kbAP2T%2!XZ zt^}Qz?+x+bBYeF*-{A)Pv&lO!RK#=STbJs}u77=k-ewBCG#LF1^ywA%?dd)a&~KkF zUzR_E`S)FX0*8z6@DB0f2#@I9epZx^u9x2&3gYs;%dQXW_kY&@HQpEUX8Ze&6CZE! z^IPQ0`QnECV68he&!oKU7Ila4BQo9%e#&*+Ebo>-hjg!P-rM)!q1NDMzNUWHjkqs; zjps^F!!zcK8`7P$YV>KuiviDt3I7f4%Mj=ED(CQ1^yaPFzo#r;#bevYY%Ag+`Zd;J zU+~+-dUX8F(WB?X8+um{;N|atcZcog3;+HDJk51{R9{r+=fU&&aIgB0+fgr8|M{Br z8`0mT{Bzts?)LXVKalVJQr)Xr$d{PyS6y;jlt=EEE2Hxi%Zj&*)V-`6%i()oOQ zpIE-UQgfslG2cu4#_#b7_#C`z9qiZiNBxcU+r-0se3blznx6ma{L;SV_+=eX z=bJ_6@J-a&wcq)0l=L-k15fB)U4i#}4}7tQ9^gK8i5sE+mH%VD`z4?C=tE>K$FoX* zzNqqZtBL>o!_a%#KU;ZcK0L2_WH)puitjVw2dJMn@C9EFzD@kqI{nP~;)Z_jd!9>4 zefdK1oNx6F^`rk#`0>-Ie|hep-@`0lJy%KbV~)P$_rnLk^QPoezZCuf;=h*)2erzUP-~AHr?zx*ENB;)AD0PMob%rV8<^=Wg z$I+ML-&2;a${(Ax`@J|{=G}O=nziali}a`54;_{Aw0!&J`m*wkRr*l=Pw>+Y`_!nZ z%QBzsyXf1~{FPRnWuN>x6z}Xid@Fc1^Ps9{HksG9>3c=|n1aX2hoi(p?VDdJiXWGW zb35oyy4Fka^V0=?W_Y%Ie6RE-jaoSGb-YW$SG)LaM!sxbKJ+*`zxnpd{u}0Rl{fwk z_;3{7(Dmid$TPn~|8R#nEBWw-@~&pB^*tw!pQjEv$M@D_Kj+;Z<2>HQM4VX%yB5oSLKn7@L^XSVjjKgm+vr%P1z2ee(X?g)$-lD6Ln|jm7c$nWj+2J;zhfWpP)-F+J~?4>*>Q!-NA2T z!F3hp3FLd1H7}r9TkmAOwROqDiLKgx;I98E4!$b-_w&79I){7i6E)vNf5W}-ZB#ye zBXo#fzi?u{IIn(K`+3b2>u8kU&xKE!>dUC{AoxZ;xMFzH=zvrbXTS33adCm>a14x#xOjC0smjogVl;U))fxdI>6s>}}S3SI00pMN^`QSZYY@JPNqMR=o6 zoO@F2|HAtWFY0;p7h;~4`!Veo_RB|#pMC=OljGwUIEwmK_{&poF7#7HJw@wk)j$2O z{_@{xj!&c3=Ub+qVTm|?k^6PT2kR4W!NWb`UCzh%j{QUT_38hogFa{o{(TWVbr*d~ zmwf+u@Yvu-zR#C0i@*4FEuQ@qIO==rc+u${tKHwBbND;%;SO;=Uq6v_Kz-&#jlokt zg>RcAt{;Fi?%KbdxeqhosC;o=_oe4Lo%`hNvjo2AJOAQ+yFuSWVZHgzq3S^`bd|S@ z<_J#Gw|o`-uKR$;sY5KHOBu#LCExocerb=s*J9l~ICrkRb0}Qik2!&U*X$cB{FpEQ z5)X4=p2m58QJv)}alaG1fZl`g(8+3@`PL=;)Q!5a>y)aSJfW|qRV)9TrXITn&-N}n zYQA$QJ$Z{c;4{>-UWN|Va|5M=+7ACi)umq%Us|=|Po95zZlLz(8uw_6ICmbLyBB<@ z-mfj{%MZ~X<%{#W_s44TD&pMl(K&n&{s69bNtd(Yz8m}k^Q}w$=AKi$Wt{_YVHZ7l z2j2I)c%Rj0zRkYnd%u(?zm0j6s=MB%E;&nm=m36ngLQd+&ldeg`FH@~zFy#5@loza z9OYg1Je&dM`V2EK=6R9-cRoJK^V*bOT|qyy4lXUmoF^L%k$ zal`Y?9~A3p+?TEM8NLVbh@153JcT#Nw_mOw6DN#kd6!2*rzhXuZ{Xvug-%LyEArtD z;f5x9(S>+_m0xaCceO5TkaM|YAE{!VE)U|>Cy`#L7yYfym(de(F1yyLxUNz(-*^FE z@O<~nzCGq|sawJej=-;Z&d)t~(LvVxq$tkkdzYoRF;9Ia;9;6wQzB!q3m5zgsm=$hqu?E>HTy z*P*wOpIg3nS@RI1U$4k#ehYqWto+=MS-jNCV%-YwR-Ntr@;pS|VeZXc`@9tU2ns*m z4W3u`?=1Doe0W3o@&Wnc4)uhsB3!sjU1cwP9;M6PMt6Ce{A|y2WYTP#aCw71`7`jm zZ=65!el=^=Gr#1$+A?2Nq-V=l*Ry_CdWU&w*7KNp!WYyX%x^r2{(AbR(6!}@hxTpMJGIHZ8Yt3Xzhs>~_|F}1{wL_d zw&4%+;V9Kfx~?m`Pl)q3#5>kte&tJaI1l01=HN;5-7oogx2gM2fse-B$IQI!4(IKK z<0bmnP3o}u&Y}2?zWG3OzMoU)e~ur!=LwAPeyuR?VF=xCzPKSCpi{w{Rl@g6!57-U zP5pYGoA(XgRQyQ3`z1Y!{qhF!xtfjomhgHvblCX$gkC^(%Y1o?`ZzjK?^nOdOy&5f ze6o!%TnIiu{d^tbQFvK-W47 z|MV5QnvdaUI-$1|&-1`KOZcdK=TPUaLp@ zr4I9xUdFs{>l$27(R=Vo=vEX5hvAYa|LTvWq%u=Ycnt9~4fgAU(7Wke z-i*G1Gz0)2M0MD<=KvT-g};~LN-y3s;|}=~Z^DP<<2S4m*XO+IcdID=ja2rr@P4R% zIc9z%=G6J;S-!fz(t1MW!G*f=1iZ{!-nRvOrDu4zmZ>l0!}H?Jn)Wqt-(K*D;z2#< z@ddob`}DP4r{6rEZdv`VZRh>YD=WpjOT_!P=A)?7ei}Gf`<)NZi_h%@juj3b=AGIm zz8mik5I_e=Y{hTbFJIaJ^1 zKIiN?z7#Ksk0bCjorvq64@TcMaX(+&(EDXw*97Nk%z8Y|n{n@bz8|EXd7E>YZ@+$e zzXl_2xIU@Af&ZVmclojN+P3??y(!&`0BN{kIDmlzKVSs6lSYt6TEYH+#6|!k026o8 zaDxbpfE~k(!x(G`IE=u^gdoV2NKq6e@%?_*i}hf!xa$2bvWi7YI_KPD-#BqAaNG#4 z$lA|HHGX3jj_x0D=4rTYu}a2Xwbq(z&N0Ruvjd*-QRrXAD~%TE>+|iG=ex?+Yl%Gf zMT>q1a5wu^j)s29>lPoKZ(ZV*5BjpD`=2F`m=kQ;eA{w=v-Ep&&U^Wq4+l`)7uLDre7Y~ihmP-;^*|?uJ1xUY z9`xDUkJI`a>FM+B*ZKeY=Rn_sM_#ACT5(>%-1hJ>EBgJl$b+SW%~yX(-(_E(#lSA;@!)aN2!mkQM=Fyd|qFcE`ETgwrQ5s4eY<5`lyjOd&buE- z{T#nDPgL+7roYB>RENkL$Bb*!-<&U>SDjzg|MGAA9N`v%caSb|107{myN}KB(tMF{ zfPDKU-k?!y-(E!=)O))M59E1?=Z%+gPCw&3=G!m#1JTz;et5<2Fze`0pA_Q7k>a_C z?qkft=lOG}FRDl0@D2WWyXcc|QE&FFGh&~1IG-CW?Q6dMvQImD?5O{Rzdc7c6#cUN z`~rHMYen^SzIs`FWwREKvWvgLOY-1X_%-}VEuL(fJou&KJ#}S1zEW|phhF0`dHgf# z`|HFF&m+AMaf3Nb)YbX&D9zWb@R8msI)9%Q&3)J=|EOyEablkoM>mKs`EYE-!EVcS ziTwiq)%;)n*-x5D_{pkPAL$gh;tj7aZQrQ+?L4>rsPlB=Qs_>?r^0!CF%BnvN51`X zzaISy!GD<-*M555=y2#!q(6FI^j_p!m-sxtU(bs2j(PI-FPQt%;eGoccys%YXl{1C zd|te&eQaL^Z>;;YQ(VVbuWTQ4=?~}dNyvvsDbD+I&Dt--x@`2X1K+#Q|Dro0KgqXW z@>y;Y$5&hU_i~Req9M?b9n5z)OGw|C!f z+I(KPOV540?zizh1IOr6m)#~mJb{k>Ebl?Sc&Pla7kHHLw$I4dr+>kJJ&OKA{BW5M zlP{mw`7#gm$b3W8S&D<6duBc5I{Lf?)|qd=6vwTnIm15PkNo%l_<#QUpHfEm*^js% zm^^Bkdd%@OOVp?;#ft%T@-g>+fqy=235R+Y_!GEBOZe#t?p;1!kH4^WLd1(*e6d%F z8-373F=8iuj6lYETw-7m%KHt~F( zxcw#X@)_b^kN4`l^I_hFA@pqd;-ULO#RJVcju-o=svfCo>2l^mmxvy{C?Co9eo1HD ztcBZdqwlp}{zCYsYTYM!kGEq#|EK*1%Y1p1ePFDgXMP^@M(zcEf&Lf2jal;gJE3>W zlR@c|xbH^u+jp#|1n=BIpJG4QXVI6UeHtep$rm@4{`SubsYb0hcQ6M*`OyvdhHmt) zkcSXQKEn4aAOEHMWu4G5>ckKE+_&&c=|r9{oz2rYhgtn9Rfh}$?^N9~5k4Ejv-`1M zii7jTe%i_x^W_^lhkNkF58%z$@a0{geynQ86?C$^UniMgl#eG;KdXHx&XOm67weRN zP2V_MQGWOWeB(Ov1oEv*K4$OL=2fK+nkKJUr%$RupVaSKs#n&~b^I>+?6bvr`R~}* z;1Tcc>Ckc5Uy8gi`V+-}E%JT7cUe5FeUwfHzlyF4eOD*&0qd8gZ+Ov?e@DK!p}NrX zVYkqyZ4>Vbz5xe#ul8w-dgcjVn{~ExD8G{)dB$b?_qcAxZ_fUR!;z0zrzajMU%sKj z%kT0@^RUG6Gt@8L@JlfcrLU{-()n;veZDR{@oe}tD32Or{)YRTcktU-2;FE_U5e_e zp7kcwi7$%hs2E=~UWM;>;JuEg+3uJ8Ln?It9}(y8kpCS8N9=~K-s_UD&2##6^5wny zJ9i_0P+t3S%)?SW-KgE)t2psF{Q}SNIm@>$;ZgRLTQBe_=xEm9VF#>x)V_4&$v4aX zSf>AwFOTxPQR_3%WfSK=YB@h-AA_g0&%Wx>eCv`9rKNYhS!atI?oUx2wM5?ZbrIjUh8}8QonGK<#!!_v@+W$54OeTbKL``t*N& z&AN`0$4}!cRn>ZrKV&^moFAiK&R2hVzoZM;fzNw_@AY>@y5@o7Joxny@T$>b--K*& zUO0BM_I<*in0M?7K64H9zt`zU86$2!D$?WRt5cM>_g$aRue=5xFamDfu?`Hs-Z|z3 zOyJjo)?gH=n7zb{2i&M&OBAePGl#)bO0F;LGWk zf{z`=Wp?=zGMw+qGWa|Bl}BWy~STR;L*M6K*&cItTYX z=zVtmbMohtGuuu>r(&dsOv}0 z*}#7D)Xf)TU&XuUyI;~dbggR${fT(7)1l9Re>EOJzHy9vBww6YzR|4hrzbw|T=Dnf zg__Jsm>~Z>3choSd_P~EVm`$_di1$^T}8NkoB4tF&_5X8euzHypnl5|7sY>jp6D9! z;57H=TJfA8;X7;euS|oNZZj_+U!5ZT_>a853*cy1>`N4SQPtI-aIX$KuB4qqr~dvP z_hdBUf&Gb{X|Vcvlu=Z8gqmuBtzn&s;s`U=mQ zcOY+{3!aGiBJRs6*6Yb9EMI;o9Kijs&%tR9=8n2=z`7xyPkh{qe8BU?L(k=u-h3C{ zYY1HJHF|(XZ67mz9dTaIoX&jxU+Oz)F!%OZ@RhnJv-EQe+!x^U7yVSy5xoU3$a1@? z%6PBiw$9liej5+)FYmd}mwux+;2@je0?j(liu6gOi+ZnC-Mr4cvOf>pSLd^V&(bXV z#D7iwvdlY|4;Ph>oOLM${Cdju7X0NP@I>o+iI3FT`QnEBclOY`ju*{k-{c*;f!|l? zh?rZn3J-gyWq;pn=TLL78ntlwaq>U=9NyqwRkiNj4f3oB;>HBNBKh(|`&9h9?Z5j| z$+aFltNc_~@Ly}xKbg}1)Y5#9y!e98C3Q@`{807%0G-1y`sF!rjJxnh?TGW{Tc|tn zA!af@%?wncE40Ndj9LTkw=-g6i?(iBzKJ4_z`TX?6JG@^Dk9Ys;Q}XyV@<8jOc8%9q z*M@IEzWh*et{-?5Iu7!uY1Z{4eBXWYz**|BE8rse@(t#oR#pIwrVZ$KYf0_^=qymZL@59mCab+&v%`i#&6 zpbN5J=$_7yXs`{b%?QZMD=Vkq>9n zejUu+wC+Uy2P@QH-B_pnm+)1Md_UVcls@fH#6xtw+_QP`js|n3pW06lo%V3idfuY_jNe8(FR|I{zk^SLCym_TPU39pwg9;!~UugDqj!8`8f zC9XHZci+Ct!X0;t>+Njsm;9EE4^Bs&W&Brlisv&QVZXj4&X18d=EFCHZ`u#`jCHQz z%WfP1yf^SF-LIqc<>ZSSigykAg(krPzIDGPeasDfzlU4$4;%^n*SgGX=TLc5za_kF z4SmLn^FRDhI@F!R<}0aJ9>BxqMUEnykS$DoVMRCKv z1{d)cdIMkbnD0B}Q#Zm7!To&lU(UB*>Z^Gdybpda9WN`>1Ou0KV!VN$AfS7`-cB3IPYQh?JVoehqH+vvj4`n&f}@K#^}fHfFte%pJ%_o zU+ovx`R~fv z7rJlX!b9X+m--rdalgd7+VACj=*W$)sqbjm{s8Ek^PNNSA`SdCMyMwzea}KaFMXHs z-cN`d_8-lc_lj@WLpQT#ADG}Ve`g=?&8JHdE?U*@FSoy(eUubG zx=|nFs~>vwf4WcX<=ZdypLFbF5ch`oLB25{&$WO1P~=^TH~IEUfA2$qQ`^7C^A2>* ztdBYu`MiA($eddwFU)lAQ>a>qq#udqr@%hQe6DjU>t*fN|x`ChmR#CjPud4I;YxGCW(`T8l zUe>v5*zXIUxlPwu^ex4ReU|ywr97$&4m!>L48=XNPlfXj=G=e}+$BHG zhqIZ#5}$lKe99E}X3>XwPQuObBl7*Szjn5GsC=lZ@#%3s8~085#(Vhc@4By&et`|@ zntZ&2^E=(2rSN~ze$2x6*&p#fb;&cobLi*u#d+lsJ?g1rEx)(!^KxGf`H15`y80RP zqWSVe`3oQTuaPH)-p+hyGW*V7~n_pYrnUKl=G-!HeG}pTAGvZ?o3# z&BEh7CO;g3XU~V*D6ZRw^N8`8s7u6~+lO=4^=3=`AMWFRug(@Xgs)Y#`rEdHCsAJb zhW@;&mOkt?y3uibMVIj($rtC9pC8NxR9&)7U(}~Xb0Di4{T{mW&6e|E{XO!%%huc2 zuhDZU+#hP5RrTix{;;ojx1LdF=G!mj5qlBmG#7ET<-U3CXPZ2C+PFOT?{nr#=fg$i zf|-oGx$~J>(`Tip7l?2E$OotjaRocr?~64w^TzGdFsQE;YJ^20Xo+JnFWh?mhHmhJsg{$!or$HYPROWnoK zw%3yH&>HXC<-iG4N9W6Osf;7C&U|&H^70;f!_cF|yaS!n zu6cEMiF5pXp1I-q_)6)bJePf(ey-{yd{;5frmv&$4PON=EdQH)`G)k9{m>-~2etp) zZSX<&4V(m@`Wijh2WjVxDvq`3%iAR09YtsTg8XAI^lR`Fp{vyUm#;5dI=jC8@yOrb zMBXHSuP*i35%P=Q(swyqIq$UV%L z_ew9>AJmx{M9@P7Zo|5Rqa8=3|jZT%=!RhkUU|e)tXRI>r87 zB<|;Xm(@St!B^-3J{3#Ul{diEy6#_z{84>jo7}T}`TQT&PqVzYpTs++K3@0v9uFSH z{gryZ^4%}#96U$l48CMroX30khU~eoH+*~4hxDMOJSyKglpdoQ{)YA`6F)mi9acqO z8+jr+k1M?6%{trrW#4b%7Te~Bc)zxb_@h4G&!R_MLyxk8jxXOi)LfTA^l9L$hrVI8 zrJwgWhfiH!x_Ox#^k3$ym!+$$YVqf{ zS(c$HYVjxEnOEQ(Zjz!afuLTYwKjwV?z2+U$#|lwa#bs-@LQ#&~Y&G4)HDd_)6(A zy5!~e-Dh6VDM$aZ^Feq^e(!2gyvcVC?JsJ-EBf+U|7>4~vjncY7dlILOV6Dy$`A9^ z%i{Iy=Wrx&Ce@3dkq6s{a};0I>*Vzlkw0e34}~8(pZFF$ew4gvJ#>2L9)eHT{(b_E zov;2}IcoyqcPPP;; zPxC&H@jm2Rm-KcG>X3)7L(H3iE1GXPZ@m%y97E_Z^Tm1lv;BG>0C)}0Kg8$XhBs<3 zKlVKF?=gMmmw31H?U!`%J?hDafxGMetk5^rWB=A#(!afk-%}oyul`cML*Ms``*Xqm zB7E(@H=K{3+qXjbN4_|3-Iw^FdGf+>bgpaEUyWKmqA%fz=IL`-r|&nP4%R+PigR~} z2eX{JDfq3bcK^BOKU%+S-XdEbC4RX{U%*Y?+tYl#z<%|?tG`8`J;lD>;_uL`v#m?` zr~NlB!KZ9eUsaWG(;oNgMED;HZ@hwkPrm!5_}-{Bm+eu+J$NGGNPQE-!`Jr_u$G{Ia;U^!E zPc@lWe}jIv9lzVm-^&*d-FM@DO7le0d7UlFAFKKg^Y~MtGZ$WvZ@;7)ub6i@7xRgw zkNUiLZl~~_;lN9kf30#4^Tk8^tb2dJ4aw^dw{(Bpzx;*s!{BTGtNjEyAHHFn+Bm3n zu0 zd5*KxBg=dpC+>OP@6#gOV4ZzjFXl6|<%jB@uWIQrUI$-^?hF2|W1jdPVp>#8KL7qkJpVp9O`A_#sm5X^3}`gpL|E0yTiPOZR<6_KYuj7NdMt? z;8ExJ{d~NG^46+Wy!O3$8ug{}D$hx{0N?N&9O?`DWAnug&vy|1zTr9xKeH9V1WmtR zH;dl!d-0Cx9Oqk?aZ%wL*UTS-Tigv_NBfq!PQz!PcP!uivJWSHU(DH)7m%cGPB_U+qfehWO~D|j)#14oPE%h%2q(edTW=auiat!u!yY>B$^ z8}j~j{8RRI6})t%r9SR_@z8UIrMtOY zgj=kI9!l}@9Xg^F?$wZeNYJz8+b{d~{vm(7;k+s8E5(t+_-*Wx2T$=XwtdI?>L`V(22b&(tTykH{PqIUtZ?DT8FQD0)N*xUf9yU4dGWggFiyP z`z2jUvj*RwE^+_H9Pu%HrUU1;?#q5E+3HHg`HFR2w;nn0-m%qx@slK=X`dzf^p?GU zMe#7-yDWWIvvxcb{oZ;(U3O;ziRw?Zxvs^?4uDhwc8A?_F== z2lJY~k9_+jJhx4JyXSc}?uU(j2J29)XG0&AwoXyLHwZjRb>c$cZNlLi_ATb#-EY~C zQ+4KlRA-ADira0+@0QlJ%e#DqyrSzkSv(JyI+J`c-@7b)qv`#?e{D1LUDAaes@322 z(s?BJYKe23@BOkrw)#hZL;XAHd;*-Ks`ilVw_j&tFqlM-TQ+% z#QL1uu`l|(ZQiRP{2g{(&w%IWyI;y94$&vI!uz}(Iy~#+)n~sDJkgJT_H*Q4O{+f? z|JA7FU+_Ke(i8Ol8}#cnYIJ$v@=N5s_9xD_U&1roZ$4}s&HXpvXwlzHoID=HrQ^m#J#c1GK-5_~Q-czBFt5vgzv$_xl$0o#u-hszdtW!|pg} z9k2Y^`Yr0^qIc^axbGA?;Cyl3{Fm_2aqiP$@{SK#$APX=eDtR+=fUPXv&9YZA?|BC z4}U!7cbL4_{V5;CzgwTJ_2s)?_CM5l{D$}T20Y~)?^T=lc-cG(`PL`JK7!fyOZ7?5 zI4`>LCC5kb8uKmJ$@izpqbAexUlIzvN4z^u-{fuU1K6Jq=Wil#VE0M6Up?D?Sw|$j z`5JwpQ|NjZ&>uEx@m$ZM|IPfD{`-7+l=8koOS-4$yjSDo9ffzf&$(OW9NurCe@mP9 z{%YT-u>-z2ANp7Eb=%C@Gw-kxc(3xW9r^_F#Y5qRed^;Q)K`~T-$&$wjasva=uXR6)@~lSf zIWYDE7tXU4a|N^Qmwd|_^xb^s`(+#hJ%IIY&&Z31it}XiwAt3BxY3EaL;Tlj=(<$z zzY8A~bP(}#=?3!k>&Zu?8~C;7e7O(I{@M6S2fru2{eE%ZX}0}Rz2Sbnlh##o4j;ik zxX*VLpO^)3`X}H5`Q9(p@dM8pMIZmlydCon-^Kf;bGi*~I){IDzJ3Scy^UIR^CrCB zD0SEvKJ>ld|Mhp62)&2SVYAM54z;dGp(|!v6`we+;zV}P@a%Fxi z@^p9w^t+APbKDg_=J8*eZ~0x%wqL^88nr&}E9R%{pzr!E_)S&Q=iz&3oh&?TzWq|Z z*GI3rX5A!t$5Y~ZC*p?ix4GiJ4%IpN*5&y`o-=CSeCuiK@9TL|s*{JkU+5h2;fL}U zwLjZQ^b4oqThD=qcKsfc2alrT8;O3SZ1+p~tMS8+z}pXaaqZ&(oZ}63*eHGuzahWM zr~6Xg)D9j``Rp=y=u-4AJ8!ff5qJo=Rlavwyh)q-_$776RuO-)Lm%&+{VH4HUtXY> z-2u1Ccfaf-O??0U_ zgn6>8ec9SC`;>juQl9aYbzMQ1?6`7^^EV7W{|&mSeCN>TOm**?`}M#twy0B@k#A_- z>*V`W#Q%KfP(GZ^TJ_`i=2^i7U*pqmpP29B-9nE|{@<+g+$?<(!Vg^{ zzq0?~5%Sy_?%N1BcE0^GAEG!gOulgfzN;`7d|5)bgKzt??-_YlKL2^mrF30+ zIO-L}!PCq;?7^c9oA(1Z{ggg}eEX$!b>dypdQQaLdDUB0EgU1}(}7>VroTDg{Zd?u zxk}(Rcfq5cq7SHQ{J^<)XW&`p!*4HJ9;N!E8~dYvwFS=CGQ4Cv>N4?on|=?9>YRK$ ztlp_6eKpIj8`-y=BK)Jrx|cm4*MA56Z}R0EI$v#k@~j743cOJH{sI3boNdneG5%5c z_!R5<^*%39C(hb$1Ad`zoX!1-{CpEVOTM`APxb>xBlJ;smjB^TTR2b|q`&;4rS~r1x}K*Rcx7MEJQ6TFIx|w zdh2-0{h*2u=g|T5sGncLuRIOEcKu$y_e*-bKDvfQ_ra1E&hULF?w$0?^YF^Eywmyi zOE`r;_Z{%gdE=hs6YZ#5q@!H)`%NC4FF!O7OJ0N?V*}mtV|d8{JlAeZdGI22^iJ@8 z+1@YdGOAj5*;a90q4>W`9QXY5g@TU&IMpq9&3yQw{WHJ|@x^-TK6-eCMy>ku75d9n z>mjIb^4%}_0d#}+!9O(W%Rls0z0bY7R=`I&kNM_T$``87yq*>E!zaAA)113*i+-@s z1L%EN3LRgz`z8Fa3(s)bI6d{(5cryXIu8e5ssHX%@Q-|Xl;XC1^{)mVBV6Ml@A4jX z$*sct5%R60^vCAoiPT?M)%X&F-_C(sf5QD4MBYn19sQN62lB-Y`7FJ4{}6Kmx4_?r zqVMK!?Hh3{73YQOUs2#=CGeSk%#o&UHeSzsj(qQz{P8-(i$!q5 z&DcNby7tVokOyy)C*I*5$cM9u=dZwfZxqdmDY1X%)$IpnybT_`=>5xAFRQP*s^MP? z{A=Jgo=+qmdNl5xe8T4Ob;##ap})IzL}!Zhly`~yXTjBa^oRbY=u(R@Ei3Hby(+xVm`wB zgYv0-ab7rRRsa6){k(I(Gaf^K?+)KbzG0jy@KeRbeD}+9+x7Le`%?J)-@~g^%o&-C zd7Z)!=Hb8c)nET?KM?E@7hcgX^o-BD(^7rbBOe}SuIg*<+3lkHnXgWfj;0qlwe)F| zanC$oS#{XixMzA7K47k3KD<|Yo2s^7IsI$Ax9h~aL$!n&^TC_5PWXp>_si!=w) zrGCWe`06}RalXNP*fHY0^T+$}d->v_eGjasL1zd)I!xZ%4*whR6qChs>$2h-&ZC6) zPJ=fcj=WdAYqJ*4yItHT=Jx=hUb=IrzkAG^cU=V@zueM#-=n{Ig!lVS@O!GK^VQ4h z2dMbm^Ylmk9=_-%@u5w;_yFD155&p4==SpA8^UQiE!8Wbr!+o=e~9%0H#nb@)_2fP zl@Di=?_5>?wfzVBMc{1sWf%GFyRU5?UUq?X-Dbb?okPz7CvF7qAe{GU%kvWH(+k{2 z@A+%;<9v0BbX)JZKMz6=z`DpkI-%z<-lq3q4Lm2`{Su$mkGZzS(dfGloDDq3{4D!6 zR+Lxe!vTa(4A9GuqeFhwLf=bYgYo+@`g}(M#}{6oFK&2_2zACu;k}F0BafI5=y`3A z$nQ7d_X>ST`RhJsEvM&I7fMn6HAad>MZ15j~U$e)&o z8?*R34A8ATC4ZV~ss6uS%)4j90X*M9_vKFT4)Cnx@7>@ht+Vm`W$PEStxNAzv;IRJ z%^mjb3;LiA)#5Ez$Rls^t}UQX$(Kjz9`)^mRU995&UTB=X*cFBJKtgsA2?~gdRhEe zRpXxoAGsKLqHust_*9Aqd>#EoSN`VD4%zwm6wQq~RO7owKL3ID4}H;lwQ#pL@DS&T zn}1&H-;wQpnb#9uy$lcb6kKYWdf9W&&hqc~;mJM)M{U;G*5$r7{oQ9+$JhAiO_L|J ze}NCUkL3zFy?lJ7;`Jc>Dy3(bC9hq;KgIoFca7`7BfTur1?R&>)faFGpWDaizP8LC z6F<74o3b9({pfm^^VKOje^o8K?FYYGE&0U0g14;DW83%e1IG{gjq>f6`YW5Y_0{+% zMc=&Un#G(S`UIvT|4^NlZ(YLwy3{AP(A(TFZwWr%CeL4`?tD)F{4#lczPRCaIqy}T zF&a7)`Ifyif5g16Df$NHig?_7^_TdCK6&34;A);LI?9|t>sHQy?>s|~UZ|h*)hW_r zG;8!2MepluA-^ZDjs8pUjn9j9*~%C5*HZNDo=}70zY8 zbt%uSYU?WPlWiPZK5TvKh|p)hW?jch*11bMhyC#F(SB{PZ}*wMWZq$(`?UrxKNs_+ zvejRz^Q&5Yd1K+r=DtL|V~xOd+z(*=P1bzl|NY=+=G*>J$G~|<;E#uxPv2=Nzdd8# z0leUowE3ZZD4bvDTs}ibGXWp@BYE*h=!QnAtM8L1=i^iKPW9q*s!n_p`Mu&{+wT{; zyw~jCEPVm_;-UDun0G_Gdw?!`n7D9IS1PXDkGbvgNywK+Sw}=1;5}M)KMLQs!8@+Q zi@orCU-a`?^Nl`v#yaoPk(TrTE9i;dua!^DaqqrlAD5_a^7*AWUq=_^_X&OSEcIoh z7LUBcy}AM}y4Z4En(h4(j@^qs8}Y^S)R&V*e2M$@E`*MVdsfV!Wm}j1LB%uA;3q#D zIEMOOt6Kcf3(n;n{MRUb+4<@eecnTXqdCs&9X?51Z*mT=MqDwkCmbtZoEI)y)%FX~ zd-Vc-Xas+#p8MR&;r&K9*DD{RE{RJw+rCYye*MG&5^gyGf(QEPSJgQN**-{51TLFP=9OJdVlisbD^Ktc^r^eEK#40 zL_ddgeEITu&kInUa)CHFZ=MJ~<-m7Nd8B=Aj^e|YFYlHA#Gdtq@O1^8`Z0J@583sG z@zL;IHvW+<-%y@$s8(OV26+2x`VF_i2M^}?j;;QSpO*5n;J<+mC*Qj)eBJZ&PJ=Hk z_#Ff1ZPuE@bRAxHy6F54;rpHM9ICH!;Ji2dD!|!V`gx-^PGDaw<2J$#^3^HIH$rcX zUSqd#-q;`S=V}pdwdlSF{5SHQL*czWbl2n5m-k{`ul)ASXPz(8u}m4CD$?8J<15Yc zsGs9i)RppUd`Z6Bzz28AdWd)j{7z(xhwiJfuOoBWisy6Kr$YPceHtRan&DpNJBQK( zwEg#QNvEX=m{`!uG#Xi!~Gp}-=I;?k^Jm2$)x9D%q zw=VSow5k8M$>Z-3KYj~;S=GktHOJ^+e!KdF^ZC!~Ju<)bJJ(lLwR7MR-U)X zKCFV{ZpQm0-0cec6F%E&CO=h1tAJO_Yz^u!7C&zqrVBcG>k&bKbrT^04$bU_CY zIxpb>kv}rmamD&5{Fd_J0KeWRiYoBj;i7u%KKj;0@TRIp{}4I{;rer}to*Y3(v0`w zYv}rsdhD(7!{CW@-;PmV=X<}T*J!dI8^!a8<;U}?2-o%f`T%{yua41E{4+16I$#lh z>3sOS>f&DT=B|s?FEvUYb*NVUzD+z_VgKgPRppBt!krqm@PHBW-3R1-rNLG5Di==TLRZd)6OX-w-^QbPN@E z?}GdJ$V;Y6R(@If@BFgN;qluszryEMbwJ*{S2)`_`m>+W7kZZb z_ANNTl-EoAe~!K_-}|Ne)gzu?;q!k*{rCm>df)GI@P69o@uKd z=iLV;-Nqic?-Tj~F16gxl2wnbeAj+M=OSLvFU33DjXXm*=!fW}t}<6JU;U+haX*p! zk{2U>2w!x*@d*f>^|5?q{tLW2Uz}InKZyB6 z_@`1=%@G$W`uUav7uCIcga1Lk`=$4{9dSVW^T728>+M*7?ccMW9{qB@b7;S1_rZem zy8bG{X%6s3eB3yX*=kW?IXPno>&6k5O0+49LnFOS&Mg_ zjL&O-Hs`hC|Asi9AF`jv=@-bC_X_{)Qg7^f?k4Z;3OwJyc)jme5l(!qCEZxQ{Zf7S zW6SxG=O6GrdW{bKm9d}Ta8IaP^2K?@fvB%MCpPk5;T(Hj7d*ofI?+Y=_I&Ho{Tje4 zOrTTtywo9leEeSytN z`Sgdv$@&q$%?DY>EZ(_UODC{H-FcmSV~F=V-}~i0DdB84ybsGqLQo;U}d*r?zAqn|VVZR#!Kh&z$*XUh+jAN?3}dY$*epWu%*2wcZ_ z4ZIQkU-{lI)g|5FVeNyWeB*QKlRe|6;Pa!dQ>d%+)hW(5%-bnmOxlNn{?WI*XFI`5 zsBT{4T;_Yf`r@q6?ge^O_=U($U!KdyQ@qGDsSn$ep{kle7*-Up1?Ndu%2JiA5eMOJyziGs}6)&co9~b3O`TX>BKiqeHg7<2g z`gycSr#$dnX7?ioUjIMu|1Te(BA@m)c>G=d{`ak?p^n1Av(Z08;2lA~;x|F@p zp}1aDUa^e7Z3n&iE$4;)9$jU=bqU|_{BrAeZ-QTZL4DODK3=0PyFvW;rl=ms7dNDj zGEa0X`h}!lm_QFz+4s@;1~}qn^d0%~4bN?-?!p&vp$PX_pwG7-z5uGLH|hUcC7$Hl zFX2rUdDMj8Tl-bE)JJuQI%Sr5MVqe2=+n-(F2ytZ8=epT82|aAJ_yH!ZSwceJiooj zUo~I8A%3pU-+w0dLGScEc>g~Ap_}k0ORiTqulfAU?E|Cz*~Z`Yx6CX19=x}zb>DtR zf8t;8?{Dbu$QL*4lO+Gc@!(U?OOe;wcW%x+82ITDcxpa;Lw(sD`r$XsCxCx0f@eou zabD>9nz|()4j?`?bmZtNKQN9)e$_KxN?+++`Vzk`(%a`dhr)9YG5_@reKc#vtMRQE zSg(v<+dOe`xFwx>KKxL)jq~|c_G2gJshDR~T{dRlY_AiYhu?uJ&Hu!k_v3TQ|J(hd zQ}|R2jFT4SS1at}FnmkCb0{36ia7|v6Sw33s9v@o(GAYuT=?Nghm?a@-Hx%l>F^iuhF2jSCw;@t#sd>wvrwTK7m&^NT{_2Q4PP~_{Duirua zDLv}@75WhG80SDwW8cS(xMw<_OZ2Jd%Maz>)#sf(4gY_a_v;FIt>fh+IQ`jzuRHy+ z`QnD+L_hj=ty7c!=4|ntV*TEg!W>oZ)$?MX_cQ^X4Us-f+{CscmE`L_+BPf35Nz|3Xv1XVjm2X{|gV1pPXB?OKuu0zTcVMx2u7mwz+~1NX z!t0YLAMe@EF!*tuvm@x+_IQUMInOG@d-@Rb#Y6X#6aT``OZV(JxKl6Uiu9DP!XH6= zV7_`;pZ{Qx-aGB@40^x-@jMnAEWT)4g32NCw8On&F^2f z`{j9u{`)K6yBqyq(l1+2`i44sn)!2U)XmL0+q%?0(`PQ*A~@*0`BwI)6FMX2R>sej zf91n%q^EAw=862?qPGtJ8u6GX{tEx4Iws$`gdbX$yo!(cs?S+b+-O@LTEw%@xA3b7 zUOn46G_OZKfxdb+bY0@V4(7E{U&1%+;t!Ay$5tL_zshUGxhK{^F?ZB+akt6{&YSm$jEqvOdPepZD+xLn(dDeYx;bW0)UBaR4dvMC1 zJMcm2)!wnLOYrDB*0FFO^VQ3`_igx;qk%WkcSD}kv2QW!K0`irn|CZSZ^iZf&AmS3TZ$j4VoFMo)6+3wf7XZeqi|AWi)jI;4>O_2Xip}Wt= z>k04moYcea;|RTj@+#|bhN$~X-~@^%`OcyIhz@*Hjmz8D+d3opr|z~CKhJ~?R(VN2 z{7`Z3VE&i;My22Tg!o}y${qWP@b71-Tk@?-{pQAPw!vw>ZT)(mxO`1~Y}EEO(|(<7 ziN{QQTOK%2dzubw=TtR&mTHU{G$A=&*}Tfw=Urq9qOvP_~_3T-MjlO^;cR~c@CUlvk2E2#vdl%{n9dT#bHbYNA@+-dT{)#zV&cKqi`EZ_ao zy1Mb+x{pZySdV$9y2cgR$072PhtyyB^rD_GBz|v%KDOr(Ka{WcjH?+3F%O83V7_-* z@t{e+>qPX2s=wnodF}w6?-qH;4*lp0MRiU-TvYmv_iN#Ti}>mN0M4{n^z*KH2XyD_ zxHi!?FLtzOd5%P^0({t=MlKp2hsvBlMNV7tf+g&zEn=*QSf^WV!{< zU&Q}khac+m9&eL3tyw2U|4qJi3BP`iIaya-f1uBsrr-4tzQpIP<16%gfD`2FHy01v zteKBhjJtb2qI_VQHGMYVIQIfCP@h%4xFNk=pZU@51Ncq(k7blaU-&Z~dHy!m$2`<~Y${-KXi>@Uh})2lN%a z2H*V_-tT*K>G^Or>z@<{mUy?$GACsXy>Hj+p?*Hk?=66n=F?TGZfw-b+h?isC+)Le zG>^Vw4)|K=jKoW9vXA-hm)@TS^L&>hpS8c9@cU-1dvv)d{>_2|d`sMazt+F!`F}MB zyurMvRr_Cu?o0DFsv1AUsQ>U?$JclY4$l2&zuhj zu>XeM=Xv_{hKUc$c9O|-l>gRm>rF^2I{`xR1F8znw%m47dY;b>Of_D%;e~SI7YUwLqh8|Xakj91c<%f#z@5qzB0`I*Co_&-0 zxQlLnJ9sjDRrvjU_`KqV`N|Xa4Ixgf6ukrQsIw+{uXoS|ShtezU3MOYz6L+9S>nMh z?o}`PMXmd?-c5Z1`SL^6lRft7ZsfhzZ%C)yu^x&%evG>MBK|4)@cVkM;GK=5>i6@#%hoq3uU{a}oiuMpUOV94J)-WqU5x8U&ysJy zR4-Tbjqbq5z6_si@ypghyf(jwkHv5J{e1f+{IKCUBgBmd_7?%y?nd6I{OW7-&Fo`7 z9#;3OAANZ0&)$W<^W5(a{NCjv-r@Vmw}i*#+b`#bnj;Jr5?*ye<_YvwRp>A@`$G`=>cA% zry0=4u?F5@{l+Ws`+Vz?E~SSK{xxxYq9uIb1>e6%pW?jh=-`{hKje$^(wqDUU;HI} z*9G?LaqvW*FX}l0dhbSw^ZERUq-$-`*LKRdDDU$T=4W}H)K}p1cd46C;`7_Av&9Yd zyH?D9eZzb7DL%cs%+KrKV=?A@z9pUPZE*a2b&C4p8@1;E=)O!7$4{e2>CjLAf^)ix z4sjU$VZJ)WzTLu`=6A^amBE~bpN4yWPjo!JS*S2j7RZ)*?+Bt-hp-PMSh|Cbv^iV{ay0iFZ(tM z&t53vwZ_RSw$cCZfv-(4S7a;r71cBO)+L?Sp<2A;5_rs9#7FH%qt^a?3!d}Teu?BG z`SM=*5p~gFzaXzY$9bIL9UHh`AoPdufZ%QU@(tlj=3y^{?o0g95c(R=^ZnHNBF^bi z@Wp)jp>Tk`$oH6+?Yfda`JQZ5nj6HA730Az>2C7n^U8w`)#86P!f(lQ5k0To{6D-U z`t#M6^#a-6Wu3zw=jwD(-Lc?z4BW7ZpXK$4gU;*KUzG10N>4Lz-NiZ{hmKzT5au)Q zpohH@{FmyKe0>4-2aqq;JUXFW{6t=cE*abgK4lTzz@4IgxP1A%^sPPfdM(e960baq zUad=<{{q~`bBaF$hs(z=8{fdM0X}k#{->8M;k*s&d+DEl56DGL*@yTE z`!K;=&MMX=TyDtymE6mG=TQFEZU6m^zY`y3dB+C$+isxWTjS?L;8pq7rSsQ!JS^@H z{U7!lz*ebOI;?k^Jbw#4*j7t@Ao#mn9ye*Bg^qq4sgJ|ioxm+9Ber^gn4 z(Ws^W`knEb^7Ax{fB(VD=*JW8v;%&9m^sZq;A58WUDjN)L+C}vi$4Ed>o~$kN%4Q3 zzWHatSE>&&-#K*NYP{Dv4dsKI%(Lm^OSwRP{3`fm&qvP|=lMKwf8b-$IgAzce>H2x zch8SqLZ>|mPn2)Jgu^uObJ%1bR%2e9^a7Q2%k-(NMxLmC(|qev{n)pkTrqEl&rLB7 z$GI$?-$OoMR5#~am+G#H`fC~;!wP;Oi&0lvU*mbw)SpH7D<2M^Id-Ahv}~j-p~2H>Xv+Z8_&mc z91wqbFrVUA`+ybt;G0MaeL3w@3BYMY+7fo{!xH^b(U$WNXZ^#kYQ z^^_mBjR!`Yw@<2c4SP{f%P;E^`mc{zXTEc&eAjaj&K1WCeI4Z;?uP%X{eA7nr1v~u z{iV;>44;=@?+eZs$uBN|zYoYmK7sFh!G4_wN6Z)Jr2}#vHDAClz&~G8M>f#^KJi>; z@+kAm`Qp6vCVrQPtoNcn<+Gyu)CL#Whk{7ECT^dBFL@Yvn|(*{83E`1 z%KR7gTD~~1_jX`k3ipS${@Z=wnLYkeVl zfR)ITlpim~d(3<{`1XAJr99}zmh&U?34G7F>xAzCKEJ#JPpDh+#ra>|C?g{w$_<%UAkWbc!df3m=W)W@bB@vJ=BsP_A>e7X?XK|=TPrfm;N=+le%b} z1D!@M{2ujw-K3tph+Zn+x>O%m?#p&RjqCmJ<&jQc3jX1)=lk+C-}|L`96jfU1zj(F z09WzT>%iaLhcDlD|9Mef=(!X( zybt6N55V6Kcp}}$k672?n6sJ<$5uYyi#%O8=uGem+RsL<`g5Cl%Q)f=JYc@KAs*{T z&xgRjEdm*V-qE#~#~KL4Km z%opc{zckP}+y}q7OW(~y?%P4$E8WHs*DL&9zIWO0lk0o!#{_Y1hIhA5Ju(}--hZ+G zzft@h^6`4&?FQr*!$tS(qV?qP44$8Ihk9}h-+-l}et~>(Lp;%dKDE_|2a0pE^f&b3 zC9YW)R&*Xu7V*pZa2xSJjr!W(|CxcTl>q-lX?;8GPeuu^*W7-)HD?)`*)Q<4=_D ze(48%Lb=;P2IB1y=&C=Ll92-#y$f0 zcIJD(#2;7amdJJxO89&NqZC^zDWSjre_xa+X`sj_n-wFN;|CA!Vulp;1u+En_ z|9ku@^X0wrJ?NkZSnzv`pVuPwWwTcP_yj&|g*fvK`GvM>LsUO=d z>nc@$Jt*=^S!a$%KHai#rb988(>i(aL|5@?JA|HXmp<6Z@I_F5nC~2_&$q(wb%y-n zEAY`3aM44a8_2&;1kR|wxP0rf|DpJW8GQ2p-1Q}MzC2I+2zrh;_D!LWD&INOy4u#~ z!54iV{au=S+X>vw_`LG3d2rx-d6fK!diXLd!;j3le;FRuzWQ_Ig+t_tC%9+%-Y?;u z?$h`LpQR;m->dXBG-~A;ThuYP(4l{azCPc&q=$OPdu)G{hknn%ZT6zSLAr?N;UAk- zKd*Ye0zbSL=S=nTIQ&WUSHd6Nh`hvp=-Kc?@yk_Be+qovz3?B>y1V9I$ul1K{X)N- zFCMx-#kxcD<>)H$t1#bkj(YhX`*sSyy?pt+=dmfT-vuWe3S29>N@%R0R71*y7NMuKibNw@0AX(S=*md{WYthYtwmb)cQJ%{`^V! z#F+2QwqN$$vCoV0i;bxFg|k()`4#)c;M0a4I3FJ6I>m9_b+P9kYMq|1_l)!UCGYke zeVF;;q49b8IH(gZ`~HM3Svs(L=GVy|@8FM^FW->vEBcd*^BDxt+*1GIGWcP> zbxG&dtewxR&YCmtP!vz9TJtUJJNM8!J@Vsxd6fHZ6z@If@L1H1@{8^9Zf}EE9U(5x z7U8P-_DkuvP;zAnlWKQE3SnnNy>Vq#`jecJFN{zJ?H~unkL*gLs@(}CGm-nhZK9~of{C+odUh*|N z&|@o3?pW^;{SZ0d4{-z?b3Xls@F>?Qa}h7lF@R&gN1gIDas6cEk&2tmI@>uEZ{FuT&3L{e zdXzbEngjnR>1%F8{j9n)Uz~UU7q~oq*b9X@=0!d}_OrZiKP>Mnc~`#u(s^qHJ`c|o zz7_Vll`dx+f7liG^%e1g`RYo=jYE-7P&X47Z?%kXSkFu!MD%Yvzsz>OR4*I%yZ~M^ zLjCbI{_|DvwE8aS3NEe*%T~?jawGXlNCg5nB_|A2V3xETh1pk~Ok?)Im3wm7f%=||^#F$Yoa(r9s>g#Op~8(&Y+ zCzekKE8Um*66*jSIG<2D((LfA%@Y?tM*lE{ekz~dM!LgB4K9iw+okA#)jn0$nKK_? z1fRAgeCqSXL*)-uEgjS=;@qEmt|)Qcy05GF!2Ih1e@>m54?mP&N*_J%b^4W;;bX6Z zyLHJE=b7vHGVY=JCGwp^;TO$Xx}mMW-G$q~f_H7!;=?{AZ@dqV{}kNj{dDp0*ZTpl zi7sU%bl2{G@H_$GHN$}qXpZ7i%Y6gc;=Jk)*UJwh4^&^6^T1}zG1hx_JnHM_zJMSf zj{U2B!A2+MqUv*>LRYvR_`G#`yvMF*=x50n4>i}Zs&(Er@h7`Uy}ZNwTXDZGQKua7 z?-jih`S1wBM-S?}c!9WaCVWMN_vVWms#Ch*YpZ_I zIs2KT+h}tRXUxCG{9E-qgZeeBjj6`^7KZ` z>G8X3|4`|K{C-`D_>M1F__$|_hxV&vpZMILJFXM|{l0C(^9>dHbJ(|h_sjD%@Iz@a z#}XaQFg^#J;KPg;C@(q9e20AJQ2qVZ+pO}Aog=Pq(bv$R-(iV-^SYdFZT;6zq-?s?xDjx@TcfJ=J4fM zghYY{}k z>ofIFX-@>M;9Qiu}Lw)hY607=)jJ=N!lvY6Tp-AN-i^)!on?>3-&`Q>2sZ zgX^vap5}G^gZ+Y~s^z!&vgJ5$os{%B`R61w3B9QB(R}%abasQ#N#Y-c@8NU$5AD-_C;GyK z>wkvMH(x$)|3l@w52=rznCA<>A?cw$@V>@=sgB9_F6+Jhk-Gjg>spF z5jcnY((~y7jBlv^oh?KE{AWc*ah;Dos;adwBj_CN5;q_7cgXiHD}FTX>*zTSoWmRV z54&&RsBuGZxcg~++J%3110U3Twi`Nm#lbGVd7I?B3+_|lUCxI`NuSrV&ITRwhVyms z(n0h?$VYIExfbL21LTW`ng`W&Tp+()1t;}fRr9pV*1vKd*U``CiyP8gSGE0>?Efcx z;5K@-W(}@LUGjzNm7@L5w=VGx{m|QphddE^yXu&#mcNg6DOcpw>EeO?8})i?=`O{J+9*=s&JQ6E{8060zIZ78P>=Zbl>BL)yy^tH(a;01-jAtM=2&OGbEtUO zurAr3oBqNDe23n)glnH6FTBdSFQA9Ww=U^>JE33G{F`m#G=UGo6BWgoZT5E^oHXA# zbiW?@cyQ6X#KXCwcdc2|&u4zQn2&be>^e2eSH%tYpUg#nhWajNndiF)p18!m9{B4k zugaItd+w6r{X9B{v6lVK5sBs>}z}k@~z8$%bsWBJ`MNFt8cWbgtXum+k zo7t9d;C%5=?`&18A90EP>rrssdGtat=O^wL^$c}NzIdp5(RIp@`ElcesuR7}e8l`&DwE z9+s`mpa1t7-?n_~(z|>p@_E%;XRXIW7u(?eJ)v*vHuC~!nfsIPeko6?%$rA_9(gV6 zsA|q1K0dGQ8(ZY_p6~to{onh!L-bIs_vm}h*Cut?I(67VzpVSCRJYwi-;r;>#1rkc z?AN1yn^klH-@-Rrw{nGiW19NQ^9Ay)OZkRzfQ!*jZk?g{qXFl0s_1>*3>-vy?R@7@ z`ilc!N3G*q@V2SwpVYqYqG!8Tq)Yz|{AND>OL=d{dP(~L@a`UAp9cJV7CqEh_{FNO z$)`&Ze%AMWa=sfpgyMeWkMI)r183B|e3wp-t@~m>hf%-JhpVszkWV^!}y`u}<=Zcv z!yf(Bo@;v^UB+5be|ZnR=mT^O6W|_Ciu_~p@#gy64d$|cL0#8O7LIK1Kn?A-b?#6+A?3JcRkP7l`*XW1+4m_;`uYL)g@*DGG;05{OhH&_Z(%ASr zH>4Bm5I;0Wd|3$p>@4q=KZoZAny=)3F+X(%-D{)PyS0EWYy`gLJUZWed9Qui9oO~! z5c9C?liG>=UHwIK#Q#tE_k6gGe8}34^Y~qFai5+S`D-+43G$soh83$(mQc?buPv@X` z_gDQYt%H)T&HXD&_K)&>So(_c#d-6U!mm$)Gu;ILJPdyGHgI0yIZwb(AEec9hzII~ z4n?@zO5hpN)wr+iG)9@uK%T zA5WzEq+=W)`mK3);fcDTOC+CfQ8)AN`SxpW{p#Nz|Np95{&7#x_fC-4USh88z;WJv zrR4RWr;YRO|B}9c1^v)P^5EC-CXE_DdioPb(YH+&@tFDIp?CxP*F53AEIFSldbi%M z>8oVl)pU)@=N$#_oNcLouHe;|i6@_$|10XJZPvyWvV2t@Sk>yI z9HTxt0v-=6i##L4I2rHkZ^`RYpPV9kGhUBr*y;=G;3r^kA=S@SL6Irg2) zmq*D*+WPp@ykAd(SMYwR?`RUdcZoihHE_0k_@VH!y@(g+Y`90`;P==Geil75JksN| zal<}l;$IiYdq0l)OLb>2@+ZA_r;2cq6D{j?{BC6VD&Nz7=nfSRm!qFZby%~87YiI4 z|55sN^5wmr7ohy_k@I-==Lz*@*Y8%GN4zyq@Cd!`46DHxA5q4*!#V!l6U|q5V*X ztUEzZ?>RrGiv16qKU&w4C0_V*=v^96Z#^yGq2zyK;PB1b{`2T?;Qg-AZZ}3$`6l~yB>eR3ukCju%U9z7_}4n$MMpEvd<5&+ zR=`1r!Kt26hvloktoPKq?in8>4}6HY7))1ECx9|F?hiul7lzPAtk7mqJ&g`m6T~e`)Iy$KZ?d#Y6FpRp_6z zz6;bLWBAXz|LbPpi_SmPcayLF@*HpGM8&;wpPS-)%#$)6#XfzA?_<8Wp}zS(eE3=m zeGs4bbxXRie(1~54S+AsP*3N}4~5$_f_DI)C!arsUbII&{g`_5x^aS{`P7)@jF~6uis^k z^jmPYOOX#N&KGc-eE7WjQ{JmR_f>rztKr+De5pr&+j#hI@_wTa$cJyZ|I2zO;zIQ0 zi7$zIn*8d6(C6r$<%=7_VLS)?a^Si6F0+nypDSw z@^w+%f9w4rFZ?|G_LNuUJBPw!?2C1^C?8z6-Ui)QyvyJli_C}JPRmz{m+P8OF5c@Euxe$ya~cK0o9g`EVPptE!b>jJD9b2Hz@vrBUm?4dK5r&kh#Esj;yBh_a7yWq6+Wl_!vC#Qk#CN}0|Dvq>qkpSB_8oelIo5R!TxO~0 zzV+OnLj1dLJOKXkNBXn>=*x)@;5m(&@Ab@nSMHZb_rFK|I2HJ|@VQCiW4?UDa}dBm zd7mcHbrt4~wC(?j?(%$5UU?P$K)!rK{a=C0M1PlcV9J}Sn!Zx=<&n>8pYzqr!m+EG zIrPNwJJz#-H@yozn|SuS(LZ3_a~$j>|A%m;gZpFOa_PXHbFcP7zu}*QXGi{+WI6FM zJReqlk|Pn{?e8TVt{*%R`Uc{M{lW6-0jyJCexChBLw7B_s>!_3ht$jS=!KHPftOgdU?6n z*HJj?N>RMAzCMokAN~K3?xf*)Y~;P0-VfIGel4Bb27MdL#KUFcNwfZ=FXR^=K>HEz zZcFFyTIf?07u)v#g0DPFoji+galUhCzEV2#*M)hhp$nA(fbZW#k*@bf&;aB-dk=}PLcp>W?z&{E; zF}`2$%=z9g?MIh;bDjNKi1@fqXa1|UbzyO@(4~Wm=EDJmNA<1C;Ji(vmmCFeJLs2n ze}MW4o>O1vrbcb{LX$;`}M^zvg!v{X@Pu?|vfp zGr+%sbDSW5w@>Gj$QwO>PyW66?w5So?E5{1-sVd9EP2j>_VWt+bT!VYaKwChl={V+ z)L&o1AKzuaE`;v^^_g{F{QPLke$Br)-hcFOr6X$ML$Sl>wcqzPy3sy)(|7o8PU0W^ znz)hg9NIS(-%a{ozqijV@7kZ#`n?}o!UcBV#U|15<--rPU)C+J;J3CBIELQl47#x3}fIq>RR z=wQ3XcVj)^^Tfq`c&~hxdJ*6Kj=663TwD7*x<1jIvvKAx=X;l>x3+)OD(~(JeaSE2 zl{@z7#20XvdToI?k`LeT{8#PIH`H4j;1^5iZ3g7`XV7;{Snog|aK3n`zBZr3A#kW2 z@UvU!Mfai~M|zZH|QSn;l0WaW4<2s{U!8YWAH#d;{7`H%0qmZw()bwm-nhKv{Cy|B@0hgjV=Jpan{*jH z_wCxJB6Q2@E3fK5)O~p_;wRuy`R>=R_xUU1)DNtWr(T=|C+s_p_?+_nN%~gu<%iOB zbwbBreTMf(KAi8OKT){UtofFf@PmADLwdZbRv+_rv7XZN0HhCSXzuD$>dR|s zeS7}e{>D_b`%$cSekZY0KeKV!2Yx*@1-u;DCksJN3-_%bbpxp zfaS}S?|!MTa?ksLFY)c*CslXaM{pIr)K|uLcpviN0M0jzTQJYZ{*>sY4q2y1oi-L< zm7nCRD|N5jFEm=j({12`FoB;#CvYgmkyYaSVfHKEx&UH$cQ2#Uoh7e0fzEtj9)*8D z9sTFhjpfsQi8px5eH!Ckx?_EB_{vF#Gvat3_^A5g^3`9$53BmKfB(TxTeps`Ynr&Y zOFz+D;~&OvIEP1y^dI^9IMg?KkS9rhWB;=8qJH#V@aBpWOYknM@V5DI0KKn+;77D? zPmA)UWArNztn2VSV{X6_ef0ToZ1wBCLk}=d-20HPBjkmBbg!?A>X#eVw=oAT-~Cb^ z_+I$S!yEJYkAvSd@Q>OG|6b*lt1b1n`Hy1`uJGb~ z`=xW$2e%$~JS@upZc%@kCmJDte`5b4{=0l}L->p9kfqQ|>U^!n{08M)OOE%@6=3>r` zeITVT%2%fdPuc_L9EERv#ra!n*(ZiMFx1QI)}^4U%=doDZ=Vw;bpLj%HSiU%Kou2y_ z^gdmt?pgr%ZMZ)ra7E#Bi!H^CeEX&NR|S4yyjT14A$f0y{P1he-3#)n572MtTbJ}3 z#(Os-ztwr$01pVAZ1|mN-ShOR=R1euhmFge4*pKM-wF8SCVmfRjh}`dR`DcXoHyPJ z?}L7Rl)U0N{E7LN2mJgxc)$hDVLp6A^=_B@@(pqBMcgOVnSH+BCEwj-y<70K`Q9&m z?lyVeuzjM#&s_bN1M;d-;>L61_3#w=_RGEp!q1MjJih`zO6q*iQ5`nF%-`WN_{)5_ zsQMhN(;EY)nJdqY1HDg9sh-eu+M1LF3bh=Y1}uQ4yR-;!VF zU*H??n7Oe3mV7?ny3CtXe^J*jdrk%XiFFXiSobu4htv3I=eu9(TkD2SO>yxwI?!SI zef#uBt;IV4uL=TQB3-QZWvLrNz< z2d-;f$_e7-lfZApTjV>3;`95F?}F#jN4d?r+~(`^Vt*U=og2@plKoHomG{Fu>s|QR z3F=7qqkkUwA$Sz~oR3d&AED0UBK*%Rej@YqTUrM(X+IeAnc%_s>MzgN)A>72KCxJI zzAlmncIe-jBOjatubL`~Gx_47@QIj*XkW_UQ?$M=b@NQIe$Rcr`tS4Ym(Jl^@WX}R z6ZEdl!$a(~)W@;_et+NRm3x>E?{(d%_xJ)j|Czv@)Q?`(tcyN~=isL!==Ac{Dbm5d zLk~3Wyx02y|23eVeh_*A{P&9cSCWPK%D(y1Ma>cihUo{GCyw;3Q)!`-b=-iz&4&Zn zuN?fz^8SF(U+U8}A9<`eKBy1p8FOLWZ+?zBOEdHZRF!$b zZ0At-WZ*uiLf@Bpbow6Jyu-J-cUR(l6aJkq&Wqn_(;sw|xUe0%HQ_fMuhYInQP~)-v~&__0%@pK8?dpL@-Fb(_3nhjW`R&U=n3JVW$R!gt|AVLjVCapR=* zz4U$LJBQM3y~DSB$e$NH;C#$Ez`rf>UihT6z6Z_^!Jjx^R||g8)RCTVc|G(GewT$W z=JSEkeQ(sNr;Y@#V7}kF6@7mgJ=-FA<8({&e)HWg@#c-%{z=v==MKT)ic$acT%n~&bG<#|!YJ>^r`BmcN%U+n1fbw6#kb*b;^VE(fEVwu-N zKW`7b?W*sWb))!#=ZhQeckuhAyy6UX*n^^X+wt&<&msBmVcvy&_`LXqs`mc)t9?ZD z4!i@Ox<|kAMC4n_qw?j4%9E;EzSmz7Hwt;um(*Wf`|U*@rQf%YRz7{N^r3CnC7iPx zzDH4C>706g>Qmmg(vsdX-~AF!-G|RQMSlK$^w&tw(T+X`@$5VH@ue>y-~BTGC7+u& z;mfA@zfx4E^r<&j;rX7}|AzSw`Ocy0{kOcclYvh=uT>wS=cvAnb1HwqH_X${hXcs> z%6&EW&D(+7e1>mN$Mf8buZ4e)^2&U5rEzTDFLYdA#k(s#c?bQ_l=Cdl?L;q?5APK| z*r=KNMPKq%F+L@~hQ9SS<|pW<{}#PozWq|1u4?>~zz4r9!eeH*Pi^kqJoVTpx})`$ zaL0W0viHk*A^pC@{TuWncF=Q773CjyIH#A<(dUaB=2_|6fQKC=KYYgA+d<^>TGwg& z;Gmnzw_l2fy_nag&wZ5hwg6s#Fh|Qc74uc7Yx1p2bypwV+H$NIG z9#QXizIs`8NmbMT6z{HlQZ|T>9qRvM@xDp_b_d)oU%sLE9z1K*UFydfrGLo%d>^4( z{xp71J`efcFWsl$BjLe@*|+2L`E~-o_IiyE%Xc*2IW!NeI6v&Zf98sg;pfw~z9G(` z>ox0|v*8=!D=YA(S>CO4p*R0S{NoruKLyWn6TL^i_e=T)&l^2!J+g5c@`|dKF5p(+ zhv=5cukytW^+omgyhqTLZ#bU_eFp0)s?)ZtqoNNkUtOvBo835fx@U9bx3}QQe+*n+ zzD1Ml=V#>i`PQZS(Q^T>QeR$h{Aj&7@$-oby+GhloUtM9(myLIbaM1 zV_RUTQBfow6dIK^5Jm%;ClZkeAt&lmFB@g1x>T3yLJEZxo&FnrFGYl;Y~FK~O<^z| z6~;JIaa+HP^{pLbT66831QDV4bdUc3>}IXC*AD+7=>uFRea60#ozO?2KdgSPU&)u3 z?T2UGm-`U-Wa^4$tv|QoekIORaVH;7WIsLY{FSHI`P{3_3wciQrtvl70-R%3{4n}u z_azP0y)V%tnuq-^_%Y29tTGpn&woR6|6Rv*>WBsKi@WIMyUZKT!;?QOiZ?Ul?R@W- z^-3I(MnoluTzrPmmGHYEJx=PQt<;!38J8>WEJ-y&l=ueupaSzq=bCJiC$MZdx z^ic!rl!IS&T_HWGeFSHWM-}`7(&AC(zl4h(#uwma>_haNht6-b629|I;BnURWve&D zE5tox55o6F`RhIQU=5tNitg=S7S&%*%Af2CaPr}z(y zR;;7adzbJ1694}L{o-Nj^bzz?7r+TSo);+QY3*mB{j&M;vhGzM-gPN>6#I&JPSJig z;*Fkw`+f|LlMg>szS>`d$J?ikd4N%H`ew}>UlH%MK)#+#%U3ENw`%z;eIB?dzIybP z_P3ey^RS<8fqRWUd-;z#m$h^vU#0&ZTETH$u_gwZBK?e)odz-zz_BW4x0`&j4VjrFL_1WrP?YruQ zJ_;RM<@pqyXUBMy>nr;N6zO~OotK`=`21DpW&F!773snnwRpTu^4NWN%oUzLU)`%d zIp({VW1HveweUaGKAep>H`P^(%njr_FZ(hI*Ih2k)2qB+ZG2H0`0Z`;yrax9uQ5N6 zkJl3qJ^;sf890;l*xTgk0eRqR__KK*f^gJ)d09A32flTLc@FP+Jj?#g0eRYe%uJk@ zbn5wVHt9FI)}7!}IYr$2fH|BW17Acp%Kdx@-j?tEQolGPA8!_LC_eW#e(eK%=wE|Z zouIE=uDtg>TmI61l!1Aoz&F(A7y4n(y&Xfpb{G8m1wJbI&dYPW)*s5xank%bb#S9r zoLDWIXBnr?U#CvY*MB+AOXq8U+wti4g=cur&wA(qgqJR(bIzBS^*(uTyM134tutr7 zYj6La-lylp!>{p2$fpO;d{>{kVUhP|gT3bN!*`Z+l7 z1I}w5eN->>Hr^*79sLaTW4`>Qe%bS`ZE)-Fq7M;XHKg8{E5doc2hVwh-YDPur8sC` zo(=Nxb$G88`pB4v1=n~SdI0kh+47hD4}}-b^Y1@T+?xjn=z8vuI@dhqZFDL5;=GrZ*z?rG-UEIj?kV%I;2!z< z6wOBt&`pj8?}hH1{cX)!z9>&(enUF$3Hs}NaYOvqP9+|%jSl4^{MKyX@A8M84xUzc z{xrCDKAuSLSI_!|=vUw)!Jh(WBcANtQ@vOD`bx#=Mvadnx~GfiyY7S2_hXNR^|G2@ zIa`FQ=Bwx3KbhZ`u4@tf@(g*({MU8nhsNliZ-DpaiyPt{hU{@D^k1{Z^{xE@q_5c` zt{h+vXB$7?d~w6NW%;!)MLd+A;dvo1)#4@AJ!j6G*a~%HzW2-f4)uKN=r`ubQ;YBn z1M2;;f-g8YzzOQceD9a`NqJw}1p6Fb1l}t=bVywEyqo=&4pfRat(tR9^F@87{jcBW z9FBl%e1SjP0H2##et*z?Cw+Ck{H6ECeaZoN@-h0VhwP=WpTmW?kE)kPz*F-hWOE_)pt!$ zAKU-%Ha<-G^w`!%Nx!uc@j`X}O88V*=cRmiqq6V5aL0T&o97T!|4x&KZ}WV!{irS=EJ-vUs9a?4!3Yw)*H-F}J3; zao_zi_>O&H^3}b7^{DKx@$1M9O=g?)0GpFo%huiRD%dR6i z&wPBP`V`L-t%BEF0B3rR9J5Kx`-ONO(Yj>EL-_KDC+gQWz)|zvL(MrfYWK_Z!}Q79%$xV&Tc?Td zXTv{0ykNe5S-3^hJP~o>oPA&DOZwn6V~&T`(N|gbf2m*I`NerWv`(e+d1%i5Qt-~g z5kF?{RKD}l=k1|Sn}YwjLVy31eBG?AgV6IlE!tzhL_eLc-q8GYv)0!;)PLuk&w?kC zPW~Qz<`n(%8FXLy;)Z=HJU1yF^LLS#UqqiT z9c8Ol{c)J*owqI`tzJ}f8+~+73-lk)i*yVd_`nXszg_bvi=OkP4$gO8;*YzP@SEk> z!=XO<74Of0bNQ>#1qh#eoz@3NICj_b8I|WX&=*#Jw412C$M|ruFNf-?S#Xeib(DNG zd!Yx=p0XA4@krpJ#C7t(Y|(o;4-Swof2ptRR`PXdqnCdcIGT7N*Ts{>jce2$-ancT zpBL}gB;LJZp6eTQQEl>bFLZg*!EO^bza-z~!`Zg};ZK!(qc$EYes2c-*A#QVoj4!i zH*-F>w7#jDKin}M1+Op@`AfbCoyzZ%eMRwQ;^+C^FVz=sm_J!4nh*M>h%cJwzU}yZ zgD>Bt?tg@CHedb{4qzYKMdp*Y9Y?4G_n{*?if-jw_bvEyKgcbzWk**n||~s-Y+6N`vJPwVLXTW=_kaEn6NO_|Jn|e-=I*))S;59QYur=bN?b zbn_|V`!|ylL`n`S5@|~CGIOy|z-|$abs>J)b?mZs+Ro#aQhs&40r0exO z`Az0BO58X5RA~Nf0o~LjI6<4bE1%B6e57;zD0-q8vZ$7 zzbu~4I`gx}5$V6K75AO?1!Vp$Z~);v`S5wwhaLA@@ReT#zF{9}&EtGvzT7%K@>f3n zhW9)w-cNA2P;3meDP5A_`X_r#b)5W;G^^_L;9B+=pI(ljUL0V zBVWDY_ly45d)eSy-zRSL(bX@2Q=N+EQ=gr$-Vkr@IgPQH$2PtzJwwmFHsyyu{?p$} zJ~o-tTW8KK-*dT7vCe`1!{>$W#(N$=H4er5b{<_-zPO<|F z9y-Gf`p9Q7|7BmuZ1{%ysXp^~x0qvCB<|gz{%h6RC-qVI1)3lGC%ZshzPO=%8~)r2 z_(iXvQ(nW@yN8bBx#KG|1!|m`fSDi8jzLW3$(t8#9 z4d>m;&mrEg@e3U868CncNGFl+yoB!#qd#`PqW9@BI#=&ey&3y^bk67DVe<7Us)Ks= zrv!(Z1`j9{v?`;8*$1%lkw%7kbsY4Dhr|MSM$#_icsWU%_{8690{S z=jA=$)<-c9ThNDcKHfuTKb32R_b)BKtoJ_Vw9D(5o-ahxe*J z_dfdfnBUl{*gsUnXLjRz`yU9uIbA%Dl&#(nj@z+MCHZR<-;_moh)(cgdY<=UpHEhQ z0PBRfXW;`QoaPGkd$;nwN}caAdG2TtZj&z_YHqCqf8zaf_rpI`Iu!3ew@=n1>f(8H z_WAIQpSs^a{?jIW|DC|6m3K#}2mAOrJdFNK@7YD(yL{)RzA^R%IDRu1@SsSS68`jo zFPgX2UWk0prFgajE`Kuo<%JhsD8k{p;Dj%XZ^xcN<-L6UvUD}ATJ`Nq=60viPyVim zmuS}NA2*8nnsx5qE9#AWc(2Z*AN-c)Ij*8VJj48J*SswK|7q%x_u0#luZ|Kgxf41( z^R3RanxhKeEb!j1Ij@VHU%q>&d)4>_zxBZSO5%Ojevasf9#~i3P+n`*{&&doRe8A^ z`?9TzLN7)A9)7XJkx#r2A+3%`_~9UQ*6jPHzIaOhZPfJr>yr!AZ=-}@zecZhEO zL-4{Eg>$3-8dTDwe~UhJgE_F3qWP+P`AfQ!4!HG0c#=b&gDSeGed^)E=2e)tpP`=5 zr)Tr$Mt{fWKOS>=>O1WZuok?!{YSLVBVYd|f4LsIjqiAWzJdpPRjH2|#C!*PZouD` zIM;m7rF}4=KP2BijC%d&dq}QV=9lqD;C;+@5C8HX{p1*BeeVb0PzO1;k2tpuIL1|Y zqO0^@r>OJu^(n$d2h3xPM*ZY^O8l|?+fJj)8FRb@AI%pJrI#GI&j)Xxwtg92GJJ4( z&z`xCVtz5-c_}Y@j^i5o!*3lw@bl?_4~+5i*WAl<;ZL8Xe)8wAzE?hGb45C^*?5nH zBM#Ve{)%&bQZ)CLS8wC`(0dzrue=wKc-|*(pLINO-@=?gK7LvFVAH;G%$Gl+j+jOV zW}o&M*BiWdukh>0#}ni74tA9lK2`g=bir8DR>?lA}8xzVqgqo0rd(*3scVV19Y?|U&1;5jM#zREAPOa8h- zJvfe@?RKU7obUb8=k8b9Q#r|8;i&mmbV9w#&uM&sz7ibZN9IejoK~YD-BYVp+`bt; zx8}JtC)>c+=j-qZv_GustbFGs-k}@!(0X#=_S4+Af$JypDTVnr=Hc=^m-iD1&;BHI zHoj4wEA>2);>>OPR1jzK;cVuI#b-^SJDCe#80jGT_O*3i8T_T{=6v;`&ZSkOFU0q{ zzzf}vIStRHC|+Dc-_d3-U%tGod}aUMrI_PY9C=DT*s%Xi@D7SA$BO1@^Wiqa(R%S- z>GQV1M^9ItZ}$EN#lKnVl85kL`T8%#flkG~a`31V@%|{D48b!_nMXNhowNBf$C)f& z)r6ze{6jsbxOYc zr9A9-ct7}l^;cu)=RM~;4_<2}!xuilU^{*N)oKwJ!5TkqQtUS+eOkF}ly z-EzM3G7cbr#|Q40UFTNP$9rGEjQQHAvvj`so=g1d5WHfu2;W#nN592;)G=PqUdv_I zS(W;Me7Y3*O}1+DTFmVkmx?;d`$yDw-7feN@!sY0gB1_dsD}{G@$TW6sd?59kJhhZg^w569Md?W@%<&VzGY zGcHB{G7No<&glaAdCK*Dou!WV=kUCPaCYM~x83g-={LHer}X?K_ZGc<{l!vS0`*e6f%d;*@iP`5j8pFjuD4qcmhT%UiIubN+O z)a<2TzF~%Vv4|ha!2L3QZ8Pu=A5wqhJ1_ZEHppM&@WwBR?|V9P#lw$^@a{G9VAPZO zo=f#z6CC?#)ajnPaowxDc8I*~`GBiw=a>J>Z~o%V^0{@r z(6+vi{WddvZ{MtY__gijW#M!A?xB2^?bCjnJxdSaQ9fp#&Ai@P=%R$beGk8vFMp}- zwJ!OB{iB126_3=c)vr9Kp4uXwye59O>MS^vKZp7Z&($0QN86;Yyv;mmuTua2hZrvFf5b5f8;X?BO!>W#*BJo;%t1~*yzK-y#|?BV`Rb@&?*gE$T6N-j%$>OY)qB^dH7B+R{`*Z4 zZaM~^neV)W$5{9OS>V`;AE)qJ?o)qEcz>Y#Gwx%)=Tbf%fD>KA5Ksw&C@UU(T~p4 zPu`95()?Jy`7g~!8t=V89Y4<;;xqPcG-~5iivJhU^KGJg%cnn7oNxTXC;F43diOzP zKTGQagl|t5?UUW4j@+w@fByEqyYK90?)t($0r;pE%@GZKPlHD>E|+yrmhw@@J_pQ2 zEf(R5>pYk9+vn`DSOTv)!hW!P^`ZKC`yW1!c?aVF%4_B;50bw>C%zwLz9XL=TRi0u z9AKRHs%^fNx$8GkS4p2U$K2%v^L_d9m-M3Mm!G0bzU}A455+u^_hdaV9ztK1?;aY* zR)07d{i5o`dFFnHoL3uO=sxo{tMCr_^0IXE1N!;<4M* z5LF*W-$*~Za{=D3^6oO#+y7>r&)%L?^(_nV%vXZnLT}?er2PJU z;%7emP&!fbDXZZR?YiFkwBh-{cV1C9z5uVv7Z2@U$X-tP{xighL-0a7%-77pqfems z+GO8ezVni=@V;7n)(ZFPwc`Z3ul>=-Xs&7n9O@NwQ~By%>3cign-cj~zVwUWh&|5* za1YOiF3kH9-~RT#yPv8~>U$3x{OkAV9;Ped1bh1{rGMCtxahq8?w;W7f3tAxf%k0q zyqNnshu(JppYn{l@Kor`f8o<1uqrnIqyLeo|O*=P`}&+-&i2d9dsRqo}*dYH%oZI4E{vhp<8)- zRC;$m)#n|?`=mO3k-Bgq_7vNf!}GGtJ%ldo-95qE|7Pjg`cWV0^I!6Ffmd5EdcyjL zBHZzETHhY|v=5l)U1Bb8tQc4Gz5u<4M*=7C9JBZ3y!~J8-ThQJTQ~4q@0Szruo5~4 z_8*`>T=ss4(8<2LCwTkctk2uC-V5B~7IR*c_-|N`bCtgHWau2kzvRQC6t8=}f9B7L zi%00k2JA0eD0=@+$NpdMeSiDE+PnLyeGfcWqkj1{_0%2C$G+eP125G*y%j$0@9qiS z{x>UswV0ECZTt+q==b>dG;44}``VDVZWZD6`RWbfX`UBv$K1K_`E}-c-jm!i*99?SP!(%Up@@pE&I8^ni0oKHXI^Uy!o zpN@Tr`S4!-dv`1ILaOJ_7W?K4H*M750`!|#jeivN+pYTTZ|}SNsp4SAdN1CaP46Lw zH#dHNqA1SHdrk~|G++J_9~8RDs5|USWPe1?lXLnAz1P<~f4=jQ?zmNZ&zyA3F*j5+ zXTGl%9yd*0GF#|Fi62?Tq(;s$;w<{!YL znw;k{b15Hy;VYrTW1cVgB>TDP{mU0O%oFLU+r}>{ z>!jo_WgpIU{2o3l;sakX=bI157EiRVMvn}BcGU5n_&BVD>%NTlO7m4)=$7-vL*2iQ z>pysh$KZqysB=5iA=kip??v6~{_X9z{N4T3{=AOk;D?SQ;MWawU$gWt-Wzz7J}n== zYLulmYEg?vz?gUeSR3hy1#??1!WW~^wR;1Y8h-fua<{X9fn z{2qBa-yEX)DfLpELNO3F5^j`i*?&WuItpUUXb@QAde)9-=Rr zKo%*gp!VzDTf4Up^uf`r`y??!!$5wy+A$jg%?6-XT zzuLR|srR4Y{}=nD%mdk<9^ZZIq>9f=eRaNgsJ+X3KJ4@>Q3w9*U7#xF5Xsk1T>lpB zWzH85|7@4YYt)JZ?^*8vA9RU&V@O-+2jt_nwh)@W9wNNseqjfFFn3D?t%Bo`$#w8zpgQ7xyBr3zVkA_r8(C%;@wU9{cZA9mpbIS`xWC+%n9Vf z=T#56ue5IWIsNi2>R<1}c|l*c2_Lb=*ZuX|Z^OI$srp~f3!UbkjdEVMIiK)HByODL z_b0&{^Tm1fTLbE)g~-458L>Yi{(65e_q;E}wQC&--(rs1LE9`BC(; zu}=V9WxoDPb5MQH(J;6Cyn?q2ose_@*SL4?FK?m;$d{Kr$D#W+VcjVB;q6NP0A1r6 z;G|Du?nt;uzWz(^i~XZ+fyXbwmmH>U->L8qfInHquV?{XX})`?eB7$dpNI$BDAwNy z&-L8fsN+WQ`6_PYyNBk#^!d(t{>J__)Jc2yQ262__F62%&*sa^>g)UN|Iwj5M&~+5 z+~`NW>-g?CA{;K?J>0b^`yhpYkF7^b-8*B+r%aycDOsuWbwewMFAn=zAOZK3-;?bi=+6=w(~!`Y+8R zH)_uzGA9-Niv0noFR1g|5ig~8$X6e#-e}eGBb&!>ZQc8Nh=MsK1bJ=~e~x@{ zL;6YIqc%LyNYQ-gDEga0;8d>j?RO>~HXm*ye61gT%9<0{qW|AU7uv86!g`|cAiQ|>3G(4lp8t{$_9VRIE#?^(!xvTYe~dYfbMaO8 zGGE-VzDfOu`H|m{pB9-X??*i>z1uA3`v4w2U)+!mcE~*Jlh6ZL_n`O3I-;qVdsBZs zhaYJ^U8VIjo|95Odlu&` zet4EybfwM{?+%au<=-s+ap-!T{^WSUhnVxRU(p0}0bk?u(B}Qi*H^kvQQy1>E`1k$ z`E$;vi{JMa{7O6WnBru<=W^Wee2R1rdp=S+&(M8=pM{ws*X4sd)qyy!Ct|rpA=_C@EOgQm*s!jsx`-UpM9?n zJ*QIS&(W;CAJO==p8q2M9{J*->ZoRI{gC2iJM;jahj1MA+>!M*!nO0^z3MaUbNkqJ zA9Ddm+4D8zT(-bzu7iiZB9G;ZhuWVqz+Y*G=XwcmIa7ph?3nMeUYqkQ;BNWihWZif zT(5&Kt)QQL3~$mw$F>wa3VjN?oP2TPz1_mL$8#*8M}9(o{4w{;{qhmxqvWyAskic- zm+Er+&RynSoq&g(D9T$+`v-(>-o9DtbMoq3k8mTvH4j+_5sx9}eW z_sv(&yG~L)b*p#|R?mBh&)sz&iB5Z+`?>_)mM_fMdKs$Lo6JGPv3Z z@86NYtDN7me6`Pn{pEGP7U7N8hz~7%&K|=*&s3^IR>*Vt;)c#^VE&jrwo9JBX3n=~ zUq|Zq$%u=($NA#Mui1B_`u8UDCL7$ZS>mJj&|MAx8~4}B>-p+K;Ti6SZ%5zA-VN$t zpXY4+UOf5&x@G(RWO=(94aL0yxZ(Hs9n6B;d;?$9WPbTRx`(^ym2ZFpcpi={&~;Qdu>$W?V7dcW2J9?75Wb8zVh`c z!vDJ5tKSsO?~M{Kj)d=laGmRcN0|>3Zki7l748|jfBKX;^449R!~3KjRo**lJXm~E zzIsESs{{VtMi=m?{O4VSbR8Xb$9%K-SML=B-^h1f@?-ZL;%Vxn8RFjO%unszQ*aLY z*4_b9t|(bZncBbKp~3=-KQ)zXZ-U7WkF$$9(v_aHV~F@Hg5Mj)`{cb$-j|?1pATn~?yy-a zew^i=9f-Li;q!g!kOSZYw+lWvJa@kHlHPydcn~@w;TX@^2hgX!eMDT0{Y&5?`S1r+(UwW$9-rM_LGUO!78xdVT6-g~l%kMDtx=EDKhKed>5_|9>@QeFQI_5A?fn}y&} zl)pE?z4P5e`x2}FU&a?;-1s3n2k*VT%Y5@Q&T%Plf^7Y==I@#{x@Gj`k)Olg5Z!#t zH?tQ89c;chuRd!B{rtLoF`)FlSr@Y_iL<3H~J;_Z34f|hW8=zUU_fDBy*OQXl8T*|Z-e{MX1^H<_b<#P<#FNuVCN#XVe#`Z3#iS+^`d ziupJ<p$jTJ*Q|r;$-NS`JtIT&DMdJb8$IQR)c^V3uV3#9POJymjQp#Wov z-38xWFs=jNlCO@^bM5Uz^tqw?r$6pSooii+=cJ75WV?svBh{B2V!!f#b)R1ew{O&{ zw_f5e^;hs@+eL-~`QnE9l2-UN=sh}5JiLXDVaUBZNgZ;He)C{je{4O+8~BkM^kH-G zb@#Z3d%hw+$7{}jPvz4CXpW&DK6089xMBP=_S%|15&!a(czBd~seJXJ_*M7$v&`YS zPnm%~aUJ!s@wQ61$m77Pvc(PiElXc{C-Sf2+}H3Z9rs`O(2WOvBVVn2eTw2?lX<)a z^6agGPKfxlmm1s{XiCygyHIXG zeN!9cgD>mx=3HSQVAz$Sly1*Wfe07v~bMq-n#dW>? zQ;dIT58tiOi&~eid-`Kqd|rB9e#JmwaEOEUZxT)`TmamQ* z&RdG#dw8#X%&3dWU+3u8CfLuM?;h&?-^U)}oACReS;xWL&HkFX%OXDFOo6XuuRy*! z%D(!-ZJ+Bvii5Y{_}co91l-b@|T`rZnWd)K~H+0zIv4WmhT?w zz1iEZ=eZNrQ4^eBkLQ};IoH8QpQgpx^qzQs`P1kZ?aL$nq+i)D6`qWEIgS5FzW2+z z6vf4D=Doc4Wfh*hS$oftpO3tN?j|2Ds(#sh4#BoDIWTNpCv0d8mccEwSAR-zSs99x-R=m%@q1c`myKC z&*po-)Q|ivc%qNkUz``;It*OKI&$v|aJ>Ou z>Uo@qAHqfRy!Xa%3s204W7}6m_3NDdmN~CemFnIed29>6s5ZZUh5tsr^Ai8xunz@%-eL1Z;a92p z{uIAIAAPfUvV6}aoT5=%r>uHo2|tvF;4{{J9f|W){rq0lCtT-K{8RGrm5PV1|2#)?FZ3zG6xIK`*#?ekT98^`x+3tyB*&E87tjCJPD$H9B^ zya}Vv?cof}GqqfcgA9w1-Hn?fNc&K>Li#{J*-t{3o`j7nnS;Q6NA?^pV zusH-_=g>EjS=)y_qneJ zzCiW_npEGX2@fA!0+=tm*RJ`W*#=;xBURE(^H>(q8Rt} z9Gv|Fvb|r5-;J8N4RleTGUqTw-fh;>H_w+h|Nf`{MZ3_4qI=8Ne@WkD-^RP4dr}`c z#l74KoR|8LzVgc=JzPFqRCPtO=H7wNY%vco#T-O0&QJGtGk7xjI^?SlRX@E!fA^C7 zblCpG?00BV&(Fm?Ec_*U^L+J&c-21q_$<8YDEF+eUvFq%(TMl%ujOl;kB3zr_Iuy< z{Ec-C^qT{8*H?+}M`M0lb$-6*Qhad!X%oj!FjqMj{!q+G@my2R&*0pxI$NAqylB*_ z<8R@=v0^_!-ls7(dpzcw z^E{D0$)E53*QkY0|0Z-bdam2Vk6|TV{gvl|;b(sfUzv|LmtL}mo^{Ro$=FL_pHA~% z^1)i8o`1mk<%{#$*E6hM{PMiT*t5Iv31QwIu{;y>DK1{Te!&&&um-PE4*ohd$>hDYt)J_ubkJ&$LqxZe0_>^Ks&DctkYoc&?x&xe+WF7eHM<3Y3Ea< z!)OJ+Y(G8wR!C2@zt(%S9&`5Ew=zrpk?$T#pD_p>IrRqo)?@Yqbf|NWMc=7<|I;E} zNQzPzk`H(mO}JI-6wiIdcaUEZ%t)(^R_ zjQuKkDqejO)lDt<$nj!)L-~5U2w!Z}-WSXJ1+V^ux<4OoW1kB94|xxw>b^F4xo?3b24>Q}Y?-0#p?&aemluZzwx-&~ve4)3M7SA5Tuug3zH zv94To*ZrtByq_=I`{g-A<7~c%#Q7rrxliBxl&?392Zx?C+jHrD_2E&j`SY`XZk9Ra zVfaP;{awI!0l%K9v_2K8^Y-u!_Ak*NU%@w|AO6zvjhW{8KPGPE%gf^bo5bX3YKUiIyO&-b$UoTRJ& zzDTFns8#Ql(5=Y7bAkRj-+8J3vTpKH=tA{eRIPwK+%l=k4NpLpVadd+2!w<=Iv0h{b}R6m#-DaE@{Ml12E5XVk;_@I&)<;M3&c z>7sb|gmd%$qC-5_1?Ok-LB8j5|0NyyCxv*xxn00_?nmQr;BRZ8Bl`C0q2Ej5^6{18 zQ@v*-^fct-$L#-l!}**Dovhz4)hqe%L*Xv{zzg+xXNY$*@O$RXkANGt@o9U^{7OFF z!TNdYB&{>Izq0tue#C#z*)y+5JjwT5_R}+;fPUG1Kj-1N(dK=70#0=lUawVW%U|k0 zy1YM6i~7k!+`nhkht1l3zkYwrcpQH|9}b{@=Z@n7dkyUC{b}X=W*;Kehu&N1dHXl% z{6)QoPWN%Mc>c@pnclno%s1b)4uJW~v&?Phn@KKuy%L$lW0-aPs082#qUqPU;0K6D-Bd*=NI z<+rU8Q!l|e9mX-pY!21x=#)2sCnzXiuc|KCvDZ@BbLLz zO@5Ed)cg6)OE}w3%x}=&$2^g6(JuYtisJ|R#CiC#eCMTm*udBEG5P37#6`^mcu&F; z>*k~Gmo6+{U#U3n{jZ0>trzG=KII$-_}M&&bCnM6PUr=)^~;(Q8iHSZ7`i9;2j+e| z@L$VCe)aRrVa}s7$`|K_w|AL)eMp`3Ug-XsHGbeXtADjih;0!c^XVLfH|?Nz zo#6A%gg#VrHiN+7tRu2dh34Jz)xFYVTbHs-|8cRBK4pzLU(fU1FpeGbREn4R;=J;d z`}|v>kCzYP73%sS&pAnb_#5)}CH(30#d+xgy5JR~;22x%3tU6ryW_kCf4LU*qx3BK zcs;#e*6&^ckKZJIT*Ci#$NgCF=GI|q?lj+XDc-%|UY)M~tpozpgO@AS`JLd)mG9Q6 z3n$47`QnD^jb@E*9v#;R@#7Bq(MB!bjZNOK8|cB>%t_^YF6E&v|NdXY)7=WbOL@Cd zYj68jQNErcE`EcLK)&aae!+2i({ViVnEW^pE2V3=YW{`(G9Mo0{SL-=)n^`H?&K+X zdMETn>d(&Lzj+H^)qHtbIJNhU-l)(4#QE9B!uxIDTfno|iI@5MFY|i#k5>G<4?g-Z z`YZ7hx4{$dIKP2w=X<~OJ`JoBih9cUhUOqz=%c-Up!RYFn@dqAEGw% z*pDjTQ|ZGla6YT{zd=Wzk9Y9?FZuC4jCn-%oK^DqYS#bl-~IF_e^+_VLx29a;5Yf6 z%lILBFW0x>C#CnQN1cDm_$c#K=g`UK!?9I2bp5^*>7-U%2N(I@bfT`Zu37rGtGti- z;)Z>()UP}Szj+?@g!tqEJo1?1JKtYt{vqFUNk7@FbxvFKUGLd{4Lr*C@RRBn_ciug zX2aQpPk0{h48ApY(J@>Jy^ZIzsiVM85Ar_d!`Z}#v})Jq@`rtB9VPL;8}FBSXX`u^ z7xTqK^;7me*!Dd{Z}Ob{GyvbY?>@PBzEU`MzUR`vd#92-CItTgPnQVx8M; z(Vxp#_o}Y#o7dxWj@Z|M^9%ew&dvHanHlo zP4KCF^`YWh*Ln=+DbDG1QC}9%U8ws*ukF3Lc}_~7M0G>6wx5P@oEi7~@LX@uBi^&# z1|I2T+C3xcQ}+5wd=Bkb#ynp5nOEvBSHOEegGbMI4|Ptxm}ArXw&A`XJn_d0d=cNh zJ%3gEl4QGw`n=YeKjnT+#8>NtjGGeQjaTu!`EVQMtwyc)f6ph-^HAn9m8UL8AA|2Q zewO+A6zNcg{JT$~KfKD`z**|4u64}Tg%KC8RMuBzyN8O0!^-?4@ep2iHuhyp2mB~> zq&mM5_8{cDhxXIcy*pUY{YTvR!(HOF6a1HbEbzT8S#?Cxef5lUlAlh7Ke2H*@m-HY zZ>hTXICy?O{!4XlFXrZr6AJe|9rJp6j!~ZD1MrVWe4pm76awd;g?$d7c9k9y-rFsr6A?&TH(c+YCKUwsdaKc&Qhd*~aesBK=oo~K-C_l=;ZRqch z(`TNeA9>?AU%?|;@0K<%U-%b4byT6J0cW~t9T@vzyXe_2Ils}L9SEPZZ10!u-wyHc ze8DFVJ-{jaZMuP9gZB~_FTj)K!`X!AHfwO3@NHzj5`Dh$s4wANx5EcPdoS|6U#g3& z$G#f6Wa-wvth|p@_3cK?A=-aKI{SQiSvrORJl9&>NAEpQ9&6UrP2A6mf!}C zmkX8ki`n9#{PG6YkvksJk8KtI{ulEp@RrkQeU^mJJAXZK{Rhu*89hKh{FYtUt6y6t zzva7!!p*Fo|EkzOpMHhU-A9+Wj4o_Ebc*V8^2I~-|9$X{Q|?pTXEM()q))jX`52un zdlK?JmwhVm5#j#LP)B`%Z-src7J2`!^Za}MQrY5${VFvNwHkU!&8-;M9n!DNdA=9C z*L8os^AcZd9_U!)FZ&Q1mjOQ{FI!i6y=3)glP^!B_C1vTWDb009=_lHhj-YsG9UPj z=1ud(L+PnIp`%e8oUqRg^N} z$G14Qe9xu4Y&_~(VeiZThl>iQf8qRW{-shqlJ6cW9(WG%n(GX7yJO*RXuX&98>$m8 zp>NLjT=FmXe2RUqK7a>$N?p4X^`P)W@BKYi{J+nczXsLc*=E9hij7sb;*NOuoCr>pzeW=`m!>kEA4 z8ntw!SC|`}WWMbV`MZ_wxm5T5%Dz~E%c!1Rq<=QvdzkxbeDOWhX=|d-}Z%-2^hL!tG*M*8V3(TYEyNBX|2Eh-B zS6PYtGFJ@sLZ@wFBO(iMXHEtw;}+58v>d zyyw=8w;9Lr9I*1i9nT%bb4sV4uint-@0kB0E?lIJ_^OB>8{n(AR_r6ld&r;5_kQW$ zJN7qFKP^#roFMLX%}<6d(fa~)&iUe@c=Kj$zS8?)t&_4IRQTy4@ox!U;vRF0`Q{y@ zcWTwfnLJk{pPmJL09&>C;}e13n;+A^OTNBR_h}gQhR$gm{rp0)u2Q&Y@pIub^%&eY z-}~i#06q`RkuK3UU!x!E6E|Km_cl$RypRa?3`Q9(_K27@@R=Srj@ps%rKi{vca})3P5+3~m^>DsAO1Mpu0&o)O~6!y!V*n+hL5;nvo> zjf3MH1CP7Kd{MsqWq$y81>-h+?oZ$yI@B5Sah?L&X5l&W)xD|@!&i>@J&OK+5 zH!g=i41G0o_WAZ!sD9szcizk9eCBy0J;$>`JOTH;RdjCo;=FWU``EYfIldHo@j-q7 z%^Kd1`KUSYf+_Th`R<|Kt06e`l65w`XD|4E5ISbfMJuiyQXyQk}l-IZNg> zmWlJtTJiFk@m|*>@DlmXOSuo`wC#|Ev-Hu;(-EA7VWXd~)P#^`qXqn(w*nS81Om z^vK|cN6O#bEgZYSi&+mLJ<22Y; zr;hr;em`kmGwG6>=rbNx^3Pkh&nWtnU%~sWSJYA9Q;WopR-NX7_#pb6onQ3JSE$#= zsWUnidqraY*S;dcH}dg%nr|JT_kR`jpK%=ZB{4@8e4q3~=Kb>F*y>Z7wd-`}TjK%Z zkviZVUsE?73Z1X`v3zk}bG{Ay%uf~OZO})3PF(DfkF6_u;P;Dom=BMV4m;+2@eQ4` z&K%sc!}DD$ey`*^br1b=zWgP;)_cp&7+)*qE!}tGmjW*~%3iQ%ur=akl&=+_Mw? zuyp~#2d2TVjUOI&eGCrwHMl{(cxW8J`-#;*KL@La7r+j+Tf z6#wvEk^b;(=nqv75821D5j?c&-WhPWd~sfWE)D9xJMjD?)W1jQFFi*!8g(OhFY_z; z;=J=x(sF*=kXa~?N>?^EA$ zFs-gq_*1Kv&&`(mucA1xSyYb<*z5Q@@EhsvUlBL*-9z;sz0kQzZ}=^|>uvOVf2-F0 z{Ej^pTj4V${bjy>S?AT`o-LJszAGeJBQDMs>n_!Qu6b?`AHA38R`Q*f^coF(Ue1GC zx4jRGc^mt59x;C&xQ%e{e9xtRw^8f!U1vUT2H%G3>^bQ199QCb?6>5-7uoW%?p>dA zx)gIK>MM^D|GKW99N*D>je#HL%ggc;>RP`>-d(DM8$QQBy372{Jo6hb?Wau~$%k(U zZ?|vi#n3O{%M-d2)mf*RA9@0>a+vs^FCI#7<9qmkII%@Oz7amQ!a+uh>P62nkANrU ztLKG7_XFotJv&1Ew8DEdsKB4>8^FB%8Tzt(_fT=Ifj`uh!W;wlZHa!Q=l&QSn0fPy z=u-0O98@1#=kO_?{|n<7;j^Utyh^`(*L4c%o)4KbO}$*Yiqo!{7Hp?`0lS zd2c-C6tm?oJy)Z)PYV92J}>IvMlHU1EAp1>E&B^(doI_1iWir`aYvZ{YU6X*_x(d3 zyG;JNitZy{-0;3p=O@QK%|DEBUOmUZA|3N(bfYK1-E@p<8I=h^$%sO1~`ZKXc>De>YJdeMCKyzsR?`0kYFt~tL` z@Inpt&=u<5XZ}5SRKEA?*Sigh`II}M50Y3sE}e0RFu175VZ zXR5M~qx3l8i{R%o-vWM+FK&ppYt+n_hfavOa(JLl_?M%%bf3vQRlfd9`nyK$JqW^Q z9z;A>J{}q$FyBy^2ZrCv7dQTBw*mVD*S(eZwf}q%SPMRL%KUi|ZZKA9?jhg%CEtS` z{9R^Z?q6~KTKEx3-@FVi{7Iaj>Xdx<(DjCSRrFD=o6uM8^E?E7+Gu=LeUh)Q)O+<~ zC4JN*<~G)-FP@;k?gkE}yu3_YJU~9r_gnxm-v5dosLgpCW*%%9ab3@KBIZ@(gSfxW zR?mw+{t>=^l+S-N>I(7CKCg$4_koY9ugMn=)kigI=CSSDLtK0bANHg14}3XCxrgr+ z=|A%6mgSqeS0@>FB94Qz^}(eUz;71lUzV94&4&XhzlJ{md3S=(eGqfT)cNhZg&$<=r z_s4+?2nWd*H-xXb4_OG^B=sP8d>1^?{s8CAC#9`7fSE(;b+`C&!rzlzn$;7#A6xnU61+}KLC7sLMI!0q=f6t;=`2h9(s=6eK7u;#sSz* z*Qym?&KWI z)-WF`coxhpm)J9NHtdD@FemD?8hADToh->TBi#Z+4+q% z%pE115A(|z-ZAEjHk=Q0_dNE){A^6XU1etfWBYup?;{PE!+cl3eKCs)_RL&3!WlCE cDtPY9Cna2q*>#IOGY8r+hnby^eTLTeKamb5n*aa+ literal 0 HcmV?d00001 diff --git a/source/tests/pt/water_tensor/polar/atomic_system/set.000/coord.npy b/source/tests/pt/water_tensor/polar/atomic_system/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..baa2c0a7c38c17d134c5157850d0e7bd5017692d GIT binary patch literal 138368 zcmbT8_aoK+`~QU`JA3cF_jVrlE22R|dk<-s_E1t$k))_7Dp_SD6^X1!!zif~UMfOD zNJEMGJm23x;p;c&{P1+1&-1vh`*pwHE-};R`pjH7hHp3DeuLHiJN&jAIBOcXuCX;R z)ihWWuzlzDm0OkvY+vpFzt=riZrb6`y}o1J%B}v~?}ql4rkZvR_C}_fL7M-6KO*jz z-=L~!49T3AB%ApE;P+R8qN7zQS+W#K8-!`rDj`yG`NRTETzTnJCBW0IV-ay~Z2mbf zY#UR;#K!QmEXO<+a9D?~KYos8+X2>jUXRp@YM`6zfa*=^)YJDF+2WNf;POuNeW?cP zY-QpF$B=*W6JCA0%QiIIA}zNHQ4Y=Q>{)%bZi@gd+BJrhU(aFp%zL4b(T_i!TZ+H; z3DCMH<7vu{)95xV=f%A8VFAV|P?g=tyVLiQO&qrtMY({&WROdF(_}$D5<3O@W@hH>X<5P*ly( zC%I?BBrKT1e80GotDPe8qSKg+q!Sg~=BJgdf-ntor1P8X=#+&n-T&f38;*O>9U*nP zdc%XPiydgCgB|U@ZAbV2bi&Oz9v%DtVyE~UoVk~c&P~F!{J?*ReVBj?4IlAb;1e`0 z`6%{P8ur=`K>4l+6=a^k@`_@2jfKO;r0F3*;eEStf37n@S&lA8oW-me75H8!L7L&m z;Ps;!^P;3lI_4NwpUXhMmLw?;MIiT(1#4KPL%+|4V#&v`jQ@x&9e;Ki?=-X6X+brr zKjcJ{rrl*j)#K@SoiQ2hj$>0g#!`XOL>k^WovkkBr?4G$kj{Ax@lDpWuB#S3hPSc8 z-;PXXJi#23cd&NUB`e{#%t84S^v{WrvULfwTAG4=;{24+wVqwJio{P>X}a8RN$16d z$wpC~9{kfIr%Dmpw@aE*Wo>AelN_1d)+Oz3b=rR50{DyE$XQ91;& z6xtArzSuEzqePsd_-^9YzrVP+a4a=%s$z3Ib0P07LKi~B;eGKk!qfY)KRJi>U%U<1 zsb^S9dJ|LBKMARx4mK#N01c@q#NUWxr?2F*Grrfcn%@vWI86FUx|AARPJ%N$PahPIxhb_nwM&DU|nv&UxJq_G_u2iNznL!kt^+2ex zE=9Htf~FGws#W1g)sY`y9CM1|~3M;27&~-m|^5HeY z=8irY2pG}3$Ib9cG^Xc!oaj$cKL+zGsohGKRJ?dF8@mZA-be8Gst25>oWaV2k+4Xe zhKZk-AXR=kJX0$$a!16q{-hfYyWhqB=5ja39|tf^rVTfKKHzOWX@z*V1U!iNf~`4~ zutO|bFSJ2HpaTlq?xQoh16zbkF(Frlb}pAEw}Y)L@q+<*J1f(ZSVd5S{JVrxS{0ab$%<_LZo=`F>*$sFfVxAwAadykTO;=pQ}v<{ zDy54u{qHz%cmbxhD0Tjy`w7%!xLXZEr_wB@aVhaU#9l zWkt`Y>BD#+2mQX|XtCI279al*B_)3_eS9Qu=Eq04eno_2zGblw>u$2T`~vJxbR@0X z@2n=^1q}9B(@rB>Ht)(!xJFEK1w|jxdKuhS-~p}iTCCY6Tq3J=-uOcn3yJwSQlZoors>Dsd;MD8mv7mK z*LutrAFU{H2rkE_xMG&P%8P9}b_2Z=h#3TkPwTj*q4|?ZwuqCPtPrhG zYhk5!pRnVGAZdqN(|PZ4^m(rkJrnn!@-iXnRFI&Q)fQxN{twho9AR?e75J+18f)F# z+1%lWaOW#R;G03-ji1-ylx;wKjC=%n+o@&Y*g#JKgy^f{ms}fjE0=UfzW_zcO&-s6Sh}Ta2aU zzr*|TN9@?#IOg-~D{6P2W8Gh>O7@O^z-kA5rtZ+q#Kynj6A6;d3v0nj)u+;Fq)=MPLl}c-2Hkv&VXh&x58@18>A?iQoZ3<+?!tpbrBWX-Txd7 z;sY=_wi_}lok-(XC+1z*h2z@O=!dNkJ>?5Swu=MBWSHW>z)$A>dK~3!lR>614WP3{o7vQgvmJK z=NBS_HGQaz9hS3VjtXgpP5fHnSlV3?mu(5o}43pvw=;X#MD3w0~BiMqyLB$XkG5vGG*w_YaGgweo%~ zw<5u{5;W(sG5h|(gj%lkp|S1^Tea7M3XCnNW2HVNbI&C!!G%U^bx2bTR9#|4|EAi| z%|+H!;nj>q8kdoKU5u{xKZZkk0qj?Bb)xezJ~*AnIw3yVrv4o3Eyt0-idZyF>c!?v zVUk~xgjwgSFnVqRWosPf>a77)jXKh&e_7}gwxxJYM~a_U2@!K`%4+Z;=f&wLbm98{ zMN9G?JAppU%YaX_8JV;SQsKE69DL+PzM=ATFk>Y?&CY`DDoHx8ZwTFh0=x~BB}p$& zTs)tE@A=~>cTXgy1O>7#etnv)l?<`njqI9@0nHUUg`&g{yl7@gIf;hkmfz0qhWtRH zg%SlHEob*Lz9Vv#0l7GQVe9TRAUKxO6Q@cb+2=rYQBSe6EE8f!Koe~e;AxYCra?=3 z{;Qs`c^hCgo{vQDsZ>5@};dc=t=;_gDjIRa`f z&oTTi6(x-c7~WcltIbKMnz;$zYTjV=Jb9X;^#~mae_nmDvHOS+#hMAzVBdW( zMM0YGp+c>76L2#78k`qvQ1(g>e3%yl&k#{Mu}BzQKAgtw7K6*LMQr)ALy&u@fuz@e z*gw6)kkw9PCsUo+BD1sj>1mDS?vs!)86*=w1MZQNaP#bRI(lFxW>;(A=5l*F;xmD! zE)Qj~kqs=WP=dBsE@CfN=&{O6O4M{bofoeAlN}B`4~yfvTo3AEywA75>-9p5J|A8` zxPPmM&0&kVS|e}#Sfa&gn(1(b(PkfNtYO=s${!DkvQYix;)d5u>IOYp~E znQB&8Arr`{4 z`4T0}Pw7YZqb#0mjTG{?y@1HEHr@)sIXE@{IG*4742h|o(EON;z==b6)c*|!pB}+E z-+Jsd{DpvKd2-?UWsyKGON)`BQel2-f2GF!^|a|Slc6KG_p*Dt^l8bBMC`Fl!7Fz? zI#f`IpyDX>c&d_D^Dg$Jwodbn99Wo+)P z*4e0xccDPv3+UKxMG-UXC`ZE(KUB@h=bRm-ORvRxJ|2~?cco_$8eGqhgz@aZu;p2> za|20udRds9h2xmxngkr&-izN6zg_$y7PFkW$&i(|r^ykA+2p&$D7auoyJtRTALgGz z$xmy_zOF`_Xc7L!Sm4wGBl>r4KGIvpAZV5XeHx<+!#pF{1Wq6u>v7~%P>IN5b!w~> zr>q^h*pw$vUA12^sQnVNANevZsM0FuwJbE@3JWYZq|?8W8Dz_t{Wc}qn4iasJ7z(- zMe3~fvOZb9=}zFC2aLV4(% znabo#?qZ8oBs;O-C(_N2ut=XV^scK5CXe0Ns%S}?XVi{rt$(}`n^{CO#A7q?_Ioa~{F;LJ zt~5NCjl$GBqwGjXGwSX3;Y=_;7D$$1>5Tn|Q7vO{1-`(=wuUFV)D0!SMWDQ43Gb_^ zE^dCF2(h3^EN$5i>~2xT@5A5N7%@dsPF{_VHjPaGh7v(W579zKSmUEkYp(8wPz|GQ zOHrzMm59|Y)5s)g0{zz+iDJ7cl$tY!b`530=b1cHed$QsXSU!!$!xY^+$1`@ume(^ zXW309D>}QV3gi2J^Xk)nvZ2SWv`^qVqZN^DJdJdg*26k=9h*6t z2(kOcsFt|HR;%Aew7_c=Wj=IEA4)@7>|0dxDN$FW5Z(y?fyr?hlFSzY&$SWrM)gVd z1y>7SzCgzpV=Dgp3LCT^VbEBOROuP&X7xZV#E|wkzsKyDZmbwOi4tos-j)VIx<2_Z zoDaP!e!X`9`j=1Peq0=@z9fau?vd=J#W=bjYYK;x$C&UWIg0()%RFO$v!gyD)Ui*D zUQKC(uBbiD%#fy4A-}P>$B3p0{>G}Ojd<5+Lm%V1(3HkU4aWcANzKTHD7`-@L3``R=J6E!i(RMbxcuujAmI{3DnNvb-3u-@m zp-9`BGKUB7*)$N-oeZf%q8Gte6>xoYA*wch!ri3V=o=pli?M@vI8hyQ+Kmxd-T=vZ zYYhA4xfaC=lF(lf$}QI_{U|q{cFN1q^=W^2OTI}_thO-S%8P-qL=#Wt!fF=rEDb?= zr;0VjU$fZkt#JN5$lJQ)7@PUz5er`4fhB_mB)Vz=n~>XwGFf$Us~1P(=OH+jn9-#B zPIN2zG_+?(QMjTljj`Phk%i-^;f5Uve~vlVw9EL#@b4&yadQz2U9zXEatwpN1F46fLNM zTRdpd9%ZU*PsiCJ2a4DsOmYE%NDisMmRHI&>7Fa{#r8tlpSTU%8E)u?_60t3I90jlCAi7f#SLEtYEe})P4V8SFj3+RcRt7t{rVI zhE($Q3sVjjrrueP&|LQjXTDg{&Vje6OyD%hH3zExk&WfSr5NvFM9-~1vHROXz&F58 z+vf0NfW_dPV;787wy=nUC*eD9JZ0J?P+47@tFus98Yh(958wG93(|=u}J%I?1EP?JkS5f48IpLsla_OsqkfY zt@zo?<~a17vqEXX3`EBAXw+veuCAUB&1KVQ{j4Qe`Ns+^;~dEAx-|K|T*C5=TUkf^ z1bVvQGtaunht+ZVWf#e_qqq4X*LDg`o#3?n4`y>QmAiNAQ1C$>d(K_L#-Q<#9`0bV z?ZWuzrbh0;f1!P1G92@zDD@CO)tonmj+r6tyevQ)6O<9y-;Ue!?jt|X6O)|!(J5Gm zfl(VI26duP{VJ?WI^pZ_mU)gC(h|Y1__)~+T~|!#Q+Y3jgPK|TJtZ2-UXK~iCs4p! zE9y!*44uuwZSV@20GoUD$*@3qq)oMDZEzjN^F{c~)(?Mkz|)M1wN84Ww! zNl8EsWi{0>o#RF})NsX|Bo z@zYuJn=FIV{R46m^g$+>eM?cHJyyTbJ64kI?6#z&glL>7h=s;jGYUVKj?l?LNUJj= zv;B$47`esG8Lz;Tk3xW%B&w1JurFsH{u=AUb7?Ii6;siWVT*q`cd^vB5DltluwJYV zW)Ur@+H@W|oF-}Ge}w${aBikv35jpqjBz&zv&T=w+W~u;<9H0|@^i4~lNOz~iG%4A z3;Lz!Kztuv;q=dy_{7Y~;HN9>>P_i8H~X3|+=-jvX$UYKLQE--NoeKbyJ$O#cqzrx zhl`-_ONbVkonQhtOxT#fWKPSv(H+&DjKARl{>-dLgt|fl{yL1OZhUa&efo!X9SZ$;WmJY+LpXw>!xXuOx_G!hy?lH7)!0(wUcH|r!W0~ z@O678>Le$T(Q*meay$&lXJ)fq5*~C?s}(CpPO$PoTlys4iSWR5w!+efhMxXF(BMYi z_?i3|x$?mI*Mv2$lKF;6@HA(~wuLao3z8TqGAVK0_!_!jq}c=sE^irqj=wLDmMmMH zitKwukO$%>oeqp7lBnj^P%rxHq2m5vvnt9R)zNczXHba-5 zAFDv0Tr1*+H7R9wGu}LZg0b-mGKE2ee5T)*UQk7j4Uh<`iV8cvb3tJ2}KixNPl1)N&J?_ zm~KX|Yl_(96R+3~XKy-rIgkD97yo}XyTec&SM}w{=3Fs4#OC30zZR*Ve1+;mUYPYv zfs`_@qvehTr`Ha^`w@4}R}8YZLH2Mx^S^p0gz~ce+;zM0FZK>=QJT;DZYf4hhkDVx zLYwFB`xkt(-(cxXnbM^KqBKkJFVe1`gY~UBEF{;N%{g@g%B-39b=nnH@3ak@ehRav zx(8XF{2=@IZY%}r%8_JY9D|!E{n#Q(;Rj@})?A7fm#I+2VQq>}--f^KYLxL+k8)0L z1n-L#{YcQFzUP+M!KX~~PN`G-bXU4B;!aTqOzFXOX9~9DkvP91m3upK^Q#jr)mP$8 zQCB6OsZKPpcsV;Is6Z0uh#dB8;7QIirf@qKQg^7skA;!o)d-NZ#a(E-ryxvv9K}06 zL19@0V)?tEIQ}i#j0H$yZ!F4FzTk$QC^`7<#=whyWPBe6uPFt)o*0qVPfJSq`xvw3 zE$M5uF4fO^4fhKwv}mg>`Hej1eA>0h`D;mzUnY=%{wb_hH=%!3V@Wr7AIA7F(ri;A zZ_a~Qxv~zc0|e=#?L5S<%fmvqu~cKc5?hz`!PI{osop+`4et|J^fVn>;&cQ04jD{% zyC(G+97E5@Ubdjkni@yU>3u{D+rxPhLf+QY#Gk-o{r}?H1Wj@&S;dm`Bq{dbSM0o9 z1zTqa%6>9{SIaYS;jja}UEPIz-)5LBG^WO^0Nx*=GdMX`jQ0O}TKw5L36r<-k?Re) z(jeJm(4H(s_qUtVX<>0LS5YQwNf(mw=RC1pdepc`i?X~WY3E%Ha+cPiloyAp3GH8Jq_Y2z|p_ z7ak2J z9aoRMM_=C4GJBjM?+`$xZbTpFVY~toeOC6HpeSvAF2CZs#q+C~qYqSiN zS?kap7zo{qelT3=g^FNfJc~PyldBiQLGK;@zCYqNJx>MvZGXAGwvZRUOBIjK*CFC# z6fb$lTtuxrirQ+f-gg$DujwilCH_SDj9bW#k4CZc7fhYV<@Yw+wRPkuYTh-*{LHA! zOp0>vDlzTxdK5TYmtvxhvzBE#)TJGV&0L0kqe_KzK4qc#TpT8?SE0*27qEoy30@xW z!}pE5u%=8DOY=UUR`w#Q{u&^-c>r&XreMt!XKY{b7(XW!BI5T_3{Jm=P3Nj$A?wPW zzc(mcSckd``w;w&;l>tAs%brpCEpHVZ>}{(q#ne+XV$PcaiDEiF5`>6EoIKIrz}Yb@dE9fWfD$*$ZdSa_3m>RL%qdRm&AiD}ylatS(1X$a z@vgUHy_t;q6HbqsQLX1yUe234$oQa5X~r?ko8CiUybkSy2+f@-jq+Mf!`_r6o#`)` z*@cy8ydp;fNtReXDHc*O;`FS11Y2glfL_l8O3m)TU8^oU`Jzm>tOcm>a0L=KY2e&9 zIntdql`RXIh{_~sy4dJbQrBPriM6_fSx1@Jad-Nw@XoDahb8B$GZHoI<&8YjA_=Lf zv_Hd}>2GzS{ep{`Z(1dGOn%FB!1(N*~_?e`7wr$vDy;>040Fd4L~HyI~@$f_rQ3qyCf!PB$9h z)~0k!+q(;W=N+-p_!ZjsxYFjV)o7TbjG9iv6YZ{NY*w+W#w%bWd3 zs%J;$8=<$TkjGXC@><4gp?z8?`*{5t@(2BS9>Z6#Wz$_0?C~pEwYCs9`_5zA$GyzD zCJQa)qo@tw`rGP+rf&o_ae4Ug6yB@(PfVp*#AL@*A!wyx4ZsB_wBNh-hM?& z-uVtiE|*Kz+^9npH+!u5E=zyhvhXIi5&4}H=;HE^ST>;>@}}ZcoOzKI@%HmN=UZds z2_GBw+QG&KOvJk=ehww^X2(=?P~IHOvM#^EOyh?*8d%R11K+~kCb>}T%SNFQo>zm@F;Udp#FLt4` z;$x`gz&Lj5vlaR4E0O%-47mEmyNL{)V)I8*q4@oKiFv~@p5dx|_-yAdEq>U}bV^#- zz(qc4+A@|*%Ex1Ct|Yl>kEP(oeAe*cC!QI8fzM)VvgC`vMsaz%X~pG&|7PR^fb{%ho2Vjg#p9no8cqA@0#?sgdAm z2f9?~LncF9?VoE$O`4M_p;V75Lncz7S|>KEoPmzcZ|E$2gCE<@p!QcYteyVD4E`)= z`Sa70CG{xdxCaCI<9Kkoi^~ank>qy?V@2Mxm;nH2Jt;CLD&QXdN`y@ zjTSFZpC(ItgUx6`XEFF)&O@WinpD@T(LL|WkT4^nB5j&^;U+dTno>3tDP+-cprjc` zxSFVWcNQjxcR^l7n9KH$<4#^N+VAqyXjL}OUJYZDH=9#~Zx!@zB(k;zI%K>4A@)k7 zv%~IAWOd7#j08WhZ|+j`LzSC_t%8`{JaH0K^(6J*d2IJ*K@zEL#J<{M2;F2f`)V)F z@ugsUE2wi}Gp6=FLAt9oO&Hq2ZvTuyZPPGZ^!?dInG6iS_<-q`kMcOL77ruG(fbcN zRQ^Jf0)MK|W+_v;rL9Q;hmA=qPJ^BuFee9|3g>GX(XF5)bT?0=q%FF1*0LPaV;K#6 zu_p0Xm+^knOu8-j8evj%keJIcE!Gt<)6>E=mm9e6`4!C{ToDJ{nvfsU! zabJs!G*jR?UVvl9L`ga&6NQa~kh#ZCdpiT&TrHX)_q79W$_|#S(teKJUj#@t-h&-D z_!vDin|N`v7GUD;^^pBk#Fl+q4zDv?Fnr*r>#l_>FvU3v>pT3>(>enedrYXuVJEhZ zgh2d*6)o}#gS50B>WwVuxQ+}J%6d`DXJ3RY)uw&P6KP4*dffUUL-l#?6jvdK0+p#K zPH*u(xNl(5@P8u)F|&R!h~XPq&?m|Bi&bLP&*Covqfv!cHFW60*( zMyz;ZOp5D8sbz^OT>5+QYiTGpUKnOc_xUL)a3lCe-Ee-^FYsTA$IGL=@F-VCnY<$H z5$Hl~EysO49#6}-`|>xB2L}lq`f+bAzP&Oc@m_bzX`6>_O(t};%8EX@A4bz^b*c~t z${)5Q`D@;gTDTe)a{_Q@U>7o!B4OCS z0$tWCk#Eeyp}Q{l_7>5kyCy?zjAU&Bx_!`9Go7Q{0STG4|ihEhhtT&id zG9OFdSs^#H6J|P6Xnr695iTqJcBv3DN2X%T%nJ)O`T}uqnK->Wh}%ddyxC0$))~cS0EO{ zd90ULvzeRI0E;p}ONy%364eUULo8UF+KYYYtwaIxeFR8I+?BLp3}u1kPV{iL_=Wx-U@}Vop0I%8*yaA11o=3TD(Ok!!On1b1G-;xmf0b6+D{JNXzo zB8BMp6Beb%qnw+y%-Gk7!oTikoAh}Uy48i7HzSx@mKl9{>`p0ZPg&|yPcol9#Hu_R za5Cfq>wG4Idarj#3=l%3a1~Q&>qqjliOkzik#ZY7>A4VhR@x>|h{a?w^2qP05`CQc2le$< zG$UJ)aw3JPw%wD;Uy76dDQ->**^ID;4{VKW353kH;>ryrjz@Tnr@Pl7y+j25{Yl75 zJkARZK8IuTquKp{Qn%0d!tl*$l=V!W%lsuWv2S4>8?0W-o(m|^tmBjM^1y5+zh9Ny z)yCmfd@Ktq)TK4%Tj8bYM9mw9uq5O(jt=oiIBhINuG$3N5^HMHA5ZJmE+FDCmns;H zvefB~7(44GQ=e&xH`bqUPq2=iT2jrfYSth!@FANTtc>T=J!zcb4>lNNi+~IVDq0%C zWP&E4cX&EADCMCzk4HMrPp~ELD(7L?(tlGpk0U7!$;ne`?c5T$uIogmfh3tf9b$v> zA7LY*PW%(paAinbDAKSBD{TgEXv3RJfFE6i@aN3xa-&E)c( zD;qhV?%-x@3MDf4mZN~mL&((fAU}ywjuTmrwAV98+H4jhfj5s4kKHpSESgp|jm)^CsiKHx;JJQ)GMZ%*F;Q0SLA# zv0slYaCqK6wr}Gb2o65Q*6t2=+VwsBCGG-J;yiJc{}AO_1Pz%`teIAb)w`T&nfXe% zZGDKjah}va`zi$aiV;=LXp!Sf-g?fK;(sDWC;rs%{1k{Pp+=*xB#SLhAayasz2vFLN}c95I@8A8 zJf?rxki7PIQbOVpo^-P`H)A={AN2y)H6iZwZn7rjjoQ-X{ZnWpRf>jJ+S7!sQz>oG zm_9$@(bS3_*msEtvW$8cUCH>}O;hj?5lH`QA{7ukOa+ z-XRP{#UM(r9s$Apbo2Zil*+5oZ&@{3`>2A;j^rq0y*Y(uS72hAAvNq(Bbh00;E)!J zs^`{JvQU#G6OypP#DmL@b?F~+IDKkLZhcBr&2eP+)-)k}%orM4v=aL@KS8o#JgulX z1kuX-7>fCah!eL_wkwkLYns#Cbv1D7d&)E)=yN?QA7ZQ2*}Y^}I$ml|>y^9NrsZ61 z{bfXa0%zEoCE^s%d6#uw`OHU9n5+liLT_goW^02Q?B3$%#K&;SFsE;leVBcen~jQi zq`S$Ob<3Z^y6JzQa^8iV;5_D&p&v0~m0NT)Hy+|Yx%tObhkVl1sqT;lh4-t{<8)Js zs^@aBU}L)QT7xe3n39388Qt5Ej4%J|t-Bp*e)47bDR|QlF5B7hsT6EJ(X#umF#2;b z$Dn7z{#!oAY;i!TYBn^IdU5HJ4FtDc#leS)lwp{K+c&yVzI{9;Pe??(;{g6m)geQv zB!pLt;z}An6;)56FTu@RZugn~J#_T*p}^LwR=ib=gJW?(sc za5@0y&QWaj>F4ZIrX#LjGN&URV<TPQ|4{@Un%_Z7?A zjv`Lp7d8JzVJDJ;&u_#L!xNxA|E)ntZ$BiiOh9P8JXv{sfsBzWPTdlv({K2=+*A*< zdbH?*$tv8=Hl=YFJjv|NR+yw%(OMBlYS9hChY}5n&YnW1tA`N9Twz@jS+?1dLeS>VTs5XW7jG>WjnQTpuCW)_BAt|98c7@CNt}YCLmS84Q zmg~~N)-yP`qZ&sK=#iXyJfd4J;Luc}N`vn|mR5%OU(mycU!ikhz zdLDs~RB%Spfd&kUkf+b-AI)qxlW~_2qh?Fx--1}dNIL4pyr@WNRmq<<8R&XjgxtLo zXxy!dR3>l_@Bi}A)@QDCNaH2Krb*C((^Dxd@g#fw;~N5`E$EqJElWN63-u>8>A~(j zOyBP~=bSdw${ z9UfnRGxaQ8z=Y@W=*TBe>Pm@a(uwYL>x(T7$0o4Df;O~u@hH1F{Vld^zR4Q3%n*9N z3DX0mQ99Vpl7Icc#*63JvKl%1;53=OI?0m8b8)&?HiOcOG|6b8O zX-C-vx{x`@?fh)Qs>yv!{A)h)P6VLTSRF|tm$6HIAMA3qkkMHUZ>ifnk;M@ZIh?^R zy}i$qKM{o1i}K*#HIG%VPD5GlJGRszoTW`sq6UXq`1p1iyDzCl_c)LCKd(Y|PC}Q; zYWJhP*qKtNkK(m(EadGx$&O=JqV62WyCY_ldy%WV-0bTzZYIY;wXv1qji})x_Uxkr zjDtVnU`HdHoZQBQil5-PQVBDCse<+?Q|Q~rNvv(&6dY8XL3Kew@B~BWJ`XDSbsIOE zcr-b+7B9vXz{TE%@}9Tj*`ZkOjC+t=X9H@ZKH?+iyF4oAhwQHo*iKNVqqlVt?88T+ zKg38j=QVq4`V1%A)M@8t0lIsr2)cDL z49&U{gn8TC>DZLt*gfnIgU;!+-f10{x`*LxJspr-LwL9Oz z4YM0O{((>Ia7z@HD7`3|c`chcJoLh{jzN5E;XZedH&%}yOV*A$bZMg7?W2GmFHrAM9=S2CCRC!?>A3`mKaKmT-m1~2?9 z3Q~XJDChlIB%i=LuNJ&AXy90rbcoy^#@uo5xEZhy65~$eP3TWl|LDfr^i;@A{Q#*# z0TR3s0>9l7WVza$db!NoIh3FC&YWmrQU%U6kEgp=4Jq?eD@HCI$54#}rMJpZ^p%U4 zlV?FJQ-mb9oyMw96DfSZGRgR!M%;{SB<$y-2s(^pju}q&8AK!WD*;r=t{CWwaBdLW|FxkRDHvo<{O-0yCz7{%1utxl(UqHND9*T z@!E8f?ATU65t?+V3ql1i;j6-Ehxagw2RQDb)Rhj*YKM?%6UsJO(CAGCrmJ)W@$zHn z&w=Q|7A+^-sGWu5r>v;uS43w09YyIV6W;p81GyW)?ufNZohsB&IQ66jzrx(Y>TS;!SsHV)v{7ab}Yz5uDS0vu!d5z38= z*tFVbm}va~MuVStimBCD$<02`ua>Z{XG+m>l2Le|EP7KnVqm5>WvtS{jN75e545LG zZMInb^B8JwZ-wGm7c9GEM&~30k!HLBBYckJoOl}hVl?n|ojHlBOVEwe8tjXg57LU1 zY0;lbURcRCED)MNZhw!tNnTJvimD_0B)Zv7v7gNIjw0l*31auZldR`y0A|jtVq(?R zY)GJ+8K>LQt*|~knD>=s-qxWnsy(=|Sp{>{97(EykKUJa`PJ*!Tt0sSnmwFo-SriH zf%`EUx()~Yn{m-65u$&-;AWl^1UVM#t_>eIi!6nQp*h7*>4ZV~Dim7E(y`rJp~-PG z7Pd~DHV(tlRDG(Gw5Ec%(>QQjo@CE6(i!@S^>#KCR?VX(^Ur90$nAEdThTb3{~!}( zO#3c_9#7LlEnAML$Ro(qb;IK2k??=C7k)+-I5))&Nrp4=Z|zek%6ZVQYcr8~yBnWB zcu?8dbr56u;MH+^Q1=eus&_nWzkk8V_9|Gux&U{VSMa;`4nFg5ahjbvy zZCW$-9Q<4F;;?WF-Wsn&_0MLOGr1osg?>ozQAgpT2lza=8PZKnY}=eXJYQb{lg~9O?W18@Rsp5xOr(ka58m-lYk3@K+v1 zVdpEJ-L*Pce3z!4$2n}`=VX?vQP1s5SdqHYS2lM^8z$=Olc-TMuX1)ZGWAU;HexI- zESUo7;w{)TBuXjQ#8F!Bhj1ks(p}|;A*oO}*eTPJ$6s;9uN)dj4e4go82Vk6kJRZp zw9Whjs8;i0M_PWKn16+vX?20hR>_>8b@qM zh1;p=u{2>V<2bhR-c+hMGmZIFInXlyepYj#5Z`^cz1)Ht_I^?Y=L2r$MP3|-4gHJ`w(NouDP0_(Qcmyq$gM7bXHv`@yNRH+raX5&} zMU-(N`w8q_*P~6#0@0dXu=SpgKoxf!=)VUUM-O_%`IrjEZ`d4PANoo0kQ*Ih^EWYS ze6a*tV>J*m@{UD6ks&el6_6GD!^?O|quDITCa@>^Qgx zo2u*NNJ{%Cw{v7juen{22EKLJDEfr=&MlO6oo$9N{}HC}a)jm2XoFeaNhaobh4!7134%PiEm{Y^EbggMewn*+>tog+@l5v3o$0M+*%bkyn&LPSm?YQUKs zWG_iNSgm%Hnh2c1GLq)}EVBAKO80>=^nnsz7<+wlIp#!HB0O zHxqXtC@B@$eo}NK=mRt)U*OJT4SE^#5uR>Bbb7;K?BUh`{;ZHElXEAb)iIGuXN^K! zc`yE5<@~4-cdY!l1PcENaZHOZKKsqVqd-L(su>H*g0-*~5~6EK+7ub|6$hfOxjpYO zpy+jDNbG@A>9PoEx^DjsZVHEr_j;DWcfAljFXAWLp;|m!)eTu0K?<)g#j>;GX?l+| z9cv$Ct`F|IEp4e}vr?Y2OvP6v3)Hkw`ZA5_wwtgAkCIunmIM8#T84d^v*6G1<^{e@ z7{0d;c|qpXbh;M5T1}BRas>PLaydIpue|TX~EmCAvv{ zkfhoT(sbL-y)~P&M*2Zg6+Y@P2MvNijlV+rP%ZaADA481( z1oE0|O@e$;+>)gRHNLl|k2gcenVR<*lalIjmY7B~Pnq+?75PgyC=OuC&1-1mv~79Pe{f!! zg?srE$UUP07Bdpz67dbPLLC3#%18blaY*P9r9^d(8E(Y&R^^^^?@r`h)TPxE z71-ua_i<#7GF7FeG5&4EkehEw%C|PNWp665{u);Y6`e_sV`Aj4-@ykSamP!#RD)@dlR~z+}?&SIJU9?i9H&mD>fgWB2uyXg9sfv6oH2MsqC$|IX&RN z3D?wGW-)9;SEfdxO?n|KzT`q@T8&BQ>ND1)AWr4~G-z=8K~|{4W&8pbB;dZCg|;ei zJB;O6f8jp27j8u3H$H}2)&)F?<2dds$uN?8fU`1sw9WMe6HDY~q9+36`)xAwv*Y-4 zV_~Woe9ej$MI-ZF4-)37ag3H48APbiwwJ2({x;`Db#OZzvo}C`{mAc zefyrr`5Es6`vUmB)_EA?ZRf(_`2)69>ka|xPb8H0kPlV#c`hHX`+Z|0DT_a0BTCj(o z2Y;eTPvmjGM2+n7K%)wm3k)`-aZdwiyn;1koVOw_f)LTSN@!^(LGMAESc_uuWzSLU zX^q5=^B2YPb!KQB?nGA~bt2rLFG>P9557^3{#yxX?69DS(?5|IXoX+@8sHbmPU+Wf zoRzAB-~R28eH097yAJ%;xsKB*pCBc5R17qBpozNQaVAEC`))RL=fXRLESn;#eT=ws zF%=JznZfX?8yO8)!x;!wikRA)_C8I-=+}DeWpSW`H$q zJI|R&XG^Mg@*vxPgbDG3v3q|U?yfe2=bzO$JwFAfs)ob9HXLgkMk3mw1bY2EX`Yce zX8T-+>`H$sw;#>^*hd^V;7<`+mYn|{gS8I)d_Db##vcOLQW~M&Ly5NPC7@bWiqy+G zP`B8Evq5Ian#PCpbs-g!pl@0He2zEi%Y17lsS>AhKqt zWZEzV>L|0td8=-aNjIQSeRJHH-W!K4Wyvw_t=Lp8_3p#F}mteX}QLrnS+s%7JQ`C2(kMcM3jk zDyiJjg;p)-Ll0D>i&i~#CEX|qsZ^{G*7y8rdE8ww;7~nIulgn4)+dPU@3rs>?FtjW zRN;B&74&kl#5|>s$jm5^gyl)ozVK#@I&ia))-`c%stzV?n?=p*Z+J12y)h$C^4`{f zKQN_7ZQaOz>Pr+n)S=d2wv=o38u6UB3;u8rd7)w7}Eo$quWLm?)_cF`S+7>ug85+v#SbemVs11dk`*eBs^;jqI)%Q zn0QTs(>guqj9h;dz2xre+a$3ECRBVU2yZX_6e0gv(}i~far9=Ym^w<6`jY{I18pR| zn&l|DH51{76GU=_Gg!8%G=EX@&X~QE~K**q&lY zVt_K~eSOdVof~OOsnUYzHbiIJ)2tUQ__pK)X2iLW&W1|d<$ig;1@bg>MJ>`VtJ1Y7 z?#+*V2mfVnAhK2HrZaPHE`?Cpp93PI@U7S?C+KBAJ!HMPE?VYA(Dp9T;v;AE$DHLm z;f`SlUFJjbH>K(8qD{DF?LZ%IcH;5}AB_4j8E+(I78FT2Ln^61bPPa-1T4w%dV8g(gCG#&i)R zdk$^Kqa|(c4+~${Mc8*vOY-KxPBF6Ry~ur8i^(}I)X#RmI5*=5{)SpmSbu9wf7Sx` z7p}BX)0Zw!&p`M^O`89!3$<{^V%<@B`m)P~X1eUeppRx08m&gEPm(2H9K1>DvH`{W zcS^$I9jUNHhHTC(6AFQz)JusuC3@DB786QqWZYSkcBk57gK2XeQB;{ZNnR5fK4XqS z*bzLK!}Hs!VuYRBkL7ip*tVq-l0}EmIoX)Tzjy-+=KNegl8T0(HAt-C9L=bC*uJU* zDRY!bhxKj4p)U07L=V!cxQ^qlT}b`r5E@c-2iH}*(=*p#3U#=H^X{n#58-n|TZfVd z9)YEe2R&aWM}>z|5&cy{@v9B!pyN~+rR8I`sV+(6Jg~*37{?}>a<6_c?l~U7UlnG1 z%-F*3%u6D&k39u{SdSj(CW)Q1y(w}15j=ZdBZ5usDNV+PWJlZLw+3e}qzveWhc)W= zy~ePmM)b5p2AL08qsq&Ja4d$ui8oDoeh>{4ijn)vkzAq*kmY(74i>EA78HrB536An zu0YckZxpAdrC^Z4OQcKSr&+v0)bVHAago#8lj&6ntq9;v$b6RYMw z5r;~3Df5?x_!_7$`i`}x4wK!Iz9f&CJ=f#(o*-x9>S2?;V7Ffg_XJq7(S+1 zJduAcy6x4%I73Tvx9o({!vOpnXHV(h6i9iN5qf3nvG&}6#9S-nUH^pHdKWOij|Kep zeSq5fWcFu9LrJy~20onMnDY#IKIWKp)RA`dug0?MP((fQp%=Ysade&`=W0x8Ty7-F zoD6AgF7s1X#zHOIk_M~r|8Kz>G%(NPJM%r_GV1WRw+SsX=3d6-as+qSQN?~wYFH>o zdyOno;1Tlmus}r;3?TIcT5rE@*f|lQ^ck9VEbW1mZANtXl`*dTkcXC(1xY^1(IuCAxJT@z1u2l- zmQrZ`F{hJr>+w499%sn5i$3Els4ZG4SPcFrK0Y_2^HuIeiYu>(Tbr1>w4%RQtks2H zKkvflv?*Oi7n0ch%f6C-m9B?Mf+sUO@WKU7@A76M+-1;z((=P+5?VGsABoy4^;cJ(vMM z%i{V+O)x{84`_{PT5b<2-K+7t7J%{^em)P7K=TO!E|j|1=8FSMAgpTq#N4E z9)Gqtf5w}vxQ{m~yizjs-cMm%8$x|_6GVBAEh?C;U^4iq#Ba0_UfK?%ijl9-wNXk` z)m*@7tBa7?9V1FlUPKSYN|;5d7u~D8#QoD3NE&8LOOy3*bmA|_F_W!kz)#Vot{ia_ zoau24EhNlK?>933C>5s&E4$I70dq?D`TB1BniDK4p z&#R&Vg_i8IUQwXCADZzY%AI0jnY&Qj2_tVO8q-#UabIs>V5TlDv44)r>8~Nh@A4kH z2eEW?5%z?ukx+I?nUX4-5^OX- z0+psrF~83P9GBjS)^VMIp`5k<_Bw;}No!$T!smXjM{viU`N)581hHbq_$#-k6DY=JnJ*?MBlF z|70W7nMQO9${!wH*jZ~Hm~L)J>W%)?645E?eOj3u*(<*_+FE$BPd0W^C@r}42V3=4 z<8K!k%KY;Vw|^ZfrEEBDvoK));)*uKK) zM^enrSPK(5eTw5bu6*=)G_ihkW1}n0kQ8CDoDC_Rw4(CK6Yr4 zO{p)Pb~++nbabNCy%NoTia9?MNG_}D@oCg)Bt8hC|31D#&4oNH+T4ZeWIsuYE4E|6 z!*^)guO(@ka1uuzm7yhO-Bs;9$>=ey1;Y>6(ZV;T^z5cB_rEO3$MmSR6onExa z#FEajruX?w7uuPY1veSKPSn_u#&G8LFgwjYFo2f19!6AsZ<=uN0p92=g=76q7%t3* z&Fmg%bGnWt?KN;xUBGwAlX%oFSH+fs@a7Y?K1%LQ;>REan9d(nn*4`9;kisXZC7;N_r#I%L+LhjuVEIu#` zqn4T$x(^?Yu-S_-`3qtCiE!4CTxd`8JUq9twn^MH zhho}DQ|jJ5q;Sq^1GqeOpfw3H64Rg2xHK`C{kw88`SU-K_H!BH#X<4m)l>1yzyXuW z-U+|?%uds860WM1qNg=1X{6-}fmEi4Rk@~bhKeY)dQ z2I5~!C+8DaU}5k~3`u7nQ*{qKX18MRS4GI}aiZTj9o(PmhMz%RRBzsf`7gSl^q2)L z*V>AmRW@|sCn&RN1@^@{(e&>=^v`$)W)0IL^I<(mlf9vD<6J1>-A0LYNEuF~dC|&g zb_G6j-y);Jf_{$aEo4Vakh6Fujzq=cu#P`-B>Ljwy8dV>UBJHaLNuLUjO^p(Xc!Vm z3%Gms&+iX@L`vwsX^;Q!#W&TltjoC!Rqn3r&z2?g!fVJFc?e$T{=nkqJ7naY$0^P~ z4>mf1C7rf(UC)5_RJV#5e{4zPzCEp)cS9Ij+Ed6Ibvn3nmv|iNMz^oTVRuJ5Qhqs5 z7vChzKX3^rJG;=u!~<|%ntI&yVhAuEhPLIAcHf0-1 z*E5T@UoU106hZ%`AGK%oruznm@ad(Xx2geTHtrxkXc9eg8BEXh&OvU}F}&~Efq4DD zl7GrYXmk0B)*1Z@x_akh$t8Ju6q72ZyfMV54ee<7&H7*tVQ%_oJTG-1nJ_bKRZyf4 zU(CooT8^X-cwyv@E%-f~yHu-eV0R%BMW))cx*#0(XOi&RM~Tvh)FSlgBP?60K}&tv z|46-ww^2H@?#2@Y^?Qt)Qy+@q103jJ$|Ru><%pVgM{=un5hn3h#6^h@MV`FOxt<;* zaSfOBck-hbKl@XB`(QC_Y#?VIgJ{fhLosiH3;EUl5N5|-VW;;dad}CLP_q64-`;KF z<)C6AQEo(@dYj}d|7;a~deD)rdK7U_lRR^RsLvvK>YKwkTcwg9d>a@gXp9Jg*TO{IlRZ4!P zt-->NUwBU1DsI^vhSiE#u~ul}+eBk($e)fVpH{Iv*os;sU2#=L2g}#lQBz0)mjCTR zgTl2*wVG#wd4s8Qw>~YKy9sA_b{XHIPlpdq##OZ)!Ks_xiPcNX5IqRuz`p{qw(tY4 z{Js<1XT1v!UVn}^zYmLb-g5Y<5k_-$48?Q+ko4Oix}O2jWbr^~@XQ9m`Xc%tjPE>j)k)^&h7G zn2XB0BZbdtU0No!8K&+jk`*g`@T&JxOg!6xSts8KbC(V5D@YTSb;ExdV~}d4P8F=- z_muqwjrm05iYzGpsv;#n?nW)sG%2LyCw3Z!QNV@QQ2(qzaqs0w``I&$eDW1`YdYcf z{1q%&Z#u1|M-K1nM0>gj3^&UY`5WJhc7@nr-w0Fe_stin%3flgVzlUC?MvQ#Rt%du z37^@2lum8Jqho#;xB`@A+ziR~Iq(k{f{0J-H*d7Z@yvyo9rp$=r+pV&xx4?M><+5F zXABkpD(GE`_i8+K@`Rsb-*Cb2Ys(X=DGuD4N|4| zUHZGhP_dyw9It4R#(%u+$i zQFh&ibX5Y#MxF1RF%IOS7DmT59!7DDJ4JhYlKHFiP!HXW2e;iw)=!(_cI;=bNF-N^C&xOokepVws$x6|k21nX!ILB{EPam1k-U?Ip-1I2U#(|D2nNZz*XVUYr zCq0i{%&_fBF5SIoN5fI5yGm&JGz)s!dJct(J~YiL2flec&$KXaGBXn~rxu|ixfJD+ zqu6r%5KK?y!*#d;r9Zuc3GKhJ?hNNa0?uIgL>W4juT1ZXiXd+;O>=CScPFET%}b8q z^Tgxrx{}V9L|_`s*}Ni6rr&9k`US|q_S~_ zaGqKxNis5}`Q3s=m(e%HC;9EH=euA-WUE+Oz6B>^-icZFJ_`FWr?D#015FQga4pRb zxg*W!;euL7(#K*|)&G2oPdJh~2^%)qQ$}qQg2Hui;~;moM;?Y{gg11LzsFq9ILNG( zAULc7eiaumyXpaQ`+8z6pJCblk1(h&`!vieQA(}AE!$mpvFS!CTn&3G$%Bj-T2e|}<0jTMdO-y?Uq z(RlKuKi;{q@4A?KbdUbyUVap|7IZ`2?WLgFWcbEDz@A)f$>wx3{JHcJnfuixw)=*m ziJzhK*-9c+UlX?;Z-V2#FDN}N$qFo!wZin`h60V+3f*vgFxcU(Zi zniPJXs<8FvV!WB`gALrPAO2tqx-S~fK4%LyuULh=TLHMyH5YPs_|DdwJ@|R)7!q6z zIm>9gQ7FXDr4=x7jECVW?m}?hH6lL~E4~e6FU*Qu<&R;5(tI?q_AIBBgzH+~B=6LX zEcbQ8{qa5MbG0{fBi-?1mMhgL`*BBVA(CuTkHAse|F#ho-7ibJEM=`q zSB-`p-YsHFR*6`Pvq&CTdpUmNl7J@a*${~c5jD`eI@6lG1ORI}WoQ1ya4xysZ@W`i@B@d5O3c!Ie8 zpEC{V=RuQC2Z{1SoLkBc6w{V4n}s9j!<#YrnAZ`}W)B1Dr+1MR8AqXi3ItCRZ{15)2vi&|zZ zswsBigZnMI?p`Y{JxxRX+zYIu9TyV|&O>wD2^1Vn4F0cdFV;Js#rVB(Xqi|brpr7- zZO3fL+Lno1oN@bTKNeGleG=Dj9SMung>$J7)*aBoGfz`dd}ayGIa;7?fnspmHaE_? z^uo;8YO!gbA)T5w3XuU;aG0-0cYnlU+2Bv2f3iN=msq3y^Twd1h1#?)z0;? zYke$6rW56Tt(Ih5QNiwl{M+6R)i^bQt;}{wL0!mZN$oh;5pX1CHZ8RjAtL?&a%`0?-xzg8i9lUWbhIOn94d3|^ zWhEEU)53;&vWE76?>ojlZD=HCeJl2U$CB|I&>o{9$&yi~GLH=?a1({sI~y?4cr7Nr zNDzIWDx#$O=(1jM5=L=|&KiZFKx}F!yU+R(apaR^_=FY3GHC=tpcX!9Wm}6l? z%^#0oMQ?Xx?p+SG!pHcv@h58vf!MM65%#}R#Q3|*@uTW9>Pqv)80QGdKu0w?uib#k zL^X-gzYZ+vT8_4X21U-JnFYWaZubpoDA_VZ$R2~xdvY3^JF6t)xtl$6cMOJl8w;5m ztHkMEO=9Y4c`_NRPx>d*#R6?rDmGH1meLY0A$jvbW+f@nHN#?f zy0D+0UyJ*5U*VIKETv1Oqb08%xB0uh9=H@|w|s;0`F5Pl&%!#+-weL&NfY(&@jPil zGiG$9tXYpy^3Q_|6z#}*26qx&r{nS$PkPQ>&o9T4pl0bxvaRy8ICB~IFeP-A*%l6s zYw>2|V`MP*Juz@B^GND(TuF%rW{=|c+C@Y@m!mP)wj#ZKub8*PmXbDY!qe*$#HbP8 zv?TNd^l(H(UbZ3~&iQ<8NfT}lHE1gPP2F>b3;my(WOK=z2Hf8v9*Jzawkqac0f<(XH{dn7Xn8TWejYTULx@+oWWC{;5h^PS_VcN{_{t5-sYsvqxcm zBMyIK`r>BMhbULXk}3V9cb1f`RVpFKFFHJnQL+v#e%cYwsgd6EzF%ZO3vDN z(((h*XzaOG1h9W@Fy;UvRwhg0DvOXai$52yi#W_(oAce4B3J$_M$Ra}nSOh*>cVaq z73xre;aM#1-p0SP=JX=qIMiqIj*k#IYK=RL55+BTxh_MM8!k$Ar<}#L^6(%LMotEjLF!C*DtS&hQ8`37&;GQI?Umk zvKeoyLa562KNzh{#Kz*jv}1=df-fF`Q@0>;NjIYKmtI0K-2~d}xxbegDu#BR5i392 zQTHp$B^et2m=?DkAtsX%;eHIEaDVP}U23?g5q2m5YtPb)el>?P< z8F7~Pa@bR=>|2CyHpcGD-KbILH~KrM!6NiFUi0jo6cvcmeV?Jh=@@cds>Ooq)v$Y? z3EdAh>|wIM8Dc_@I=`ZJo)zoe&XgZiiF*_L;4njr-p|~CkE3*%N#H^L@0fYiUyB}J zW}e>3%~;vOS)(+2N;ob>!78@IjR2ai)q%MR#t(+CCt%BvzIYd859M7Ok<@QA^UAAGFfSo^l7lHeB|l^>Xp`jRyHQZ++lJxR=47RpCyQ+qj^QM*qdt|62^rGa*!Uwo6k*Aw(r-h`l6zj&-aTe7g z_-rL2Fq8g}_*pgz4GX4Im1&sx9xcZPY?p7Ws zjmW^;6b(w_^T0W6JdO-dCsmU+j8=RKMfR|58b0xiRF90?#x%IAECt`ZiSh@QsOZ$E z`5$|UrX{@)U8cjjilp$%4_oGr+tKt>Tg7JPIxJRu7917jNvp5+p|XI!;#*AsMe7Ti ze*3+|`koCdSAHX&-=7gv zTG6jAgtV-#LvzvtlrwKKz2hw0n+j3AeE{iHT*Qlem(ez7AvQOdKySkp1a^k8w(o)B zSJ_x}bRkZ82E)|y1xBw3qKE%Y!TAryn8SYj=v~QJ(qxOlO@jW*=mEL9=SchW6lkaKCP+CuQ12m{B*_g&&RKst zBG0^7odom?OO!+|xFJmU@!o}tQKH0Si1?ZC5V4xWM50Bz_%o*lDMbrK4NSAVEpkRLf=|!4yent7*f&oC?WSw!$onDeN?pV}6)9R&$zHpGs&Fm& zgj)0Out>8m9QIk7zB?*W#;*hf-OA@&+z=6R`T+8pLkcCqcSLMp6jmHP8=N9mhz(DJ zQC3-p#;2B4x^5uK9XW^M>`Y!yG+{FR4-};hX<2W7icU+#DqsHDo7`!D(sl&iWd;l1 zr8X$e#SVKzk}6T6kbk}+LMwp2SQ}F8zq#z2b|VvJzictjlK2etpyB@liEJ(C;L$(| z+a2`(@A5qM<3}^EWz7I%YBLX|c%2HY`H+GkKFrWt^a!Ej+4CP#4)YQB(I@FFwp?gN zgB0^_dj3Q2{|+JO^IM$gu0+G%BtfCJ4QGv-5qWJjk+&IY>J5@G>Qs{F3zPs`Vy}+I<1F z8Sd3AU$sH45-yo`bz5^QFumb8pd?#tnLc zW{b1PZekrW;4VDAKjyxPJ&j&oE5;VaV}w65G%Bx%;5GZun*0-yCo;s-8;Owd)u6{m zSns-{La&;QXi--S>T!Ud=^a{BUctTXX?kR5XiYP?AMY`9GxU=|>W@un>%(l+qy>}V z32Qp!u?anM`qQ9)=b2f%4aLJwF{|)2M8YIIw9UdV#|sF!v=6rWsknC7kaj#df+-gp z0p5dmDunrnKmOwCBnvXVd>EAt3iSJnG?^675*xB}pk${=*&5G8PwPxnpZJW)C-j7T z)hT@V+(o!eDiig?m*A9g0+w15gB9ww|$@S=i3s_SVZyX_Z-7GGj*Q(e9ikTp%-OL zdeJX%P(2)W=?;{W^#mdF`r~`0CdChn!IHJc6d&Y6)5pi*)p2`TdC-}J-xjQ|=B`SL zg!X-`;k{nAw1;)OR_ zU2lf7w+Pwy)?wCyx%lLM6S42Q(d=edjF^(gJ$eaQ=1qary>j+Ic$b=nAs)?6#<9v@ z=y~}W|E$MxGOrG258cJ?I~%du^F5GTi_~fZ>iA$pW5chDK|{@{Y@`hx@1u=3<(71% z+tBIBWZaWDI#Z*Vh;_wBvV$EZH+ zpTy0_kBx5Fr2Z23xUZ}}#};jquHxv&LS(G(kC?=LP;R&ZVdjd9(k0mbk@pRpTm+lr z{>%pB{TF=adp6z;r8C`0QhFJ;=g-4IRabhBBrJO7PST6I({||)?|GqQ{beY^ zUjMI$;ZHlZFUQGMS=^^nq;q-wM9jiGoXAiltA>X{K9&0s-MI&)lrA}wa9k{UQiwdB zF@ofb#8at!W{JC!=T%kCP(6h-Gx9Gp8+XJUH)yv_K)`>tc=dY0TVsVN+^H$?-vSuzHDjo*hUHLlnf| z2}OA6(%VZ-0` zb}5qClP|n^C((B$WxDywn%t$^5Uj_>;R#PlKKd6go^!6x&ydXT*FttrKe1fre{DM+mSB8=13 z*yne_R$kghUYUks%1mKu0IKY)y{uO#6` z!9sf4bEK3U7H*SM#LGu-&^kX~BC)V69Qx@Mw(Rs0Ik`WC-oFsBGIUq)Q7Lcu98MFb z!_J8AmL`bEUMmUcxQvB8OM~r`vtYmU0_wBs3ukrbon5;Q!u!n-u_KG~`^#QKD?*2K zC%L0`_CL4`)F$0EDj0tFe+({bTKl;Ro^xLJd8GsGU(I_ddOSrWYx+y~R%1Tz4_T_s zy7{egSXX>RpNJSN=RIi~f63G1UNfM*g1txj%)04h?$3pj7TodN`nR|;_#ZA_>5g9C z>O~XhW%F*BK|zyy+%|vU6|7GV^D6PD(~mM=%QL^Y3X@0pP^yawnJ6`4jz<^zR#lF* zGh~Q1O_I_|y^%y~#TK z4kvsY{}^($a@aS2KDyLYVerJ~;+*$jF>h=;yi#ST!r84b_pCH6zMxF&jl4u)XEl5D z%~+zw&)A*H;J!P1F#DP_0L@d1ZhQglvB^a1pKDjYt>{nP72Ab_TPwfg+$dM;oEWsU z9hpNdXoQsuLR^%|y{{*oxAZ61KfDL@lQr2Zc+=g!QTUQ=MPHr;P~zT9-Uro%Vwg+# ze5-+ww)3I1H#(HRbh7Z|oq~VmT9I0HUiiuQ^MCJ8{+H}&*dpFN_@M_Kwz6hcpM>Uj z=NwbCFD+TdzsK6Fx2-t=nU8H)%Z$mr10G^?PX#)7{w`JxKLO>ZHTe1aA$I$Y`ZR4=le z@=sKAS94gMJ$J7Dh!26v6umi!I-2f^lrO3@cH2G9mED0UGk?5ljzh(*2=|9NQmX9> z*t2%xI?{;}Z4?kXa1-oqsL_e#H-x+za{+&~qS*SX=rGxW*aBnP-QiB3`0uM6%0BmT zFRCs#qz>MlFz1yEUD>NcFJ>#!g=jk(_?|meZIJvvU_+V>IT$iJOHBLEgkH}p!19e_ z3i4Lx!*2FAOqqNU9VQ3RXDaWMDl39>{RQ0qodv}?Sy<55jCR>y=6UoljBM1YH25?! zr?f$>+=}kVpTvj<%2YR-GrAWJi(;9>IJ8TRy41$g}sNe_1hj zc(N-J28Gdx7iv^8>Vae;bNQ0?7?SGhv_g-W=5U{}Tkjlmcb>PY(R z_m-49qpBtk*BW+1P2LIp2lA}5As%P$_Q$X%*KlilE_3+aAZcoM=uFq4cX|!n;~s|f zH_Ta6eGSXK)6v8p-IkU`P}Vo5rYt`ivm_a7Uzu@k%bAQKHsKcMO+98y==MlyG77Pw z#s4|c(6>$a>g!2y16@gVyfP`ybfUe=X7puuf6VL?34_fuG3(Q0EW96y$FA-;0)oBQ z7F;b|gmrbdu*0DTB^795>+4$hP3cX3=JF`BFGSr}K?4U5!n#dMu<=ekB4$*wCc28x zu1{z?(E_)1Pn{TJsw${Q8>Hl7}YL&}Fyq=Orqy+x4lr*8l3< zzg;No?M@UwKZ{9D_GGQF8;0FVVKs+4hOtQ)k$DJj?U<>(C=vaN6_9tl3`+vC5O>oZ zv1>RVmA46D*Ub=f`6^y-D`uw4bnLiRjMEF>LgnN-jBY=OiAl`He;khVdp_vB&XX=Z z%f>{W!xEOc()|G?=pVw*Nx2uZz_PJI(~$|0ezp&tf+wX- zaK@-f*=X!&;m(bN@O*a#TZ-jLCH$O_XO>I4c|9_B=t&aJJr|#@ok4WS|Dpj#P~@Ft zI-7_x*0&3V)%S65On3UyN1vA3`ryc^ei$RsWuAdFEHf=|^0qBSwG4!MM;LlX>QL<* zDO%4Skn#{6+Mmm_;OWQB=bxDic$A};5!fAhF2t=n`*v_Awr`Xwa}uD*omZ_0Qk z6D*#+y@1Cq86vRbc)_o+58!OC2PI~PtbBY|%(~Nz6R$gwF`-pNaR)fNnf-vZ??lzp zO0=@tmZs=h&e76+h5z z%6f4=CQwwzG~oEY+2ZC!!=f4In(;HHtGHPBP1G;*73%AbNG5Ur%PlHb%A(i14-fAgvNC54?zxxD}8Ee;*wyTearDdv{* z9mJW&l{E^bq*mB1)~6MGPMl4Di0^N^P(fl9qU}p?^Q$GP_jtlw^v^Jx=|Q7jeZZ%` zvUJ~f6lXO!zkfxQT2{p21@j$F_xz5?dtd%m1$QnMw^FMNYn($+M1fCAYc{01c}TlRTxA|~e-UU5e3 zT>k?o-%tkiP;A4Md|uUyf^JGCKulj(VQVseEk}u zwl5IBqMcFAXMfI_7vjM$9bx*NnPX*5FqRoucv`Iud#1>d+7AOU;Byn^guQ?Rd(m4g zY9&k0k)+A)B*u(5F3}s=ORE`)7&TfKy%wqy z_v6qn_C&uWAfVy87`oP!;#O^kdG|v?Gr*13DW1a3{V&9{v)o&*_n=;5?h5Bg%2bo) zM}6&&ip)eNG z+Rlh=Lph7XY^vn7v10DU6KERu3k`9W;%jLd7DZ@L9_Ox0XidV%lv>WRIwJ2%-tMbUXQ&q2GEw7IT-4g20z_P z=u>_IK?7&Q@y{9jPCkN!_ADgIGCQ}AA#JF?j92_Dj$Fpv>90re@eFqlV^wJ2t&_~P zm8XYM>>*gVh=0p2Bc_`Q4PQ|#+$Lt@qWFO=KhH~kY(9bAmN}ArsSibsaU2xstSHTr z$C-jStcw2^ykz$iF{$MsRvoj2y7@xbdkBj7DS_q2l{g&LpMt{M#pA)LnDc}8s@%|^ zE83I9jRRHUYLPKTn;wD607ecW|zWB}e-67h}r17d2L%u4ZSB)FFB{IuV_LwLdVM?LPzcDM_4Il45#rVEk z(b1%iw4>kgVCgc9-9G}uY^snHxR3LZ)o^H+!GFajbTs=TI(B;CD0ie9^FKiP24`(5 z94V^rWPIMPM^RB-=)KxjL|E!jgCBEcA5BBrB`Yd;%Kp*~?uKqLqzN~@C|tP%17Dlc z`VrO?FzExz6Pf+Np6QiL4J>>6AL=cp0`Yb@y>>HPj?BdP9A6wt3dPr=AS{l21kVHs z&8?F{)YQ8?R||5u83^x~I)r`;p-+ot{(pDkA5SToz2XLXY(9>6u^l+ql8fq}NxUzt z4m%^BqU@>#-5J6D!n8)t;X2XW3~Szpql|6pyn}re&-lKHA}rL24tw&PGx;+1Jhme1 zbz7i6FB7TCmUMAyDq?;`jUbc?G$JNYUop%>KlTz`1rWe{RKf?X?D6HYVYRm5A zV1PHDAC-~3A1no9uiS=myo7fp-Nx7EC#dc=2&zv#IWNopkJWD89dC{M+$S&Ddj)mp zX7a4#LzjA`<65C74Svp_pZ_Sh^ZwIn-jDObX)8=W1W@}v-iapZk5S2IAXmX>dr7jy z-~I;bGwNX!zp(JZ>S9=V%Te9#k>XY#YvIg3we|LHRFJ%(V4!~ytm8arLq@zvT=)W) z4BcsMqblX=2f$u36T4)X6S-H8=cjJCUdH>j&rIUaa2opD)uzTCvK0RQHl|$Bq@TUN z@t;*gF3pmbe3c^2kn^0syC!OkjL2@LwP;a!!x?ucTHzZk4l1kQc#b~pvoR^`wj_Xe zE54Kb)aCcr#t@o0tdH25=1(Izcd)@}i!i%rMIqDFs+|H@vX;4m0DkFCq8x5fQ%U z8qW5}hWxC}=qxox@9yW28?*tprEPG{HVFfbrs3oslzqG3WX<)$@0KVG%xli znYYb|G2P4z@E|I_Qi;vi(=gMr2c2_Qr06s2QI)JB#_r!QHq5GmywPRRRQgHm9a{@s zw=fayzeRHA;(NsRj}%uU|A{)iNn(}xad9`+8^L?_NS6JY6?|ov1DxeEMdrSXm~loz z6Mr7W!~Mswaa3P&nS2Io{H`M1%!eix#AE;mn4k!}k&5KiDH+f(u&LU&NXTS{PI?3Ob`d zA^wy!mi6o-QTLFc3EyQYb%ke~Gd&Tl7o3>dtHreP;@14Rvnl2k0=P zQ2gS(0?U(7TcAoynB}*lZVbHUvxcC|ES=`nbTc7F<{o{GDDgl(_JVI zar-}x&O5Bpb7@*NY|}_ygCyhnY?4K*OA!sp72-J)20hatY6X zOt|~B+nXkt`O(}Yb6UjilBO@OQ1id{Yn2LR{k@I-riY>S@eeACZein;EG#qQXKwCu z9PGiYVTA-N+5ZR`>${S52&a?v=0p6qVYw+^D%$7f(x z<3M5TOsF}-JC|99@N0qteXC$+X1{!FB<@N5s$@W z84DD0_Qrge3OzaI!uh*G{)~5{`3trpWAkp2GSiB#Y~6MQweXQ`9&E8Z!luvM_ zhxs$HSaA~WJqo0T{_322-!0TDFNuHY?3qWNH6fKiCXW zqeqhOF!`+uKj+u5|CEk?kB36hq84NI&m+KGlZ#w0vAJd^DCQPsjCDd|EBAw*S0d); zSSY5u(8RolP_A{s#nbxib!0y&wCMu(m_wt&@Fs{|dR;Baqj3cu|InftbG_-}rB@iT zr5pWicBawyOYoODgWZ@DrC9M1meb5AFVu}@#7=_GfuYPa-wVY+2XszYgR=E8xEC-B z+bkrQzHTViUNS4qo-=F*dpF^O>RW6+ znuRw%nX#JC426`}EHb{Z^3k(C=xfMdVn-jAUB%g@2ap}`63W9?upi1BPcQz#U+0bJ zc~&5%=q~2XjzLV?Sm;*eA$$H+eAL*AHMfh{r~VQ;MSBpvG#PfxgQcVpY|Y())oab@ zX5kTxJ-r5=70%@JG!bWHRwA~cI~kq2jH(7_>geG`ah5(9#m@INJj*`y#TWg;?WoUX z?g2Psxb4TwHrYQEH=_6$+^Y%p?362x{4(ZePCn};w=CV-Q(vwul zOgs&GCGHK=p``Svq6Yzv6nC{5sV&O7$L%SQ@>p_(+9=j}3MhKfD|`FU<*CAKl& zabdX}^Ng%T&!+R}X1rGvHAPG2E-uBL;2szu{S{i4N` z-`He{TY(f!Wsabxfh8$8e8nXGml+^5n(`?vl`&m8YL>{#Ntt9J~|gn zyHXaxvs?BezV#~;NycZmJ9-Xhu5aO;;W?rI_yK2QBGC1#H1BK9W9-KXc#x4RT(8vQ z{76%gq~*fFzI2F#H3qnvIR zBx}wpp?CREsySK<_w%|UXJk5>4p+iQ^IehE?bDc>RgOZf$>N{=2^fuk&KXxDKAS0G z-r82U=W?&^Vx{c?io5EZ1sE<}JYs67$VfCqQR-JL)3l(S&wIm$ z^L_}ghIqez+Y59axtZ9e8fFEqDe{3)I%Yd z+p|ry><67SGoXTA2Sm{hZ`#@+LyKd-vriXvy7YhdEjt>h&z!*mLBgme8X#k#5@^k4>l)!DC-v(s*& z^mHT)ruC#}tr`?~JP8Z0^&|@idD@V$7yXyIQ)r$(tr)zIT~YU8$mj3XqxK;#AP==y z)JUy%A>xibMG|Lh?ygS3vP=?xW$Z}y_%TGtjus2*y=f88LPx#1#moz9s_5lPBUi+W zrLpQ%J+d5 z$63h243u$}qK}IT=Q2ME=L`9GC(pcw54!N0cL>j#zq9kONOYT)2h+j%BKYQg(Ga~A ze#Ma@$M34xVx593lm3a_Q?-yjb|=q{+_2_u5HlP7m_s-O($!PgTRMn>=Ih|`vL(ni zcc)fEOSDL--UFc63jaZ*H0)H8W{QBj`zNQAZ6GW z_lDoW@4tyWk8nW4$2t^x?ZU>&FkI7ljvxC^1HbOU!&qQJogwX-{1{gzE<_|}KzgsM z!mpsQm^Yc3QJdzV<*goFJL*lIsyh+3ygS+KbRe(1D41W=C1qXajcC5b?>Z|QWZ#SL zldXv5>}@6A*Jc%c!_AXUGBpa=hKh zukM5-H}4`cKJxQ2N}V=D^~Qkby%65Pw_R$mVPfPmzWr4px=~6vEq_U zv9s%}P-1@caor_c3LsK5UiW4d%;xaK#pu3J~i zdZ|b!rh8LY?qc6w*^M?ByU|5GDf%#2na;biYpG3vind3KrLV8UX;3w`S-lnm9PeV* zHnua}J1Y5TdLA0Ck0J4k#>5sIC`CL%)2CI4@CnAJrYpGJV;Lg$>O!{mJthVSTCmp# zMsuuTV$+|T4HiM)QUi8f2GZaKmPpPYj2cO{2%IiOYmNG0lu@aO*5U8gu*pz4H$y1c z{e@+c26s1l(@cZ!=)F4?Lw<6WRlbtvq}h1fA*eQ=*>V@+QNTUBW!;C0u@CEUDk4T) z=^tKn)VU79#<}8aVzQ|B>0nodwRlme0Fy%lC4mw!b57idWcJa=4aS8KARf?^@4AG}eU2Ng--*fEZ?@QYv z?x}~$Pz~o}QcINJV)g)G7oBLgat#J2K0w7?Q)-N^hR57SC?va5r&0?h1pPn_@24wE zIjd^^7FWBk$J!@@$ab*`E%4rn2ke3y_s$*TOxlI=No9IzXvMuqFW3aDan^7GmiGQn zd}C&}V~Z}CdAx`HJP8e0D@Q~5vuAO~hi0DXPRsL|vFOr^oxz-Wu;6<^ofKKDy3b6> z4_G!%hMDKZ*vkxJ86R~@G0PQE2aLtw`Jcp?h(h5yx}P|e>IJ#Gj^b1PUUB<=Eoru}rI@)6%)MO&Cf>q7CLTj0;RXiYI_@IuruQJt3u<&=a&YnT5oY8zobw7p*P~!)xESsn zDY{STi{?ce#O%$t#DvkiVAwoavbN_qaV<^-dUf*jq28SNRRtnqvj%PFbBS85AEsEV z(4rM~JlFLn)vRr>bJeH!?cTJEeS}g@*5sGho7}F<#0XtYW|Wvw?lB4Z+VR|e0{a)9 z4j>bD80{=@B>Ve)sgZYKUzo4oU&V;3ZyYK2_L zboqqmd`Fjivz}ddwlp?vt;A_YG1BrqXu|MTNtxko6kRZ(h`lpK>910p86S_NeSPU! z1$zy@U0_#QU#d%#p_L23rS_xFU6X2V)Qgz3bX>XJiX+?~*`sL8_drv6sjAPeS$=Oa1Id+n zRw%Ki;-|JWEyjuR&gDUKx1i3S7W6vlHnT1z)H>OR%FnUubafyVkF3I#D_hVpumU!G z*6X=$KfWC~hdRCMIH@@iBl&$kG~S#<@^$Fh%253aD=Kd;!5s_k(=}?5(}psfJ)%G* z?5J8ir9s41Jc8p$Y4(NLqbB7QvXB165sz$fke>|?CmYVW$#0{5?ttiyJ;o@(~@w!-MQG^E(BAr#K7>#SmAj=pZfOq zqjtr|qBYoTSMEKh1?tUh0I;q{|}Q^)rsXBzM@ZZCUSpv zWA5!=C^haumZ~CFeEZEiBkrpP{KikW10uoBoyJJZP+*BHq-(q>V}~?5-FLH_!HOc5 zY)8L2y7cJ>sIxi&3)k{JdYc>Byo#Ee0-H(FTm!V;bHEo+|N{;-!Sog!49As>1 zZTw}7vvs9U>kTOF6W>chgOC*!jT^jQS^jw@PIZ}q>?T_b(OHcF{f{s|;3g_xx{-Na zrReNdgZu-af&I$G@76MC-t0|*X%2W^+ZQ!j|Dc@s3tq*%3swDxkRTb_9NrT-iS0PZ z=h^XBd57hrNSg`|i5O<)?|!F4cOT3Vc4G}l`u;zxjr%QG&n!*5| zPp6*LWVa5H+~*6J=tTFr%!c=;UNr5FKNWD^`uXnw&Jpumcu^SmVyb^& zr}G;~74+c0{)3{Dd*~hZTpaG@OW#uX-|0~#hPnk(R@rb$*D}V+QN4I(7)Vu`LDch7 zr)1f52lDhBMnUtE#Pj&xlz26mj1syOb*b!4q3j>qrTYXoHPjIn^IX!%p`g|I+A!{M zLwH`VV$VaeP?Grp&mFJ*qeE2bCA+jIIr#~bsDFq$Elq9hKE6U%Xelw#y;$VExovNFSiq!D|Of>lgBJ$FUo4&fUf6(9>e1DI>ud>fLm(sL-5FEcC+hvPQAGp$n-Qo3d-dpB}pK zy=%@1p8E|U*FEBzRNQg@DDf?Ljl{+0#fRJ-BL3SKJSmwb zcCC3?wA-c`_iGmFq3H{ZrqbvZgzy%1qRoV(ie8}o|8 zaQ)e6de%>eCdsYEy^RB?T*(|$)~O(NmkcdB@<%x33fL$}Q^Xxt99B`r`+D}B^Ly@f z4(E`wJ!o2@27Snpqr5bGdVWBPPNp>C;1G8jH|rrbpJ~Tg-ff#i=c1u!JI24!rlJY2 zfH4a6!%@0XL z^aioa(Dd)sbn9z&GeiZy9fa2LXUUDf7a-be9 zUEoFXAzoB`Ur(aJ-Pa~hZ!-KjTcrNgCF{jR1>eFY^R#((;_64!tskPwdpGpIbN(mx z0wzyCj_8F7R2O^`Q@2KA;QIH7`|uovePwB4Nj!?)e?*T|RZ5?^64#D>VPDU0^clsz zl&qfAA>WH)zuv?tX%~_>SyAMRXGlBMotEml(ee)!XsDe9bhwapwI&T=j-$^CdwRo+ zGnM~VlN{N8?^eKFk8;>FME6EOq~k2~C_4n;ngsyNTk3 zoduQu%0Cq@yZWw5e;k6Z z+BF`-F?LZ^4OA`88My;p&UK}j2~HHJqek!9chNJ>mjWJX(lhQ*_u;Je0OzhWz{!lZ zSXhwC=}nkCa=T=~Y|cu&S_%WNa3SqtPR6H?qH1NlM8BgLO;2MW7M_P@OfmAOAB9KG z5%_b*-Q0976yBb}xV_943CzQx{6CPtBui`8XCgVY11=?e&P~h4sVz!$;sElm%@qIU4PpDEi;Xz*Yr2k$U2#IKF5-jOC@oy?zZMvENac=iFfD zpgf+PS%TrSOpy`43xA7M{C$V3p!)AoO!}i#EL}7f-7fCI!1~9M%2E2Hdc#%ZRT=Uu z*^<`CNF*^@z3`-;0hQ147ZtAx#GaK0*!MUcdDZttjN%z)C2z%`m2bq6E(dXLjU#qj zCWxQ>{A?ZJMj4-9;cP*s*hQ8U9#w}IYI^)$_M}&3-*{)&1-TQ-vF$@5G?ZS7!rFR_ zNs zJCNi|FOQ@{^Ogo#+j-HwI?n&HYh~1POLAQ=MGE}+(%#@qvAfw(^Ur`*y|bXD3LkNA zFJ~6{PTu#51oGt}D40D5lfSKlvgdRJzv;*RQ3q^$y%UG~OhWdA8mz;7e~(Gs@g%(y ziFbn~8)6L6bHZadcHb@W@g0F-L${%eY&#Zmw!3OVD(1`ofU9pF=G@!Q{Gt~AnJQq; zvzFd|-6$wHL!`MmQrG?(^xaHdXm7BhvT^L2*sx7};NFwk=tD>hI)yQZj3{hI4$6`b zA)}iSnN2;6g6f-``}qWlSb-eFF6b{+kKxA>AT!ejsfWJ6u-8D;_2IJ+_aD3VFW~^hMX5hM2{{A*l>f3uQ>n`>bTC-d#LaQ6wF? zToE304<|N#fYgyFUkjtbqQd1CmN+=l+1dLGw`5e^#_8^m`Dx~v4 z3%xsbpzU*4g6ku3D>M}L++`OFT~Ylg7XGKy=&br*=yCt6AXSUv`25$~_8m$#8d3Tu z8G3WNl)v|;SlQT(p8gsn2C53grfHFPlTXnWJv->Jq3OZmFsx;?1f0|?jbUHW_4h2UvbYkMKWXcE37P+ zLYulQ8C71FT%623AI`ZCnI|Lmj(H2iif<@fHcs@8Q=uI#zcAL?m{dpqMQL|c%8@pp z*oiX!KYjwgReq#9ZwBYW3}H~;j~*!RL93-1ZuDRW zo^KxvTssUeyPp<1?-eQJ`V{sa-VkkCiZt}2F-n%!i;P*_sJPE8oa%2w87rA-?YR@B z&eml0hk5jKgAr=&N$#9aja;z@gM03hWSW%;-|z}(FPkdT&XVY(b`P=%BSgwrSqxOI z!_nCFW7^)g#C^4 z2NoI5FTkgj>|$tLE7F*$pRV~2=jZCvhwR=sm7+{OuQ?0dRTi4de&Sf673mIhWyXCv z`VX+8+%-Y#pCd!h4 zv%R?!P=)o8B}G{$4e0&*8gwZ;UbLXCm@`48C^@K28oUGl=39j@-W|#;PGgpS5q#cu zq1pwhLi&Cnd*@uB8qgu;whbf8hn9Gi^iq=eY#^O@(;NOPk|lfN@9-Qs6DUp;&X4av z{S-4OHhNtS>Gl>SBNKQh$o-|9RQx^a=bwEn1w$fIv3BJs|5>w_LViTex@5 zTu==s8b7o@-Z$#e;a=Xf|6DX?@Ej*@Vh<`y24&ZG7ak*Z$o4in-@A577Ao}QT&JMq zAp4@-3xlZBPLHg=c+&NpVCwsw9rabcXnW!a`p&=KCc{8_71<8=LmP0L=Lu(}-{9DR zSX3Bwq574LXq&$jLqnTzY~>3;zZn|6_OYk_GY(GtfHUuo!`J==#wkjZ%hSym_fvA!jEKO_vCPlzvfR*;|(ac`5?yM^PpS{H9F-L z$$sJiq;JlhL(4E+x^)*b!?|m>t}kv%T|}c-E2gUs!TX|GIKKXgnTyh3GQU`u3TIN) zEyXG39Og1_diIkv7!@rO9zp)}Gu?p_J4-}BoEr6A(SvgATt&|+4H~B9PU|P05NnM( z@$m6WEDpO38Bbp-iD|}@-s~N%??bZrFObuE8FtSdXyApnl1#0GxVNDmVx*}gt7{JW z%QHi2?4+v;{_Vqq{NMlYI-k!S$(Oe+sNKz#er_?PS^kbB``m<5*;%rDgblsPbEF-S z$JtXV*b8ArAG#J{Q~_xJHZNK>GabqLK{RdXJ@!VeMu%28OgTScR2jh8y%P9Lc#UaE zoI@U#gEeg?WZIO10o&ihdr?cS-KQre?U@FHn(Khrbjhdwz)Bht}fC+hfA0twKB)-4mg^8bt3J?hlV{6@x!U zN#w(M??1>KK0`DluTCpaw|@u4>XCzre#c6a^86rFD%4^wJ5Q1vhQa01KYq@wV@^^S zYO5dP(60o19oK;#4T`*@vZX}M%>?!Kz;*v#)Zh3oqI%o2H{OK0zm7tDeGmG`oVB*R zI0SFg=I=}Z*)^_*JZAtV_i&@=W1pcS>}YUjgk&Uhh7$HO|JhXI>a|IA_~!b5HqhF_^fE_}&qjSqTluYke~BBKHG zv01(y$;Tyh<5~bho}^&j1z9?rb`7frrosA1J8~!2W5a;+eBb^F<+3b9{k5R+J#;BK zu7me4mYkolrURo&MZSv_v)0w+V3Y3wPk$-*}#NGr!+I1{)t?!bgLiOd`BxZiBSA4{{VAC_N6 zcSYU{J>DxGEYgDy&-#pB*wSsEelW~=NcbI*1 zn8}rCg=b9@u|Hmuv|bFv@(nRC^H-vhecbKX`~Vu9Ta&Vrrn0s&40h6{@%J8M$?OV@ z47ewh7g$rP?M(5h!Wyn6F>IbQBeJ75!T7Pq|rNt)17v)AoU%#Ui=|E>u-y;(k}>?k`n(ZY0@CS z05Tn*Lnl2nD5c$xY@OxVb-@mD3G?tz=+dQIEm%?|M{C)4WHL*ZlCCLpwy+z;Z)A3F zKqpi-|6|^gKTTMmNE>;!asQYXJ#<#4)+u+uHx%+{|BW4=KO$}8PF%{>z_*p9aM`i} zs_OoT(|W~uf8LAgc|bfpjcRo*akcwy3^=(`WEnk>Z1&#-nIrFoUh*#S_-Q)!Rj(80 zCU?aPo;jvu&Bcu$zr?zsX0*o70V?e}82i(b`Yqaxmg)fdK2wwO9iox3cLXi&ugmU} z9f)l7r4`JVD4seCS8J2}r;cwBeKtSA_rMnac*CAB*!m1@9&X~u?@}?k@(n)u9~blE z(Mo29ehC}@@ZQvj(X5Pwwwn3avPRB$2B>ek3hPKzAXP@34 zytqJg^YRHO__yFP_d(~3?}-n=^5h(=MYp=>a&G!97OL6vRuyEh3L(9ML{_A+8 z=d!L$VTIW20o)O2QR-EF11RO*Q`g`cMb4aR#4fO zYVpdwn)hFW=;!=uG0xi@|2g{5l4TaSZKp|P1Jp2xmE}KwKv#+AS zefn(Fiw6CO<-WExO%CSn_bwW9F3E@3K^4Bta_cka5Ky(7x4_}y^Gmu_*#=|;UY{d;Z3x#+{<=maII<&MFIleh7A zd~b?Ke2Jmwb8&MM`<$6;+12PVjx4aEo&8Egxk>`A<;v65)f&P^I|X}Yb)}YJM@0Ud zEF|WB;`tLhA=XNhbb=w-?Q^Ev{P%6Rjm+ zMtc&bGYlktX587^upSc~xnrVqfq%F0XzG3mD&q<;(Jd2+(uZ+kMkep-nVmQ`3lB>5 zY1e~7C~+QU^+L`ZnV*NpY8eXOU`}60pTz1fid3bgL|QY(i|hyIF?q=^ESwm@Zh@01 zGgc=JnR}w;*DTznWM7Iz$l&wW3P~ zhwl3?gi0S%N{h0jUrB%P;+hfN)o>#PSt)W^uE#8OOVW<4M^>sKl^b|b+DBdNzcUiU znEgCmsV}=EydcGyedR?;*wwNFx2$I2o5K^{bNP{y!fz3x`whdi0x76i8rxjU@GH-c z%q)B1`{sT4)J2xumy{s6{4jSfKA_jsXK2yN!_YPI6m{bQv<7r1*?D^OJpHX`KVeDl zuW;W$-2hJxFe}yHkSbjlidQ{)QeewId`>OI?@UX2e(X4&@8WaeaC4fxiTRmj=~x~8 z4FBaWfd!v2uXjE{zw?Li_m?Ryz4{1ab~{$b4M5hibDT9UMO)cus9GnXETsf9CKw~s zwHU{hYmxMWXXynV2vc{ZYc)qu%5Ibd>E7J8OW-bbH>{f2i$+MF$96t1%8-@qLrJN{;#J@iLKi zQxvJe+(Gm^R)jSgMUV;1D9XtLi*Y{VY#Oa(~2{2OE(6x_tf#NGlv;CGm-bh z6_@)T#nAa%(5m`NRO;Np@Gblfwq1m>KwHGKN~!b1aBRz;%)1n#EXRFlK5K`X<=@2U z`x-RGat`ysn#7S{ZCc}Rhh{%LxO4Bqe88sv?}fJbN^x&863z;OidU&of>tCtw+7HF z-8Og}Wgmi(nowG`P3Z1;g~U@S!fsKmm{rJ5ydX!hS|wDX{tRFJaYoq4fAn5(TmO=xQ>vDQ#fBSgzoJ; z0ml(9ocnJ5f2qz%mG8vVW2kZIWl`V5Vs{txVT@HiY*MuWJ49kad$qx(~R?g-_df-i(W45LeWDw z9UZS0U2R@BJ^>(52WzGmoN!FfL7(-II#W>MkSraqRY%Adj1gBPa{!~QiaE% zuaWil2jVCVFXjGV@9ST9@jV+KgK7{it4Mps?dLhKF}>#JUXj8HTr%lNMkcOQJ+v4x zeD-&q>reI{PeR>!8E!pxp}XBQ=;m(b=iPQDuVy(?{J0GBV*(Uj!aMeB1Z9E ztbP3w)Op;50(Yr*#m~i*lB0N&EXOX(Em-&aglOS=u!ql9JW=W|>?d;`GByoa;gw>Z zq8X`hPVoIz8B95-O8Wx6X*zX?Vbwn|F~N#wm)}K(qXJ#4dWgUCD)2zbogVRwe`nW+ zIM850J$F{%S{QS0P1&V$ph?W`9)%%6vQ)ZYmzXb!#x&0V+}u(jVmV(rkY_JnZ&=gK zNvbp>Tc6H#xYB?_+N5;Vf-XjL=U_fF;TJNuWxgr-nC?P=GN_lEAqB){L$*xNoeF-Z zF5U_MuEWTKGjA_%?ncPrTy+0?5%$f=%zrz8LH}i8;Muu2t&j$vTs^9+%0!>8t&nUr zp(RpBasIR-1#^~ib#M~0GJnF~Oop0o%oE{19k$yaS{64|v|g#SvORg6@Tyfo8=YGe%N#Cbuv z6IScC09X;0D z)4pfFQFYlJv19Mz^wCIUZPa7d&pY&awh6AIXTZYe309sz3|H-^s2XUB;x=Zy%5tW6 z!w?Lg*Oh8EK7+aZU}V@>($`IkkjnWY%W1vo5PO3DJ!5v>9X_uYY=hHQb~@ztq&rZq9Vk08pMSp~l?^n7I=@@&&wXcB7ISH8 za}ZVc0YRlV5#$kxS&DTqUD1RQ4Z5W2&*#g@MdHyj6H@JGPWKKeVckkoTJxHDeP%)8 z^Cd@e85M&e53*6vi*w$-2Vl_kCd}L|$m97oXfIDfm*uY!(YXq~xAgH>=?}ioTFNtM zC(N4k9J@W^Fs#WQ`?p=e!3RayzhV&j&b*7Ln*U&tx&}`hlMz}{293Lp7~tfIl4*|g zaN$ARH}~K?C3iDaui%~0Y9u*1k@NgL+`V$9(s&ke@A*kp46bX9+6kPsYIhM zEj>0Ai^GrM{2N75-91*QeJaAuP9-W0yD36?=i|bU4ix?Lk!b$Q7JE|I`M%Se%1R-o z)SSn@kM<;I{7aMsJ;bW5ZnWX44!ek)c*H*qljdnsK>QyLK)Au^yBRIE>IdEJ{SiRj zC_qz|LZ3dy@ly6DuIxkruELQ0O%3|3%vpbj`Seu`z03^#e!IoG*p>bs`;JXD>|Uxdq;7t1ncX~EoXRPIb>y(RG z_in?vnEN+tG9~AgPvBl?9x7HvV%UBaAV&=VE- z#QsIk-!`y0{uA?eXwlc{>PRg=i|O5M$o0Q^oZ&3Yo37^UE2_qCrBpoiFru01FL7%8 zTex;E#vuiNTA3?H&(4IQ@tmN3JQF^gFd2IadFOQ95wZV$6MHxQ#DO|POtInFH}6== zX7|Pi-wyHXs48t5-i_`v=ic{=2ifsneQ9+otkoRpSSCAF9sk3>seGnxZNS%?9XKT= zOUcDgpj=Xm;1LQ`Tv&x|{gpT~tWNtS-^Bg7f|eCt7Y>_`i=mJDk^7@9BJs^PksK}P z<(=->+@}}kKbo=c>ky<5kkIzMQq*Vf0t}3{C55rS_tj%; zlIOpCO)t@)yAYQi%r2_wzK6@c8CbvEvuJDEanY~hqtKY}20Qmzk@XEjtUmt_rg!Zq zO?8h@-}(^+Atsz5b)dI82asy3&W?+o)I2#3KU_^|MLYY4cwXK0f-3Ee=G>O|0CALO zMZ;ut$?dVD$g^P&Yefe}MO_n~eVl2|B>poUY(s~1LnO1yeMq~_l>8MS$$ex;vmWqw zchbqC;gf67GB1~T`-=3hOBHUf;P3g)&Wgs$??hqO7m`**H>$gq3Ju@uqO-_~stZlYlo|gu+!Ov^ zx4Gkv5&Ax>$Mb>gI^djFkKLW{c#)1nnRjrPc_jhvdl8^pj6QNURN3tkYHPSdP~u6I zr_YHw6XxNYzZ_*0 z?aJ>?b26wmp|B+uH0EMYO0PPG+cM1jJmp1pj@(ap>_??;W;FWY75vq9qt?|qa1M{e zpj>wJM5W`J`36>BSlY>h9j{=l}@qC!gr50Zc{3L8!=W}e;) zgnuiNm;_tHZ|5&u`O_i^*{Ovd=_ zq}L+w;D4}u#(s46m94v}LOD67h5j_&EpT_m%2`t!&(Nb-o`svsv6rF7o=%4xN6@bm zNN%>K?^`ZG|JYvCG+I;Nl(X!I<$U!m~Z^3Z`6 z97#pN@dbDj$_^i+Tr3;pME;@PoI~=*qbhq!tM;OrXPdDt-H(zQ`2TNp!Iyx2czjZk z3a^F<%brKzZS(`(H&2(WDN2XQmabHreoaiXTp~XEWO7E3+1W1&#VW!RYeD)BFac^fYM zL?6y1WN&Dbtk&UKVZ9^$dlV%i^1D*&Jm#q#4-kRdZRmQPrl{jPj?(fT@Nuv&8f|eM zk>-wAZ(t}QnXT&8c0>Fy`v$k|%;@3YZy@{Kbtdf;jSW>O>)#5?m0Dtyl@cvIug2Ud zOLCI^hDvvJ8lS35>62=qbj*NkFFEios1cd_&xxOp4&Z%FJ{+dkN*)Hr!C(3W(jMOs z*JtJPy!0FvjErQDVwPymyN-JEa7YxIgxM4Br+3VOwnjgZp85o;XBUemS)GHzSAL>Ok)7vKm$5Euh|ulYjc1Vu+2LR=Hf8&hOyP5!3|%M`dj*l( zn9pclI8XHHbfRm=t6*nZA_-S}BtCQ!_4>@5>k^5SHk|e zM2uB9iLY6gnHRZRG)A9AWWx(g)ZANi^;sTna!zCNI(@oO*)D3FnIrH(iQzbQHVfp{BW|bc`u$d?-k#xyU?Cfx5V!YGU)q5krscnMVIm0MCKy? z9+MhL&#l8X-gyM_XL4S*Z;<-Nci9ykcs__(^R~{k;WO`#hnJ!7tTvrBdWNh`RT$sQ z=Ybzb*)x3&qxY%O)f=h8y&;fHi=2_#@l&L!29u$i1>~>FiH=``$iU1EVJ(T0%rkfK zM?I6XoXO&2&->^r!~XY0*L_#{zQJ{$!}xQO^FOVe|2bmouk5xA$G%^I{rzi_SQGXP z#iZi@I6CimuG{ww+k0da+1Yz$eeUz3NlN2s@13@G+D3&$Lpvpr($-KRv`bPd+95(I zno{~*-`~H_>s5OAc)#!ay3X@B3dU^<+ArH(GTg(P+Rv%e&acxX-qEh)#hH)TVX{Ss zPB_y1i7K>8_b5D^`jYY2abm#kgUl84r|ybM;`P=-OwaB`>A9zcnk@H24{@%Cc|qIe zE`o$}*onO7o~^8l$RCzuIb52(Dc)44!+QzN^;qw8qu`KGOB;#f6NQWHKjnsW|E#M0=lHgU4Ow@Ciri+~Y)> z_g)h>7OT^XPn?y?7Gf}WwJRrZ&!;*|jGH4xc3WOxbo^bs2??N#K_79mJr_?3+3hpE z3VCLi;UVin9S!d#<5z9tt|K#lmdiuK~^UON_3MT#FO3U?YIDPUB&TL$O>tl1F+|KXbx#`H5{{a`K+tQWVROlR) zrk;geXlm(J)bKMzBKsTrmiMNyJMO@!wG*L+!|3tYLd?JR0;|G$v+wU7yN|C*?k4tS z{_;R{ZtqiA{csqT9$Lcrta$O`^C0HF%*E>K1aHn=UE!Z)dH?xHIb_CNVP~?~G7N=R z*C6uuIaT4ul;_Ob`oa@Ku zXLh5+cVNsgHzeOSr8_*s+wjto_I(1~Sg;ai8FuXP_9S;^(R|g`qc1&!>1#s^JXbR} zXL6z>uiz1K*SJ$hOK?GC#yb=`no;SYP|xg%yL&BWWRMX`fTB|T!S}kOr(*x`6?csGv%(@ZgDoC z2yx1h7!s|FCp)vyLPsDaJrD_E7wn9$aSzK6@BUuFi27Vu8b`6S-HdmkpflE4n0Uhw ziycGh$G~JLHL78FRxiqD?p`GCz8o|A&=`edtJ`*k}prnRDw9_{aHE}tx z)#=2Quy%?6i$eCnH{*B1q=HwwFXO^yo&oRJDc(dF!ffGR$gA3ugK;-B4pN|#3f$}9 zyS2pg143@tP#*j5E*x}cCK3B}ZKP=`yEzZu9E0+vE@U`i0A|Fcz}Z`g3cMQd;o1XC z*`z_aoVCm|DaDUGEz;ff5Nfd%FkJRPEa%K_$n{Ags?-)Y&vACI-B!$RzA7R|da{%G zs^}9GOvZWrCB2P&$fMst%J?-zw3`Re>TldZZ8s79TkV-+)+%04se*RoMq%vyPYf8` z1nGfNSWtgY4DHv7_O_prPi|UN)Hj%XZ|c&ozt% z3#9?OexsTFE!StVn>)7)ZucyK-27#bl@G!ZW-%ZBFa^Vx@|;KQ40@JpiZ;v5I6Hvz zKTm2Ui}tQX=lLcPQMyHR&CY~47AMAx&_wTLM%2B0CZ0U_&D=F}N?z#5y<1JlafH1zbSqkSVII~&u;5lG3ejH}MrBV(CT7AUI3EH%MwjGu)t3&BJbMo#D^!Oo1 z_2xQs=a?Rb$R0yuS9U_Y|A(mIPWC(e~Uxcr_vIS zpkEkZ$6c^}6GcpK9U3LQ31z+ZNOUbc5mvU0XVIO^k^UfLp2kA?KWVD83xJOpgY*OH zw1Rt0ga3U+DQ5w{nrl+P+^-mK-~DbL!?)1f zCrf?{-eMCobGDskFI{YdNMnbRR_zO67@aMQPV}eE-5-kVk@e#Jbw7H1%>mcWa!y(9 zE6i1UV}C8rw9a)Res~XLFY>0Z&D_H>o`jCE<1n9i-}BCexoe}DZ_$8yIR*AG`$ESg zAHx-F@XBboWDj?X{|-_k;}5gM=V{Qm5vt_0XCKs4_Db$GxQIF5 zHsW&q?Ly1P3x!LsJb0JN3iIGJadb$Vcox!xx&j-TU6v;1@A!$pOk>JhU>A?#Y8B1M=&T zb2SGOQk1BC)*W1_Jb+04dEE$jje&|8SP-jEcQjwabDTWoZ`=;$EY8U=&#&hGLhSne z6CXGi6lI%@oJa>c@jHN0>k5#5+?O_R{%39K17^oqQ?FZnDRS{8=!NdY*%^GM*q}$* z^{MFnIFNRI?MgGJW)lJ zu4rz=?9Bhf%NaJL`|2RyX^X_jL@NsG&(FSebCIU(P4OkB^d-{-G5w`zTdg?>6I(pJ z{stMvI#iroD|+yM*Y182mPhYno1r)Jn~$Tc+bJ9@52TSQ`_Z@SZ8#+HzCS%zEE&84 z&xXrWSjS7r_1PP6^NtF=C_F4uRTFUP?ME!?Z9}Hz2F!3Zq~F&an5$$-@BX)&8_r)1*Uwq_Ff5CUy;G?}9ZAEG&TWI`+kW$;Fi1O}w*ti5beL zF)QZ)e8!Z*J7g!`evgED_s7tkXh52!mr>GOgN7V)8m4s_{kVI$yg-EOM_ zxx3vaq7nvE>(m+O`nDG;mM!4th(Go4n9RJv>9D)rms%9Z;jUpXbcH)ze!x!cylBx+ z>y9vy=UI?UxG?_IUF;ZQN~=P)OFq187guQ9%aEJ^FFSTNA4?M+*^<7>_QlDLqdO&5|BB$E6MS>vYUJp>Tm1Qp+er_=Apvv#n%!h(gM6WN!Yi7^)AG4}019L}ACe%$A9*&ioa)6pGs z{3}tgAWd?%Z8pMP+3h`bvT(OCX1_=rf8N^Q_lG;bF>7)0Ja;`FF%Q}z6JayIKxTgn zMs@J>&QhHmpI;Ew+t{;f-Ia_r*NSfJ;#FQKOV)2ZMdof#qVMTwOUS?=B~Q{^kdGg) zlW^pL9hoE?MR4hPT>1C}vbz@Je`kl3W#zbdVKZhc&c|uTZ_w9S1k?-kq0(;yY|og}pwrt>#UQJbrnaQroQe6&Y#2Vl zjw;MnqIx_tnstLHwiT%3j>wy{{*?4$6|PCV=;swzy1mOAhR^wK!M*+!+ZTu#=XT@v zg)hk4swDmlJ%wTOR7i8rMWJmUC5obQA+6Yx45pQf^s1fk&J=XxUYzjSb`!&xD>Q~Z z25#$yAu!$%J?+ft{Hryvv)9C8YeV|+$qEj~v~Zy-cNHy}ms8Jffi_zj7OF~_ymMP~ zRi8|XKSSyGL&Ulq7gydI)4pjXlKA5dqTr@2Ib0Ygx}EwXyv}lO)b?_F~`wi@lu#}9VgkE zeg~mxfw=Yb2ktD|BQ^xd&|v>Y{IZCWm{%&&`aR9~S{o;xud9btmI~>0YAb4r+Kx*JUMragt+h!nn@dQr)kYFw41 zidAWSX#t=6I&?B5p6}j?7lP=K`3f<(!U#>Hd(h&@s)9-1)DUSsfHclLL*{v1(S7;_ z{H?l#g?q<}bg2tyo&Owx11}aH*SUiJ*;PEVG^U6eMVR0jUZ^>OHW^U! zyeHz+!0Y_?@$Y>@4eXcJ;Q@C)Css6~bI(P1FEyozkO$nY=ikL{$uJ8Grh%i|Fz4h( zG@R!yQL+N(b+;kNRzhy}kAxVuM+AGS)0kt&#I%>nNHtNRusZ&p_TcBNlQtzW)1YQp z1NzCk($fdh)bH6R_K>j)bDSZabZo}}UO2WaIR|6oPJFm2ORb$(F!YBi4N_92;$wGk zC#n$|E9Gg$O?^Dm?nQeo{Y0~`GVbo_Ny*Fz*(xWGn8qQrJp8*v^%3U;>#nfR4A|G( zgsgs4!CS=|<-D($n9Lli;9;;oHIKR47qI1cKcwGZkIjz?@jOKn=YRJ^CC`g=clN~q zuVC7}%z#|4d9YW|m&zV;hCA1e{11fEWV$`S(3Mt^UyKuMnP!a zP_eRQ1D=2XCQ%9RCrlK|#L9{64ISf38KsMa+D298;W(4heR-(=r$E}=@jBntopJ_F z!tG9G?*!P;C7IqhwOyS`H#^f^rC6LBWJZnKbjVntU9v67gWS8bw>DM?uMck2|GqkT zjLZ~&N4irW&o4e&xzO(xJ*=?~r0=6`X-3mOao5J1(s161!6dm+`JfLf4l4 zL+AVx-0_T{0y}LA4i3WSsH4orv8L*s21veBf~h_>%-uD^%3C`zVW<(Ex6QyqcJ9<1 z;CV}Z24?2&6e;{QA2Va@_dX5SaVNOvcs1G{X#?}hPK147kJQ|e@N;N|b%rc`81PzX zhyP<|$r<={WW$WlkjafZ@Fw~Qem`*_M~ib<#Y}4D)$IDb8Yix-jl;ANJol?VEt2|g z#VY!TjyJc1j_z8E&&+ZN*~$Is&Gz(SyeWO&q(^4{z7%S0L-OCWsN$p}-81V#W3Ss$ zZS)0X9QB|p%z9MleF`}nIHOj=PKDMAW@dZSJKr)?_g{y;M$hmz>Kc~M+KUec4Bm zdc8X-glSK|ov~PtZygmz&dY&lP+AGvX z#~@CaV3*NGboBQXwIv?-^*RMXlTQ^^_3aNi<1`qo%@w^lhwy#ukDv|L4vWVYUHRO; zt#Dy=lThEK&1~+*pg*s!N>nH2qiom+Ojx>0-1EDS8$IT7|4AMSX8HKE+5}s>r;3$( z#v$p2J*gaSg>+m$_AVRKgUeF%a5v{E7rRjm^E7rH*23Q7-w|@90I!dA!$G}&u(y28 zdpd8lME-)7VJ5a!wD4@u0T~9iWaso1(?cd<`85|ZS^g6z=2&9Te@1lW$#Ql~=+c8* zK{WB*7MvN}mA=jHLvDUsQME#smQ^rEaL^4bsOI@@pecR&_6F)%7BpSOmOgJOz-qo* zL=ES2=;LvSjvRnGE&i-{Eyj`s(WsuV4vO6!v1#XItew0I?wn`LoYI3DZZwMBy>6g= zJLg3*>~S#r3y$pxWM+DmC^3*A=$8yFc%(#6wi{u9pENnM3%KW-8OSXDj)=t?WSd~Z z{7hMrolqrY-&)a(Kbll?^0#PdH6r7KN)+lgR@_?aM`0<45zs3K?>oI{-s1vfY|Q6b zm@jvcPr(0v62_~1M?Cv>H=P>?XTBGoG1>%mcG2Xf%Tm8-6EN9+7+g$`V{_US4EV=; zSAJj4>GBl!MjYlm{!UzKD1bt@G#oh;im%5lX*Yil6trA1miZiuM=e0=ghi+;?naXu znDubECwUe5(6flm(Az(Vs&zpIPZnXZTQEJ!AvydZ;GEabDy(7ni%=66w|I{9L_s+iJivo>T#JcF^i z16_1cq*?06u&3gUC>!C%IX*8^wd;;k<87H=(SyA=Um{}S`+x86+soV`a z{xOUaU%!(4m}Eyy%0YBX^RX!9=hD}xQKF>dG1hGhg}kzZ_?^_k{=6A*I>=5gqbFGS zO$Wz}+Hq`Fk>siETm1Lt9d@_cNd6YzLlkF$UJO}PxZ=OJupMbencY2U?XVBf>DG<@ zNLka{m=~C7&wJ=i5~_Ic8qaSxis6S2AYP*cSN4n(2Re@+k8_-Jk~NX~^b(5p+`>Mq z9SB=oCbrzVg%R(UVx-O~(b@R|IS~nn9;l5>KL7MjcA&`T!;z%dOPCEer~2}x2#qO` zco(}-mse}C(|519`rJk|uk1#fD_cdLs+>?wGo;$Di-gsIn_^O9SE@X(0jC9hsN}E` zbC(X__TVte3FEWr{4Jcl97_K!lB4g_*dy0Z7Ga;?OUky^aX!%$)-w!>D))WFoh7QQ zl361vO@3q4?WH2FNe(O5qy}mE<%r4qeDLQ#6VXw2`0Bs0()jCdD!FyM1QlsS#Xc!G zlXV%j7d&Xs+&!2$=Qv!^pVmJ~!}osgv2&U-otvnL)XTCo*F~9T^{y9K_8h)SoSkdw zf^|nSae7i$`l5Io`-WV_$|ve{nSDxj6L!O9ya{Dn-9{hwbIJEi#^S5JNw-do!g%L) zcSkUd?EVJ{&kvy7Jc6VL>EmZZrFi4QIrZ%k_;lf)_>*GHS!!8mOtVBln>Lwl)uJvB z*dwvThpagJRhsk@hq+t$@3%Ic4PZtCKVM>pKEk^RU+|jmYn9#V@P+rbJqk4`^ZaSN z;0}`cG#%!IszUo9(f>H#nqS_Edlo!vi#s8{yGY}Ixx1R--B9~bmk!0|W6FnKnEF(g zUgaIb)46K6-PeR7XH~$&z6S%k<>QqirYdts5#b|63>AV&-Dg)9;dj z^1Fn)(P{Qf1ql6_?1DE{$GB7fQ0rhora2dc>Vm)6#d#XlUh3>1Rin7nZe*QkNui(S zVC~tibbPigXq~-Z|9j8dE44)0&uUcsJLHg7wU-;}-Vp#{I%zlp4)m7C`@b?iC>;|G{^5cgh^< zA*L>3*R0u34DNS_`wc!Md#)BEe;xyj597yxeRwmUe@9Ht;#A^als;o8u<2Rgfeb}98&XEcLFlYdqScYIBz06% zQh)jkpE2s7YWao*Ie8El6zK5Cv7)v|7QR*t7UEdDSS_`RJ5lvwq`m^;Z_mZPN8Lr@ zkv~E)Vh?t-=wR>qSbn~QaGpZ}b=&u1YOp`EQ2S$+Z9H>&dXu7ESDO9ZRW#>VA?dCK zby3JCQ6!n17T~oYK-G<43`0SQih2=VPgXZ-y$GPZI zENkvp_-S7_uFv6)yMKwKwXZEsjAj4DnSYq&z&qFXIT$DXi~qjUykp+TJIEFoX;ooL zfECGo)}wuM&x^zm&fA|+AlWC8V%bqE`pCTEA$uo_URBJ*Dc=UCFQ?G9-;}H`9z?{- z9T+;pgbFUFV~@`bDDgaL_P8j>S}XF`YJqm~c66(AMa<5xxEvON#{>Ofe)c-H^tply z1rdn%tH9xc7dUxf1x~YHX|Z%Mu0;Fdk{n>3=|(@8C2M+Q9;$`}QMN)d@}sOUJ;9si zuiV4$J|8MN?oBmvlW--$m&Wh%rl0KKupT6#cBc>;zheldFD${5b5fMl)l{rmQI0v) z@9`>qk3_?z8f}~#@ja3tO1&M0WZOMls&ge1dj)YPgPEV3%&D}?4q>#ej`^LgWYwWY zD>;KT`{Ozon`lw`;&4fJy6}F@>LLzn z>Cp944a~r<#i~?8G|lFm^Cmyh^R+h|qjl+u#+*X`lUh(6+?^i(7%l$zNd@&7GDb4= zvls2%vOzMrZM~4*Cg^uW--76^lSIH9b~zga2-8Wouxr+Pk+v&Q9Ncgjv!rH;73+Np zHymZ}c={)C%Tb@zHIkbRaG>QP{Lc=>)y15xbk9P}j}d55c7vhC0}NI3qk_)4aJcS>Ir+V4fW05O zEaN^P@90Qv2j=Q_#YO9TLZ(KA_XCrmJm{{7d7(`=Bb*SHs*3Ubv?-R)y{YV;e328PU^6!`>u6MBh7 zm7j$(?+e$PM2PS%@{rx`FaE8G3h4V%A6mmt3zOAn#Ql}7Fy664(qw%MuUh6vOkVQL zvvCifZ`KJ%oWik-SqPFy6}`&1fEm``F~ifC;*|7I`bCA$l7BwQn>ql3EuT?!`Uo7GPF2>Rr>5X31_|}?^Jgl zZO4YuDm3e&0={Yl(?^{jVrR}PF=^pI8Y2H$+&QL+h5LiZrQRKeKQ-yatA|KW83A7- zGm@WGhdDha!gjPeEm3EtXgmK*&o2O8-{E<6u_)%qbJK?>*fNNBciq-w^sZ;98_-XD zTVx;{v4?OC{Du`D%nZb$WLj3mdk70Z&}az-Q(V}f2t{v3>zTv(fh<+G<0 zG?%}Wtl2aJf36KKd~x)z#3R6vT_dfCA7Mz+FMQb7EyulfD`rhT7r%G^#)@(c@;~KD z)~jQYHqVIGCOgqH1J0V&=~A%7mHvETuDpRg4VcbcuEpaezc2Z6R*LyHsb?f>Msm;H zQG*>%Zbc=(J!tf9e@gk*jTU8kP-8+tg_`=OLAJdH?|N$RU2!HXMt#MsCo;5r zW;(mM97z6}JDn=Kg02hMyGZ^ayr-t;ywV(MR0P*?^)i z*F|NTG2I-Ui=ehLQNFAziHYm+N-3Xn@a%L6bK{x#2~pFb$SmjpI;OHzq-FhPo`)4} z{W?Y@G0!>sTN?)aSBgJ|d?q_vj}3z=P|EYRh@20YF^lt-oSok{aFftz+=d&iN|e4O zL&&~LhvA<#jNTV08MSB^GG}Pe-Zm%NeOihJZr7*a6(*!~Tbj1zb5=v!l{~ZA8((ft zC#Ad7{(>09AcPh~ai{1_E*@-TPInJ`+B-4|zg7;S@8-FfxNi@#HkF{e#}RZsi-JsS zF^*k21r>`UST36d`)%5!ote*$VHwVw%F_D_ytn3Tw$(2a&VZc4#ks1a%+Jbz_HfZq zQOF)nCHfg~TllnPqr{>CzejXPybk8!Rp0HB#0|CL&J#XA-aRR_hU=q3@b}e2rSJ%M z+n#;g52cYhNN-$*y_!TpPOA8zkb?R&FY2A z#LGI->0^yhu~{WXEHfny{g0SFMH~8t%tkGiBI~D?+-G|P!|rKFd8q@hZ7qnsyOFa| z18`LOIdV8hWfA@o4=bebix~_7=j(8FTNlJAI8vnfQ+&&E!Cmenp46O*9p${Y({!bO z52rwiXT)#wEy>e987h}FNP%}V`Sau`T)}|m4)UaK2b=KnlrDvs_*2RDe@L;`;2eVu zeQ>klF7W~^Sh57mM_J%dN*cz<&E}qcFSIP14!xV9xEN7^pVGl}VzxHyt~|u{B|T}; z>JfN$_9G4`_NCZ<8aUOGhV-)En0MzUW^q<^_R z)cyDv^qAew5An-jHq?(8Abv(U{FJxi!z%~8{I4Fi2P1IB-UH3w&Oz0_6y3Uw!jZ;< zI5_t~d_0Vn%%Qb0K%9H==9}DS25RNcWFH(K+rY)XLI)}N#&Pz;ly$1^m6{zcL372lSP}8ti{Ig05kk6<>Vu?Lk zW^rb8`?jDOc1wS3{0Frz7Gk=hCi(HR)}iQ*b!GVeYEO%8 z6{+d65>*V}I+YpIR+(ZtPAh9u|oY(@Zefv;wy~eCWc{3Cw`+h7&Czl%>7_pQTOU?jA^{ z9n5?x>5C(w&qZRn8rc?v;qhx_{yJUha{pOauJ~3wkmJsWuLi#9`OqKRPTWXN!r;{s zT5yXqq(Aul{WFMGeN~|@{bP||njyR&CJTr9N_-o1R2+B`B5-I(QM3BRMadaZ@ zzwV3@^T&P`SC0g1^G*j+*gj<% zIcFS9jX^K_5YBW2qwTX4?aKU*8Iz`X)vQkY!ga8;-wZ_mQ{>s0B~{(3M)ct&lDt+& zD&;f$m|t%SHQ4=Wj<~U$LGUc<<4py;#gwmOzGmW9j!hMBaICQSeWW z#;%m1unBX;+x@LL;PDkfJ6j5-^pGR5h~JOzx8wMhU6O?|!-V$Y40I%hOOh9}*T`-K zu0`q;)+z^xMay;Y^h+CzoUN%j(GxLGWoh+b7xvdS3x`X8A>(IAPdL9a%4Ip0*6UMA zo+k}o?t+n=TM0kpN>PtDLK4xP?i#A_dwoCe%sE3Jr$={WHj00$6i!E z`)_n3Y3{*{xg?>*>8|u?zBly`@u#(`0_kD41>H00O-6k_V43m(1U;3aF88Zpp2l-T z&juJv*W$t0E7+?fL(67=z%MQKC=AI!#G}8ksWj|W+`2uI{-D%d_P?m3csr5ug?vmQ|* zfNExOHfQ1v?ql6T<$sz~;+Be>jmHtXT9?vnmgCkJo(u8W>oYs6$3&`#)NFRBsUOF^ z>3fAiX*WvjwGVS|r%8N2x{>?}FH)bqLwGBxQO?K!x_;kG{9MO7&<&%dIysK(E+agYDq~iK4O~|>-4Nq_vN7G( z2`g6)<$ks=b=nPueNzM`Ifqc3vJKWL#bfPzFH+*YbYP{iXQ$<72i*sj~a6|y4~{;mgt8dfgOMaZ)ah17b6O`VCIirEF9*U zljNim^=?kY_%iH_c}W5YAFL^N>*{u~ngzd^I61o?cPIrdDSpJk6l??2Ww z{;3HKaTp`Ki@Vd*784q1ZOASyFIvDXp>E9lUEEhfcM}%kTI@v}*yT!Rj_ik)-z}82 zJ%hVVG*0cV5=S+v_^!JHqsJTLCVfP)(J0&>uYm08Qk0}!#UZC*(4UowmH%#|MKK)r zhg2{htQy5dX?J)+?}MhcU6+ECfL6HqJjp-%1_ z9&cffh$nN-AlJl)bQ{!!!Mk*9V4hHkXSs-ac@Ia-ooUHg4O;Nt74bR2s9f8Hie>7A zbh{nae=z2LaDVPn2BF6-LkhnvPgEti$jj|$xCCapTY8AG;+bzaT zGNTw8BG&sJ6jSFrk;~1K;?u--aUkA^#?B~`d}PMy6z_rJc%dg1*n><5pB5+UcsEw& zL5mJ{7naGEq_*&!Wax!#c+R=U+`glQ+gDsg!su$DGJS}U9&j3wf5wUizMD8rd@Rzn zWhtZ5k3olixAPFT%6@CMf1r z!Tt7|D82OwJ!*}p#?J*m>uR`LYDZ@4oUn;`etTn$>GUl%ycu`{<7U{>=)>%aKmQIx zpK*rmWh?XWnSIWFYu}Q4$Te%l80j^rtM{X*@%5;?yb_NU`;pCMMe=*H9z7Pa>vya@ z6emc-=^gJEaz~+V{Sz_gFz4&X+Tq3!e{A`oP6tc0D3$jbso8y4ickPk4zwxr*!;e*>2SWg1_v&MblNLTWP61E~U$ z`Z<~RgP~ORK2z*+eIi`P_M+o?`Uq=sqpO~ca7|#&ZDjyWnAwgwGIP;g+lD5x07%2t z11`QJ5q!1`h!n>8gc1?c459l(|fq z4*Drj@Uj77QUU)zS<2G0H%a(YrWSNOI!N4Lreep$@B$;b6k)J13%<813;I=U6P0V< zh>ofn+#1dd;5$je=h;WJ<{FW~MH?8LY(>{=mSn>|O|{W{FWaq7+snFBc+oUWtXHC? zPn~Iv@-{rJWxw(SC6YCC6YksH$^5ny?cQ4^2CI0{{7&xmo|zy{j<6$@IA=a<+EQVR zoMaF?h=z9arhQY+7e)pKush6>jPE>?oSggyvliq+@`UdbqweEo%Q5_Sk2y9TcM<4z z1n+w@PxlM6<$m$(%Wx-jro7>dnF{^qG7(R9bih-eXAL0-kn`G{?(Xp=%inp3Vy}6g zSpaDz6~e~Qm{KS9rOpG)VOQgxtiB`ffwbvjVj3*}G7r|kkXD_`gda2gr%sor-;R@! z9LdkR-da2(^274L-53RRnjP&0C;tMx_-RP8iW_jWHbxlq?M{)j0h>c~MCEZ0TKM50 zv#igGMt0sl<@t8dUO7BlC`FrcEUDKQO@!*dgZc_~Z_IonysG{oIX4RJgw;C3$*hHNp*`0Xf>Tv5vT#JVyAv-i3%c#P|A%%)#@fEeVrR zzq}`I&H?p|9D2f3?}yY#NU+9&b>FbP%}RKWH^BGr525QCBtm3Ip!P;Av_3cE z(b4yCkcq|q+y^ik{tKlw8BpwO#ed-ra#BoPqGQ^rP_{^|i{WawOu_61ivj`cs72^(6Vu?3rULOjik1R*; z@+1sPpU>}w7UV6Q$(-~4xH0V`5wIVF=qqyT9!uD&hQ3?WN%i9)VXVDCtfCw=`by|@ z;9are!ggSr5B>UkQQQhG#+oL7x>saCUD@5xw%ZPRkC->X9l?|PxhqzwPr6$PImKT@ z??PQ#`9hw|nHjU2d&h&Heun?O+sL|PNiDzBNLTqXCRm;ny8KMK{Og{iV_Tt+;ZEcA z^^Rh(%y02^i$3qw(+h`KI#a@($C91n&FEHdXF4=tm57MwLM!_8pvl2rqR`Ejwq=`( z@T%i5?qi5({d)yzhBuLv?u?j!PGZv5<7nxAPIURuhLfu^gsXc8{%}t5#hOQw{#_I( zF77?f6z>uJ4!uT+r3Pg_wx%!FTT!!7iM|9HP{Q%Iuvwr-#^0TJj{FjiM-GdnQ@6ou zY93ymE|p;GF36rfg67-L#J9YY(0hLdJIprXk>PP+R>x-}x2ep%`zo9>FQZ0!K5ir^ z3zs|h@NK)9xFhGnOn5zvHW@A6+b_VH!&bOdFuL%~8!OyCFTwbo9pc4S1N!(g9F|G) z7;Dmnk{o%jGox~6n7B!YQ97LxI*y8O(`#kmQAV%vLN%4^t)P~(@9TS1ECTzmy} z&o#uP7ySPJ^bFM&Nn+u*-gN8uXY@Y1PS{7d(ED5O5cc+-b9H7sCN*KPZ(TvFeI{z8o*{jK zAvGS5LHDL_h+C^q>l+@4e&3&vk?^Lu zHO^BQ`Mtw%z0a865sz+{Kpm-#csXMwb_WckmSOVDWJ=|3rG(yoYY{qa8^rufHENIZ zL-(L~QB_R7OliFACDoK*1>^GSPd4Lf!sww`4u@2@`S86{8g2d(MrZCjeMcr~;h7}L8J8(J|e1S2nYrAfmq>Gs)J>}$6p zEo=55WcZL%s+rK>`)#D57mX?iC`7a!eQyY&=wEjW?yRnq+&sW+(^NNdI$$k+@LBii zQ$Y!TYa~y*xYCl@uC%eW5{bK$F_iBVy%v?hYT0^Rjpt55aRKiM55b0e`tq3{5n-JsqI+vYAT-0 z;+}m+2eW_Y!pou@`IRaZc%&2;jy?~XF~g3;KhBx&_Lii#a>jFg89alRh^#H#t^4Rq zu?O~x5l1zso4FtTR>=}SyT~w4-;73Im?k2lm1)IHrJwA+ashH8=O z!mjk|t{JUcYD_ZU|HstpLB9E%>AzymtN|O^es?zt?3;s9JKgBxxFjUq(-vnhGqX1& z5ABCv1t|Btf-93aBl-Cf?^5z{Zs$gXUfhdA?6&DYFBBDbE@N><7us>_IIdNH!GX?B z?9IvG9q%*ZU?!jqH=Xw8_lzXD)WhUgU zdeQ8MJu&_-?+haaC0j;e+}eFO8}=W$e~iFE-%XJIF$1=*79yQF|IWb?2s2B8%Z(tq zQ5S;+cllgmN6b&|Mq#RVC3d#n=o95YzdTmn?)|TooP;sdcDTs zfT3uZr%QDUdG0-B01g#8lAp~od>mmw^xlukWz*1~XW$p)xU)Nb15EjB6O!*t<7{PU zm?O`o1DvQ?>O01KH=zgPJ?Q-GpHR^5LCXd?(9bdx=p7w_<;!PasVD6K6d_M!p z!zApFs)xTN_XrN1LP6h;xEg+(edl&`pk0&9L(171$4-X5X3RLw7xRBQkgtI<^`9Cd zvR!z7=E?i6>3lxi-i_8DJ&2i;uH$`A3)<9t7Fq|k^ZEWI+6+8#lq*125>3I8dJiMyO=h9)c+rJ#X`O?(!4b(6G=0$8B4TE@;@F~g zT)wtnY~oy+YaVk>TTJQN{XZxQ<8J&KZBl;sjh*T3_>X6qdCto8FH%u5A-@Pe;~wM6 zfPkWIea}Ib^9GrZJ;lb9QXumx>_1I`mh?a2-TVNPRh&_^UlBR%*m*jif7jm`!KL~m zdW;_?x&L?-R-2v?e;D%Do-qlY#d>hm-X>YTelO(i>=jeqT8oDZlxV_;xw!A+C&tBU z(5V&fkXpP~jNs0L!G9ZoaW1s;CUc4oW#Re}KaxDqps%5u5%tc2isxF;RMqXc;ch89 zA72!W+xX6G;M|C@WvS;W1>6G9Um;d z7$~Dn$q7ks4-{5soj_pE97%QmET}x+iJ{*8MbX@gSYyR^>XEgDYA=|7wBZ+KuH-%a zO%o(dSEE;ky42c51=CzwaNmI0Sbc1Y5k4^R`|oo*Ms;r-u!Coh6_FZuft3ZPEyOXhXni35)l}6Qp zmFV+E`TsaN@3@@X_l-BSq=EL(rkzT=?&~;18Oe?(viIIABV?o^BdcT=iqev{GBV4^ z&WaEr4O;r0-`{`F>v=tc+@H^VUFUfm?>F_$ABHiWBM|4SMd^q6-jGs`ZkxUm z{ThibzsVO$B~r+6VYkshUHE^=68Fu9(Y}Z4#iTS{s=xJ+_XUG7Z#FXo%f6t^#1tRz zD3gaD^IyJC!*HWjkc#>Mf7uFgpt~0~e0_}HU#o=Un!U(5_7WPw8Zc~C6;DRU^1NJz zTKaw~oOMHyCgsc1upCu!e^3i%71p5U^C9H#9Y8G_7sPSCS4VJ%;NOvp!t}~TZ2Z}u zIw#3M>4pQ&>HOnfoi06+`YmRc$&vzdZyJaB!9Tex<$g1#*W=hb{wfr18iuq8eMsxf z7Ib&DrLsx3wC0IFYJ;`u^~GMaDb$sscAAKxDjJ+qaH91c4uu=5t?8khC$%q9^%-*V zgQUK@F|AhTxon%6uz22=?wwWf`_4&#mjXSSpMEBv}VlG%(pbf>sQ zVl8zJg9ppdRYgA2d3LAfPr4*Ab*FicwMn0uWrGd5!{%l{#XIy#iaE0Cu?MlQj;Ws1a70TD0lx5qy3jN9AifG3jdxCPXx2v0Xb>>4uA|kvVwH%!>B|YQ%_t z=lJvW0sHd?iv2ui8a>gE{&eXNwGBJ5>dAQacQChX!B)sS_%P3YCKR_H$03h-NRnTH z@iDHHXx$&uyc@l^-HWQ$@4%j=qmgMNXeskL^D5p+%)|OIhtZO}HKq7|JPzr@jF?U3 zAZc4&FXl$>f_I@kLR%gRwJ}MMd+*J2WEspnw2Pe)pG16dj`*DTUbwV5lJChnOg$4V z4hL9LzkT&+_@smr5uCxsN?Uq_-Rm0sIpVy- zm7S>gt4mfzwv20%p|@*n=zmQ0_Aw0zT53TzHoDOw?M588aiwnqEol98c1XS%2j%{= zaLB7Kj?DCe=DXg^HR}Z}rD$k5OvAt}FJZ#_SL#sz#b_=TSFX-K_n$y^LI+Ogn{*!akX#&BQh zb^cIx{_yh^V@N~FPl(V1eaMP;srAi?Xg+63ad}y&`MVGP^@fzW?*vLF6(j8ZD`sU3 zM~uB0LQem}lg$C>b9Nl|sNBcvM$TsO{cOdgT<9Fi#phAeG4;?xOqlwJcL_5ve*76^ z2HnQW&ug*e#vgHbE`QH89ENdxq!@6>jUxH1{ysz~CRIkHcp(sG zRr~Pghz_mOQH14y$+%nAll-J*=#5V$hTmfs_9A)usa^*2SY3Kr#yf};A91pQ-BX;q zG_y|-rlp0#?v*7)SEh++8oC(A&vmH5Wbu9b06IVWp=7g(BXfI*-Zg9!EvtLexRb-F z#5F|xWN-4kN0UYGl>5jYd|7lnj}rMA6|l{@E3v(7Dtw;QLOcGxICiiLeShgB+OrjC zx!PaOt>+1UV|BWEU4~w@ZWgCCf8#|a?_72m(Om9A-(cqda9dsGzc(V{Y&&+^vp3+R z5_>ZWe9rZ`2BYm2u$efbDE&?b65V;v5UC1D=)~?6<0OzcAWgiQ%U+;48{uQ?Kqp;y2&2D6 z3XQewbN|o=wMI?y7@&{I%$dmg-kX%4m_sUAiF=e32o&iJBJD@}DQIyP!rdm%fDJ2_Mqi zZcGk48!#=#mQEX2Atz3b4o+01sDu*8KbNLhX3&`LdW4u%HIkgxp`AUhie5^lk{Hg2 zdv)Xr*)x73Hy~MD54p2!;7Ibv(1593DCk!2pXwsi2IIXZoT=--_&kEkd zeTNPnYlh;X`5hda-UU)Y%yC>>g9W{B2!}#Xp2wD8S8%(CPw6KrmE|ZPT82V*E)go- ze)E020c|>83cCH{{jDB%P!(fwDKl207aAlYD^9_n-w;XDoLX_)a}C~lC=|BWdWcZY zZT=2yz$@+peKT~zkx}fr>%tt`zdwb~r+?U5uT9UJ`jOqiwfM(>HWii5q%_hIQJf=L z|IdLQyxGaQN(-8$(3NH;9~0;A`%uI_O}d&K!TuyZLk!{no|+Wr*F5OW7ItSCu!oYn z>GhrtlzV`^s^2{5UvF=k+|7h@y8}s2lb?YA=Ad)_C+_JBVCOy*_4&f?z-qY8&O_fJ z()4ZW7iJ+zQPrqZFjeKg+Pc5k9DEgirS&L!u0-+MckvFrH-+c*r{tZb=wx5Y&6dHm z)b0vg2RYJLo1t`TFLUM-x1unOv&rkb)BMUfynSaw=8nu&UcQyR#a^^RqCs_0v2f!2 z&#G`WddhPpihG8^qg9y?z+Ity_J1zZpyBuU9;>Y+y2O~#FW+QTJD(I66nfFXLGg&3 z5-S<;%aPKz^`pGdgW{WXH*&G^B3n0oA)KY?{Ba9f`#D8O)Vfl@_X>PSxredjN&!Dg z@$l6Xl;*L!*y;n4bu#h0n=>sa{~_Kyk3sQfHD+0U5aWLx#=>f52Wc-CA(oMtt=XO8 zYV2rUlRUi-F`*+BpX`ItsS_s`+B?xEZVyU;NhKHiP_KSA&a z4TO5CFO4j=z~h74aEbF|9zD8Io0)-R^c`u4D{2(JsJj^b-*yrAUW>fH$`l!<@!#LO z8Ri>+TUO7-^Tq_+I}(BciN!*}cN?Gaq)>b?Pt2PARp|ToA(ND9RAw9yhRl<^Jb}B= zm(AEIY)bEjG-F=)0LU)7jy^qj4spvIB^u9=nzjx8($i6G{tUjc7jg10&+)a{wNbA{ z53asO(6q&fQSL^a4v(-a#t%PhEJ-917LKeD(6FbsB_E6x&J;*BAQO__{TEU&5k{n_hMD-^+dSH~e?08%_Sn^%` zQo6`r(;%#l=+dsp9Ld8KPSk(SDB=6cndCUvG)0QLhX*q1``%$M;GvveLQO9h7qOg}(dUX$cn9IB7Q7gYkqL9mep2o&r z;-&uwo-5}IpSUdXtV;vVygM$w9llyvV^@t84=jX}Y@3L$nIdtU`_repu0DE?iV{JO z&WXtzt&nRqs&M|z60Dr8DRj1{!caXO&Mi4&#i1NbW}l~b%ANw(gj;{EFSVq{jSe`y{RV#X9d+rwPxyTJ1LkO1QlAI)%l=KA#87OWf}=0n6N8 zieVMq$pf~wPJSY^u4KkH(2q$@D1tvgK*y(QwVACV;8DG(pN6pPUH9tG#d>ap7} zQapdYNB9jqBMg7LQt*r0m^_&k<`K5c-F*XtZ@N(B-J`7QL%uVvWXI5E9QyqgeN`Rd z+qxgyR)59d<{Ht%ejuEJKfyx51{+m3OA^Oukm3_<=KVVt-Bn<|(`ijw)^~xpH?J32 zOyoPmnrIv-(G!l}cZs+2x$`wdy+DjR!n-hbrH2*zMD;kwbBAwYX6Y|99JeC#xL8q^ zCrzo>ji`%}0q5A6VfxgX--+&|JuMo7cPdKQN={(kz{exrwI#i5J!}7&Cl=%A(I*-ZI9OHP* z%$K1(FOsb3TMh$9W8vd zZeZr-gLoh5MIZU|;GMo7Gj9#1=w`lew_QSEvlnS^R-(bj=isw$I+{33;wn89_!0*_ z_J@``x#QD^n~+T~pfT;cF;M-g_{_fR?Z(;YbM3L%F@PP{syiX`_#}7A-AOvMH-*l+ zE23|yQCf;4ElUa)N3LnpYWA*q&7L5<>buc}X(jl)=L-CeI8fgYr?Dj=1*5xL(}$~1 z5Mq87H!$sKbrS zdfbEKs0e&W)V{uk#`njdeDf-nlo(U6-5p@=KLoe;pza+d*bpO6 zNnfmK=e-R4-Yi4b+#~!rM+y2D3vp!N7hHH!CJtUck1(Sj%)JRSSpVlm{{I(I5KizPKLfN)LU4FzV_Y zG~F3W+mlr3;fev|%6EjLt8}UIyF29;YvWLv3e~ZHIgN8#yN5==QgtlaU0;j7K^LLR zUGez7e?+Y_cm1yEVDqFhkvE0EgPEZ)a&IH@Klj1CEPW~%`5Q_d!*J?@J?&FrMx1S5 zg#G-D+5=}0J75Cje7_;EWG_Oie4)hWyb~@(xV54Nbu&(g$H#5yj&cn`j|7U=<-O=n z+HY)(Rz%;;_M}=h7qZjUsqCj0F_90->OFZs>OgC^M{&1No(g{YQ2g~mv>6#tW1Csv1k;wpJYFMX$y3vqu{md4<71w z`F~E(hppc+#9W>>cH^1m?haU`9~NP%##BD>5B_Tkl$Z=-o+#fz@?MM*#j(z$)g=y- ze&=C=uRRU;dm6PvI7=O9Pp8gq!J*|BaUit@7Zo;RM0<^>;~n$b{lk&-xhpo*)?n|u zI6TnO!dQ)K$St_ZdDZ=xQ2vyaoGqFjr8iL#V2|(A8*nx;w-iZLclKrdd$d&e!5#IyX~Q z{7F>%QvSN$BEZd&X8syZQcoLw9$e^4ey;x1Ju*u4+37=X|K^DEREH`vd9(!8NG3hpmrz` zIKRjA%rGIBtR@Oad_k>mk>unoBb*-XB)ldKFT7#w!~6H+!r3KKl+Jc#XXqzMOKbsN zUmq@Mn|TWJ4xUBr-auhjbO|HkGhnw;yQr~^^Kd_#vG2Y-1%`Esdz*e?8FL0(L#3I` zqd+n2GRrvhOf2-u!0-v&RopU#m@q-g21wRv-1m z%!NMP>1RR>;wRct$U6;MKCT^h%ol5$E<*}Ot1;z;HFcU6<9pY33~7?0J@F4z zehC{r1|gjpNY@{wVuPX^bNG6Z+2t}s{&Rs+$yA=*=V0U;U$~6fhOL?vi15|N(22cq z_24yTV-LlG2+pqO=<;mFni++I+4X5j-#+xFRrQYS@6e|@{`*RsPP3OFL?l1oCwY>S zgS98!Bn>ux;$ZVO9Cg1bNm+SY;`8IKu>bc5vWfQ8d$%#By2#N&-cO}(o+gZ$WA^T< zEycRokZMzZ$ZGHI!%1&YAzJY-w3&N4ev=s;*l$K3{%Vp_Oo9tseHtIS zbj7bz0i=FlHu@FyK=!8?yuP?klIZUVG58?vDen{)|Cr!ReM~y-!%#--0 zm5I?d8Wet3Q_P=sU$Wr0J_YapQn+*T2QfGP0<@wuxdEP0j~ zjtz^ulC7!-r5}q!gsLW0>pIbyP0@&5E~qGyAV)=xtHcc$0UK5=u3*z{@d!rMWZ>Vh+rX4kIkMFHKuGvy{GVHJIqh3H!ek?dqoNrR(s+vp5r*pX41&-2hL z%yLUIrSd`C|C+oHrM!n8Q5ufQwZTXWe+&P+>rm>k8>6OlU}o8DJdc}>>shDK6m}n8 zkI!SytE1Rsd5!mT(fE1309As$jU$h)ZYc8*pTwmfQat9}2LRq^cu63`ZCE*b+WdA8N z-`#^8H)vz-#X*R5Hl&%$B)E`gjU6tUG}!LC=qBA8pStVNRUIYzSWwKL?cUV!NS-EJ zI9gtD$U(TT)9D1XVlueZHuP=+^otC)!M z!_4VuJ4oe!^M_mw(kmW7&d#ZPFS8-fgIh({Z7bHdp%0K?8^OL9j3S16xn$DW)eB0K*wO0zv_q5gIRF~8q* zia)(8JA}*Y5;^~5M=^8v;h5)Z1U^uqE0w>*9DeUM6&g_8%!i`6UkgkwsnS&UYT<~J zuv?@}W$V&0M(qlkUAxhVCq;1lOO?0EA)_=Xu$c$gAqKQ%$>Y&~)c8ivS)Peto*6LP7Nr?Y14jk;k= zUw*PXp7X!U>X=I=TaO+K+OUK3&srmygTe2EuTR>re8p9E9rd8f6h-P1Q-s-^|M_a! zg({w&6>+^jN^ZD66pFhaippJzB5a@~?+wF5!rCM;c;+cFM4>n7l{|vg!I6lyvEok2 zSA^MYMi}=zT%W(ihVc$?N)5!V-#0mCX^QNOl4-FX)VWuqa&EH8*>uinsGv_)9Rg^P#X z_u!xNVejzg$HYO=BtCV>kmh-wzi=Nf-=1?Qt~ONIJxTQB4#Eg#ZQHteQmw^K98A=v zIJLesb^atg|K6Ql7VMvKh{xKmCZzVX7ky;rL^5ZXKPWO2&(M#*^(D{e zwsifaJH5WFOYxQElEqDa6#ZI*4EIL}g}2^xDa(MmC|eda9e4$c=0XgLHfyMvt#eaPG}JRA=t-?5x6|_nkNpxDZ-dRQw*Hyuw-#Y`0s zTGP*s#unwGkB^|)?uIlf`7Zh$bD=wL)XCU0tE*HTxq`+5~+kEOZi$7kR>Suk;9A1(J07W=&yts8GcYLFCp%54*h z<1!KO>o1=rdm$#I01FPvVwjJv5Epkt+Sv*d?`DgvZaa}ADtUMNPx3hAJon}u@O5%D z@@H53bZBx%^7U>cJ4_H$-w)#Z^bQ;t?pCyN3ePCxoJsRvnOM7AhqQLsQq%12c)dWE zwic4r z8u%7#K>o8nVdKgUatMVz*BXVs#Ir z>!-i?Te4fso$g58e|O@W)IYI%t_NlI`h}#7ZKB5sQ|dZzBf9bV!BN+nWE%EzE?L`cWX7c z)i4OX+zfEt`3=su&Bgp9V=z8MiaDkxXlb^_rqq1g9-n}3%QoY)+cAtEl7u&vyEun& z6El-@vF*Vos9l+X{f9Zr=gypZ%R$VmbS2B=&B$204%;rX2TFQ7GIY3iP|=56j*i2+ zk8boXs4rbC-3IlQ14vR~LU!Lg@Qv?`o#xdj7JOc4%!TjE$MAu=FrS=-;Q@cK@V`Ql zp%f^JO`l>2cU@k(9TCT7{y=q!I~gC07GLJ&VCE4)gE}>-+vj)U)>CIp<=$oKPep7y zq=R5LO=?}oZjJFqD0fq(poR`qS6@dvw~4goa%6rk6MuXLdsp_T;G6|vZCWkjoiqglB1!4N zUo_bs;BN45bhB?lTg{}xpDU#)Ilddsp4x{}N7TTLce-uNHG7r#5c7H0)WHmgN@sq5 zn{O8%oACNssvnwj{fl?@4!EVe`V1^pd7QZMr``h$8Ww`I5_Y zr196SC(f_crtqo8Xxfr0lz5LPyG{pkaH3CFv^L%99Fd13#x=)Vc{fL^f-Wq-Jrh)9Z2uNyZn*>y6Nb`KAE2X z&kOGOGnDh#3UKr2K|0XD+p2-wlkUKhH%j!kFF2JggXXw< z@KY)l53CMhQRrjTmd_KFu_KTWSk1rg1fl%rup}f?kt~&eV||K&k3o7PMh3q{@XFzZ z)w{dU9w&K`Fy(B(RBMV{;6(q;t%UB3D9mt|qnMCucwf917XyAl$^9md=O4jUo$hq(ZWUU5 zm~Z?r3hoW9+}%_r^|f*EJn#W$a=OyPkYFsiY(pU#4m9&v8g@#V(=+&xSyMKi|8k)B zBYD?%^)9ycpNZGiPPD94mAp^wz`=Aoa&ecZy?y3jfIMg%`(FLWZU(vCz#aC$I_(>c zyMrHMT#zz_B(B6hpL2M;PMR7dTj27fkoh|%%r#`MV`aTKHN%i+(wpJ?BUF5wX2)73TbBc50+2 zd0E%vCFenVvhToHIYOKc-VbZ$%Q`ho6_XqiP-oqWs6l5X8U;JBt6YtwJh_KzBuxjs zv}xH0M_Lr7OoLCE(+74Z=yRX-dt*=P{mYCTGPa=g@pW&rlh%}8KMPw!1_-+d3)&ZO z95!LulKtFexYGXsPE9I6*Srf*+@FkP4^QK-eg?{CE=E}Q%gi0@Nu4K)A+kD=k|#%5 zmYMj$Ot**o`CQla3VLZOk*>5Hg-`Glc7KXs6!aMjoR!6fy_rbwqeOL&Qbf(*6jTSB zh=@slMdjX2+=(g>9|KHLb8$6fM~)LscBR5w@d%X8=%TMm7%a9E=d}d(?c0nPjp5Y% z;D_kH@-V(FA~OB1MGNNFO6aEw_A=Kr|G$I+4S5S>@%Q!HcxQ?E<7Bbv%U1Zy_#(x< zL5!S`h;C~}BjU+-;k1-FBy|ROa%4ZV&_0NcE%wxMpqiP)hBzDOK&I0=IookSq`ovI z1`9`NwEt&boJF5NBQAV@g;ORu`(G$d5C_>o-Aj%L{CM-6n21=+cqnA7LNW z3zto;NoMqOtU2OtQhgutQFG`4p! z_8T6+=L?@P=~E>ZrKMwFPA7AvQ}KS2DFsT@>D)(Yr0p@IX^#~s;YW-x3hP7eN=CFj z_?y@m#jc-{c&wJW4CxhmbYSOkNaj33oY1AVfHa&w#@VZkXK-D)5~d5xuz$%Tow%a5k;YK+|Bst-2MdSU9wNJLFiqXShHLf)1$ zsmpuPD89!i=HG+bn4V;@u@`@csHP0l(Y;IBSX-ud2j`>7sm>9CygS#jyf@ZK& z%Ar+wC+A7BkJYfVP7$)#W{AXehDND6UCJ*3U;H?fc zX6uV46LlIY(~ig4dNiCJVNT^T^m~*QjlTO2T~>A>$x1b{n%jsr^Kyw+MImOf*HySB zh}+F~u%mwgj(rO$oWJQJ7B4SF-Q?Si%;2gqCfi~dv|U@ zzwi!G!Q9Dx2Ycb2Nk208?Z7d$-8@t9r3c#W=oglNku@%)*-eF_Bf=0^7cbrfoe&Qa zD`BQ}T&#B~kz}v_fV=U{qSXG5u((`0+T#nz(M32}6%*k`<%lF`7;x(k)yJ_D@81ekNKYUltlxro%|C8k=~Qkhif#+?RO^ z9p2qM9p4pF&!j2uxfZRx*9B#xFX33OCH1)fmUoAD&~Kb6_1IU3Q%Yx0enyYt&sCu< zr2%K>%|%$Ugft&2(Jbd_SU3&zv+@I8?OK7-B|~XT=U{dWu(zPQ43#^oBWTl7X!2}9 znpx`KOB`U8t3tO|@m+XtC32^plPtJtNS4_j*fEq+=p^ez)*}sJ%cHmpl zu5hkk`&iCP9kHjqDOK2W-5(v^PSkpnIUxtvz);qbp6&b2JXdJI|exIUo^AxFlw+2~{_$9f+eDCmH z3Y5+J^1BybNlFLI5LJ^BQQ#O}F!s$85i@)-?(}SuT<#Yo&L-Rv!8V_8U)qwU*NqgP zHvPn$IlXBAZB?Ah{fmrAc09ZBpyTYCQ1<7pPrfq^O<4)WecX}!&EBZw-RuYGMb3(< z)M5MFdsvVcb!jrBUhBF`*3RYJlwud=Zj(6j*qP=ob|hzgOR^pLTGCn}D1tebEpsmA zf2y;mVX=Klf6H;P^u!mG^*ay!OER?c(_7wkT!hWl9<+4&Q`t{K zvG++ltN4LK+@m)455-|kd77L32SL9sVEAcU3QfuOS-v;t|L=X*L(Ig5+)_;3YD@+b zEJe_$D=6Z5Nr9gyh3rzHQ5*IncEbRY*`Y_fKc9nwqZ?m{6iByoEm{wi;&GK8)m-$# z?}_(d&ikAVUlyT<^CeVWRO5T&UMT-MFKpge)9`@Hcy#NjIP;j#5%&)vwcJmgXUiN^ zTN7$$F2U?LQ;Lywps$sjRsOFxEnzP7jRFh0`R5EKZS$o3?k=>e`Yf7%5LNs$p)V_n znH|dA@0TgKQ?(Y?A3bIt&w1?cy$ApE#%k<13YE75@agPBT*@&b{rbx|KKKI~k6O_n zg(SEdNRwNj8p*!SMw)FMyPcG2^>KB~oqiGTM|5Cl&IlQK;mAyr(A3f~c>Od87V-gf zxNSVXu6M?#I9D=G;MwDfabn8FccSn1?zGP8x1^y@fM^aert2O0;^6&%B60U_OufJk z%f_}j~n_Lu(SEcZZ|zfvdV1v1pv-xlrN zEJF`bEKZ%O0>jQ?Ou((~-7DYXd??TzWKi8&<&o5T9HJx)|@#k^uA99%schmP;& z{pwJ>)pNpGr(g`uE61c`-eml%7hdbvVktXm2HI(1y7N8EVsE$Ug&@TA*a`a>61yP4E4WpNoXp54Zq(C3dWZ4EuaIo32JUU`CTt-BF9UyYs9zwp>8 z6!RP{a6R!p6uzCo>LzC_Sd@)P`*Tr-z05=@M~HJ33?{|G-aZ~rjc($~n+RMQG>sV} zmb5W)1NWMMo|ViHHQvUbiLuuM8zDXCG!6&&(C?CY z;&1yk>>2G!N;i{4>coAhxb8-F?75K-AH=>b31p|~)0%GcaGO6DuFTKr+LLoZpNz4e zGlf6a$!*^i75_oXrMx}qh(h%%eqC~QlFSP^AQjo-bfb;1gt(V^Tq-kKyDCvlJC zOsi07)fL~al;gIC5puVz5~nhA@UAdX$avLZ*t)%vnEA3)q%1}A7QQUp+|qzK2S4EP z32X7t_$PD{HR!|w8ydZ~9-+Uq>DE0xdg}Kb7xcNuy2zOpUU`i#_7_FJt4Cm0mBjqV zYVkWM8;|pI;OT!$vT@N#?5#WnLq2OB&B*2dXBm$82jkqkX7RjN9uAF}hpqSh#rb_t zkrMAg<5szG{@DuA{ak5d$5@1%pN@emo#@hxE_f;JfV`fI#jzDSv{pfaYaS7zb*wT; zLVBY9d8PO;*M#~F3BuEhpgBk7NaoH7)LaTAn6{&OX)=bV4xmwgRA@ar1^161E6#Zi zpxgHEnKK(H)-vPq=Y^X8&9KCh`NJr{r5OckQziYqWYF4&b6+=vkE4PmTpk5dV%IGq zpiKqpTnz}GUWtMVbuq%_3^KS2Jk)Tv@bXB;>t5wBXxLDAUXlgrx9=glQ-!7&n!wQe z529k!nDbaGWTIZ<(Kmfk@U_C(E%&g-!<_ENmSXhQcc_2H89S$!czwGJWvjF)^F#*9 z83;gOVp?*0|9M;?} zRR10pUF96S$`y^i)^rH*aFHk1A8-qwk(v#f!F!T2wGwhfRXj-aCej}ps z?QxnUe(`pZdL$8t3q@h)p;S@3cPiTU_RcpyvR9mzR0^-%Um^IN-u&d8Sl00u-gES6 z-U($iYyZK~Z;s@q=SIV0V-P7+sql<5onhZmiG~rSoU

#S38mj9nBJs&s|FKPR_& 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 literal 0 HcmV?d00001 diff --git a/source/tests/pt/water_tensor/polar/atomic_system/type.raw b/source/tests/pt/water_tensor/polar/atomic_system/type.raw new file mode 100644 index 0000000000..6c71c85e58 --- /dev/null +++ b/source/tests/pt/water_tensor/polar/atomic_system/type.raw @@ -0,0 +1 @@ +0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 0 1 1 diff --git a/source/tests/pt/water_tensor/polar/atomic_system/type_map.raw b/source/tests/pt/water_tensor/polar/atomic_system/type_map.raw new file mode 100644 index 0000000000..e900768b1d --- /dev/null +++ b/source/tests/pt/water_tensor/polar/atomic_system/type_map.raw @@ -0,0 +1,2 @@ +O +H diff --git a/source/tests/pt/water_tensor/polar/global_system/set.000/box.npy b/source/tests/pt/water_tensor/polar/global_system/set.000/box.npy new file mode 100644 index 0000000000000000000000000000000000000000..652530cfe8557963039781971f3e2559c9ffca0e GIT binary patch literal 3008 zcmbW&O-NK>6oBC|gTiPCY7<0Ug1QkwGRc zDuQYg1^p}<+P0C3AP{WPqD@ed2vaClh(rYG&UbiM-Q9ENaPB$ZcMfx-?ePAC9W}B3 z*g%+ziYE%;wjkV*ZweDZnC~r|D`d}RdJDN|{(U-orWnn=FZN{nqB+-Z*_;TrHY9>l z@V`%8>fWwoY{6rW-`b5B^WtJFV$7>LQ;0FweA|l{^U#4bV$2Pr?8{uNiZMrhtjBzN z6MHwW-@XSmo4Y#LmwDq8#+WZOvL5qv$U8O9R@l3FbRX+654~nR=1Wt&FZ1#e^O$Wm z7mRmo{`Zk~<}n|6!&x;S9cInuXYZNEJl@7LG}o4x$9!lVW6aH6{FeDtKkwZ<@|nGx zpMPOJ=E)i6F+Zx>Q*-A;68mL-ca!y)@BiYSnk#YcjyX5Oddy4nJZJNrS)PwMvxI$_ z2Wxrn<`rf3Zoc@JyJJ57o9APWA7jntHDio1|M|ce^UHeHY#y6t9&`H(-l=)nSLQLN zx_KAosq@TZo~YUjb4wlbm``5ijG0^R@(j)8PyCkoM-OM-+*slKm>;KkhUS$&*_XNW zku{sYx3YKhhH=h@dANx+o8Mkz@8(6#%wyg;z+E<%SF?BXgF$}Fd^yA3&H2;JV@_86 Zzne2x*q8a?E8ds+`cvjHubpIH=6_axjGX`g literal 0 HcmV?d00001 diff --git a/source/tests/pt/water_tensor/polar/global_system/set.000/coord.npy b/source/tests/pt/water_tensor/polar/global_system/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..4f6c37e77a3ecd7729cce35d5982e87f7777cf45 GIT binary patch literal 184448 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

Qiyzhb}9_aLB#D?1SVHvMD7_Vb?!oJYaWh` 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

Hg>!N%nHiHD{I8hu zNSjW7F-@E+9}?wtvuNq+-$XI*gnx5GEQa*@8r z|09!_GoofDEKfYg_#s(x9@iB%M^q>+jCY>-dQ`>lnTEW(IKhm)@#^%L z^I@L14TWxBDHgvj#!kEQBH{5BSpNJc!p9n2%7)IOr}ZK#jpMlo)I#p~IqXv1jI+C$ zMU-2McG->4jMQi6#;-_?V<*Y8b;v(DlNp(eUMNq%^adk#=?Y2@bwx_5JEB(fizDBv z&;ic?R4tT8)_tydYopLV+$)YUGiP#xCH$HlX|&@#q|e9m?V+G{_UC4?`?)^lGwO1j zOotYQ!t=efP#8)Q{uTGJVZEIwJ@(wiIHa9ty2)bjX`{&8&i&|9Z&ADEl~|v>PMoMX zCwfy&aQR8R#2|lzq&!v|^P`GH&i+E42XOYn_6!QF3)s^+pCTg4u>aOc1ev(gXDvC* zT<^sNe>G~aQ^s3ns~_b)z`1x5%1haWp*5Y#9F;KMS%9m|FtPZC-LnBaySTv~sVQx6 zX8xa1z7h2#4PnQlQOrwQ2G$YKS@zXd1gwXDiWl9}P$0Ele-xR0M*HebfKnX{=Dx$d zxf(Fk^~0U932f7K!?({AU=^l7)vkKf?9&D}6$!}>8dB+-A!cmb(X!QwWU{XjzB3ol F{{X1TbpikY literal 0 HcmV?d00001 diff --git a/source/tests/pt/water_tensor/polar/global_system/set.000/polarizability.npy b/source/tests/pt/water_tensor/polar/global_system/set.000/polarizability.npy new file mode 100644 index 0000000000000000000000000000000000000000..893767e5654da9e82f8ac33bd782e2b18c55e8c1 GIT binary patch literal 3008 zcmbVOdsI|)9v=`ngoH#Y;sY@s=phagA&WS_@7?bZDS_cL;us<||twq-0$W zk*G*YfJiR6NFz%$bAMBy0*dBqiHJBvhAaVV@NN4N!T=S9Im}0Veeui`ns%yw6xzLH1t{M zyJg1sqX9zajh~^sb(zp$b5g{_8nEnCJKw2Nf_S!rH$OFj7bZ4{&1+RaXNvEMMsyCe z)3ZBl;M{4}RW;7M6*??cOoO_4W$f7Z2Ih6rjD=fFXxdl;Zgu56WAFgn-)qIK$3El> z>nk}X7jW-L%9oEF+{S%; zAJRDzo_RD#`M5l(w>ry+J!U^1{)v;25b+|9-eX1YmNBZmTZ}3yhx%O)#Ru_Pj4JYg zYEi`<-Kq#3#PgqzlVc#u-BcIjj z#hu`t|5q`3rGd^mL^J4uTT%{m#!8g4gftTU z5{8C;%`0E}H#GTO=Doi?a?~!=&HD+8N27$UMK-LQYDMdpP8#v-5uRRbLcPOlZH? z#9Z^j2p=^}?9*c5tZsFWISE4C>Q*N%wqwSi5#NW)tU9X~*7P^CHT}ala4wSHer_1T zl2-9f4--~r+3>rLV&1+-RB!P-%KMv5n3k5t(u+scoj3l*V7K`Xv09z=Kdi=3e`;NA zxM^UC7}*yt6ptPhz4I+tbGmhP->Y(gV#`}L%M^hDGR6Ev7lD8;&z3t2X*E{d;yF`8 z_p2-B_-MQmY#7-m;ei>hf>%O2_0k>igY392rGh6vsNi|WtN5lWnZmSj z5(5ioflO8pKHZ*>cVCMaU;cvk?^hBIDtKegV{GV^kg&WuLb*nr8ri3A3^fo3U*pTS z9#hA7q;S}6NBI#WVSb3wJN$0kuPl765yL;p6T7P7xuX}(!+K0=9>;>=_2P_+*TkKw zT(Rc#1orRG<(@$eBK4bJd*vZ+*sH@BkMC(t9mM4_7WBdxdIot@E<2nHstKAavjK(UiC9aqQp%sJ%K2#0wZon?z%+3BB_LmKw={G=_QL7!uFbrHbqD z!A_O>ev<`wY$&*GUs`Fo1=IhEfh1 zd1#IvgL^-N;NB>TTLGn0j5vRe33r$H3y!n0<#PmOn~v_AIN=U1Of>M!I|fWzr6bH= zg54!2SyJr?Bb_A^RJoBtkJ(iSY13oS3lh!ULsLAef|}DFnvvTQ zD%%=~CzpZhXX61nEO)VB-f`mHE; z?*hln($Wl=EkD9kl(T$;ylU}SIT56#f zR=gz*5GEz^uivmWWaqxhk5PQ9>U>g z+?;~{5HsA!%tD3`K!ABK1>*^f-C+Nq|Y_r_Kul+(MXy!S0K+K&6z2ruh$V* zp5mk>tYXe|?)9Syor@efuh*n_)dD7O))Z%$$rrCuA1=b+B=!EqNt6jXy!~8{dVXT+ z6V5e1HI9RmYay_33%~gCpTYAh9ce0Q+0gH-ea{)7cbU&AJ)Qjz;%p^2uHsocj@&lW ze0GWs{d4f6I?GPJoFL|;KO^3fHi~X_25fIMlP0x^K@C%dq~>EGz~2 z5eZ9Xw}P*WpWxor0xG2e8|Is7-i5 Date: Sun, 3 Mar 2024 11:02:54 +0800 Subject: [PATCH 174/686] Doc: Update PT tensor fitting (#3385) --- deepmd/utils/argcheck.py | 8 +-- doc/backend.md | 6 +-- doc/conf.py | 2 +- doc/development/create-a-model-pt.md | 4 +- doc/model/dprc.md | 2 +- doc/model/train-energy.md | 4 +- doc/model/train-fitting-tensor.md | 81 ++++++++++++++++++++++++++-- doc/model/train-hybrid.md | 2 +- doc/model/train-se-e2-a.md | 2 +- doc/model/train-se-e2-r.md | 2 +- 10 files changed, 94 insertions(+), 19 deletions(-) diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 22f71c8319..8bc9104b16 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -1053,7 +1053,7 @@ def fitting_dos(): ] -@fitting_args_plugin.register("polar", doc=doc_only_tf_supported) +@fitting_args_plugin.register("polar") def fitting_polar(): doc_neuron = "The number of neurons in each hidden layers of the fitting net. When two hidden layers are of the same size, a skip connection is built." doc_activation_function = f'The activation function in the fitting net. Supported activation functions are {list_to_doc(ACTIVATION_FN_DICT.keys())} Note that "gelu" denotes the custom operator version, and "gelu_tf" denotes the TF standard version. If you set "None" or "none" here, no activation function will be used.' @@ -1097,7 +1097,7 @@ def fitting_polar(): [List[int], int, None], optional=True, alias=["pol_type"], - doc=doc_sel_type, + doc=doc_sel_type + doc_only_tf_supported, ), Argument("seed", [int, None], optional=True, doc=doc_seed), ] @@ -1107,7 +1107,7 @@ def fitting_polar(): # return fitting_polar() -@fitting_args_plugin.register("dipole", doc=doc_only_tf_supported) +@fitting_args_plugin.register("dipole") def fitting_dipole(): doc_neuron = "The number of neurons in each hidden layers of the fitting net. When two hidden layers are of the same size, a skip connection is built." doc_activation_function = f'The activation function in the fitting net. Supported activation functions are {list_to_doc(ACTIVATION_FN_DICT.keys())} Note that "gelu" denotes the custom operator version, and "gelu_tf" denotes the TF standard version. If you set "None" or "none" here, no activation function will be used.' @@ -1138,7 +1138,7 @@ def fitting_dipole(): [List[int], int, None], optional=True, alias=["dipole_type"], - doc=doc_sel_type, + doc=doc_sel_type + doc_only_tf_supported, ), Argument("seed", [int, None], optional=True, doc=doc_seed), ] diff --git a/doc/backend.md b/doc/backend.md index 0b49f1ca00..2f0bc7ed20 100644 --- a/doc/backend.md +++ b/doc/backend.md @@ -23,7 +23,7 @@ DeePMD-kit does not use the TensorFlow v2 API but uses the TensorFlow v1 API (`t [PyTorch](https://pytorch.org/) 2.0 or above is required. While `.pth` and `.pt` are the same in the PyTorch package, they have different meanings in the DeePMD-kit to distinguish the model and the checkpoint. -### DPModel {{ dpmodel_icon }} +### DP {{ dpmodel_icon }} :::{note} This backend is only for development and should not take into production. @@ -31,10 +31,10 @@ This backend is only for development and should not take into production. - Model filename extension: `.dp` -DPModel is a reference backend for development, which uses pure [NumPy](https://numpy.org/) to implement models without using any heavy deep-learning frameworks. +DP is a reference backend for development, which uses pure [NumPy](https://numpy.org/) to implement models without using any heavy deep-learning frameworks. Due to the limitation of NumPy, it doesn't support gradient calculation and thus cannot be used for training. As a reference backend, it is not aimed at the best performance, but only the correct results. -The DPModel backend uses [HDF5](https://docs.h5py.org/) to store model serialization data, which is backend-independent. +The DP backend uses [HDF5](https://docs.h5py.org/) to store model serialization data, which is backend-independent. Only Python inference interface can load this format. ## Switch the backend diff --git a/doc/conf.py b/doc/conf.py index 22e97c974c..58181f9e1c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -186,7 +186,7 @@ def setup(app): myst_substitutions = { "tensorflow_icon": """![TensorFlow](/_static/tensorflow.svg){class=platform-icon}""", "pytorch_icon": """![PyTorch](/_static/pytorch.svg){class=platform-icon}""", - "dpmodel_icon": """![DPModel](/_static/logo_icon.svg){class=platform-icon}""", + "dpmodel_icon": """![DP](/_static/logo_icon.svg){class=platform-icon}""", } # -- Options for HTML output ------------------------------------------------- diff --git a/doc/development/create-a-model-pt.md b/doc/development/create-a-model-pt.md index fdb1defe8a..6fcddd33d8 100644 --- a/doc/development/create-a-model-pt.md +++ b/doc/development/create-a-model-pt.md @@ -156,6 +156,6 @@ The arguments here should be consistent with the class arguments of your new com ## Unit tests -When transferring features from another backend to the PyTorch backend, it is essential to include a regression test in `/source/tests/consistent` to validate the consistency of the PyTorch backend with other backends. Presently, the regression tests cover self-consistency and cross-backend consistency between TensorFlow, PyTorch, and dpmodel (Numpy) through the serialization/deserialization technique. +When transferring features from another backend to the PyTorch backend, it is essential to include a regression test in `/source/tests/consistent` to validate the consistency of the PyTorch backend with other backends. Presently, the regression tests cover self-consistency and cross-backend consistency between TensorFlow, PyTorch, and DP (Numpy) through the serialization/deserialization technique. -During the development of new components within the PyTorch backend, it is necessary to provide a dpmodel (Numpy) implementation and incorporate corresponding regression tests. For PyTorch components, developers are also required to include a unit test using `torch.jit`. +During the development of new components within the PyTorch backend, it is necessary to provide a DP (Numpy) implementation and incorporate corresponding regression tests. For PyTorch components, developers are also required to include a unit test using `torch.jit`. diff --git a/doc/model/dprc.md b/doc/model/dprc.md index ac1ab0e261..4699db77d0 100644 --- a/doc/model/dprc.md +++ b/doc/model/dprc.md @@ -1,7 +1,7 @@ # Deep Potential - Range Correction (DPRc) {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DPModel {{ dpmodel_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DP {{ dpmodel_icon }} ::: Deep Potential - Range Correction (DPRc) is designed to combine with QM/MM method, and corrects energies from a low-level QM/MM method to a high-level QM/MM method: diff --git a/doc/model/train-energy.md b/doc/model/train-energy.md index a4760b8375..bfe304b5d2 100644 --- a/doc/model/train-energy.md +++ b/doc/model/train-energy.md @@ -1,7 +1,7 @@ -# Fit energy {{ tensorflow_icon }} {{ pytorch_icon }} +# Fit energy {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DP {{ dpmodel_icon }} ::: In this section, we will take `$deepmd_source_dir/examples/water/se_e2_a/input.json` as an example of the input file. diff --git a/doc/model/train-fitting-tensor.md b/doc/model/train-fitting-tensor.md index 3272418a7c..0c9c0f492c 100644 --- a/doc/model/train-fitting-tensor.md +++ b/doc/model/train-fitting-tensor.md @@ -1,16 +1,33 @@ -# Fit `tensor` like `Dipole` and `Polarizability` {{ tensorflow_icon }} +# Fit `tensor` like `Dipole` and `Polarizability` {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} ::: Unlike `energy`, which is a scalar, one may want to fit some high dimensional physical quantity, like `dipole` (vector) and `polarizability` (matrix, shorted as `polar`). Deep Potential has provided different APIs to do this. In this example, we will show you how to train a model to fit a water system. A complete training input script of the examples can be found in +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} + ```bash $deepmd_source_dir/examples/water_tensor/dipole/dipole_input.json $deepmd_source_dir/examples/water_tensor/polar/polar_input.json ``` +::: + +:::{tab-item} PyTorch {{ pytorch_icon }} + +```bash +$deepmd_source_dir/examples/water_tensor/dipole/dipole_input_torch.json +$deepmd_source_dir/examples/water_tensor/polar/polar_input_torch.json +``` + +::: + +:::: + The training and validation data are also provided our examples. But note that **the data provided along with the examples are of limited amount, and should not be used to train a production model.** Similar to the `input.json` used in `ener` mode, training JSON is also divided into {ref}`model `, {ref}`learning_rate `, {ref}`loss ` and {ref}`training `. Most keywords remain the same as `ener` mode, and their meaning can be found [here](train-se-e2-a.md). To fit a tensor, one needs to modify {ref}`model/fitting_net ` and {ref}`loss `. @@ -53,6 +70,10 @@ The tensorial models can be used to calculate IR spectrum and Raman spectrum.[^1 The {ref}`fitting_net ` section tells DP which fitting net to use. +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} + The JSON of `dipole` type should be provided like ```json @@ -81,9 +102,48 @@ The JSON of `polar` type should be provided like - `sel_type` is a list specifying which type of atoms have the quantity you want to fit. For example, in the water system, `sel_type` is `[0]` since `0` represents atom `O`. If left unset, all types of atoms will be fitted. - The rest arguments have the same meaning as they do in `ener` mode. +::: + +:::{tab-item} PyTorch {{ pytorch_icon }} + +The JSON of `dipole` type should be provided like +```json + "atom_exclude_types": [ + 1 + ], + "fitting_net" : { + "type": "dipole", + "neuron": [100,100,100], + "resnet_dt": true, + "seed": 1, + }, +``` + +The JSON of `polar` type should be provided like + +```json + "atom_exclude_types": [ + 1 + ], + "fitting_net" : { + "type": "polar", + "neuron": [100,100,100], + "resnet_dt": true, + "seed": 1, + }, +``` +- `type` specifies which type of fitting net should be used. It should be either `dipole` or `polar`. Note that `global_polar` mode in version 1.x is already **deprecated** and is merged into `polar`. To specify whether a system is global or atomic, please see [here](train-se-e2-a.md). +- `atom_exclude_types` is a list specifying the which type of atoms have the quantity you want to set to zero. For example, in the water system, `atom_exclude_types` is `[1]` since `1` represents atom `H`. +- The rest arguments have the same meaning as they do in `ener` mode. +::: + +:::: + + + ## Loss -DP supports a combinational training of the global system (only a global `tensor` label, i.e. dipole or polar, is provided in a frame) and atomic system (labels for **each** atom included in `sel_type` are provided). In a global system, each frame has just **one** `tensor` label. For example, when fitting `polar`, each frame will just provide a `1 x 9` vector which gives the elements of the polarizability tensor of that frame in order XX, XY, XZ, YX, YY, YZ, XZ, ZY, ZZ. By contrast, in an atomic system, each atom in `sel_type` has a `tensor` label. For example, when fitting a dipole, each frame will provide a `#sel_atom x 3` matrices, where `#sel_atom` is the number of atoms whose type are in `sel_type`. +DP supports a combinational training of the global system (only a global `tensor` label, i.e. dipole or polar, is provided in a frame) and atomic system (labels for **each** atom included in `sel_type`/ not included in `atom_exclude_types` are provided). In a global system, each frame has just **one** `tensor` label. For example, when fitting `polar`, each frame will just provide a `1 x 9` vector which gives the elements of the polarizability tensor of that frame in order XX, XY, XZ, YX, YY, YZ, XZ, ZY, ZZ. By contrast, in an atomic system, each atom in `sel_type` has a `tensor` label. For example, when fitting a dipole, each frame will provide a `#sel_atom x 3` matrices, where `#sel_atom` is the number of atoms whose type are in `sel_type`. The {ref}`loss ` section tells DP the weight of these two kinds of loss, i.e. @@ -118,9 +178,24 @@ In this case, please check the file name of the label. The training command is the same as `ener` mode, i.e. +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} + ```bash dp train input.json ``` +::: + +:::{tab-item} PyTorch {{ pytorch_icon }} + +```bash +dp --pt train input.json +``` +::: + +:::: + The detailed loss can be found in `lcurve.out`: diff --git a/doc/model/train-hybrid.md b/doc/model/train-hybrid.md index 3014aa869f..c82fe8e961 100644 --- a/doc/model/train-hybrid.md +++ b/doc/model/train-hybrid.md @@ -1,7 +1,7 @@ # Descriptor `"hybrid"` {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DPModel {{ dpmodel_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DP {{ dpmodel_icon }} ::: This descriptor hybridizes multiple descriptors to form a new descriptor. For example, we have a list of descriptors denoted by $\mathcal D_1$, $\mathcal D_2$, ..., $\mathcal D_N$, the hybrid descriptor this the concatenation of the list, i.e. $\mathcal D = (\mathcal D_1, \mathcal D_2, \cdots, \mathcal D_N)$. diff --git a/doc/model/train-se-e2-a.md b/doc/model/train-se-e2-a.md index 6a5c59682d..e99f14518e 100644 --- a/doc/model/train-se-e2-a.md +++ b/doc/model/train-se-e2-a.md @@ -1,7 +1,7 @@ # Descriptor `"se_e2_a"` {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DPModel {{ dpmodel_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DP {{ dpmodel_icon }} ::: The notation of `se_e2_a` is short for the Deep Potential Smooth Edition (DeepPot-SE) constructed from all information (both angular and radial) of atomic configurations. The `e2` stands for the embedding with two-atoms information. This descriptor was described in detail in [the DeepPot-SE paper](https://arxiv.org/abs/1805.09003). diff --git a/doc/model/train-se-e2-r.md b/doc/model/train-se-e2-r.md index 1ec768017c..c543df6b22 100644 --- a/doc/model/train-se-e2-r.md +++ b/doc/model/train-se-e2-r.md @@ -1,7 +1,7 @@ # Descriptor `"se_e2_r"` {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} :::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DPModel {{ dpmodel_icon }} +**Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DP {{ dpmodel_icon }} ::: The notation of `se_e2_r` is short for the Deep Potential Smooth Edition (DeepPot-SE) constructed from the radial information of atomic configurations. The `e2` stands for the embedding with two-atom information. From 4f933d88aee886af4c0e95c5db24685ea87100f6 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Sun, 3 Mar 2024 11:37:48 +0800 Subject: [PATCH 175/686] ut: add null test (#3391) test the cases: 1. system only has one atom 2. system has two atoms that are far away from each other. In each cases, the force and virial predictions should be zero and energy should be a valid float. --------- Co-authored-by: Han Wang --- source/tests/pt/model/test_null_input.py | 145 +++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 source/tests/pt/model/test_null_input.py diff --git a/source/tests/pt/model/test_null_input.py b/source/tests/pt/model/test_null_input.py new file mode 100644 index 0000000000..93a3ff8511 --- /dev/null +++ b/source/tests/pt/model/test_null_input.py @@ -0,0 +1,145 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import numpy as np +import torch + +from deepmd.pt.infer.deep_eval import ( + eval_model, +) +from deepmd.pt.model.model import ( + get_model, + get_zbl_model, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, +) + +from .test_permutation import ( + model_dpa1, + model_dpa2, + model_hybrid, + model_se_e2_a, + model_zbl, +) + +dtype = torch.float64 + + +class NullTest: + def test_nloc_1( + self, + ): + natoms = 1 + # torch.manual_seed(1000) + cell = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) + # large box to exclude images + cell = (cell + cell.T) + 100.0 * torch.eye(3, device=env.DEVICE) + coord = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) + atype = torch.tensor([0], dtype=torch.int32, device=env.DEVICE) + e0, f0, v0 = eval_model( + self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype + ) + ret0 = { + "energy": e0.squeeze(0), + "force": f0.squeeze(0), + "virial": v0.squeeze(0), + } + prec = 1e-10 + expect_e_shape = [1] + expect_f = torch.zeros([natoms, 3], dtype=dtype, device=env.DEVICE) + expect_v = torch.zeros([9], dtype=dtype, device=env.DEVICE) + self.assertEqual(list(ret0["energy"].shape), expect_e_shape) + self.assertFalse(np.isnan(to_numpy_array(ret0["energy"])[0])) + torch.testing.assert_close(ret0["force"], expect_f, rtol=prec, atol=prec) + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close(ret0["virial"], expect_v, rtol=prec, atol=prec) + + def test_nloc_2_far( + self, + ): + natoms = 2 + cell = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) + # large box to exclude images + cell = (cell + cell.T) + 3000.0 * torch.eye(3, device=env.DEVICE) + coord = torch.rand([1, 3], dtype=dtype, device=env.DEVICE) + # 2 far-away atoms + coord = torch.cat([coord, coord + 100.0], dim=0) + atype = torch.tensor([0, 2], dtype=torch.int32, device=env.DEVICE) + e0, f0, v0 = eval_model( + self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype + ) + ret0 = { + "energy": e0.squeeze(0), + "force": f0.squeeze(0), + "virial": v0.squeeze(0), + } + prec = 1e-10 + expect_e_shape = [1] + expect_f = torch.zeros([natoms, 3], dtype=dtype, device=env.DEVICE) + expect_v = torch.zeros([9], dtype=dtype, device=env.DEVICE) + self.assertEqual(list(ret0["energy"].shape), expect_e_shape) + self.assertFalse(np.isnan(to_numpy_array(ret0["energy"])[0])) + torch.testing.assert_close(ret0["force"], expect_f, rtol=prec, atol=prec) + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close(ret0["virial"], expect_v, rtol=prec, atol=prec) + + +class TestEnergyModelSeA(unittest.TestCase, NullTest): + def setUp(self): + model_params = copy.deepcopy(model_se_e2_a) + self.type_split = False + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelDPA1(unittest.TestCase, NullTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa1) + self.type_split = True + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelDPA2(unittest.TestCase, NullTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa2) + self.type_split = True + self.model = get_model(model_params).to(env.DEVICE) + + +class TestForceModelDPA2(unittest.TestCase, NullTest): + def setUp(self): + model_params = copy.deepcopy(model_dpa2) + model_params["fitting_net"]["type"] = "direct_force_ener" + self.type_split = True + self.test_virial = False + self.model = get_model(model_params).to(env.DEVICE) + + +@unittest.skip("hybrid not supported at the moment") +class TestEnergyModelHybrid(unittest.TestCase, NullTest): + def setUp(self): + model_params = copy.deepcopy(model_hybrid) + self.type_split = True + self.model = get_model(model_params).to(env.DEVICE) + + +@unittest.skip("hybrid not supported at the moment") +class TestForceModelHybrid(unittest.TestCase, NullTest): + def setUp(self): + model_params = copy.deepcopy(model_hybrid) + model_params["fitting_net"]["type"] = "direct_force_ener" + self.type_split = True + self.test_virial = False + self.model = get_model(model_params).to(env.DEVICE) + + +@unittest.skip("FAILED at the moment") +class TestEnergyModelZBL(unittest.TestCase, NullTest): + def setUp(self): + model_params = copy.deepcopy(model_zbl) + self.type_split = False + self.model = get_zbl_model(model_params).to(env.DEVICE) From c31d3760f5095efd95f4df2ddf37d86eaa88fd66 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Sun, 3 Mar 2024 13:50:55 +0800 Subject: [PATCH 176/686] Merge `change_energy_bias` and fix finetune (#3378) Signed-off-by: Duo <50307526+iProzd@users.noreply.github.com> Signed-off-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Co-authored-by: Jinzhe Zeng --- deepmd/pt/model/task/fitting.py | 144 +++++++++++-------------------- deepmd/pt/utils/finetune.py | 3 +- deepmd/tf/fit/ener.py | 122 +++----------------------- deepmd/utils/finetune.py | 142 ++++++++++++++++++++++++++++++ source/tests/pt/test_finetune.py | 143 ++++++++++++++++++++++++++++++ source/tests/pt/test_training.py | 9 ++ 6 files changed, 355 insertions(+), 208 deletions(-) create mode 100644 deepmd/utils/finetune.py create mode 100644 source/tests/pt/test_finetune.py diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index f79916b36e..bd38fca14a 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -1,6 +1,8 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import copy import logging +import os +import tempfile from abc import ( abstractmethod, ) @@ -13,6 +15,9 @@ import numpy as np import torch +from deepmd.infer.deep_eval import ( + DeepEval, +) from deepmd.pt.model.network.mlp import ( FittingNet, NetworkCollection, @@ -26,9 +31,6 @@ from deepmd.pt.utils import ( env, ) -from deepmd.pt.utils.dataloader import ( - DpLoaderSet, -) from deepmd.pt.utils.env import ( DEFAULT_PRECISION, DEVICE, @@ -37,13 +39,16 @@ from deepmd.pt.utils.exclude_mask import ( AtomExcludeMask, ) -from deepmd.pt.utils.stat import ( - make_stat_input, -) from deepmd.pt.utils.utils import ( to_numpy_array, to_torch_tensor, ) +from deepmd.utils.data_system import ( + DeepmdDataSystem, +) +from deepmd.utils.finetune import ( + change_energy_bias_lower, +) from deepmd.utils.version import ( check_version_compatibility, ) @@ -87,7 +92,13 @@ def share_params(self, base_class, shared_level, resume=False): raise NotImplementedError def change_energy_bias( - self, config, model, old_type_map, new_type_map, bias_shift="delta", ntest=10 + self, + config, + model, + old_type_map: List[str], + new_type_map: List[str], + bias_shift="delta", + ntest=10, ): """Change the energy bias according to the input data and the pretrained model. @@ -97,9 +108,9 @@ def change_energy_bias( The configuration. model : EnergyModel Energy model loaded pre-trained model. - new_type_map : list + new_type_map : List[str] The original type_map in dataset, they are targets to change the energy bias. - old_type_map : str + old_type_map : List[str] The full type_map in pretrained model bias_shift : str The mode for changing energy bias : ['delta', 'statistic'] @@ -115,93 +126,36 @@ def change_energy_bias( ) # data systems = config["training"]["training_data"]["systems"] - finetune_data = DpLoaderSet(systems, ntest, config["model"]) - sampled = make_stat_input(finetune_data.systems, finetune_data.dataloaders, 1) - # map - sorter = np.argsort(old_type_map) - idx_type_map = sorter[ - np.searchsorted(old_type_map, new_type_map, sorter=sorter) - ] - data_mixed_types = np.all([i.mixed_type for i in finetune_data.systems]) - numb_type = len(old_type_map) - type_numbs, energy_ground_truth, energy_predict = [], [], [] - for test_data in sampled: - nframes = test_data["energy"].shape[0] - if data_mixed_types: - atype = test_data["atype"].detach().cpu().numpy() - else: - atype = test_data["atype"][0].detach().cpu().numpy() - assert np.array( - [i.item() in idx_type_map for i in list(set(atype.reshape(-1)))] - ).all(), "Some types are not in 'type_map'!" - energy_ground_truth.append(test_data["energy"].cpu().numpy()) - if data_mixed_types: - type_numbs.append( - np.array( - [(atype == i).sum(axis=-1) for i in idx_type_map], - dtype=np.int32, - ).T - ) - else: - type_numbs.append( - np.tile( - np.bincount(atype, minlength=numb_type)[idx_type_map], - (nframes, 1), - ) - ) - if bias_shift == "delta": - coord = test_data["coord"].to(DEVICE) - atype = test_data["atype"].to(DEVICE) - box = ( - test_data["box"].to(DEVICE) - if test_data["box"] is not None - else None - ) - ret = model(coord, atype, box) - energy_predict.append( - ret["energy"].reshape([nframes, 1]).detach().cpu().numpy() - ) - type_numbs = np.concatenate(type_numbs) - energy_ground_truth = np.concatenate(energy_ground_truth) - old_bias = self.bias_atom_e[idx_type_map] - if bias_shift == "delta": - energy_predict = np.concatenate(energy_predict) - bias_diff = energy_ground_truth - energy_predict - delta_bias = np.linalg.lstsq(type_numbs, bias_diff, rcond=None)[0] - unbias_e = energy_predict + type_numbs @ delta_bias - atom_numbs = type_numbs.sum(-1) - rmse_ae = np.sqrt( - np.mean( - np.square( - (unbias_e.ravel() - energy_ground_truth.ravel()) / atom_numbs - ) - ) - ) - self.bias_atom_e[idx_type_map] += torch.from_numpy( - delta_bias.reshape(-1) - ).to(DEVICE) - log.info( - f"RMSE of atomic energy after linear regression is: {rmse_ae:10.5e} eV/atom." - ) - elif bias_shift == "statistic": - statistic_bias = np.linalg.lstsq( - type_numbs, energy_ground_truth, rcond=None - )[0] - self.bias_atom_e[idx_type_map] = ( - torch.from_numpy(statistic_bias.reshape(-1)) - .type_as(self.bias_atom_e[idx_type_map]) - .to(DEVICE) - ) - else: - raise RuntimeError("Unknown bias_shift mode: " + bias_shift) - log.info( - "Change energy bias of {} from {} to {}.".format( - str(new_type_map), - str(old_bias.detach().cpu().numpy()), - str(self.bias_atom_e[idx_type_map].detach().cpu().numpy()), - ) + finetune_data = DeepmdDataSystem( + systems=systems, + batch_size=config["training"]["training_data"].get("batch_size", "auto"), + test_size=1, + ) + finetune_data.add("energy", ndof=1, atomic=False, must=True, high_prec=True) + model = torch.jit.script(model) + if model.get_dim_fparam() > 0: + finetune_data.add("fparam", model.get_dim_fparam(), atomic=False, must=True) + if model.get_dim_aparam() > 0: + finetune_data.add("aparam", model.get_dim_aparam(), atomic=True, must=True) + tmp_model = tempfile.NamedTemporaryFile(delete=False, suffix=".pth") + torch.jit.save(model, tmp_model.name) + dp = DeepEval(tmp_model.name) + os.unlink(tmp_model.name) + bias = change_energy_bias_lower( + finetune_data, + dp, + new_type_map, + old_type_map, + self.bias_atom_e.detach().cpu().numpy().reshape(-1), + bias_shift=bias_shift, + ntest=ntest, + ) + self.bias_atom_e = ( + torch.from_numpy(bias) + .type_as(self.bias_atom_e) + .reshape(self.bias_atom_e.shape) + .to(DEVICE) ) - return None class GeneralFitting(Fitting): diff --git a/deepmd/pt/utils/finetune.py b/deepmd/pt/utils/finetune.py index c8fa1e5185..d555478af4 100644 --- a/deepmd/pt/utils/finetune.py +++ b/deepmd/pt/utils/finetune.py @@ -27,7 +27,6 @@ def change_finetune_model_params( last_model_params = state_dict["_extra_state"]["model_params"] finetune_multi_task = "model_dict" in last_model_params trainable_param = { - "type_embedding": True, "descriptor": True, "fitting_net": True, } @@ -74,7 +73,7 @@ def change_finetune_model_params( assert set(new_type_map).issubset( old_type_map ), "Only support for smaller type map when finetuning or resuming." - for key_item in ["type_map", "type_embedding", "descriptor"]: + for key_item in ["type_map", "descriptor"]: if key_item in model_dict_params[model_branch_chosen]: model_config[key_item] = model_dict_params[model_branch_chosen][ key_item diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index d605fbb0aa..780ae76c96 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -53,6 +53,9 @@ from deepmd.tf.utils.spin import ( Spin, ) +from deepmd.utils.finetune import ( + change_energy_bias_lower, +) from deepmd.utils.out_stat import ( compute_stats_from_redu, ) @@ -793,121 +796,18 @@ def change_energy_bias( bias_shift="delta", ntest=10, ) -> None: - """Change the energy bias according to the input data and the pretrained model. - - Parameters - ---------- - data : DeepmdDataSystem - The training data. - frozen_model : str - The path file of frozen model. - origin_type_map : list - The original type_map in dataset, they are targets to change the energy bias. - full_type_map : str - The full type_map in pretrained model - bias_shift : str - The mode for changing energy bias : ['delta', 'statistic'] - 'delta' : perform predictions on energies of target dataset, - and do least sqaure on the errors to obtain the target shift as bias. - 'statistic' : directly use the statistic energy bias in the target dataset. - ntest : int - The number of test samples in a system to change the energy bias. - """ - type_numbs = [] - energy_ground_truth = [] - energy_predict = [] - sorter = np.argsort(full_type_map) - idx_type_map = sorter[ - np.searchsorted(full_type_map, origin_type_map, sorter=sorter) - ] - mixed_type = data.mixed_type - numb_type = len(full_type_map) dp = None if bias_shift == "delta": # init model dp = DeepPotential(frozen_model) - for sys in data.data_systems: - test_data = sys.get_test() - nframes = test_data["box"].shape[0] - numb_test = min(nframes, ntest) - if mixed_type: - atype = test_data["type"][:numb_test].reshape([numb_test, -1]) - else: - atype = test_data["type"][0] - assert np.array( - [i in idx_type_map for i in list(set(atype.reshape(-1)))] - ).all(), "Some types are not in 'type_map'!" - energy_ground_truth.append( - test_data["energy"][:numb_test].reshape([numb_test, 1]) - ) - if mixed_type: - type_numbs.append( - np.array( - [(atype == i).sum(axis=-1) for i in idx_type_map], - dtype=np.int32, - ).T - ) - else: - type_numbs.append( - np.tile( - np.bincount(atype, minlength=numb_type)[idx_type_map], - (numb_test, 1), - ) - ) - if bias_shift == "delta": - coord = test_data["coord"][:numb_test].reshape([numb_test, -1]) - if sys.pbc: - box = test_data["box"][:numb_test] - else: - box = None - if dp.get_dim_fparam() > 0: - fparam = test_data["fparam"][:numb_test] - else: - fparam = None - if dp.get_dim_aparam() > 0: - aparam = test_data["aparam"][:numb_test] - else: - aparam = None - ret = dp.eval( - coord, - box, - atype, - mixed_type=mixed_type, - fparam=fparam, - aparam=aparam, - ) - energy_predict.append(ret[0].reshape([numb_test, 1])) - type_numbs = np.concatenate(type_numbs) - energy_ground_truth = np.concatenate(energy_ground_truth) - old_bias = self.bias_atom_e[idx_type_map] - if bias_shift == "delta": - energy_predict = np.concatenate(energy_predict) - bias_diff = energy_ground_truth - energy_predict - delta_bias = np.linalg.lstsq(type_numbs, bias_diff, rcond=None)[0] - unbias_e = energy_predict + type_numbs @ delta_bias - atom_numbs = type_numbs.sum(-1) - rmse_ae = np.sqrt( - np.mean( - np.square( - (unbias_e.ravel() - energy_ground_truth.ravel()) / atom_numbs - ) - ) - ) - self.bias_atom_e[idx_type_map] += delta_bias.reshape(-1) - log.info( - f"RMSE of atomic energy after linear regression is: {rmse_ae} eV/atom." - ) - elif bias_shift == "statistic": - statistic_bias = np.linalg.lstsq( - type_numbs, energy_ground_truth, rcond=None - )[0] - self.bias_atom_e[idx_type_map] = statistic_bias.reshape(-1) - else: - raise RuntimeError("Unknown bias_shift mode: " + bias_shift) - log.info( - "Change energy bias of {} from {} to {}.".format( - str(origin_type_map), str(old_bias), str(self.bias_atom_e[idx_type_map]) - ) + self.bias_atom_e = change_energy_bias_lower( + data, + dp, + origin_type_map, + full_type_map, + self.bias_atom_e, + bias_shift=bias_shift, + ntest=ntest, ) def enable_mixed_precision(self, mixed_prec: Optional[dict] = None) -> None: diff --git a/deepmd/utils/finetune.py b/deepmd/utils/finetune.py new file mode 100644 index 0000000000..b6d04b9bc5 --- /dev/null +++ b/deepmd/utils/finetune.py @@ -0,0 +1,142 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import logging +from typing import ( + TYPE_CHECKING, + List, +) + +import numpy as np + +from deepmd.infer.deep_eval import ( + DeepEval, +) +from deepmd.utils.data_system import ( + DeepmdDataSystem, +) + +if TYPE_CHECKING: + pass + +log = logging.getLogger(__name__) + + +def change_energy_bias_lower( + data: DeepmdDataSystem, + dp: DeepEval, + origin_type_map: List[str], + full_type_map: List[str], + bias_atom_e: np.ndarray, + bias_shift="delta", + ntest=10, +): + """Change the energy bias according to the input data and the pretrained model. + + Parameters + ---------- + data : DeepmdDataSystem + The training data. + dp : str + The DeepEval object. + origin_type_map : list + The original type_map in dataset, they are targets to change the energy bias. + full_type_map : str + The full type_map in pretrained model + bias_atom_e : np.ndarray + The old energy bias in the pretrained model. + bias_shift : str + The mode for changing energy bias : ['delta', 'statistic'] + 'delta' : perform predictions on energies of target dataset, + and do least sqaure on the errors to obtain the target shift as bias. + 'statistic' : directly use the statistic energy bias in the target dataset. + ntest : int + The number of test samples in a system to change the energy bias. + """ + type_numbs = [] + energy_ground_truth = [] + energy_predict = [] + sorter = np.argsort(full_type_map) + idx_type_map = sorter[ + np.searchsorted(full_type_map, origin_type_map, sorter=sorter) + ] + mixed_type = data.mixed_type + numb_type = len(full_type_map) + for sys in data.data_systems: + test_data = sys.get_test() + nframes = test_data["box"].shape[0] + numb_test = min(nframes, ntest) + if mixed_type: + atype = test_data["type"][:numb_test].reshape([numb_test, -1]) + else: + atype = test_data["type"][0] + assert np.array( + [i in idx_type_map for i in list(set(atype.reshape(-1)))] + ).all(), "Some types are not in 'type_map'!" + energy_ground_truth.append( + test_data["energy"][:numb_test].reshape([numb_test, 1]) + ) + if mixed_type: + type_numbs.append( + np.array( + [(atype == i).sum(axis=-1) for i in idx_type_map], + dtype=np.int32, + ).T + ) + else: + type_numbs.append( + np.tile( + np.bincount(atype, minlength=numb_type)[idx_type_map], + (numb_test, 1), + ) + ) + if bias_shift == "delta": + coord = test_data["coord"][:numb_test].reshape([numb_test, -1]) + if sys.pbc: + box = test_data["box"][:numb_test] + else: + box = None + if dp.get_dim_fparam() > 0: + fparam = test_data["fparam"][:numb_test] + else: + fparam = None + if dp.get_dim_aparam() > 0: + aparam = test_data["aparam"][:numb_test] + else: + aparam = None + ret = dp.eval( + coord, + box, + atype, + mixed_type=mixed_type, + fparam=fparam, + aparam=aparam, + ) + energy_predict.append(ret[0].reshape([numb_test, 1])) + type_numbs = np.concatenate(type_numbs) + energy_ground_truth = np.concatenate(energy_ground_truth) + old_bias = bias_atom_e[idx_type_map] + if bias_shift == "delta": + energy_predict = np.concatenate(energy_predict) + bias_diff = energy_ground_truth - energy_predict + delta_bias = np.linalg.lstsq(type_numbs, bias_diff, rcond=None)[0] + unbias_e = energy_predict + type_numbs @ delta_bias + atom_numbs = type_numbs.sum(-1) + rmse_ae = np.sqrt( + np.mean( + np.square((unbias_e.ravel() - energy_ground_truth.ravel()) / atom_numbs) + ) + ) + bias_atom_e[idx_type_map] += delta_bias.reshape(-1) + log.info( + f"RMSE of atomic energy after linear regression is: {rmse_ae} eV/atom." + ) + elif bias_shift == "statistic": + statistic_bias = np.linalg.lstsq(type_numbs, energy_ground_truth, rcond=None)[0] + bias_atom_e[idx_type_map] = statistic_bias.reshape(-1) + else: + raise RuntimeError("Unknown bias_shift mode: " + bias_shift) + log.info( + "Change energy bias of {} from {} to {}.".format( + str(origin_type_map), str(old_bias), str(bias_atom_e[idx_type_map]) + ) + ) + return bias_atom_e diff --git a/source/tests/pt/test_finetune.py b/source/tests/pt/test_finetune.py new file mode 100644 index 0000000000..226fed3c65 --- /dev/null +++ b/source/tests/pt/test_finetune.py @@ -0,0 +1,143 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import os +import shutil +import tempfile +import unittest +from copy import ( + deepcopy, +) +from pathlib import ( + Path, +) + +import numpy as np +import torch + +from deepmd.infer.deep_eval import ( + DeepEval, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.utils.data_system import ( + DeepmdDataSystem, +) +from deepmd.utils.finetune import ( + change_energy_bias_lower, +) + +from .model.test_permutation import ( + model_dpa1, + model_dpa2, + model_se_e2_a, +) + + +class FinetuneTest: + def test_finetune_change_energy_bias(self): + # get model + model = get_model(self.model_config) + model.fitting_net.bias_atom_e = torch.rand_like(model.fitting_net.bias_atom_e) + energy_bias_before = deepcopy( + model.fitting_net.bias_atom_e.detach().cpu().numpy().reshape(-1) + ) + bias_atom_e_input = deepcopy( + model.fitting_net.bias_atom_e.detach().cpu().numpy().reshape(-1) + ) + model = torch.jit.script(model) + tmp_model = tempfile.NamedTemporaryFile(delete=False, suffix=".pth") + torch.jit.save(model, tmp_model.name) + dp = DeepEval(tmp_model.name) + ntest = 10 + origin_type_map = ["O", "H"] + full_type_map = ["O", "H", "B"] + + # change energy bias + energy_bias_after = change_energy_bias_lower( + self.data, + dp, + origin_type_map=origin_type_map, + full_type_map=full_type_map, + bias_atom_e=bias_atom_e_input, + bias_shift="delta", + ntest=ntest, + ) + + # get ground-truth energy bias change + sorter = np.argsort(full_type_map) + idx_type_map = sorter[ + np.searchsorted(full_type_map, origin_type_map, sorter=sorter) + ] + test_data = self.data.get_test() + atom_nums = np.tile(np.bincount(test_data["type"][0])[idx_type_map], (ntest, 1)) + energy = dp.eval( + test_data["coord"][:ntest], test_data["box"][:ntest], test_data["type"][0] + )[0] + energy_diff = test_data["energy"][:ntest] - energy + finetune_shift = ( + energy_bias_after[idx_type_map] - energy_bias_before[idx_type_map] + ) + ground_truth_shift = np.linalg.lstsq(atom_nums, energy_diff, rcond=None)[ + 0 + ].reshape(-1) + + # check values + np.testing.assert_almost_equal(finetune_shift, ground_truth_shift, decimal=10) + + def tearDown(self): + for f in os.listdir("."): + if f.startswith("model") and f.endswith(".pt"): + os.remove(f) + if f in ["lcurve.out"]: + os.remove(f) + if f in ["stat_files"]: + shutil.rmtree(f) + + +class TestEnergyModelSeA(unittest.TestCase, FinetuneTest): + def setUp(self): + self.data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.data = DeepmdDataSystem( + self.data_file, + batch_size=1, + test_size=1, + ) + self.data.add("energy", ndof=1, atomic=False, must=True, high_prec=True) + self.model_config = model_se_e2_a + + def tearDown(self) -> None: + FinetuneTest.tearDown(self) + + +class TestEnergyModelDPA1(unittest.TestCase, FinetuneTest): + def setUp(self): + self.data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.data = DeepmdDataSystem( + self.data_file, + batch_size=1, + test_size=1, + ) + self.data.add("energy", ndof=1, atomic=False, must=True, high_prec=True) + self.model_config = model_dpa1 + + def tearDown(self) -> None: + FinetuneTest.tearDown(self) + + +class TestEnergyModelDPA2(unittest.TestCase, FinetuneTest): + def setUp(self): + self.data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.data = DeepmdDataSystem( + self.data_file, + batch_size=1, + test_size=1, + ) + self.data.add("energy", ndof=1, atomic=False, must=True, high_prec=True) + self.model_config = model_dpa2 + + def tearDown(self) -> None: + FinetuneTest.tearDown(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index 0a052103b6..bcb8c6c188 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -26,8 +26,17 @@ class DPTrainTest: def test_dp_train(self): + # test training from scratch trainer = get_trainer(deepcopy(self.config)) trainer.run() + + # test fine-tuning + trainer_finetune = get_trainer( + deepcopy(self.config), + finetune_model=self.config["training"].get("save_ckpt", "model.ckpt") + + ".pt", + ) + trainer_finetune.run() self.tearDown() def test_trainable(self): From 7af9e20a3456eedc28524f81334411c7093f1ece Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 3 Mar 2024 02:36:40 -0500 Subject: [PATCH 177/686] bump LAMMPS to stable_2Aug2023_update3 (#3399) Signed-off-by: Jinzhe Zeng --- backend/dynamic_metadata.py | 2 +- doc/install/install-lammps.md | 22 +++++++++++----------- pyproject.toml | 7 +++---- source/install/build_cc.sh | 2 +- source/install/build_from_c.sh | 2 +- source/install/build_lammps.sh | 2 +- source/install/test_cc.sh | 2 +- source/install/test_cc_local.sh | 2 +- 8 files changed, 20 insertions(+), 21 deletions(-) diff --git a/backend/dynamic_metadata.py b/backend/dynamic_metadata.py index 5646169907..d70e8b9102 100644 --- a/backend/dynamic_metadata.py +++ b/backend/dynamic_metadata.py @@ -58,7 +58,7 @@ def dynamic_metadata( "sphinxcontrib-bibtex", ], "lmp": [ - "lammps~=2023.8.2.2.0", + "lammps~=2023.8.2.3.0", *find_libpython_requires, ], "ipi": [ diff --git a/doc/install/install-lammps.md b/doc/install/install-lammps.md index 5dbf690c67..21e1e72dd1 100644 --- a/doc/install/install-lammps.md +++ b/doc/install/install-lammps.md @@ -14,10 +14,10 @@ make lammps DeePMD-kit will generate a module called `USER-DEEPMD` in the `build` directory, which supports either double or single float precision interface. Now download the LAMMPS code, and uncompress it. ```bash cd /some/workspace -wget https://github.com/lammps/lammps/archive/stable_2Aug2023_update2.tar.gz -tar xf stable_2Aug2023_update2.tar.gz +wget https://github.com/lammps/lammps/archive/stable_2Aug2023_update3.tar.gz +tar xf stable_2Aug2023_update3.tar.gz ``` -The source code of LAMMPS is stored in the directory `lammps-stable_2Aug2023_update2`. +The source code of LAMMPS is stored in the directory `lammps-stable_2Aug2023_update3`. Then, you can [build LAMMPS](https://docs.lammps.org/Build.html) with either make or CMake. @@ -25,7 +25,7 @@ Then, you can [build LAMMPS](https://docs.lammps.org/Build.html) with either mak Now go into the LAMMPS code and copy the DeePMD-kit module like this ```bash -cd lammps-stable_2Aug2023_update2/src/ +cd lammps-stable_2Aug2023_update3/src/ cp -r $deepmd_source_dir/source/build/USER-DEEPMD . make yes-kspace make yes-extra-fix @@ -51,8 +51,8 @@ make no-user-deepmd Now go into the LAMMPS directory and create a directory called `build`: ```bash -mkdir -p lammps-stable_2Aug2023_update2/build/ -cd lammps-stable_2Aug2023_update2/build/ +mkdir -p lammps-stable_2Aug2023_update3/build/ +cd lammps-stable_2Aug2023_update3/build/ ``` Patch the LAMMPS `CMakeLists.txt` file: @@ -81,15 +81,15 @@ Starting from `8Apr2021`, LAMMPS also provides a plugin mode, allowing one to bu Now download the LAMMPS code (`8Apr2021` or later), and uncompress it: ```bash cd /some/workspace -wget https://github.com/lammps/lammps/archive/stable_2Aug2023_update2.tar.gz -tar xf stable_2Aug2023_update2.tar.gz +wget https://github.com/lammps/lammps/archive/stable_2Aug2023_update3.tar.gz +tar xf stable_2Aug2023_update3.tar.gz ``` -The source code of LAMMPS is stored in the directory `lammps-stable_2Aug2023_update2`. The directory of the source code should be specified as the CMAKE argument `LAMMPS_SOURCE_ROOT` during installation of the DeePMD-kit C++ interface. Now go into the LAMMPS directory and create a directory called `build` +The source code of LAMMPS is stored in the directory `lammps-stable_2Aug2023_update3`. The directory of the source code should be specified as the CMAKE argument `LAMMPS_SOURCE_ROOT` during installation of the DeePMD-kit C++ interface. Now go into the LAMMPS directory and create a directory called `build` ```bash -mkdir -p lammps-stable_2Aug2023_update2/build/ -cd lammps-stable_2Aug2023_update2/build/ +mkdir -p lammps-stable_2Aug2023_update3/build/ +cd lammps-stable_2Aug2023_update3/build/ ``` Now build LAMMPS. Note that `PLUGIN` must be enabled, and `BUILD_SHARED_LIBS` must be set to `yes`. You can install any other package you want. ```bash diff --git a/pyproject.toml b/pyproject.toml index 36851b1401..3ee65865f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,10 +140,9 @@ manylinux-x86_64-image = "quay.io/pypa/manylinux_2_28_x86_64:2022-11-19-1b19e81" manylinux-aarch64-image = "manylinux_2_28" [tool.cibuildwheel.macos] -environment = { PIP_PREFER_BINARY="1", DP_LAMMPS_VERSION="stable_2Aug2023_update2", DP_ENABLE_IPI="1" } +environment = { PIP_PREFER_BINARY="1", DP_LAMMPS_VERSION="stable_2Aug2023_update3", DP_ENABLE_IPI="1" } before-all = [ - # enable MPI for macos-arm64 in the next lammps release for compatibility - """if [[ "$CIBW_BUILD" != *macosx_arm64* ]]; then brew install mpich; fi""", + """brew install mpich""", ] repair-wheel-command = """delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} --ignore-missing-dependencies""" @@ -156,7 +155,7 @@ environment-pass = [ "DP_PKG_NAME", "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DEEPMD-KIT-CU11", ] -environment = { PIP_PREFER_BINARY="1", DP_LAMMPS_VERSION="stable_2Aug2023_update2", DP_ENABLE_IPI="1", MPI_HOME="/usr/lib64/mpich", PATH="/usr/lib64/mpich/bin:$PATH" } +environment = { PIP_PREFER_BINARY="1", DP_LAMMPS_VERSION="stable_2Aug2023_update3", DP_ENABLE_IPI="1", MPI_HOME="/usr/lib64/mpich", PATH="/usr/lib64/mpich/bin:$PATH" } before-all = [ """if [ ! -z "${DP_PKG_NAME}" ]; then sed -i "s/name = \\"deepmd-kit\\"/name = \\"${DP_PKG_NAME}\\"/g" pyproject.toml; fi""", # https://almalinux.org/blog/2023-12-20-almalinux-8-key-update/ diff --git a/source/install/build_cc.sh b/source/install/build_cc.sh index 83a586049d..3db4c92a3e 100755 --- a/source/install/build_cc.sh +++ b/source/install/build_cc.sh @@ -25,7 +25,7 @@ cmake -D ENABLE_TENSORFLOW=ON \ -D CMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} \ -D USE_TF_PYTHON_LIBS=TRUE \ ${CUDA_ARGS} \ - -D LAMMPS_VERSION=stable_2Aug2023_update2 \ + -D LAMMPS_VERSION=stable_2Aug2023_update3 \ .. cmake --build . -j${NPROC} cmake --install . diff --git a/source/install/build_from_c.sh b/source/install/build_from_c.sh index c1188252ab..e8dcee945d 100755 --- a/source/install/build_from_c.sh +++ b/source/install/build_from_c.sh @@ -13,7 +13,7 @@ NPROC=$(nproc --all) BUILD_TMP_DIR=${SCRIPT_PATH}/../build mkdir -p ${BUILD_TMP_DIR} cd ${BUILD_TMP_DIR} -cmake -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} -DDEEPMD_C_ROOT=${DEEPMD_C_ROOT} -DLAMMPS_VERSION=stable_2Aug2023_update2 .. +cmake -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} -DDEEPMD_C_ROOT=${DEEPMD_C_ROOT} -DLAMMPS_VERSION=stable_2Aug2023_update3 .. cmake --build . -j${NPROC} cmake --install . cmake --build . --target=lammps diff --git a/source/install/build_lammps.sh b/source/install/build_lammps.sh index 2b5bf0a643..fca3b3e5ad 100755 --- a/source/install/build_lammps.sh +++ b/source/install/build_lammps.sh @@ -14,7 +14,7 @@ BUILD_TMP_DIR=${SCRIPT_PATH}/../build_lammps mkdir -p ${BUILD_TMP_DIR} cd ${BUILD_TMP_DIR} # download LAMMMPS -LAMMPS_VERSION=stable_2Aug2023_update2 +LAMMPS_VERSION=stable_2Aug2023_update3 if [ ! -d "lammps-${LAMMPS_VERSION}" ]; then curl -L -o lammps.tar.gz https://github.com/lammps/lammps/archive/refs/tags/${LAMMPS_VERSION}.tar.gz tar vxzf lammps.tar.gz diff --git a/source/install/test_cc.sh b/source/install/test_cc.sh index 0dd35f5615..8c75b00762 100755 --- a/source/install/test_cc.sh +++ b/source/install/test_cc.sh @@ -17,7 +17,7 @@ INSTALL_PREFIX=${SCRIPT_PATH}/../../dp_test BUILD_TMP_DIR=${SCRIPT_PATH}/../build_tests mkdir -p ${BUILD_TMP_DIR} cd ${BUILD_TMP_DIR} -cmake -DINSTALL_TENSORFLOW=TRUE -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} -DTENSORFLOW_ROOT=${INSTALL_PREFIX} -DBUILD_TESTING:BOOL=TRUE -DLAMMPS_VERSION=stable_2Aug2023_update2 ${CUDA_ARGS} .. +cmake -DINSTALL_TENSORFLOW=TRUE -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} -DTENSORFLOW_ROOT=${INSTALL_PREFIX} -DBUILD_TESTING:BOOL=TRUE -DLAMMPS_VERSION=stable_2Aug2023_update3 ${CUDA_ARGS} .. cmake --build . -j${NPROC} cmake --install . ctest --output-on-failure diff --git a/source/install/test_cc_local.sh b/source/install/test_cc_local.sh index 73aa74ed90..13a10d78e3 100755 --- a/source/install/test_cc_local.sh +++ b/source/install/test_cc_local.sh @@ -25,7 +25,7 @@ cmake \ -D USE_TF_PYTHON_LIBS=TRUE \ -D CMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} \ -D BUILD_TESTING:BOOL=TRUE \ - -D LAMMPS_VERSION=stable_2Aug2023_update2 \ + -D LAMMPS_VERSION=stable_2Aug2023_update3 \ ${CUDA_ARGS} .. cmake --build . -j${NPROC} cmake --install . From 9c508b7d444fee251a9cb7c6151c5defb3116114 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 3 Mar 2024 04:19:06 -0500 Subject: [PATCH 178/686] allow loading either nsel or natoms atomic tensor data (#3394) A new parameter, `output_natoms_for_type_sel`, is added for the data requirement. (default=false) If sel_types is given, output_natoms_for_type_sel is true, and the data dimension is nsel, it will be converted to natoms. If sel_types is given, output_natoms_for_type_sel is false, and the data dimension is natoms, it will be converted to nsel. In other situations, it keeps the original shape. The user can give data in either nsel or natoms, if `sel_types` and `output_natoms_for_type_sel` are set. --------- Signed-off-by: Jinzhe Zeng --- deepmd/common.py | 4 ++ deepmd/pt/utils/dataset.py | 1 + deepmd/utils/data.py | 60 ++++++++++++++++++++-- deepmd/utils/data_system.py | 12 +++++ source/tests/tf/test_data_requirement.py | 1 + source/tests/tf/test_deepmd_data.py | 63 ++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 4 deletions(-) diff --git a/deepmd/common.py b/deepmd/common.py index d7e485788b..29d32111a8 100644 --- a/deepmd/common.py +++ b/deepmd/common.py @@ -78,6 +78,7 @@ def add_data_requirement( repeat: int = 1, default: float = 0.0, dtype: Optional[np.dtype] = None, + output_natoms_for_type_sel: bool = False, ): """Specify data requirements for training. @@ -103,6 +104,8 @@ def add_data_requirement( default value of data dtype : np.dtype, optional the dtype of data, overwrites `high_prec` if provided + output_natoms_for_type_sel : bool, optional + if True and type_sel is True, the atomic dimension will be natoms instead of nsel """ data_requirement[key] = { "ndof": ndof, @@ -113,6 +116,7 @@ def add_data_requirement( "repeat": repeat, "default": default, "dtype": dtype, + "output_natoms_for_type_sel": output_natoms_for_type_sel, } diff --git a/deepmd/pt/utils/dataset.py b/deepmd/pt/utils/dataset.py index 67005b5ed3..77297d980c 100644 --- a/deepmd/pt/utils/dataset.py +++ b/deepmd/pt/utils/dataset.py @@ -61,4 +61,5 @@ def add_data_requirement(self, data_requirement: List[DataRequirementItem]): repeat=data_item["repeat"], default=data_item["default"], dtype=data_item["dtype"], + output_natoms_for_type_sel=data_item["output_natoms_for_type_sel"], ) diff --git a/deepmd/utils/data.py b/deepmd/utils/data.py index 03e39e1f21..194c6b1e24 100644 --- a/deepmd/utils/data.py +++ b/deepmd/utils/data.py @@ -147,6 +147,7 @@ def add( repeat: int = 1, default: float = 0.0, dtype: Optional[np.dtype] = None, + output_natoms_for_type_sel: bool = False, ): """Add a data item that to be loaded. @@ -173,6 +174,8 @@ def add( default value of data dtype : np.dtype, optional the dtype of data, overwrites `high_prec` if provided + output_natoms_for_type_sel : bool, optional + if True and type_sel is True, the atomic dimension will be natoms instead of nsel """ self.data_dict[key] = { "ndof": ndof, @@ -184,6 +187,7 @@ def add( "reduce": None, "default": default, "dtype": dtype, + "output_natoms_for_type_sel": output_natoms_for_type_sel, } return self @@ -523,6 +527,9 @@ def _load_set(self, set_name: DPPath): repeat=self.data_dict[kk]["repeat"], default=self.data_dict[kk]["default"], dtype=self.data_dict[kk]["dtype"], + output_natoms_for_type_sel=self.data_dict[kk][ + "output_natoms_for_type_sel" + ], ) for kk in self.data_dict.keys(): if self.data_dict[kk]["reduce"] is not None: @@ -589,19 +596,25 @@ def _load_data( type_sel=None, default: float = 0.0, dtype: Optional[np.dtype] = None, + output_natoms_for_type_sel: bool = False, ): if atomic: natoms = self.natoms idx_map = self.idx_map # if type_sel, then revise natoms and idx_map if type_sel is not None: - natoms = 0 + natoms_sel = 0 for jj in type_sel: - natoms += np.sum(self.atom_type == jj) - idx_map = self._idx_map_sel(self.atom_type, type_sel) + natoms_sel += np.sum(self.atom_type == jj) + idx_map_sel = self._idx_map_sel(self.atom_type, type_sel) + else: + natoms_sel = natoms + idx_map_sel = idx_map ndof = ndof_ * natoms else: ndof = ndof_ + natoms_sel = 0 + idx_map_sel = None if dtype is not None: pass elif high_prec: @@ -613,6 +626,38 @@ def _load_data( data = path.load_numpy().astype(dtype) try: # YWolfeee: deal with data shape error if atomic: + if type_sel is not None: + # check the data shape is nsel or natoms + if data.size == nframes * natoms_sel * ndof_: + if output_natoms_for_type_sel: + tmp = np.zeros( + [nframes, natoms, ndof_], dtype=data.dtype + ) + sel_mask = np.isin(self.atom_type, type_sel) + tmp[:, sel_mask] = data.reshape( + [nframes, natoms_sel, ndof_] + ) + data = tmp + else: + natoms = natoms_sel + idx_map = idx_map_sel + ndof = ndof_ * natoms + elif data.size == nframes * natoms * ndof_: + if output_natoms_for_type_sel: + pass + else: + sel_mask = np.isin(self.atom_type, type_sel) + data = data[:, sel_mask] + natoms = natoms_sel + idx_map = idx_map_sel + ndof = ndof_ * natoms + else: + raise ValueError( + f"The shape of the data {key} in {set_name}" + f"is {data.shape}, which doesn't match either" + f"({nframes}, {natoms_sel}, {ndof_}) or" + f"({nframes}, {natoms}, {ndof_})" + ) data = data.reshape([nframes, natoms, -1]) data = data[:, idx_map, :] data = data.reshape([nframes, -1]) @@ -621,13 +666,15 @@ def _load_data( explanation = "This error may occur when your label mismatch it's name, i.e. you might store global tensor in `atomic_tensor.npy` or atomic tensor in `tensor.npy`." log.error(str(err_message)) log.error(explanation) - raise ValueError(str(err_message) + ". " + explanation) + raise ValueError(str(err_message) + ". " + explanation) from err_message if repeat != 1: data = np.repeat(data, repeat).reshape([nframes, -1]) return np.float32(1.0), data elif must: raise RuntimeError("%s not found!" % path) else: + if type_sel is not None and not output_natoms_for_type_sel: + ndof = ndof_ * natoms_sel data = np.full([nframes, ndof], default, dtype=dtype) if repeat != 1: data = np.repeat(data, repeat).reshape([nframes, -1]) @@ -694,6 +741,8 @@ class DataRequirementItem: default value of data dtype : np.dtype, optional the dtype of data, overwrites `high_prec` if provided + output_natoms_for_type_sel : bool, optional + if True and type_sel is True, the atomic dimension will be natoms instead of nsel """ def __init__( @@ -707,6 +756,7 @@ def __init__( repeat: int = 1, default: float = 0.0, dtype: Optional[np.dtype] = None, + output_natoms_for_type_sel: bool = False, ) -> None: self.key = key self.ndof = ndof @@ -717,6 +767,7 @@ def __init__( self.repeat = repeat self.default = default self.dtype = dtype + self.output_natoms_for_type_sel = output_natoms_for_type_sel self.dict = self.to_dict() def to_dict(self) -> dict: @@ -730,6 +781,7 @@ def to_dict(self) -> dict: "repeat": self.repeat, "default": self.default, "dtype": self.dtype, + "output_natoms_for_type_sel": self.output_natoms_for_type_sel, } def __getitem__(self, key: str): diff --git a/deepmd/utils/data_system.py b/deepmd/utils/data_system.py index da1dd04026..0c74abfed1 100644 --- a/deepmd/utils/data_system.py +++ b/deepmd/utils/data_system.py @@ -293,6 +293,10 @@ def add_dict(self, adict: dict) -> None: type_sel=adict[kk]["type_sel"], repeat=adict[kk]["repeat"], default=adict[kk]["default"], + dtype=adict[kk].get("dtype"), + output_natoms_for_type_sel=adict[kk].get( + "output_natoms_for_type_sel", False + ), ) def add( @@ -305,6 +309,8 @@ def add( type_sel: Optional[List[int]] = None, repeat: int = 1, default: float = 0.0, + dtype: Optional[np.dtype] = None, + output_natoms_for_type_sel: bool = False, ): """Add a data item that to be loaded. @@ -329,6 +335,10 @@ def add( The data will be repeated `repeat` times. default, default=0. Default value of data + dtype + The dtype of data, overwrites `high_prec` if provided + output_natoms_for_type_sel : bool + If True and type_sel is True, the atomic dimension will be natoms instead of nsel """ for ii in self.data_systems: ii.add( @@ -340,6 +350,8 @@ def add( repeat=repeat, type_sel=type_sel, default=default, + dtype=dtype, + output_natoms_for_type_sel=output_natoms_for_type_sel, ) def reduce(self, key_out, key_in): diff --git a/source/tests/tf/test_data_requirement.py b/source/tests/tf/test_data_requirement.py index cabea15de1..e825bc3f92 100644 --- a/source/tests/tf/test_data_requirement.py +++ b/source/tests/tf/test_data_requirement.py @@ -16,3 +16,4 @@ def test_add(self): self.assertEqual(data_requirement["test"]["high_prec"], False) self.assertEqual(data_requirement["test"]["repeat"], 1) self.assertEqual(data_requirement["test"]["default"], 0.0) + self.assertEqual(data_requirement["test"]["output_natoms_for_type_sel"], False) diff --git a/source/tests/tf/test_deepmd_data.py b/source/tests/tf/test_deepmd_data.py index 3998e0f3e3..94e1f4c571 100644 --- a/source/tests/tf/test_deepmd_data.py +++ b/source/tests/tf/test_deepmd_data.py @@ -83,6 +83,7 @@ def setUp(self): os.makedirs(os.path.join(self.data_name, "set.foo"), exist_ok=True) os.makedirs(os.path.join(self.data_name, "set.bar"), exist_ok=True) os.makedirs(os.path.join(self.data_name, "set.tar"), exist_ok=True) + os.makedirs(os.path.join(self.data_name, "set.foo"), exist_ok=True) np.savetxt(os.path.join(self.data_name, "type.raw"), np.array([1, 0]), fmt="%d") np.savetxt( os.path.join(self.data_name, "type_map.raw"), @@ -141,6 +142,16 @@ def setUp(self): np.save(path, self.test_frame_bar) # t n self.test_null = np.zeros([self.nframes, 2 * self.natoms]) + # tensor shape + path = os.path.join(self.data_name, "set.foo", "tensor_natoms.npy") + self.tensor_natoms = np.random.default_rng().random( + [self.nframes, self.natoms, 6] + ) + self.tensor_natoms[:, 0, :] = 0 + np.save(path, self.tensor_natoms) + path = os.path.join(self.data_name, "set.foo", "tensor_nsel.npy") + self.tensor_nsel = self.tensor_natoms[:, 1, :] + np.save(path, self.tensor_nsel) def tearDown(self): shutil.rmtree(self.data_name) @@ -292,6 +303,58 @@ def test_get_nbatch(self): nb = dd.get_numb_batch(2, 0) self.assertEqual(nb, 2) + def test_get_tensor(self): + dd_natoms = ( + DeepmdData(self.data_name) + .add( + "tensor_nsel", + 6, + atomic=True, + must=True, + type_sel=[0], + output_natoms_for_type_sel=True, + ) + .add( + "tensor_natoms", + 6, + atomic=True, + must=True, + type_sel=[0], + output_natoms_for_type_sel=True, + ) + ) + data_natoms = dd_natoms._load_set(os.path.join(self.data_name, "set.foo")) + dd_nsel = ( + DeepmdData(self.data_name) + .add( + "tensor_nsel", + 6, + atomic=True, + must=True, + type_sel=[0], + output_natoms_for_type_sel=False, + ) + .add( + "tensor_natoms", + 6, + atomic=True, + must=True, + type_sel=[0], + output_natoms_for_type_sel=False, + ) + ) + data_nsel = dd_nsel._load_set(os.path.join(self.data_name, "set.foo")) + np.testing.assert_allclose( + data_natoms["tensor_natoms"], data_natoms["tensor_nsel"] + ) + np.testing.assert_allclose(data_nsel["tensor_natoms"], data_nsel["tensor_nsel"]) + np.testing.assert_allclose( + data_natoms["tensor_natoms"].reshape(self.nframes, self.natoms, -1)[ + :, 0, : + ], + data_nsel["tensor_natoms"], + ) + def _comp_np_mat2(self, first, second): np.testing.assert_almost_equal(first, second, places) From 13a8adf234f09982d88aa25fbd0b910104e96cd0 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 3 Mar 2024 04:19:26 -0500 Subject: [PATCH 179/686] do not return g2, h2, sw in hybrid descriptors (#3396) g2, h2, and sw are heavily dependent on the neighbor list. We cannot ensure the sub descriptors require the same neighbor list as the parent descriptor. Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/descriptor/hybrid.py | 8 +------- deepmd/pt/model/descriptor/hybrid.py | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/deepmd/dpmodel/descriptor/hybrid.py b/deepmd/dpmodel/descriptor/hybrid.py index 46f2616b84..96640d75c8 100644 --- a/deepmd/dpmodel/descriptor/hybrid.py +++ b/deepmd/dpmodel/descriptor/hybrid.py @@ -176,7 +176,7 @@ def call( """ out_descriptor = [] out_gr = [] - out_g2 = [] + out_g2 = None out_h2 = None out_sw = None if self.sel_no_mixed_types is not None: @@ -199,15 +199,9 @@ def call( out_descriptor.append(odescriptor) if gr is not None: out_gr.append(gr) - if g2 is not None: - out_g2.append(g2) - if self.get_rcut() == descrpt.get_rcut(): - out_h2 = h2 - out_sw = sw out_descriptor = np.concatenate(out_descriptor, axis=-1) out_gr = np.concatenate(out_gr, axis=-2) if out_gr else None - out_g2 = np.concatenate(out_g2, axis=-1) if out_g2 else None return out_descriptor, out_gr, out_g2, out_h2, out_sw @classmethod diff --git a/deepmd/pt/model/descriptor/hybrid.py b/deepmd/pt/model/descriptor/hybrid.py index b53adca462..204ca7589d 100644 --- a/deepmd/pt/model/descriptor/hybrid.py +++ b/deepmd/pt/model/descriptor/hybrid.py @@ -200,7 +200,7 @@ def forward( """ out_descriptor = [] out_gr = [] - out_g2 = [] + out_g2: Optional[torch.Tensor] = None out_h2: Optional[torch.Tensor] = None out_sw: Optional[torch.Tensor] = None if self.sel_no_mixed_types is not None: @@ -225,14 +225,8 @@ def forward( out_descriptor.append(odescriptor) if gr is not None: out_gr.append(gr) - if g2 is not None: - out_g2.append(g2) - if self.get_rcut() == descrpt.get_rcut(): - out_h2 = h2 - out_sw = sw out_descriptor = torch.cat(out_descriptor, dim=-1) out_gr = torch.cat(out_gr, dim=-2) if out_gr else None - out_g2 = torch.cat(out_g2, dim=-1) if out_g2 else None return out_descriptor, out_gr, out_g2, out_h2, out_sw @classmethod From e826260aa73fc91c9ee3d3b0725582fb6beb40a9 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 3 Mar 2024 04:19:57 -0500 Subject: [PATCH 180/686] format training logging (#3397) Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/loggers/loggers.py | 4 +- deepmd/loggers/training.py | 34 +++++++++++++++ deepmd/pt/train/training.py | 83 +++++++++++++++++++++++-------------- deepmd/tf/train/trainer.py | 44 +++++++++++++++++++- 4 files changed, 131 insertions(+), 34 deletions(-) create mode 100644 deepmd/loggers/training.py diff --git a/deepmd/loggers/loggers.py b/deepmd/loggers/loggers.py index 015581f6bd..33b9497507 100644 --- a/deepmd/loggers/loggers.py +++ b/deepmd/loggers/loggers.py @@ -29,14 +29,14 @@ ) CFORMATTER = logging.Formatter( # "%(app_name)s %(levelname)-7s |-> %(name)-45s %(message)s" - "%(app_name)s %(levelname)-7s %(message)s" + "[%(asctime)s] %(app_name)s %(levelname)-7s %(message)s" ) FFORMATTER_MPI = logging.Formatter( "[%(asctime)s] %(app_name)s rank:%(rank)-2s %(levelname)-7s %(name)-45s %(message)s" ) CFORMATTER_MPI = logging.Formatter( # "%(app_name)s rank:%(rank)-2s %(levelname)-7s |-> %(name)-45s %(message)s" - "%(app_name)s rank:%(rank)-2s %(levelname)-7s %(message)s" + "[%(asctime)s] %(app_name)s rank:%(rank)-2s %(levelname)-7s %(message)s" ) diff --git a/deepmd/loggers/training.py b/deepmd/loggers/training.py new file mode 100644 index 0000000000..954473e309 --- /dev/null +++ b/deepmd/loggers/training.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + Optional, +) + + +def format_training_message( + batch: int, + wall_time: float, +): + """Format a training message.""" + return f"batch {batch:7d}: " f"total wall time = {wall_time:.2f} s" + + +def format_training_message_per_task( + batch: int, + task_name: str, + rmse: Dict[str, float], + learning_rate: Optional[float], +): + if task_name: + task_name += ": " + if learning_rate is None: + lr = "" + else: + lr = f", lr = {learning_rate:8.2e}" + # sort rmse + rmse = dict(sorted(rmse.items())) + return ( + f"batch {batch:7d}: {task_name}" + f"{', '.join([f'{kk} = {vv:8.2e}' for kk, vv in rmse.items()])}" + f"{lr}" + ) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 77e6b1c709..35374c8162 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -19,6 +19,10 @@ from deepmd.common import ( symlink_prefix_files, ) +from deepmd.loggers.training import ( + format_training_message, + format_training_message_per_task, +) from deepmd.pt.loss import ( DenoiseLoss, EnergyStdLoss, @@ -693,33 +697,24 @@ def step(_step_id, task_key="Default"): # Log and persist if _step_id % self.disp_freq == 0: self.wrapper.eval() - msg = f"step={_step_id}, lr={cur_lr:.2e}" def log_loss_train(_loss, _more_loss, _task_key="Default"): results = {} - if not self.multi_task: - suffix = "" - else: - suffix = f"_{_task_key}" - _msg = f"loss{suffix}={_loss:.4f}" rmse_val = { item: _more_loss[item] for item in _more_loss if "l2_" not in item } for item in sorted(rmse_val.keys()): - _msg += f", {item}_train{suffix}={rmse_val[item]:.4f}" results[item] = rmse_val[item] - return _msg, results + return results def log_loss_valid(_task_key="Default"): single_results = {} sum_natoms = 0 if not self.multi_task: - suffix = "" valid_numb_batch = self.valid_numb_batch else: - suffix = f"_{_task_key}" valid_numb_batch = self.valid_numb_batch[_task_key] for ii in range(valid_numb_batch): self.optimizer.zero_grad() @@ -744,22 +739,32 @@ def log_loss_valid(_task_key="Default"): single_results.get(k, 0.0) + v * natoms ) results = {k: v / sum_natoms for k, v in single_results.items()} - _msg = "" - for item in sorted(results.keys()): - _msg += f", {item}_valid{suffix}={results[item]:.4f}" - return _msg, results + return results if not self.multi_task: - temp_msg, train_results = log_loss_train(loss, more_loss) - msg += "\n" + temp_msg - temp_msg, valid_results = log_loss_valid() - msg += temp_msg + train_results = log_loss_train(loss, more_loss) + valid_results = log_loss_valid() + log.info( + format_training_message_per_task( + batch=_step_id, + task_name="trn", + rmse=train_results, + learning_rate=cur_lr, + ) + ) + if valid_results is not None: + log.info( + format_training_message_per_task( + batch=_step_id, + task_name="val", + rmse=valid_results, + learning_rate=None, + ) + ) else: train_results = {_key: {} for _key in self.model_keys} valid_results = {_key: {} for _key in self.model_keys} - train_msg = {} - valid_msg = {} - train_msg[task_key], train_results[task_key] = log_loss_train( + train_results[task_key] = log_loss_train( loss, more_loss, _task_key=task_key ) for _key in self.model_keys: @@ -774,19 +779,37 @@ def log_loss_valid(_task_key="Default"): label=label_dict, task_key=_key, ) - train_msg[_key], train_results[_key] = log_loss_train( + train_results[_key] = log_loss_train( loss, more_loss, _task_key=_key ) - valid_msg[_key], valid_results[_key] = log_loss_valid( - _task_key=_key + valid_results[_key] = log_loss_valid(_task_key=_key) + log.info( + format_training_message_per_task( + batch=_step_id, + task_name=_key + "_trn", + rmse=train_results[_key], + learning_rate=cur_lr, + ) ) - msg += "\n" + train_msg[_key] - msg += valid_msg[_key] + if valid_results is not None: + log.info( + format_training_message_per_task( + batch=_step_id, + task_name=_key + "_val", + rmse=valid_results[_key], + learning_rate=None, + ) + ) - train_time = time.time() - self.t0 - self.t0 = time.time() - msg += f", speed={train_time:.2f} s/{self.disp_freq if _step_id else 1} batches" - log.info(msg) + current_time = time.time() + train_time = current_time - self.t0 + self.t0 = current_time + log.info( + format_training_message( + batch=_step_id, + wall_time=train_time, + ) + ) if fout: if self.lcurve_should_print_header: diff --git a/deepmd/tf/train/trainer.py b/deepmd/tf/train/trainer.py index 2d29a1a1c1..1dd31fd0bb 100644 --- a/deepmd/tf/train/trainer.py +++ b/deepmd/tf/train/trainer.py @@ -23,6 +23,10 @@ from deepmd.common import ( symlink_prefix_files, ) +from deepmd.loggers.training import ( + format_training_message, + format_training_message_per_task, +) from deepmd.tf.common import ( data_requirement, get_precision, @@ -774,8 +778,10 @@ def train(self, train_data=None, valid_data=None): test_time = toc - tic wall_time = toc - wall_time_tic log.info( - "batch %7d training time %.2f s, testing time %.2f s, total wall time %.2f s" - % (cur_batch, train_time, test_time, wall_time) + format_training_message( + batch=cur_batch, + wall_time=wall_time, + ) ) # the first training time is not accurate if cur_batch > self.disp_freq or stop_batch < 2 * self.disp_freq: @@ -959,6 +965,23 @@ def print_on_training( for k in train_results.keys(): print_str += prop_fmt % (train_results[k]) print_str += " %8.1e\n" % cur_lr + log.info( + format_training_message_per_task( + batch=cur_batch, + task_name="trn", + rmse=train_results, + learning_rate=cur_lr, + ) + ) + if valid_results is not None: + log.info( + format_training_message_per_task( + batch=cur_batch, + task_name="val", + rmse=valid_results, + learning_rate=None, + ) + ) else: for fitting_key in train_results: if valid_results[fitting_key] is not None: @@ -974,6 +997,23 @@ def print_on_training( for k in train_results[fitting_key].keys(): print_str += prop_fmt % (train_results[fitting_key][k]) print_str += " %8.1e\n" % cur_lr_dict[fitting_key] + log.info( + format_training_message_per_task( + batch=cur_batch, + task_name=f"{fitting_key}_trn", + rmse=train_results[fitting_key], + learning_rate=cur_lr_dict[fitting_key], + ) + ) + if valid_results is not None: + log.info( + format_training_message_per_task( + batch=cur_batch, + task_name=f"{fitting_key}_val", + rmse=valid_results[fitting_key], + learning_rate=None, + ) + ) fp.write(print_str) fp.flush() From ec32340389f614b9a90f1bc07e6b157c8f39b3a6 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Sun, 3 Mar 2024 17:20:21 +0800 Subject: [PATCH 181/686] Recovered all the skipped test for hybrid descriptor (#3400) Signed-off-by: Jinzhe Zeng Co-authored-by: Jinzhe Zeng Co-authored-by: Han Wang --- source/tests/pt/model/test_autodiff.py | 15 +++++++++++++++ source/tests/pt/model/test_jit.py | 4 +--- source/tests/pt/model/test_null_input.py | 2 -- source/tests/pt/model/test_permutation.py | 2 -- source/tests/pt/model/test_rot.py | 2 -- source/tests/pt/model/test_smooth.py | 1 - source/tests/pt/model/test_trans.py | 2 -- 7 files changed, 16 insertions(+), 12 deletions(-) diff --git a/source/tests/pt/model/test_autodiff.py b/source/tests/pt/model/test_autodiff.py index b1745b384e..c32f202625 100644 --- a/source/tests/pt/model/test_autodiff.py +++ b/source/tests/pt/model/test_autodiff.py @@ -19,6 +19,7 @@ eval_model, model_dpa1, model_dpa2, + model_hybrid, model_se_e2_a, model_zbl, ) @@ -192,6 +193,20 @@ def setUp(self): self.model = get_model(model_params).to(env.DEVICE) +class TestEnergyModelHybridForce(unittest.TestCase, ForceTest): + def setUp(self): + model_params = copy.deepcopy(model_hybrid) + self.type_split = True + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelHybridVirial(unittest.TestCase, VirialTest): + def setUp(self): + model_params = copy.deepcopy(model_hybrid) + self.type_split = True + self.model = get_model(model_params).to(env.DEVICE) + + class TestEnergyModelZBLForce(unittest.TestCase, ForceTest): def setUp(self): model_params = copy.deepcopy(model_zbl) diff --git a/source/tests/pt/model/test_jit.py b/source/tests/pt/model/test_jit.py index a1aa9658fc..fc07267b88 100644 --- a/source/tests/pt/model/test_jit.py +++ b/source/tests/pt/model/test_jit.py @@ -101,7 +101,6 @@ def tearDown(self): JITTest.tearDown(self) -@unittest.skip("hybrid not supported at the moment") class TestEnergyModelHybrid(unittest.TestCase, JITTest): def setUp(self): input_json = str(Path(__file__).parent / "water/se_atten.json") @@ -118,7 +117,6 @@ def tearDown(self): JITTest.tearDown(self) -@unittest.skip("hybrid not supported at the moment") class TestEnergyModelHybrid2(unittest.TestCase, JITTest): def setUp(self): input_json = str(Path(__file__).parent / "water/se_atten.json") @@ -128,7 +126,7 @@ def setUp(self): self.config["training"]["training_data"]["systems"] = data_file self.config["training"]["validation_data"]["systems"] = data_file self.config["model"] = deepcopy(model_hybrid) - self.config["model"]["descriptor"]["hybrid_mode"] = "sequential" + # self.config["model"]["descriptor"]["hybrid_mode"] = "sequential" self.config["training"]["numb_steps"] = 10 self.config["training"]["save_freq"] = 10 diff --git a/source/tests/pt/model/test_null_input.py b/source/tests/pt/model/test_null_input.py index 93a3ff8511..eb8ff714e8 100644 --- a/source/tests/pt/model/test_null_input.py +++ b/source/tests/pt/model/test_null_input.py @@ -119,7 +119,6 @@ def setUp(self): self.model = get_model(model_params).to(env.DEVICE) -@unittest.skip("hybrid not supported at the moment") class TestEnergyModelHybrid(unittest.TestCase, NullTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) @@ -127,7 +126,6 @@ def setUp(self): self.model = get_model(model_params).to(env.DEVICE) -@unittest.skip("hybrid not supported at the moment") class TestForceModelHybrid(unittest.TestCase, NullTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) diff --git a/source/tests/pt/model/test_permutation.py b/source/tests/pt/model/test_permutation.py index 45790bf43d..fa97281718 100644 --- a/source/tests/pt/model/test_permutation.py +++ b/source/tests/pt/model/test_permutation.py @@ -279,7 +279,6 @@ def setUp(self): self.model = get_model(model_params).to(env.DEVICE) -@unittest.skip("hybrid not supported at the moment") class TestEnergyModelHybrid(unittest.TestCase, PermutationTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) @@ -287,7 +286,6 @@ def setUp(self): self.model = get_model(model_params).to(env.DEVICE) -@unittest.skip("hybrid not supported at the moment") class TestForceModelHybrid(unittest.TestCase, PermutationTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) diff --git a/source/tests/pt/model/test_rot.py b/source/tests/pt/model/test_rot.py index 0c3a34e2d5..19f671e619 100644 --- a/source/tests/pt/model/test_rot.py +++ b/source/tests/pt/model/test_rot.py @@ -154,7 +154,6 @@ def setUp(self): self.model = get_model(model_params).to(env.DEVICE) -@unittest.skip("hybrid not supported at the moment") class TestEnergyModelHybrid(unittest.TestCase, RotTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) @@ -162,7 +161,6 @@ def setUp(self): self.model = get_model(model_params).to(env.DEVICE) -@unittest.skip("hybrid not supported at the moment") class TestForceModelHybrid(unittest.TestCase, RotTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) diff --git a/source/tests/pt/model/test_smooth.py b/source/tests/pt/model/test_smooth.py index 88d75a040c..bc1d26bffa 100644 --- a/source/tests/pt/model/test_smooth.py +++ b/source/tests/pt/model/test_smooth.py @@ -195,7 +195,6 @@ def setUp(self): self.epsilon, self.aprec = None, None -@unittest.skip("hybrid not supported at the moment") class TestEnergyModelHybrid(unittest.TestCase, SmoothTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) diff --git a/source/tests/pt/model/test_trans.py b/source/tests/pt/model/test_trans.py index 23365f3c9a..b9affac3aa 100644 --- a/source/tests/pt/model/test_trans.py +++ b/source/tests/pt/model/test_trans.py @@ -110,7 +110,6 @@ def setUp(self): self.model = get_model(model_params).to(env.DEVICE) -@unittest.skip("hybrid not supported at the moment") class TestEnergyModelHybrid(unittest.TestCase, TransTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) @@ -118,7 +117,6 @@ def setUp(self): self.model = get_model(model_params).to(env.DEVICE) -@unittest.skip("hybrid not supported at the moment") class TestForceModelHybrid(unittest.TestCase, TransTest): def setUp(self): model_params = copy.deepcopy(model_hybrid) From 174e9086047daeb5d2d0528973fdc1771fc538e0 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 3 Mar 2024 13:18:13 -0500 Subject: [PATCH 182/686] fix github actions for release (#3402) It looks when setting `branches-ignore`, the push on tags will not trigger the GitHub Actions. Setting `tags` is necessary. (So I have to manually upload the package again...) Signed-off-by: Jinzhe Zeng --- .github/workflows/build_wheel.yml | 2 ++ .github/workflows/package_c.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 467969f7a2..007b52018f 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -4,6 +4,8 @@ on: push: branches-ignore: - "gh-readonly-queue/**" + tags: + - "v*" pull_request: merge_group: diff --git a/.github/workflows/package_c.yml b/.github/workflows/package_c.yml index ac93bb79a2..72977bd339 100644 --- a/.github/workflows/package_c.yml +++ b/.github/workflows/package_c.yml @@ -4,6 +4,8 @@ on: push: branches-ignore: - "gh-readonly-queue/**" + tags: + - "v*" pull_request: merge_group: concurrency: From bd79dec46f833e88d2c84b22133a2f03e316a607 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 3 Mar 2024 21:13:48 -0500 Subject: [PATCH 183/686] fix deepmd-kit-cu11 again (#3403) Fix #3168. #3172 didn't fix #3168. The environmental variable `SETUPTOOLS_SCM_PRETEND_VERSION` works. I don't know what's wrong with the previous one. In this PR, I deleted the `.git` directory for `deepmd-kit-cu11`, which will throw an error if it doesn't work. --------- Signed-off-by: Jinzhe Zeng --- .github/workflows/build_wheel.yml | 3 ++- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 007b52018f..91bcae3702 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -86,7 +86,8 @@ jobs: if: matrix.dp_pkg_name == 'deepmd-kit-cu11' - run: | python -m pip install setuptools_scm - python -c "from setuptools_scm import get_version;print('SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DEEPMD-KIT-CU11='+get_version())" >> $GITHUB_ENV + python -c "from setuptools_scm import get_version;print('SETUPTOOLS_SCM_PRETEND_VERSION='+get_version())" >> $GITHUB_ENV + rm -rf .git if: matrix.dp_pkg_name == 'deepmd-kit-cu11' - name: Build wheels uses: pypa/cibuildwheel@v2.16 diff --git a/pyproject.toml b/pyproject.toml index 3ee65865f1..0d1471c2c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -153,7 +153,7 @@ environment-pass = [ "DP_VARIANT", "CUDA_VERSION", "DP_PKG_NAME", - "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DEEPMD-KIT-CU11", + "SETUPTOOLS_SCM_PRETEND_VERSION", ] environment = { PIP_PREFER_BINARY="1", DP_LAMMPS_VERSION="stable_2Aug2023_update3", DP_ENABLE_IPI="1", MPI_HOME="/usr/lib64/mpich", PATH="/usr/lib64/mpich/bin:$PATH" } before-all = [ From 32a86ac1e1a538ee71f7a95d8de68487f043247a Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 3 Mar 2024 21:14:57 -0500 Subject: [PATCH 184/686] pt: Fix compilation with libtorch (#3405) Backported from https://github.com/conda-forge/deepmd-kit-feedstock/pull/71. 1. The rpath of libdeepmd_cc should contain the libtorch directory if it differs from the path of libdeepmd_cc. Setting `INSTALL_RPATH_USE_LINK_PATH` to `true` resolves the problem. 2. `${TORCH_CXX_FLAGS}` is empty in macOS, so the variable with quotes, i.e. `"${TORCH_CXX_FLAGS}"`, should be used. --------- Signed-off-by: Jinzhe Zeng --- source/CMakeLists.txt | 2 +- source/api_cc/CMakeLists.txt | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 41e596f85e..931013016d 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -155,7 +155,7 @@ endif() if(ENABLE_PYTORCH AND NOT DEEPMD_C_ROOT) find_package(Torch REQUIRED) string(REGEX MATCH "_GLIBCXX_USE_CXX11_ABI=([0-9]+)" CXXABI_PT_MATCH - ${TORCH_CXX_FLAGS}) + "${TORCH_CXX_FLAGS}") if(CXXABI_PT_MATCH) message(STATUS "PyTorch CXX11 ABI: ${CMAKE_MATCH_1}") if(DEFINED OP_CXX_ABI) diff --git a/source/api_cc/CMakeLists.txt b/source/api_cc/CMakeLists.txt index cd42594f1e..2802498b4e 100644 --- a/source/api_cc/CMakeLists.txt +++ b/source/api_cc/CMakeLists.txt @@ -33,8 +33,10 @@ if(Protobuf_LIBRARY) endif() set_target_properties( - ${libname} PROPERTIES INSTALL_RPATH "$ORIGIN;${TensorFlow_LIBRARY_PATH}" - BUILD_RPATH "$ORIGIN/../op") + ${libname} + PROPERTIES INSTALL_RPATH "$ORIGIN;${TensorFlow_LIBRARY_PATH}" + INSTALL_RPATH_USE_LINK_PATH TRUE + BUILD_RPATH "$ORIGIN/../op") target_compile_definitions(${libname} PRIVATE TF_PRIVATE) if(CMAKE_TESTING_ENABLED) target_link_libraries(${libname} PRIVATE coverage_config) From 945f1b555e29ebc4b5326f23cf6d03b8937d0863 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 4 Mar 2024 04:09:26 -0500 Subject: [PATCH 185/686] set `NUM_WORKERS` to 0 in test_cuda action (#3404) It seems causing an OOM issue in the CUDA runner. Signed-off-by: Jinzhe Zeng --- .github/workflows/test_cuda.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_cuda.yml b/.github/workflows/test_cuda.yml index 915d983663..1a66bf5b82 100644 --- a/.github/workflows/test_cuda.yml +++ b/.github/workflows/test_cuda.yml @@ -51,10 +51,11 @@ jobs: - run: python -m pip install -v -e .[gpu,test,lmp,cu12,torch] "ase @ https://gitlab.com/ase/ase/-/archive/8c5aa5fd6448c5cfb517a014dccf2b214a9dfa8f/ase-8c5aa5fd6448c5cfb517a014dccf2b214a9dfa8f.tar.gz" env: DP_VARIANT: cuda - NUM_WORKERS: 0 DP_ENABLE_NATIVE_OPTIMIZATION: 1 - run: dp --version - run: python -m pytest source/tests --durations=0 + env: + NUM_WORKERS: 0 - name: Download libtorch run: | wget https://download.pytorch.org/libtorch/cu121/libtorch-cxx11-abi-shared-with-deps-2.2.1%2Bcu121.zip -O libtorch.zip From 4454811e2b9d5c0b32d5e2451a61e205a19951a2 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Tue, 5 Mar 2024 00:59:51 +0800 Subject: [PATCH 186/686] pt: fix multitask print_summary (#3409) --- deepmd/pt/train/training.py | 68 ++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 35374c8162..e5a7632ac4 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -394,7 +394,10 @@ def get_loss(loss_params, start_lr, _ntypes, _model): f"training in {model_key}", to_numpy_array(self.training_dataloader[model_key].sampler.weights), ) - if validation_data is not None: + if ( + validation_data is not None + and validation_data[model_key] is not None + ): validation_data[model_key].print_summary( f"validation in {model_key}", to_numpy_array( @@ -723,7 +726,7 @@ def log_loss_valid(_task_key="Default"): ) if input_dict == {}: # no validation data - return "", None + return {} _, loss, more_loss = self.wrapper( **input_dict, cur_lr=pref_lr, @@ -744,23 +747,24 @@ def log_loss_valid(_task_key="Default"): if not self.multi_task: train_results = log_loss_train(loss, more_loss) valid_results = log_loss_valid() - log.info( - format_training_message_per_task( - batch=_step_id, - task_name="trn", - rmse=train_results, - learning_rate=cur_lr, - ) - ) - if valid_results is not None: + if self.rank == 0: log.info( format_training_message_per_task( batch=_step_id, - task_name="val", - rmse=valid_results, - learning_rate=None, + task_name="trn", + rmse=train_results, + learning_rate=cur_lr, ) ) + if valid_results: + log.info( + format_training_message_per_task( + batch=_step_id, + task_name="val", + rmse=valid_results, + learning_rate=None, + ) + ) else: train_results = {_key: {} for _key in self.model_keys} valid_results = {_key: {} for _key in self.model_keys} @@ -783,33 +787,35 @@ def log_loss_valid(_task_key="Default"): loss, more_loss, _task_key=_key ) valid_results[_key] = log_loss_valid(_task_key=_key) - log.info( - format_training_message_per_task( - batch=_step_id, - task_name=_key + "_trn", - rmse=train_results[_key], - learning_rate=cur_lr, - ) - ) - if valid_results is not None: + if self.rank == 0: log.info( format_training_message_per_task( batch=_step_id, - task_name=_key + "_val", - rmse=valid_results[_key], - learning_rate=None, + task_name=_key + "_trn", + rmse=train_results[_key], + learning_rate=cur_lr, ) ) + if valid_results is not None and valid_results[_key]: + log.info( + format_training_message_per_task( + batch=_step_id, + task_name=_key + "_val", + rmse=valid_results[_key], + learning_rate=None, + ) + ) current_time = time.time() train_time = current_time - self.t0 self.t0 = current_time - log.info( - format_training_message( - batch=_step_id, - wall_time=train_time, + if self.rank == 0: + log.info( + format_training_message( + batch=_step_id, + wall_time=train_time, + ) ) - ) if fout: if self.lcurve_should_print_header: From c8c941aa362daff2bd4b1641e869690c15c9640c Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Tue, 5 Mar 2024 08:57:40 +0800 Subject: [PATCH 187/686] pt: fix multitask stuck on multiple-gpu (#3411) --- deepmd/pt/model/descriptor/descriptor.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/deepmd/pt/model/descriptor/descriptor.py b/deepmd/pt/model/descriptor/descriptor.py index 24c1ef4dab..5aae848aa4 100644 --- a/deepmd/pt/model/descriptor/descriptor.py +++ b/deepmd/pt/model/descriptor/descriptor.py @@ -126,16 +126,18 @@ def share_params(self, base_class, shared_level, resume=False): ), "Only descriptors of the same type can share params!" if shared_level == 0: # link buffers - if hasattr(self, "mean") and not resume: - # in case of change params during resume - base_env = EnvMatStatSe(base_class) - base_env.stats = base_class.stats - for kk in base_class.get_stats(): - base_env.stats[kk] += self.get_stats()[kk] - mean, stddev = base_env() - if not base_class.set_davg_zero: - base_class.mean.copy_(torch.tensor(mean, device=env.DEVICE)) - base_class.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) + if hasattr(self, "mean"): + if not resume: + # in case of change params during resume + base_env = EnvMatStatSe(base_class) + base_env.stats = base_class.stats + for kk in base_class.get_stats(): + base_env.stats[kk] += self.get_stats()[kk] + mean, stddev = base_env() + if not base_class.set_davg_zero: + base_class.mean.copy_(torch.tensor(mean, device=env.DEVICE)) + base_class.stddev.copy_(torch.tensor(stddev, device=env.DEVICE)) + # must share, even if not do stat self.mean = base_class.mean self.stddev = base_class.stddev # self.load_state_dict(base_class.state_dict()) # this does not work, because it only inits the model From c56a30d24009cd84c2e74b3a0dc21afee14eecf8 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:41:39 +0800 Subject: [PATCH 188/686] Chore: make type_map complusory model attribute (#3410) This PR is to make `type_map` a mandatory input parameter for all AtomicModels in Pytorch backend. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../dpmodel/atomic_model/dp_atomic_model.py | 7 +-- .../atomic_model/linear_atomic_model.py | 54 +++++++++++++++---- .../atomic_model/pairtab_atomic_model.py | 19 +++++-- .../pt/model/atomic_model/dp_atomic_model.py | 2 +- .../model/atomic_model/linear_atomic_model.py | 52 ++++++++++++++---- .../atomic_model/pairtab_atomic_model.py | 19 +++++-- deepmd/pt/model/model/__init__.py | 6 ++- .../dpmodel/test_linear_atomic_model.py | 10 +++- .../dpmodel/test_pairtab_atomic_model.py | 8 ++- .../pt/model/test_linear_atomic_model.py | 20 ++++--- .../pt/model/test_pairtab_atomic_model.py | 14 ++--- 11 files changed, 162 insertions(+), 49 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/dp_atomic_model.py b/deepmd/dpmodel/atomic_model/dp_atomic_model.py index 96ef6d30ae..110aa26162 100644 --- a/deepmd/dpmodel/atomic_model/dp_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/dp_atomic_model.py @@ -45,12 +45,13 @@ def __init__( self, descriptor, fitting, - type_map: Optional[List[str]] = None, + type_map: List[str], **kwargs, ): self.type_map = type_map self.descriptor = descriptor self.fitting = fitting + self.type_map = type_map super().__init__(**kwargs) def fitting_output_def(self) -> FittingOutputDef: @@ -65,7 +66,7 @@ def get_sel(self) -> List[int]: """Get the neighbor selection.""" return self.descriptor.get_sel() - def get_type_map(self) -> Optional[List[str]]: + def get_type_map(self) -> List[str]: """Get the type map.""" return self.type_map @@ -154,7 +155,7 @@ def deserialize(cls, data) -> "DPAtomicModel": data.pop("type") descriptor_obj = BaseDescriptor.deserialize(data.pop("descriptor")) fitting_obj = BaseFitting.deserialize(data.pop("fitting")) - type_map = data.pop("type_map", None) + type_map = data.pop("type_map") obj = cls(descriptor_obj, fitting_obj, type_map=type_map, **data) return obj diff --git a/deepmd/dpmodel/atomic_model/linear_atomic_model.py b/deepmd/dpmodel/atomic_model/linear_atomic_model.py index 03c1249d4b..b1a32cdaa5 100644 --- a/deepmd/dpmodel/atomic_model/linear_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/linear_atomic_model.py @@ -45,14 +45,28 @@ class LinearAtomicModel(BaseAtomicModel): ---------- models : list[DPAtomicModel or PairTabAtomicModel] A list of models to be combined. PairTabAtomicModel must be used together with a DPAtomicModel. + type_map : list[str] + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. """ def __init__( self, models: List[BaseAtomicModel], + type_map: List[str], **kwargs, ): self.models = models + sub_model_type_maps = [md.get_type_map() for md in models] + err_msg = [] + common_type_map = set(type_map) + for tpmp in sub_model_type_maps: + if not common_type_map.issubset(set(tpmp)): + err_msg.append( + f"type_map {tpmp} is not a subset of type_map {type_map}" + ) + assert len(err_msg) == 0, "\n".join(err_msg) + self.type_map = type_map self.mixed_types_list = [model.mixed_types() for model in self.models] super().__init__(**kwargs) @@ -72,9 +86,9 @@ def get_rcut(self) -> float: """Get the cut-off radius.""" return max(self.get_model_rcuts()) - def get_type_map(self) -> Optional[List[str]]: + def get_type_map(self) -> List[str]: """Get the type map.""" - raise NotImplementedError("TODO: get_type_map should be implemented") + raise self.type_map def get_model_rcuts(self) -> List[float]: """Get the cut-off radius for each individual models.""" @@ -184,27 +198,29 @@ def fitting_output_def(self) -> FittingOutputDef: ) @staticmethod - def serialize(models) -> dict: + def serialize(models, type_map) -> dict: return { "@class": "Model", "type": "linear", "@version": 1, "models": [model.serialize() for model in models], "model_name": [model.__class__.__name__ for model in models], + "type_map": type_map, } @staticmethod - def deserialize(data) -> List[BaseAtomicModel]: + def deserialize(data) -> Tuple[List[BaseAtomicModel], List[str]]: data = copy.deepcopy(data) check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") model_names = data["model_name"] + type_map = data["type_map"] models = [ getattr(sys.modules[__name__], name).deserialize(model) for name, model in zip(model_names, data["models"]) ] - return models + return models, type_map @abstractmethod def _compute_weight( @@ -250,8 +266,20 @@ class DPZBLLinearAtomicModel(LinearAtomicModel): Parameters ---------- - models - This linear model should take a DPAtomicModel and a PairTable model. + dp_model + The DPAtomicModel being combined. + zbl_model + The PairTable model being combined. + sw_rmin + The lower boundary of the interpolation between short-range tabulated interaction and DP. + sw_rmax + The upper boundary of the interpolation between short-range tabulated interaction and DP. + type_map + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. + smin_alpha + The short-range tabulated interaction will be swithed according to the distance of the nearest neighbor. + This distance is calculated by softmin. """ def __init__( @@ -260,11 +288,12 @@ def __init__( zbl_model: PairTabAtomicModel, sw_rmin: float, sw_rmax: float, + type_map: List[str], smin_alpha: Optional[float] = 0.1, **kwargs, ): models = [dp_model, zbl_model] - super().__init__(models, **kwargs) + super().__init__(models, type_map, **kwargs) self.dp_model = dp_model self.zbl_model = zbl_model @@ -279,7 +308,9 @@ def serialize(self) -> dict: "@class": "Model", "type": "zbl", "@version": 1, - "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), + "models": LinearAtomicModel.serialize( + [self.dp_model, self.zbl_model], self.type_map + ), "sw_rmin": self.sw_rmin, "sw_rmax": self.sw_rmax, "smin_alpha": self.smin_alpha, @@ -297,13 +328,16 @@ def deserialize(cls, data) -> "DPZBLLinearAtomicModel": sw_rmax = data.pop("sw_rmax") smin_alpha = data.pop("smin_alpha") - dp_model, zbl_model = LinearAtomicModel.deserialize(data.pop("models")) + ([dp_model, zbl_model], type_map) = LinearAtomicModel.deserialize( + data.pop("models") + ) return cls( dp_model=dp_model, zbl_model=zbl_model, sw_rmin=sw_rmin, sw_rmax=sw_rmax, + type_map=type_map, smin_alpha=smin_alpha, **data, ) diff --git a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py index 5469ee80d2..c858179939 100644 --- a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py @@ -47,14 +47,23 @@ class PairTabAtomicModel(BaseAtomicModel): The cutoff radius. sel : int or list[int] The maxmum number of atoms in the cut-off radius. + type_map : list[str] + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. """ def __init__( - self, tab_file: str, rcut: float, sel: Union[int, List[int]], **kwargs + self, + tab_file: str, + rcut: float, + sel: Union[int, List[int]], + type_map: List[str], + **kwargs, ): super().__init__() self.tab_file = tab_file self.rcut = rcut + self.type_map = type_map self.tab = PairTab(self.tab_file, rcut=rcut) @@ -86,8 +95,8 @@ def fitting_output_def(self) -> FittingOutputDef: def get_rcut(self) -> float: return self.rcut - def get_type_map(self) -> Optional[List[str]]: - raise NotImplementedError("TODO: get_type_map should be implemented") + def get_type_map(self) -> List[str]: + return self.type_map def get_sel(self) -> List[int]: return [self.sel] @@ -118,6 +127,7 @@ def serialize(self) -> dict: "tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel, + "type_map": self.type_map, } ) return dd @@ -130,8 +140,9 @@ def deserialize(cls, data) -> "PairTabAtomicModel": data.pop("type") rcut = data.pop("rcut") sel = data.pop("sel") + type_map = data.pop("type_map") tab = PairTab.deserialize(data.pop("tab")) - tab_model = cls(None, rcut, sel, **data) + tab_model = cls(None, rcut, sel, type_map, **data) tab_model.tab = tab tab_model.tab_info = tab_model.tab.tab_info tab_model.tab_data = tab_model.tab.tab_data diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index 7f6c3076d8..807f8433e5 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -50,7 +50,7 @@ def __init__( self, descriptor, fitting, - type_map: Optional[List[str]], + type_map: List[str], **kwargs, ): torch.nn.Module.__init__(self) diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 5efbe533da..0dd1b13723 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -47,15 +47,29 @@ class LinearAtomicModel(torch.nn.Module, BaseAtomicModel): ---------- models : list[DPAtomicModel or PairTabAtomicModel] A list of models to be combined. PairTabAtomicModel must be used together with a DPAtomicModel. + type_map : list[str] + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. """ def __init__( self, models: List[BaseAtomicModel], + type_map: List[str], **kwargs, ): torch.nn.Module.__init__(self) self.models = torch.nn.ModuleList(models) + sub_model_type_maps = [md.get_type_map() for md in models] + err_msg = [] + common_type_map = set(type_map) + for tpmp in sub_model_type_maps: + if not common_type_map.issubset(set(tpmp)): + err_msg.append( + f"type_map {tpmp} is not a subset of type_map {type_map}" + ) + assert len(err_msg) == 0, "\n".join(err_msg) + self.type_map = type_map self.atomic_bias = None self.mixed_types_list = [model.mixed_types() for model in self.models] BaseAtomicModel.__init__(self, **kwargs) @@ -80,7 +94,7 @@ def get_rcut(self) -> float: @torch.jit.export def get_type_map(self) -> List[str]: """Get the type map.""" - raise NotImplementedError("TODO: implement this method") + return self.type_map def get_model_rcuts(self) -> List[float]: """Get the cut-off radius for each individual models.""" @@ -208,25 +222,27 @@ def fitting_output_def(self) -> FittingOutputDef: ) @staticmethod - def serialize(models) -> dict: + def serialize(models, type_map) -> dict: return { "@class": "Model", "@version": 1, "type": "linear", "models": [model.serialize() for model in models], "model_name": [model.__class__.__name__ for model in models], + "type_map": type_map, } @staticmethod - def deserialize(data) -> List[BaseAtomicModel]: + def deserialize(data) -> Tuple[List[BaseAtomicModel], List[str]]: data = copy.deepcopy(data) check_version_compatibility(data.pop("@version", 1), 1, 1) model_names = data["model_name"] + type_map = data["type_map"] models = [ getattr(sys.modules[__name__], name).deserialize(model) for name, model in zip(model_names, data["models"]) ] - return models + return models, type_map @abstractmethod def _compute_weight( @@ -281,8 +297,20 @@ class DPZBLLinearAtomicModel(LinearAtomicModel): Parameters ---------- - models - This linear model should take a DPAtomicModel and a PairTable model. + dp_model + The DPAtomicModel being combined. + zbl_model + The PairTable model being combined. + sw_rmin + The lower boundary of the interpolation between short-range tabulated interaction and DP. + sw_rmax + The upper boundary of the interpolation between short-range tabulated interaction and DP. + type_map + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. + smin_alpha + The short-range tabulated interaction will be swithed according to the distance of the nearest neighbor. + This distance is calculated by softmin. """ def __init__( @@ -291,11 +319,12 @@ def __init__( zbl_model: PairTabAtomicModel, sw_rmin: float, sw_rmax: float, + type_map: List[str], smin_alpha: Optional[float] = 0.1, **kwargs, ): models = [dp_model, zbl_model] - super().__init__(models, **kwargs) + super().__init__(models, type_map, **kwargs) self.model_def_script = "" self.dp_model = dp_model self.zbl_model = zbl_model @@ -314,7 +343,9 @@ def serialize(self) -> dict: "@class": "Model", "@version": 1, "type": "zbl", - "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), + "models": LinearAtomicModel.serialize( + [self.dp_model, self.zbl_model], self.type_map + ), "sw_rmin": self.sw_rmin, "sw_rmax": self.sw_rmax, "smin_alpha": self.smin_alpha, @@ -330,7 +361,9 @@ def deserialize(cls, data) -> "DPZBLLinearAtomicModel": sw_rmax = data.pop("sw_rmax") smin_alpha = data.pop("smin_alpha") - dp_model, zbl_model = LinearAtomicModel.deserialize(data.pop("models")) + [dp_model, zbl_model], type_map = LinearAtomicModel.deserialize( + data.pop("models") + ) data.pop("@class", None) data.pop("type", None) @@ -339,6 +372,7 @@ def deserialize(cls, data) -> "DPZBLLinearAtomicModel": zbl_model=zbl_model, sw_rmin=sw_rmin, sw_rmax=sw_rmax, + type_map=type_map, smin_alpha=smin_alpha, **data, ) diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index 47a20d3be9..bae4ea55e2 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -47,16 +47,25 @@ class PairTabAtomicModel(torch.nn.Module, BaseAtomicModel): The cutoff radius. sel : int or list[int] The maxmum number of atoms in the cut-off radius. + type_map : list[str] + Mapping atom type to the name (str) of the type. + For example `type_map[1]` gives the name of the type 1. """ def __init__( - self, tab_file: str, rcut: float, sel: Union[int, List[int]], **kwargs + self, + tab_file: str, + rcut: float, + sel: Union[int, List[int]], + type_map: List[str], + **kwargs, ): torch.nn.Module.__init__(self) self.model_def_script = "" self.tab_file = tab_file self.rcut = rcut self.tab = self._set_pairtab(tab_file, rcut) + self.type_map = type_map BaseAtomicModel.__init__(self, **kwargs) # handle deserialization with no input file @@ -103,8 +112,8 @@ def get_rcut(self) -> float: return self.rcut @torch.jit.export - def get_type_map(self) -> Optional[List[str]]: - raise NotImplementedError("TODO: implement this method") + def get_type_map(self) -> List[str]: + return self.type_map def get_sel(self) -> List[int]: return [self.sel] @@ -135,6 +144,7 @@ def serialize(self) -> dict: "tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel, + "type_map": self.type_map, } ) return dd @@ -145,10 +155,11 @@ def deserialize(cls, data) -> "PairTabAtomicModel": check_version_compatibility(data.pop("@version", 1), 1, 1) rcut = data.pop("rcut") sel = data.pop("sel") + type_map = data.pop("type_map") tab = PairTab.deserialize(data.pop("tab")) data.pop("@class", None) data.pop("type", None) - tab_model = cls(None, rcut, sel, **data) + tab_model = cls(None, rcut, sel, type_map, **data) tab_model.tab = tab tab_model.register_buffer("tab_info", torch.from_numpy(tab_model.tab.tab_info)) tab_model.register_buffer("tab_data", torch.from_numpy(tab_model.tab.tab_data)) diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index bd354af8d8..cd53f0a6b3 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -66,7 +66,10 @@ def get_zbl_model(model_params): # pairtab filepath = model_params["use_srtab"] pt_model = PairTabAtomicModel( - filepath, model_params["descriptor"]["rcut"], model_params["descriptor"]["sel"] + filepath, + model_params["descriptor"]["rcut"], + model_params["descriptor"]["sel"], + type_map=model_params["type_map"], ) rmin = model_params["sw_rmin"] @@ -78,6 +81,7 @@ def get_zbl_model(model_params): pt_model, rmin, rmax, + type_map=model_params["type_map"], atom_exclude_types=atom_exclude_types, pair_exclude_types=pair_exclude_types, ) diff --git a/source/tests/common/dpmodel/test_linear_atomic_model.py b/source/tests/common/dpmodel/test_linear_atomic_model.py index 79eef46b8a..aa56feb3e5 100644 --- a/source/tests/common/dpmodel/test_linear_atomic_model.py +++ b/source/tests/common/dpmodel/test_linear_atomic_model.py @@ -53,7 +53,9 @@ def test_pairwise(self, mock_loadtxt): ) type_map = ["foo", "bar"] - zbl_model = PairTabAtomicModel(tab_file=file_path, rcut=0.3, sel=2) + zbl_model = PairTabAtomicModel( + tab_file=file_path, rcut=0.3, sel=2, type_map=type_map + ) dp_model = DPAtomicModel(ds, ft, type_map=type_map) wgt_model = DPZBLLinearAtomicModel( @@ -61,6 +63,7 @@ def test_pairwise(self, mock_loadtxt): zbl_model, sw_rmin=0.1, sw_rmax=0.25, + type_map=type_map, ) wgt_res = [] for dist in np.linspace(0.05, 0.3, 10): @@ -145,12 +148,15 @@ def setUp(self, mock_loadtxt): ) type_map = ["foo", "bar"] dp_model = DPAtomicModel(ds, ft, type_map=type_map) - zbl_model = PairTabAtomicModel(file_path, self.rcut, sum(self.sel)) + zbl_model = PairTabAtomicModel( + file_path, self.rcut, sum(self.sel), type_map=type_map + ) self.md0 = DPZBLLinearAtomicModel( dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25, + type_map=type_map, ) self.md1 = DPZBLLinearAtomicModel.deserialize(self.md0.serialize()) diff --git a/source/tests/common/dpmodel/test_pairtab_atomic_model.py b/source/tests/common/dpmodel/test_pairtab_atomic_model.py index d004f9a37a..e2866d3766 100644 --- a/source/tests/common/dpmodel/test_pairtab_atomic_model.py +++ b/source/tests/common/dpmodel/test_pairtab_atomic_model.py @@ -24,7 +24,9 @@ def setUp(self, mock_loadtxt) -> None: ] ) - self.model = PairTabAtomicModel(tab_file=file_path, rcut=0.02, sel=2) + self.model = PairTabAtomicModel( + tab_file=file_path, rcut=0.02, sel=2, type_map=["H", "O"] + ) self.extended_coord = np.array( [ @@ -166,7 +168,9 @@ def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: ] ) - model = PairTabAtomicModel(tab_file=file_path, rcut=rcut, sel=2) + model = PairTabAtomicModel( + tab_file=file_path, rcut=rcut, sel=2, type_map=["S"] + ) results.append( model.forward_atomic(extended_coord, extended_atype, nlist)["energy"] ) diff --git a/source/tests/pt/model/test_linear_atomic_model.py b/source/tests/pt/model/test_linear_atomic_model.py index aae1a66618..fab6481a6f 100644 --- a/source/tests/pt/model/test_linear_atomic_model.py +++ b/source/tests/pt/model/test_linear_atomic_model.py @@ -69,13 +69,16 @@ def test_pairwise(self, mock_loadtxt): ).to(env.DEVICE) type_map = ["foo", "bar"] - zbl_model = PairTabAtomicModel(tab_file=file_path, rcut=0.3, sel=2) + zbl_model = PairTabAtomicModel( + tab_file=file_path, rcut=0.3, sel=2, type_map=type_map + ) dp_model = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) wgt_model = DPZBLLinearAtomicModel( dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25, + type_map=type_map, ).to(env.DEVICE) wgt_res = [] for dist in np.linspace(0.05, 0.3, 10): @@ -139,18 +142,23 @@ def setUp(self, mock_loadtxt): ).to(env.DEVICE) type_map = ["foo", "bar"] dp_model = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) - zbl_model = PairTabAtomicModel(file_path, self.rcut, sum(self.sel)) + zbl_model = PairTabAtomicModel( + file_path, self.rcut, sum(self.sel), type_map=type_map + ) self.md0 = DPZBLLinearAtomicModel( dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25, + type_map=type_map, ).to(env.DEVICE) self.md1 = DPZBLLinearAtomicModel.deserialize(self.md0.serialize()).to( env.DEVICE ) self.md2 = DPDPZBLLinearAtomicModel.deserialize(self.md0.serialize()) - self.md3 = DPZBLModel(dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25) + self.md3 = DPZBLModel( + dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25, type_map=type_map + ) def test_self_consistency(self): args = [ @@ -171,12 +179,10 @@ def test_self_consistency(self): def test_jit(self): md1 = torch.jit.script(self.md1) self.assertEqual(md1.get_rcut(), self.rcut) - with self.assertRaises(torch.jit.Error): - self.assertEqual(md1.get_type_map(), ["foo", "bar"]) + self.assertEqual(md1.get_type_map(), ["foo", "bar"]) md3 = torch.jit.script(self.md3) self.assertEqual(md3.get_rcut(), self.rcut) - with self.assertRaises(torch.jit.Error): - self.assertEqual(md3.get_type_map(), ["foo", "bar"]) + self.assertEqual(md3.get_type_map(), ["foo", "bar"]) if __name__ == "__main__": diff --git a/source/tests/pt/model/test_pairtab_atomic_model.py b/source/tests/pt/model/test_pairtab_atomic_model.py index 021035e3de..0576f89910 100644 --- a/source/tests/pt/model/test_pairtab_atomic_model.py +++ b/source/tests/pt/model/test_pairtab_atomic_model.py @@ -32,7 +32,9 @@ def setUp(self, mock_loadtxt) -> None: ] ) - self.model = PairTabAtomicModel(tab_file=file_path, rcut=0.02, sel=2) + self.model = PairTabAtomicModel( + tab_file=file_path, rcut=0.02, sel=2, type_map=["H", "O"] + ) self.extended_coord = torch.tensor( [ @@ -97,8 +99,7 @@ def test_with_mask(self): def test_jit(self): model = torch.jit.script(self.model) self.assertEqual(model.get_rcut(), 0.02) - with self.assertRaises(torch.jit.Error): - self.assertEqual(model.get_type_map(), None) + self.assertEqual(model.get_type_map(), ["H", "O"]) def test_deserialize(self): model1 = PairTabAtomicModel.deserialize(self.model.serialize()) @@ -121,8 +122,7 @@ def test_deserialize(self): model1 = torch.jit.script(model1) self.assertEqual(model1.get_rcut(), 0.02) - with self.assertRaises(torch.jit.Error): - self.assertEqual(model1.get_type_map(), None) + self.assertEqual(model1.get_type_map(), ["H", "O"]) def test_cross_deserialize(self): model_dict = self.model.serialize() # pytorch model to dict @@ -228,7 +228,9 @@ def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: device=env.DEVICE, ) - model = PairTabAtomicModel(tab_file=file_path, rcut=rcut, sel=2) + model = PairTabAtomicModel( + tab_file=file_path, rcut=rcut, sel=2, type_map=["S"] + ) results.append( model.forward_atomic(extended_coord, extended_atype, nlist)["energy"] ) From 697fde9f2d64e71f84dde0a14204513d560b6cb2 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Tue, 5 Mar 2024 14:49:01 +0800 Subject: [PATCH 189/686] fix bug of rcut_smth >= rcut in the test cases (#3413) Co-authored-by: Han Wang --- .../dpmodel/case_single_frame_with_nlist.py | 13 ++++--- source/tests/pt/model/test_dp_model.py | 36 +++++++++++++++++-- source/tests/pt/model/test_env_mat.py | 12 ++++--- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/source/tests/common/dpmodel/case_single_frame_with_nlist.py b/source/tests/common/dpmodel/case_single_frame_with_nlist.py index ecdf3590a8..c260a18527 100644 --- a/source/tests/common/dpmodel/case_single_frame_with_nlist.py +++ b/source/tests/common/dpmodel/case_single_frame_with_nlist.py @@ -18,9 +18,10 @@ def setUp(self): self.atype = np.array([0, 0, 1], dtype=int).reshape([1, self.nloc]) self.cell = 2.0 * np.eye(3).reshape([1, 9]) # sel = [5, 2] - self.sel = [5, 2] - self.rcut = 0.4 - self.rcut_smth = 2.2 + self.sel = [16, 8] + self.rcut = 2.2 + self.rcut_smth = 0.4 + self.atol = 1e-12 class TestCaseSingleFrameWithNlist: @@ -51,8 +52,10 @@ def setUp(self): ], dtype=int, ).reshape([1, self.nloc, sum(self.sel)]) - self.rcut = 0.4 - self.rcut_smth = 2.2 + self.rcut = 2.2 + self.rcut_smth = 0.4 + self.atol = 1e-12 + # permutations self.perm = np.array([2, 0, 1, 3], dtype=np.int32) inv_perm = np.array([1, 2, 0, 3], dtype=np.int32) diff --git a/source/tests/pt/model/test_dp_model.py b/source/tests/pt/model/test_dp_model.py index 840ba284e2..c0b152b3d3 100644 --- a/source/tests/pt/model/test_dp_model.py +++ b/source/tests/pt/model/test_dp_model.py @@ -65,24 +65,29 @@ def test_self_consistency(self): np.testing.assert_allclose( to_numpy_array(ret0["energy"]), to_numpy_array(ret1["energy"]), + atol=self.atol, ) np.testing.assert_allclose( to_numpy_array(ret0["energy_redu"]), to_numpy_array(ret1["energy_redu"]), + atol=self.atol, ) np.testing.assert_allclose( to_numpy_array(ret0["energy_derv_r"]), to_numpy_array(ret1["energy_derv_r"]), + atol=self.atol, ) np.testing.assert_allclose( to_numpy_array(ret0["energy_derv_c_redu"]), to_numpy_array(ret1["energy_derv_c_redu"]), + atol=self.atol, ) ret0 = md0.forward_common(*args, do_atomic_virial=True) ret1 = md1.forward_common(*args, do_atomic_virial=True) np.testing.assert_allclose( to_numpy_array(ret0["energy_derv_c"]), to_numpy_array(ret1["energy_derv_c"]), + atol=self.atol, ) coord_ext, atype_ext, mapping = extend_coord_with_ghosts( @@ -106,6 +111,7 @@ def test_self_consistency(self): np.testing.assert_allclose( to_numpy_array(ret0["energy_derv_c_redu"]), to_numpy_array(ret2["energy_derv_c_redu"]), + atol=self.atol, ) def test_dp_consistency(self): @@ -141,10 +147,12 @@ def test_dp_consistency(self): np.testing.assert_allclose( ret0["energy"], to_numpy_array(ret1["energy"]), + atol=self.atol, ) np.testing.assert_allclose( ret0["energy_redu"], to_numpy_array(ret1["energy_redu"]), + atol=self.atol, ) def test_dp_consistency_nopbc(self): @@ -180,10 +188,12 @@ def test_dp_consistency_nopbc(self): np.testing.assert_allclose( ret0["energy"], to_numpy_array(ret1["energy"]), + atol=self.atol, ) np.testing.assert_allclose( ret0["energy_redu"], to_numpy_array(ret1["energy_redu"]), + atol=self.atol, ) def test_prec_consistency(self): @@ -231,6 +241,7 @@ def test_prec_consistency(self): np.testing.assert_allclose( to_numpy_array(model_l_ret_32[ii]), to_numpy_array(model_l_ret_64[ii]), + atol=self.atol, ) @@ -263,24 +274,29 @@ def test_self_consistency(self): np.testing.assert_allclose( to_numpy_array(ret0["energy"]), to_numpy_array(ret1["energy"]), + atol=self.atol, ) np.testing.assert_allclose( to_numpy_array(ret0["energy_redu"]), to_numpy_array(ret1["energy_redu"]), + atol=self.atol, ) np.testing.assert_allclose( to_numpy_array(ret0["energy_derv_r"]), to_numpy_array(ret1["energy_derv_r"]), + atol=self.atol, ) np.testing.assert_allclose( to_numpy_array(ret0["energy_derv_c_redu"]), to_numpy_array(ret1["energy_derv_c_redu"]), + atol=self.atol, ) ret0 = md0.forward_common_lower(*args, do_atomic_virial=True) ret1 = md1.forward_common_lower(*args, do_atomic_virial=True) np.testing.assert_allclose( to_numpy_array(ret0["energy_derv_c"]), to_numpy_array(ret1["energy_derv_c"]), + atol=self.atol, ) def test_dp_consistency(self): @@ -310,10 +326,12 @@ def test_dp_consistency(self): np.testing.assert_allclose( ret0["energy"], to_numpy_array(ret1["energy"]), + atol=self.atol, ) np.testing.assert_allclose( ret0["energy_redu"], to_numpy_array(ret1["energy_redu"]), + atol=self.atol, ) def test_prec_consistency(self): @@ -363,6 +381,7 @@ def test_prec_consistency(self): np.testing.assert_allclose( to_numpy_array(model_l_ret_32[ii]), to_numpy_array(model_l_ret_64[ii]), + atol=self.atol, ) def test_jit(self): @@ -447,7 +466,7 @@ def test_nlist_eq(self): to_torch_tensor(self.atype_ext), to_torch_tensor(nlist), ) - np.testing.assert_allclose(self.expected_nlist, to_numpy_array(nlist1)) + np.testing.assert_equal(self.expected_nlist, to_numpy_array(nlist1)) def test_nlist_st(self): # n_nnei < nnei @@ -464,7 +483,7 @@ def test_nlist_st(self): to_torch_tensor(self.atype_ext), to_torch_tensor(nlist), ) - np.testing.assert_allclose(self.expected_nlist, to_numpy_array(nlist1)) + np.testing.assert_equal(self.expected_nlist, to_numpy_array(nlist1)) def test_nlist_lt(self): # n_nnei > nnei @@ -481,7 +500,7 @@ def test_nlist_lt(self): to_torch_tensor(self.atype_ext), to_torch_tensor(nlist), ) - np.testing.assert_allclose(self.expected_nlist, to_numpy_array(nlist1)) + np.testing.assert_equal(self.expected_nlist, to_numpy_array(nlist1)) class TestEnergyModel(unittest.TestCase, TestCaseSingleFrameWithoutNlist): @@ -511,24 +530,29 @@ def test_self_consistency(self): np.testing.assert_allclose( to_numpy_array(ret0["atom_energy"]), to_numpy_array(ret1["atom_energy"]), + atol=self.atol, ) np.testing.assert_allclose( to_numpy_array(ret0["energy"]), to_numpy_array(ret1["energy"]), + atol=self.atol, ) np.testing.assert_allclose( to_numpy_array(ret0["force"]), to_numpy_array(ret1["force"]), + atol=self.atol, ) np.testing.assert_allclose( to_numpy_array(ret0["virial"]), to_numpy_array(ret1["virial"]), + atol=self.atol, ) ret0 = md0.forward(*args, do_atomic_virial=True) ret1 = md1.forward(*args, do_atomic_virial=True) np.testing.assert_allclose( to_numpy_array(ret0["atom_virial"]), to_numpy_array(ret1["atom_virial"]), + atol=self.atol, ) coord_ext, atype_ext, mapping, nlist = extend_input_and_build_neighbor_list( to_torch_tensor(self.coord), @@ -545,6 +569,7 @@ def test_self_consistency(self): np.testing.assert_allclose( to_numpy_array(ret0["virial"]), to_numpy_array(ret2["virial"]), + atol=self.atol, ) @@ -577,24 +602,29 @@ def test_self_consistency(self): np.testing.assert_allclose( to_numpy_array(ret0["atom_energy"]), to_numpy_array(ret1["atom_energy"]), + atol=self.atol, ) np.testing.assert_allclose( to_numpy_array(ret0["energy"]), to_numpy_array(ret1["energy"]), + atol=self.atol, ) np.testing.assert_allclose( to_numpy_array(ret0["extended_force"]), to_numpy_array(ret1["extended_force"]), + atol=self.atol, ) np.testing.assert_allclose( to_numpy_array(ret0["virial"]), to_numpy_array(ret1["virial"]), + atol=self.atol, ) ret0 = md0.forward_lower(*args, do_atomic_virial=True) ret1 = md1.forward_lower(*args, do_atomic_virial=True) np.testing.assert_allclose( to_numpy_array(ret0["extended_virial"]), to_numpy_array(ret1["extended_virial"]), + atol=self.atol, ) def test_jit(self): diff --git a/source/tests/pt/model/test_env_mat.py b/source/tests/pt/model/test_env_mat.py index fee3fd6fea..615e7c6230 100644 --- a/source/tests/pt/model/test_env_mat.py +++ b/source/tests/pt/model/test_env_mat.py @@ -43,8 +43,8 @@ def setUp(self): ], dtype=int, ).reshape([1, self.nloc, sum(self.sel)]) - self.rcut = 0.4 - self.rcut_smth = 2.2 + self.rcut = 2.2 + self.rcut_smth = 0.4 # permutations self.perm = np.array([2, 0, 1, 3], dtype=np.int32) inv_perm = np.array([1, 2, 0, 3], dtype=np.int32) @@ -61,6 +61,7 @@ def setUp(self): nlist1 = inv_perm[nlist1] nlist1 = np.where(mask, -1, nlist1) self.nlist = np.concatenate([self.nlist, nlist1], axis=0) + self.atol = 1e-12 class TestCaseSingleFrameWithoutNlist: @@ -79,9 +80,10 @@ def setUp(self): self.atype = np.array([0, 0, 1], dtype=int).reshape([1, self.nloc]) self.cell = 2.0 * np.eye(3).reshape([1, 9]) # sel = [5, 2] - self.sel = [5, 2] - self.rcut = 0.4 - self.rcut_smth = 2.2 + self.sel = [16, 8] + self.rcut = 2.2 + self.rcut_smth = 0.4 + self.atol = 1e-12 # to be merged with the tf test case From 268a0fcd561906df6699b566c299ea67db392410 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 5 Mar 2024 10:11:18 -0500 Subject: [PATCH 190/686] ban print (#3415) Signed-off-by: Jinzhe Zeng --- data/raw/copy_raw.py | 2 +- data/raw/shuffle_raw.py | 8 ++++---- deepmd/entrypoints/doc.py | 2 +- doc/sphinx_contrib_exhale_multiproject.py | 10 +++++----- pyproject.toml | 2 ++ source/tests/pt/model/test_unused_params.py | 2 -- source/tests/pt/test_dp_test.py | 3 +-- source/tests/tf/common.py | 3 ++- source/tests/tf/test_adjust_sel.py | 12 ------------ source/tests/tf/test_finetune_se_atten.py | 12 ------------ source/tests/tf/test_mixed_prec_training.py | 12 ------------ source/tests/tf/test_model_compression_se_a.py | 12 ------------ source/tests/tf/test_model_compression_se_a_ebd.py | 12 ------------ .../test_model_compression_se_a_ebd_type_one_side.py | 12 ------------ ...l_compression_se_a_type_one_side_exclude_types.py | 12 ------------ source/tests/tf/test_model_compression_se_atten.py | 12 ------------ source/tests/tf/test_model_compression_se_r.py | 12 ------------ source/tests/tf/test_model_compression_se_t.py | 12 ------------ source/tests/tf/test_parallel_training.py | 1 - source/tests/tf/test_transfer.py | 12 ------------ 20 files changed, 16 insertions(+), 149 deletions(-) diff --git a/data/raw/copy_raw.py b/data/raw/copy_raw.py index 642865db86..69ccdf5c63 100755 --- a/data/raw/copy_raw.py +++ b/data/raw/copy_raw.py @@ -85,7 +85,7 @@ def _main(): ) args = parser.parse_args() - print("# copy the system by %s copies" % args.ncopies) + print("# copy the system by %s copies" % args.ncopies) # noqa: T201 assert np.all( np.array(args.ncopies, dtype=int) >= np.array([1, 1, 1], dtype=int) ), "number of copies should be larger than or equal to 1" diff --git a/data/raw/shuffle_raw.py b/data/raw/shuffle_raw.py index 51bb7466c9..b4fc1457e5 100755 --- a/data/raw/shuffle_raw.py +++ b/data/raw/shuffle_raw.py @@ -37,7 +37,7 @@ def _main(): outpath = args.OUTPUT if not os.path.isdir(inpath): - print("# no input dir " + inpath + ", exit") + print("# no input dir " + inpath + ", exit") # noqa: T201 return if not os.path.isdir(outpath): @@ -47,16 +47,16 @@ def _main(): raws = detect_raw(inpath) if len(raws) == 0: - print("# no file to shuffle, exit") + print("# no file to shuffle, exit") # noqa: T201 return assert "box.raw" in raws tmp = np.loadtxt(os.path.join(inpath, "box.raw")) tmp = np.reshape(tmp, [-1, 9]) nframe = tmp.shape[0] - print(nframe) + print(nframe) # noqa: T201 - print( + print( # noqa: T201 "# will shuffle raw files " + str(raws) + " in dir " diff --git a/deepmd/entrypoints/doc.py b/deepmd/entrypoints/doc.py index 087eb10f73..e55e84f9d3 100644 --- a/deepmd/entrypoints/doc.py +++ b/deepmd/entrypoints/doc.py @@ -17,4 +17,4 @@ def doc_train_input(*, out_type: str = "rst", **kwargs): doc_str = gen_json() else: raise RuntimeError("Unsupported out type %s" % out_type) - print(doc_str) + print(doc_str) # noqa: T201 diff --git a/doc/sphinx_contrib_exhale_multiproject.py b/doc/sphinx_contrib_exhale_multiproject.py index e05cf88ba2..e26cc158a4 100644 --- a/doc/sphinx_contrib_exhale_multiproject.py +++ b/doc/sphinx_contrib_exhale_multiproject.py @@ -103,11 +103,11 @@ def exhale_environment_ready(app): app.config.exhale_args["containmentFolder"] = os.path.realpath( app.config.exhale_args["containmentFolder"] ) - print("=" * 75) - print(project) - print("-" * 50) - pprint(app.config.exhale_args) - print("=" * 75) + print("=" * 75) # noqa: T201 + print(project) # noqa: T201 + print("-" * 50) # noqa: T201 + pprint(app.config.exhale_args) # noqa: T203 + print("=" * 75) # noqa: T201 # First, setup the extension and verify all of the configurations. exhale.configs.apply_sphinx_configurations(app) diff --git a/pyproject.toml b/pyproject.toml index 0d1471c2c3..84cc7237bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -238,6 +238,7 @@ select = [ "NPY", # numpy "TID251", # banned-api "TID253", # banned-module-level-imports + "T20", # ban print ] ignore = [ @@ -283,6 +284,7 @@ banned-module-level-imports = [ "source/tests/pt/**" = ["TID253"] "source/ipi/tests/**" = ["TID253"] "source/lmp/tests/**" = ["TID253"] +"**/*.ipynb" = ["T20"] # printing in a nb file is expected [tool.pytest.ini_options] markers = "run" diff --git a/source/tests/pt/model/test_unused_params.py b/source/tests/pt/model/test_unused_params.py index c20a5f1dc5..36080c2bbd 100644 --- a/source/tests/pt/model/test_unused_params.py +++ b/source/tests/pt/model/test_unused_params.py @@ -87,8 +87,6 @@ def get_contributing_params(y, top_level=True): contributing_parameters = set(get_contributing_params(ret0["energy"])) all_parameters = set(self.model.parameters()) non_contributing = all_parameters - contributing_parameters - for ii in non_contributing: - print(ii.shape) self.assertEqual(len(non_contributing), 0) diff --git a/source/tests/pt/test_dp_test.py b/source/tests/pt/test_dp_test.py index 08bd2ce623..095994f8ec 100644 --- a/source/tests/pt/test_dp_test.py +++ b/source/tests/pt/test_dp_test.py @@ -49,8 +49,7 @@ def test_dp_test(self): try: res = tester.run() except StopIteration: - print("Unexpected stop iteration.(test step < total batch)") - raise StopIteration + raise StopIteration("Unexpected stop iteration.(test step < total batch)") for k, v in res.items(): if k == "rmse" or "mae" in k or k not in more_loss: continue diff --git a/source/tests/tf/common.py b/source/tests/tf/common.py index 0bcb29b4b5..d4f3cc8392 100644 --- a/source/tests/tf/common.py +++ b/source/tests/tf/common.py @@ -4,6 +4,7 @@ import os import pathlib import shutil +import warnings import dpdata import numpy as np @@ -969,7 +970,7 @@ def __init__(self, systems, set_prefix, batch_size, test_size, rcut, run_opt=Non ) chk_ret = self.data_systems[ii].check_test_size(test_size) if chk_ret is not None: - print( + warnings.warn( "WARNNING: system %s required test size %d is larger than the size %d of the dataset %s" % (self.system_dirs[ii], test_size, chk_ret[1], chk_ret[0]) ) diff --git a/source/tests/tf/test_adjust_sel.py b/source/tests/tf/test_adjust_sel.py index c86bad45b7..435d17d959 100644 --- a/source/tests/tf/test_adjust_sel.py +++ b/source/tests/tf/test_adjust_sel.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import json import os -import subprocess as sp import unittest import numpy as np @@ -33,17 +32,6 @@ def _file_delete(file): os.remove(file) -def _subprocess_run(command): - popen = sp.Popen(command.split(), shell=False, stdout=sp.PIPE, stderr=sp.STDOUT) - for line in iter(popen.stdout.readline, b""): - if hasattr(line, "decode"): - line = line.decode("utf-8") - line = line.rstrip() - print(line) - popen.wait() - return popen.returncode - - def _init_models(): # we use the setting for model compression data_file = str(tests_path / os.path.join("model_compression", "data")) diff --git a/source/tests/tf/test_finetune_se_atten.py b/source/tests/tf/test_finetune_se_atten.py index 35eb994a46..40fc5b68a3 100644 --- a/source/tests/tf/test_finetune_se_atten.py +++ b/source/tests/tf/test_finetune_se_atten.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import json import os -import subprocess as sp import unittest import numpy as np @@ -46,17 +45,6 @@ def _file_delete(file): os.remove(file) -def _subprocess_run(command): - popen = sp.Popen(command.split(), shell=False, stdout=sp.PIPE, stderr=sp.STDOUT) - for line in iter(popen.stdout.readline, b""): - if hasattr(line, "decode"): - line = line.decode("utf-8") - line = line.rstrip() - print(line) - popen.wait() - return popen.returncode - - def _init_models(setup_model, i): data_file = str(tests_path / os.path.join("finetune", "data")) data_file_mixed_type = str(tests_path / os.path.join("finetune", "data_mixed_type")) diff --git a/source/tests/tf/test_mixed_prec_training.py b/source/tests/tf/test_mixed_prec_training.py index 63504134af..4a4021771d 100644 --- a/source/tests/tf/test_mixed_prec_training.py +++ b/source/tests/tf/test_mixed_prec_training.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import json import os -import subprocess as sp import unittest import numpy as np @@ -28,17 +27,6 @@ def _file_delete(file): os.remove(file) -def _subprocess_run(command): - popen = sp.Popen(command.split(), shell=False, stdout=sp.PIPE, stderr=sp.STDOUT) - for line in iter(popen.stdout.readline, b""): - if hasattr(line, "decode"): - line = line.decode("utf-8") - line = line.rstrip() - print(line) - popen.wait() - return popen.returncode - - class TestMixedPrecTraining(unittest.TestCase): def setUp(self): data_file = str(tests_path / os.path.join("model_compression", "data")) diff --git a/source/tests/tf/test_model_compression_se_a.py b/source/tests/tf/test_model_compression_se_a.py index 37d1857661..4e49dd44e0 100644 --- a/source/tests/tf/test_model_compression_se_a.py +++ b/source/tests/tf/test_model_compression_se_a.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import json import os -import subprocess as sp import unittest import numpy as np @@ -33,17 +32,6 @@ def _file_delete(file): os.remove(file) -def _subprocess_run(command): - popen = sp.Popen(command.split(), shell=False, stdout=sp.PIPE, stderr=sp.STDOUT) - for line in iter(popen.stdout.readline, b""): - if hasattr(line, "decode"): - line = line.decode("utf-8") - line = line.rstrip() - print(line) - popen.wait() - return popen.returncode - - def _init_models(): data_file = str(tests_path / os.path.join("model_compression", "data")) frozen_model = str(tests_path / "dp-original.pb") diff --git a/source/tests/tf/test_model_compression_se_a_ebd.py b/source/tests/tf/test_model_compression_se_a_ebd.py index 1ab0cfe5cc..debae1f0ba 100644 --- a/source/tests/tf/test_model_compression_se_a_ebd.py +++ b/source/tests/tf/test_model_compression_se_a_ebd.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import json import os -import subprocess as sp import unittest import numpy as np @@ -33,17 +32,6 @@ def _file_delete(file): os.remove(file) -def _subprocess_run(command): - popen = sp.Popen(command.split(), shell=False, stdout=sp.PIPE, stderr=sp.STDOUT) - for line in iter(popen.stdout.readline, b""): - if hasattr(line, "decode"): - line = line.decode("utf-8") - line = line.rstrip() - print(line) - popen.wait() - return popen.returncode - - def _init_models(): data_file = str(tests_path / os.path.join("model_compression", "data")) frozen_model = str(tests_path / "dp-original-se-e2-a-v2.pb") diff --git a/source/tests/tf/test_model_compression_se_a_ebd_type_one_side.py b/source/tests/tf/test_model_compression_se_a_ebd_type_one_side.py index 5ae8ef4990..a24bf48398 100644 --- a/source/tests/tf/test_model_compression_se_a_ebd_type_one_side.py +++ b/source/tests/tf/test_model_compression_se_a_ebd_type_one_side.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import json import os -import subprocess as sp import unittest import numpy as np @@ -33,17 +32,6 @@ def _file_delete(file): os.remove(file) -def _subprocess_run(command): - popen = sp.Popen(command.split(), shell=False, stdout=sp.PIPE, stderr=sp.STDOUT) - for line in iter(popen.stdout.readline, b""): - if hasattr(line, "decode"): - line = line.decode("utf-8") - line = line.rstrip() - print(line) - popen.wait() - return popen.returncode - - def _init_models(): data_file = str(tests_path / os.path.join("model_compression", "data")) frozen_model = str(tests_path / "dp-original-se-e2-a-v2-one-side.pb") diff --git a/source/tests/tf/test_model_compression_se_a_type_one_side_exclude_types.py b/source/tests/tf/test_model_compression_se_a_type_one_side_exclude_types.py index 3726fc2bda..a9de974e4d 100644 --- a/source/tests/tf/test_model_compression_se_a_type_one_side_exclude_types.py +++ b/source/tests/tf/test_model_compression_se_a_type_one_side_exclude_types.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import json import os -import subprocess as sp import unittest import numpy as np @@ -33,17 +32,6 @@ def _file_delete(file): os.remove(file) -def _subprocess_run(command): - popen = sp.Popen(command.split(), shell=False, stdout=sp.PIPE, stderr=sp.STDOUT) - for line in iter(popen.stdout.readline, b""): - if hasattr(line, "decode"): - line = line.decode("utf-8") - line = line.rstrip() - print(line) - popen.wait() - return popen.returncode - - def _init_models(): data_file = str(tests_path / os.path.join("model_compression", "data")) frozen_model = str(tests_path / "dp-original-type-one-side-exclude-types.pb") diff --git a/source/tests/tf/test_model_compression_se_atten.py b/source/tests/tf/test_model_compression_se_atten.py index dbc54dd51a..aa1f0afa38 100644 --- a/source/tests/tf/test_model_compression_se_atten.py +++ b/source/tests/tf/test_model_compression_se_atten.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import json import os -import subprocess as sp import unittest import numpy as np @@ -29,17 +28,6 @@ def _file_delete(file): os.remove(file) -def _subprocess_run(command): - popen = sp.Popen(command.split(), shell=False, stdout=sp.PIPE, stderr=sp.STDOUT) - for line in iter(popen.stdout.readline, b""): - if hasattr(line, "decode"): - line = line.decode("utf-8") - line = line.rstrip() - print(line) - popen.wait() - return popen.returncode - - # 4 tests: # - type embedding FP64, se_atten FP64 # - type embedding FP64, se_atten FP32 diff --git a/source/tests/tf/test_model_compression_se_r.py b/source/tests/tf/test_model_compression_se_r.py index 4a5d9ad9f6..26665e5354 100644 --- a/source/tests/tf/test_model_compression_se_r.py +++ b/source/tests/tf/test_model_compression_se_r.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import json import os -import subprocess as sp import unittest import numpy as np @@ -33,17 +32,6 @@ def _file_delete(file): os.remove(file) -def _subprocess_run(command): - popen = sp.Popen(command.split(), shell=False, stdout=sp.PIPE, stderr=sp.STDOUT) - for line in iter(popen.stdout.readline, b""): - if hasattr(line, "decode"): - line = line.decode("utf-8") - line = line.rstrip() - print(line) - popen.wait() - return popen.returncode - - def _init_models(): data_file = str(tests_path / os.path.join("model_compression", "data")) frozen_model = str(tests_path / "dp-original-se-r.pb") diff --git a/source/tests/tf/test_model_compression_se_t.py b/source/tests/tf/test_model_compression_se_t.py index 0cf1135f8a..ec68176cdb 100644 --- a/source/tests/tf/test_model_compression_se_t.py +++ b/source/tests/tf/test_model_compression_se_t.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import json import os -import subprocess as sp import unittest import numpy as np @@ -33,17 +32,6 @@ def _file_delete(file): os.remove(file) -def _subprocess_run(command): - popen = sp.Popen(command.split(), shell=False, stdout=sp.PIPE, stderr=sp.STDOUT) - for line in iter(popen.stdout.readline, b""): - if hasattr(line, "decode"): - line = line.decode("utf-8") - line = line.rstrip() - print(line) - popen.wait() - return popen.returncode - - def _init_models(): data_file = str(tests_path / os.path.join("model_compression", "data")) frozen_model = str(tests_path / "dp-original-se-t.pb") diff --git a/source/tests/tf/test_parallel_training.py b/source/tests/tf/test_parallel_training.py index 1f93c809a2..d190764695 100644 --- a/source/tests/tf/test_parallel_training.py +++ b/source/tests/tf/test_parallel_training.py @@ -44,7 +44,6 @@ def test_two_workers(self): if hasattr(line, "decode"): line = line.decode("utf-8") line = line.rstrip() - print(line) popen.wait() self.assertEqual(0, popen.returncode, "Parallel training failed!") diff --git a/source/tests/tf/test_transfer.py b/source/tests/tf/test_transfer.py index e5b7f0a906..48e9f78e0d 100644 --- a/source/tests/tf/test_transfer.py +++ b/source/tests/tf/test_transfer.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import os -import subprocess as sp import unittest import numpy as np @@ -32,17 +31,6 @@ def _file_delete(file): os.remove(file) -def _subprocess_run(command): - popen = sp.Popen(command.split(), shell=False, stdout=sp.PIPE, stderr=sp.STDOUT) - for line in iter(popen.stdout.readline, b""): - if hasattr(line, "decode"): - line = line.decode("utf-8") - line = line.rstrip() - print(line) - popen.wait() - return popen.returncode - - class TestTransform(unittest.TestCase): @classmethod def setUpClass(self): From b0171ce3e98d5c06521e5c63f54d0893dd54a5d4 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 5 Mar 2024 14:51:21 -0500 Subject: [PATCH 191/686] throw errros if rmin is no less than rmax (#3393) when rmin==rmax, the previous implementation of compute_smooth_weight will give all nan. In theory, it should not happen. --------- Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/utils/env_mat.py | 2 ++ deepmd/pt/utils/preprocess.py | 2 ++ source/tests/common/dpmodel/test_linear_atomic_model.py | 8 ++++---- source/tests/pt/model/test_linear_atomic_model.py | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/deepmd/dpmodel/utils/env_mat.py b/deepmd/dpmodel/utils/env_mat.py index 0e861d9f38..5fb4ac4107 100644 --- a/deepmd/dpmodel/utils/env_mat.py +++ b/deepmd/dpmodel/utils/env_mat.py @@ -17,6 +17,8 @@ def compute_smooth_weight( rmax: float, ): """Compute smooth weight for descriptor elements.""" + if rmin >= rmax: + raise ValueError("rmin should be less than rmax.") min_mask = distance <= rmin max_mask = distance >= rmax mid_mask = np.logical_not(np.logical_or(min_mask, max_mask)) diff --git a/deepmd/pt/utils/preprocess.py b/deepmd/pt/utils/preprocess.py index 806bacdcd2..ed46292f84 100644 --- a/deepmd/pt/utils/preprocess.py +++ b/deepmd/pt/utils/preprocess.py @@ -228,6 +228,8 @@ def build_neighbor_list( def compute_smooth_weight(distance, rmin: float, rmax: float): """Compute smooth weight for descriptor elements.""" + if rmin >= rmax: + raise ValueError("rmin should be less than rmax.") min_mask = distance <= rmin max_mask = distance >= rmax mid_mask = torch.logical_not(torch.logical_or(min_mask, max_mask)) diff --git a/source/tests/common/dpmodel/test_linear_atomic_model.py b/source/tests/common/dpmodel/test_linear_atomic_model.py index aa56feb3e5..cc08a3b3dd 100644 --- a/source/tests/common/dpmodel/test_linear_atomic_model.py +++ b/source/tests/common/dpmodel/test_linear_atomic_model.py @@ -40,8 +40,8 @@ def test_pairwise(self, mock_loadtxt): nlist = np.array([[[1], [-1]]]) ds = DescrptSeA( - rcut=0.3, - rcut_smth=0.4, + rcut_smth=0.3, + rcut=0.4, sel=[3], ) ft = InvarFitting( @@ -122,8 +122,8 @@ def setUp(self, mock_loadtxt): ], dtype=int, ).reshape([1, self.nloc, sum(self.sel)]) - self.rcut = 0.4 - self.rcut_smth = 2.2 + self.rcut_smth = 0.4 + self.rcut = 2.2 file_path = "dummy_path" mock_loadtxt.return_value = np.array( diff --git a/source/tests/pt/model/test_linear_atomic_model.py b/source/tests/pt/model/test_linear_atomic_model.py index fab6481a6f..e0904097e3 100644 --- a/source/tests/pt/model/test_linear_atomic_model.py +++ b/source/tests/pt/model/test_linear_atomic_model.py @@ -56,8 +56,8 @@ def test_pairwise(self, mock_loadtxt): nlist = torch.tensor([[[1], [-1]]], device=env.DEVICE) ds = DescrptSeA( - rcut=0.3, - rcut_smth=0.4, + rcut_smth=0.3, + rcut=0.4, sel=[3], ).to(env.DEVICE) ft = InvarFitting( From 5794a871645f2dd999cf991e6e819bc27ad39ec1 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 5 Mar 2024 21:54:28 -0500 Subject: [PATCH 192/686] revert test Python to 3.11 (#3419) Python 3.12 or TF 2.16 seems to be slowing down... Signed-off-by: Jinzhe Zeng --- .github/workflows/test_python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index 514b552aec..60b5ecf0e0 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -18,7 +18,7 @@ jobs: - python: 3.8 tf: torch: - - python: "3.12" + - python: "3.11" tf: torch: From 278e6b8becbce93b275b86b2dbe26b23b621ef5f Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 5 Mar 2024 21:54:44 -0500 Subject: [PATCH 193/686] convert exclude_types to sel_type (#3418) This can give the correct result for `dp test`, ```sh cd examples/water_tensor/dipole dp --pt train input_torch.json dp --pt freeze dp test -m frozen_model.pth -s validation_data/global_system/ ``` Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/fitting/general_fitting.py | 2 +- deepmd/pt/model/task/fitting.py | 10 +++++++++- .../tests/common/dpmodel/test_fitting_invar_fitting.py | 4 ++++ source/tests/pt/model/test_ener_fitting.py | 1 + 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index c004814b60..01bf107c63 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -179,7 +179,7 @@ def get_sel_type(self) -> List[int]: to the result of the model. If returning an empty list, all atom types are selected. """ - return [] + return [ii for ii in range(self.ntypes) if ii not in self.exclude_types] def __setitem__(self, key, value): if key in ["bias_atom_e"]: diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index bd38fca14a..22fb409cad 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -388,6 +388,9 @@ def get_dim_aparam(self) -> int: """Get the number (dimension) of atomic parameters of this atomic model.""" return self.numb_aparam + # make jit happy + exclude_types: List[int] + def get_sel_type(self) -> List[int]: """Get the selected atom types of this model. @@ -395,7 +398,12 @@ def get_sel_type(self) -> List[int]: to the result of the model. If returning an empty list, all atom types are selected. """ - return [] + # make jit happy + sel_type: List[int] = [] + for ii in range(self.ntypes): + if ii not in self.exclude_types: + sel_type.append(ii) + return sel_type def __setitem__(self, key, value): if key in ["bias_atom_e"]: diff --git a/source/tests/common/dpmodel/test_fitting_invar_fitting.py b/source/tests/common/dpmodel/test_fitting_invar_fitting.py index a31439d406..87eeb9e06b 100644 --- a/source/tests/common/dpmodel/test_fitting_invar_fitting.py +++ b/source/tests/common/dpmodel/test_fitting_invar_fitting.py @@ -64,6 +64,10 @@ def test_self_consistency( ret0 = ifn0(dd[0], atype, fparam=ifp, aparam=iap) ret1 = ifn1(dd[0], atype, fparam=ifp, aparam=iap) np.testing.assert_allclose(ret0["energy"], ret1["energy"]) + sel_set = set(ifn0.get_sel_type()) + exclude_set = set(et) + self.assertEqual(sel_set | exclude_set, set(range(self.nt))) + self.assertEqual(sel_set & exclude_set, set()) def test_mask(self): nf, nloc, nnei = self.nlist.shape diff --git a/source/tests/pt/model/test_ener_fitting.py b/source/tests/pt/model/test_ener_fitting.py index a41b4d6b9f..69bd4b42a3 100644 --- a/source/tests/pt/model/test_ener_fitting.py +++ b/source/tests/pt/model/test_ener_fitting.py @@ -95,6 +95,7 @@ def test_consistency( to_numpy_array(ret0["foo"]), to_numpy_array(ret2["foo"]), ) + self.assertEqual(ft0.get_sel_type(), ft1.get_sel_type()) def test_new_old( self, From d3ca9d72bdea229f106e1aca50a115f2a0cdbcde Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 6 Mar 2024 01:07:37 -0500 Subject: [PATCH 194/686] pt: avoid D2H in se_e2_a (#3424) sec is used as a slice index, so it should not stored on the GPU, otherwise, D2H will happen to create the tensor with the shape. Before: ![image](https://github.com/deepmodeling/deepmd-kit/assets/9496702/f5a520d8-ed83-4520-aed0-d8fed547c293) After: ![image](https://github.com/deepmodeling/deepmd-kit/assets/9496702/5548632b-3099-4fe2-ab53-5c570abd714a) Signed-off-by: Jinzhe Zeng --- deepmd/pt/model/descriptor/se_a.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 3a18f150a4..c4b2c772f8 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -342,9 +342,8 @@ def __init__( self.reinit_exclude(exclude_types) self.sel = sel - self.sec = torch.tensor( - np.append([0], np.cumsum(self.sel)), dtype=int, device=env.DEVICE - ) + # should be on CPU to avoid D2H, as it is used as slice index + self.sec = [0, *np.cumsum(self.sel).tolist()] self.split_sel = self.sel self.nnei = sum(sel) self.ndescrpt = self.nnei * 4 From fa8e6453b5cc88ae575284c4c0ded06758c6de82 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Wed, 6 Mar 2024 17:23:29 +0800 Subject: [PATCH 195/686] Feat: add zbl training (#3398) Signed-off-by: Anyang Peng <137014849+anyangml@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/dpmodel/atomic_model/__init__.py | 8 +- .../atomic_model/linear_atomic_model.py | 62 ++++++++--- .../atomic_model/make_base_atomic_model.py | 11 +- .../atomic_model/pairtab_atomic_model.py | 11 +- deepmd/pt/model/atomic_model/__init__.py | 8 +- .../model/atomic_model/linear_atomic_model.py | 100 +++++++++++++++--- .../atomic_model/pairtab_atomic_model.py | 85 ++++++++++++++- deepmd/pt/model/model/dp_zbl_model.py | 4 +- deepmd/pt/model/task/ener.py | 49 ++------- deepmd/pt/train/training.py | 29 +++-- deepmd/pt/utils/stat.py | 81 ++++++++++++++ .../dpmodel/test_linear_atomic_model.py | 8 +- .../pt/model/test_linear_atomic_model.py | 32 ++++-- .../pt/model/test_pairtab_atomic_model.py | 2 +- source/tests/pt/model/water/zbl.json | 92 ++++++++++++++++ source/tests/pt/test_finetune.py | 51 +++++++-- source/tests/pt/test_training.py | 18 ++++ 17 files changed, 519 insertions(+), 132 deletions(-) create mode 100644 source/tests/pt/model/water/zbl.json diff --git a/deepmd/dpmodel/atomic_model/__init__.py b/deepmd/dpmodel/atomic_model/__init__.py index 2cd20f54c1..e51ca0a65e 100644 --- a/deepmd/dpmodel/atomic_model/__init__.py +++ b/deepmd/dpmodel/atomic_model/__init__.py @@ -22,8 +22,8 @@ DPAtomicModel, ) from .linear_atomic_model import ( - DPZBLLinearAtomicModel, - LinearAtomicModel, + DPZBLLinearEnergyAtomicModel, + LinearEnergyAtomicModel, ) from .make_base_atomic_model import ( make_base_atomic_model, @@ -37,6 +37,6 @@ "BaseAtomicModel", "DPAtomicModel", "PairTabAtomicModel", - "LinearAtomicModel", - "DPZBLLinearAtomicModel", + "LinearEnergyAtomicModel", + "DPZBLLinearEnergyAtomicModel", ] diff --git a/deepmd/dpmodel/atomic_model/linear_atomic_model.py b/deepmd/dpmodel/atomic_model/linear_atomic_model.py index b1a32cdaa5..ac2a73a381 100644 --- a/deepmd/dpmodel/atomic_model/linear_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/linear_atomic_model.py @@ -38,7 +38,7 @@ ) -class LinearAtomicModel(BaseAtomicModel): +class LinearEnergyAtomicModel(BaseAtomicModel): """Linear model make linear combinations of several existing models. Parameters @@ -59,14 +59,16 @@ def __init__( self.models = models sub_model_type_maps = [md.get_type_map() for md in models] err_msg = [] + self.mapping_list = [] common_type_map = set(type_map) + self.type_map = type_map for tpmp in sub_model_type_maps: if not common_type_map.issubset(set(tpmp)): err_msg.append( f"type_map {tpmp} is not a subset of type_map {type_map}" ) + self.mapping_list.append(self.remap_atype(tpmp, self.type_map)) assert len(err_msg) == 0, "\n".join(err_msg) - self.type_map = type_map self.mixed_types_list = [model.mixed_types() for model in self.models] super().__init__(**kwargs) @@ -163,17 +165,20 @@ def forward_atomic( self.mixed_types_list, raw_nlists, self.get_model_sels() ) ] - ener_list = [ - model.forward_atomic( - extended_coord, - extended_atype, - nl, - mapping, - fparam, - aparam, - )["energy"] - for model, nl in zip(self.models, nlists_) - ] + ener_list = [] + + for i, model in enumerate(self.models): + mapping = self.mapping_list[i] + ener_list.append( + model.forward_atomic( + extended_coord, + mapping[extended_atype], + nlists_[i], + mapping, + fparam, + aparam, + )["energy"] + ) self.weights = self._compute_weight(extended_coord, extended_atype, nlists_) self.atomic_bias = None if self.atomic_bias is not None: @@ -184,6 +189,29 @@ def forward_atomic( } # (nframes, nloc, 1) return fit_ret + @staticmethod + def remap_atype(ori_map: List[str], new_map: List[str]) -> np.ndarray: + """ + This method is used to map the atype from the common type_map to the original type_map of + indivial AtomicModels. + + Parameters + ---------- + ori_map : List[str] + The original type map of an AtomicModel. + new_map : List[str] + The common type map of the DPZBLLinearEnergyAtomicModel, created by the `get_type_map` method, + must be a subset of the ori_map. + + Returns + ------- + np.ndarray + """ + type_2_idx = {atp: idx for idx, atp in enumerate(ori_map)} + # this maps the atype in the new map to the original map + mapping = np.array([type_2_idx[new_map[idx]] for idx in range(len(new_map))]) + return mapping + def fitting_output_def(self) -> FittingOutputDef: return FittingOutputDef( [ @@ -261,7 +289,7 @@ def is_aparam_nall(self) -> bool: return False -class DPZBLLinearAtomicModel(LinearAtomicModel): +class DPZBLLinearEnergyAtomicModel(LinearEnergyAtomicModel): """Model linearly combine a list of AtomicModels. Parameters @@ -308,7 +336,7 @@ def serialize(self) -> dict: "@class": "Model", "type": "zbl", "@version": 1, - "models": LinearAtomicModel.serialize( + "models": LinearEnergyAtomicModel.serialize( [self.dp_model, self.zbl_model], self.type_map ), "sw_rmin": self.sw_rmin, @@ -319,7 +347,7 @@ def serialize(self) -> dict: return dd @classmethod - def deserialize(cls, data) -> "DPZBLLinearAtomicModel": + def deserialize(cls, data) -> "DPZBLLinearEnergyAtomicModel": data = copy.deepcopy(data) check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") @@ -328,7 +356,7 @@ def deserialize(cls, data) -> "DPZBLLinearAtomicModel": sw_rmax = data.pop("sw_rmax") smin_alpha = data.pop("smin_alpha") - ([dp_model, zbl_model], type_map) = LinearAtomicModel.deserialize( + ([dp_model, zbl_model], type_map) = LinearEnergyAtomicModel.deserialize( data.pop("models") ) diff --git a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py index 5548147d54..ce1a6708e6 100644 --- a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py @@ -54,18 +54,13 @@ def get_rcut(self) -> float: pass @abstractmethod - def get_type_map(self) -> Optional[List[str]]: + def get_type_map(self) -> List[str]: """Get the type map.""" + pass def get_ntypes(self) -> int: """Get the number of atom types.""" - tmap = self.get_type_map() - if tmap is not None: - return len(tmap) - else: - raise ValueError( - "cannot infer the number of types from a None type map" - ) + return len(self.get_type_map()) @abstractmethod def get_sel(self) -> List[int]: diff --git a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py index c858179939..46ec808ad4 100644 --- a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py @@ -66,9 +66,17 @@ def __init__( self.type_map = type_map self.tab = PairTab(self.tab_file, rcut=rcut) + self.type_map = type_map + self.ntypes = len(type_map) if self.tab_file is not None: self.tab_info, self.tab_data = self.tab.get() + nspline, ntypes_tab = self.tab_info[-2:].astype(int) + self.tab_data = self.tab_data.reshape(ntypes_tab, ntypes_tab, nspline, 4) + if self.ntypes != ntypes_tab: + raise ValueError( + "The `type_map` provided does not match the number of columns in the table." + ) else: self.tab_info, self.tab_data = None, None @@ -145,7 +153,8 @@ def deserialize(cls, data) -> "PairTabAtomicModel": tab_model = cls(None, rcut, sel, type_map, **data) tab_model.tab = tab tab_model.tab_info = tab_model.tab.tab_info - tab_model.tab_data = tab_model.tab.tab_data + nspline, ntypes = tab_model.tab_info[-2:].astype(int) + tab_model.tab_data = tab_model.tab.tab_data.reshape(ntypes, ntypes, nspline, 4) return tab_model def forward_atomic( diff --git a/deepmd/pt/model/atomic_model/__init__.py b/deepmd/pt/model/atomic_model/__init__.py index 75c1ce3c2e..a747f28556 100644 --- a/deepmd/pt/model/atomic_model/__init__.py +++ b/deepmd/pt/model/atomic_model/__init__.py @@ -21,8 +21,8 @@ DPAtomicModel, ) from .linear_atomic_model import ( - DPZBLLinearAtomicModel, - LinearAtomicModel, + DPZBLLinearEnergyAtomicModel, + LinearEnergyAtomicModel, ) from .pairtab_atomic_model import ( PairTabAtomicModel, @@ -32,6 +32,6 @@ "BaseAtomicModel", "DPAtomicModel", "PairTabAtomicModel", - "LinearAtomicModel", - "DPZBLLinearAtomicModel", + "LinearEnergyAtomicModel", + "DPZBLLinearEnergyAtomicModel", ] diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 0dd1b13723..5e1a80087e 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -25,6 +25,9 @@ get_multiple_nlist_key, nlist_distinguish_types, ) +from deepmd.utils.path import ( + DPPath, +) from deepmd.utils.version import ( check_version_compatibility, ) @@ -40,7 +43,7 @@ ) -class LinearAtomicModel(torch.nn.Module, BaseAtomicModel): +class LinearEnergyAtomicModel(torch.nn.Module, BaseAtomicModel): """Linear model make linear combinations of several existing models. Parameters @@ -62,14 +65,17 @@ def __init__( self.models = torch.nn.ModuleList(models) sub_model_type_maps = [md.get_type_map() for md in models] err_msg = [] + self.mapping_list = [] common_type_map = set(type_map) + self.type_map = type_map for tpmp in sub_model_type_maps: if not common_type_map.issubset(set(tpmp)): err_msg.append( f"type_map {tpmp} is not a subset of type_map {type_map}" ) + self.mapping_list.append(self.remap_atype(tpmp, self.type_map)) assert len(err_msg) == 0, "\n".join(err_msg) - self.type_map = type_map + self.atomic_bias = None self.mixed_types_list = [model.mixed_types() for model in self.models] BaseAtomicModel.__init__(self, **kwargs) @@ -117,8 +123,8 @@ def _sort_rcuts_sels(self, device: torch.device) -> Tuple[List[float], List[int] nsels = torch.tensor(self.get_model_nsels(), device=device) zipped = torch.stack( [ - torch.tensor(rcuts, device=device), - torch.tensor(nsels, device=device), + rcuts, + nsels, ], dim=0, ).T @@ -185,10 +191,11 @@ def forward_atomic( ener_list = [] for i, model in enumerate(self.models): + mapping = self.mapping_list[i] ener_list.append( model.forward_atomic( extended_coord, - extended_atype, + mapping[extended_atype], nlists_[i], mapping, fparam, @@ -198,16 +205,48 @@ def forward_atomic( weights = self._compute_weight(extended_coord, extended_atype, nlists_) - if self.atomic_bias is not None: - raise NotImplementedError("Need to add bias in a future PR.") - else: - fit_ret = { - "energy": torch.sum( - torch.stack(ener_list) * torch.stack(weights), dim=0 - ), - } # (nframes, nloc, 1) + atype = extended_atype[:, :nloc] + for idx, model in enumerate(self.models): + # TODO: provide interfaces for atomic models to access bias_atom_e + if isinstance(model, DPAtomicModel): + bias_atom_e = model.fitting_net.bias_atom_e + elif isinstance(model, PairTabAtomicModel): + bias_atom_e = model.bias_atom_e + else: + bias_atom_e = None + if bias_atom_e is not None: + ener_list[idx] += bias_atom_e[atype] + + fit_ret = { + "energy": torch.sum(torch.stack(ener_list) * torch.stack(weights), dim=0), + } # (nframes, nloc, 1) return fit_ret + @staticmethod + def remap_atype(ori_map: List[str], new_map: List[str]) -> torch.Tensor: + """ + This method is used to map the atype from the common type_map to the original type_map of + indivial AtomicModels. It creates a index mapping for the conversion. + + Parameters + ---------- + ori_map : List[str] + The original type map of an AtomicModel. + new_map : List[str] + The common type map of the DPZBLLinearEnergyAtomicModel, created by the `get_type_map` method, + must be a subset of the ori_map. + + Returns + ------- + torch.Tensor + """ + type_2_idx = {atp: idx for idx, atp in enumerate(ori_map)} + # this maps the atype in the new map to the original map + mapping = torch.tensor( + [type_2_idx[new_map[idx]] for idx in range(len(new_map))], device=env.DEVICE + ) + return mapping + def fitting_output_def(self) -> FittingOutputDef: return FittingOutputDef( [ @@ -292,7 +331,7 @@ def is_aparam_nall(self) -> bool: return False -class DPZBLLinearAtomicModel(LinearAtomicModel): +class DPZBLLinearEnergyAtomicModel(LinearEnergyAtomicModel): """Model linearly combine a list of AtomicModels. Parameters @@ -336,6 +375,33 @@ def __init__( # this is a placeholder being updated in _compute_weight, to handle Jit attribute init error. self.zbl_weight = torch.empty(0, dtype=torch.float64, device=env.DEVICE) + def compute_or_load_stat( + self, + sampled_func, + stat_file_path: Optional[DPPath] = None, + ): + """ + Compute or load the statistics parameters of the model, + such as mean and standard deviation of descriptors or the energy bias of the fitting net. + When `sampled` is provided, all the statistics parameters will be calculated (or re-calculated for update), + and saved in the `stat_file_path`(s). + When `sampled` is not provided, it will check the existence of `stat_file_path`(s) + and load the calculated statistics parameters. + + Parameters + ---------- + sampled_func + The lazy sampled function to get data frames from different data systems. + stat_file_path + The dictionary of paths to the statistics files. + """ + self.dp_model.compute_or_load_stat(sampled_func, stat_file_path) + self.zbl_model.compute_or_load_stat(sampled_func, stat_file_path) + + def change_energy_bias(self): + # need to implement + pass + def serialize(self) -> dict: dd = BaseAtomicModel.serialize(self) dd.update( @@ -343,7 +409,7 @@ def serialize(self) -> dict: "@class": "Model", "@version": 1, "type": "zbl", - "models": LinearAtomicModel.serialize( + "models": LinearEnergyAtomicModel.serialize( [self.dp_model, self.zbl_model], self.type_map ), "sw_rmin": self.sw_rmin, @@ -354,14 +420,14 @@ def serialize(self) -> dict: return dd @classmethod - def deserialize(cls, data) -> "DPZBLLinearAtomicModel": + def deserialize(cls, data) -> "DPZBLLinearEnergyAtomicModel": data = copy.deepcopy(data) check_version_compatibility(data.pop("@version", 1), 1, 1) sw_rmin = data.pop("sw_rmin") sw_rmax = data.pop("sw_rmax") smin_alpha = data.pop("smin_alpha") - [dp_model, zbl_model], type_map = LinearAtomicModel.deserialize( + [dp_model, zbl_model], type_map = LinearEnergyAtomicModel.deserialize( data.pop("models") ) diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index bae4ea55e2..215bb25de5 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import copy from typing import ( + Callable, Dict, List, Optional, @@ -13,9 +14,18 @@ FittingOutputDef, OutputVariableDef, ) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.stat import ( + compute_output_stats, +) from deepmd.utils.pair_tab import ( PairTab, ) +from deepmd.utils.path import ( + DPPath, +) from deepmd.utils.version import ( check_version_compatibility, ) @@ -47,9 +57,14 @@ class PairTabAtomicModel(torch.nn.Module, BaseAtomicModel): The cutoff radius. sel : int or list[int] The maxmum number of atoms in the cut-off radius. - type_map : list[str] + type_map : List[str] Mapping atom type to the name (str) of the type. For example `type_map[1]` gives the name of the type 1. + rcond : float, optional + The condition number for the regression of atomic energy. + atom_ener + Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. + """ def __init__( @@ -58,6 +73,8 @@ def __init__( rcut: float, sel: Union[int, List[int]], type_map: List[str], + rcond: Optional[float] = None, + atom_ener: Optional[List[float]] = None, **kwargs, ): torch.nn.Module.__init__(self) @@ -65,8 +82,12 @@ def __init__( self.tab_file = tab_file self.rcut = rcut self.tab = self._set_pairtab(tab_file, rcut) - self.type_map = type_map + BaseAtomicModel.__init__(self, **kwargs) + self.rcond = rcond + self.atom_ener = atom_ener + self.type_map = type_map + self.ntypes = len(type_map) # handle deserialization with no input file if self.tab_file is not None: @@ -74,11 +95,22 @@ def __init__( tab_info, tab_data, ) = self.tab.get() # this returns -> Tuple[np.array, np.array] + nspline, ntypes_tab = tab_info[-2:].astype(int) self.register_buffer("tab_info", torch.from_numpy(tab_info)) - self.register_buffer("tab_data", torch.from_numpy(tab_data)) + self.register_buffer( + "tab_data", + torch.from_numpy(tab_data).reshape(ntypes_tab, ntypes_tab, nspline, 4), + ) + if self.ntypes != ntypes_tab: + raise ValueError( + "The `type_map` provided does not match the number of columns in the table." + ) else: self.register_buffer("tab_info", None) self.register_buffer("tab_data", None) + self.bias_atom_e = torch.zeros( + self.ntypes, 1, dtype=env.GLOBAL_PT_ENER_FLOAT_PRECISION, device=env.DEVICE + ) # self.model_type = "ener" # self.model_version = MODEL_VERSION ## this shoud be in the parent class @@ -145,6 +177,8 @@ def serialize(self) -> dict: "rcut": self.rcut, "sel": self.sel, "type_map": self.type_map, + "rcond": self.rcond, + "atom_ener": self.atom_ener, } ) return dd @@ -156,15 +190,56 @@ def deserialize(cls, data) -> "PairTabAtomicModel": rcut = data.pop("rcut") sel = data.pop("sel") type_map = data.pop("type_map") + rcond = data.pop("rcond") + atom_ener = data.pop("atom_ener") tab = PairTab.deserialize(data.pop("tab")) data.pop("@class", None) data.pop("type", None) - tab_model = cls(None, rcut, sel, type_map, **data) + tab_model = cls(None, rcut, sel, type_map, rcond, atom_ener, **data) + tab_model.tab = tab tab_model.register_buffer("tab_info", torch.from_numpy(tab_model.tab.tab_info)) - tab_model.register_buffer("tab_data", torch.from_numpy(tab_model.tab.tab_data)) + nspline, ntypes = tab_model.tab.tab_info[-2:].astype(int) + tab_model.register_buffer( + "tab_data", + torch.from_numpy(tab_model.tab.tab_data).reshape( + ntypes, ntypes, nspline, 4 + ), + ) return tab_model + def compute_or_load_stat( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + stat_file_path: Optional[DPPath] = None, + ): + """ + Compute the output statistics (e.g. energy bias) for the fitting net from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + stat_file_path : Optional[DPPath] + The path to the stat file. + + """ + bias_atom_e = compute_output_stats( + merged, stat_file_path, self.rcond, self.atom_ener + ) + self.bias_atom_e.copy_( + torch.tensor(bias_atom_e, device=env.DEVICE).view([self.ntypes, 1]) + ) + + def change_energy_bias(self) -> None: + # need to implement + pass + def forward_atomic( self, extended_coord: torch.Tensor, diff --git a/deepmd/pt/model/model/dp_zbl_model.py b/deepmd/pt/model/model/dp_zbl_model.py index dcf1c36e83..cacf59c16c 100644 --- a/deepmd/pt/model/model/dp_zbl_model.py +++ b/deepmd/pt/model/model/dp_zbl_model.py @@ -10,7 +10,7 @@ DPModel, ) from deepmd.pt.model.atomic_model import ( - DPZBLLinearAtomicModel, + DPZBLLinearEnergyAtomicModel, ) from deepmd.pt.model.model.model import ( BaseModel, @@ -20,7 +20,7 @@ make_model, ) -DPZBLModel_ = make_model(DPZBLLinearAtomicModel) +DPZBLModel_ = make_model(DPZBLLinearEnergyAtomicModel) @BaseModel.register("zbl") diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 8bf9cc1c90..a11f6410a4 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -30,11 +30,8 @@ from deepmd.pt.utils.env import ( DEFAULT_PRECISION, ) -from deepmd.pt.utils.utils import ( - to_numpy_array, -) -from deepmd.utils.out_stat import ( - compute_stats_from_redu, +from deepmd.pt.utils.stat import ( + compute_output_stats, ) from deepmd.utils.path import ( DPPath, @@ -84,8 +81,8 @@ class InvarFitting(GeneralFitting): Random seed. exclude_types: List[int] Atomic contributions of the excluded atom types are set zero. - atom_ener - Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. + atom_ener: List[float], optional + Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. """ @@ -164,41 +161,9 @@ def compute_output_stats( The path to the stat file. """ - if stat_file_path is not None: - stat_file_path = stat_file_path / "bias_atom_e" - if stat_file_path is not None and stat_file_path.is_file(): - bias_atom_e = stat_file_path.load_numpy() - else: - if callable(merged): - # only get data for once - sampled = merged() - else: - sampled = merged - energy = [item["energy"] for item in sampled] - data_mixed_type = "real_natoms_vec" in sampled[0] - if data_mixed_type: - input_natoms = [item["real_natoms_vec"] for item in sampled] - else: - input_natoms = [item["natoms"] for item in sampled] - # shape: (nframes, ndim) - merged_energy = to_numpy_array(torch.cat(energy)) - # shape: (nframes, ntypes) - merged_natoms = to_numpy_array(torch.cat(input_natoms)[:, 2:]) - if self.atom_ener is not None and len(self.atom_ener) > 0: - assigned_atom_ener = np.array( - [ee if ee is not None else np.nan for ee in self.atom_ener] - ) - else: - assigned_atom_ener = None - bias_atom_e, _ = compute_stats_from_redu( - merged_energy, - merged_natoms, - assigned_bias=assigned_atom_ener, - rcond=self.rcond, - ) - if stat_file_path is not None: - stat_file_path.save_numpy(bias_atom_e) - assert all(x is not None for x in [bias_atom_e]) + bias_atom_e = compute_output_stats( + merged, stat_file_path, self.rcond, self.atom_ener + ) self.bias_atom_e.copy_( torch.tensor(bias_atom_e, device=env.DEVICE).view( [self.ntypes, self.dim_out] diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index e5a7632ac4..2a80956b9d 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -29,7 +29,9 @@ TensorLoss, ) from deepmd.pt.model.model import ( + DPZBLModel, get_model, + get_zbl_model, ) from deepmd.pt.optimizer import ( KFOptimizerWrapper, @@ -247,7 +249,10 @@ def get_sample(): def get_single_model( _model_params, ): - model = get_model(deepcopy(_model_params)).to(DEVICE) + if "use_srtab" in _model_params: + model = get_zbl_model(deepcopy(_model_params)).to(DEVICE) + else: + model = get_model(deepcopy(_model_params)).to(DEVICE) return model def get_lr(lr_params): @@ -506,14 +511,20 @@ def get_loss(loss_params, start_lr, _ntypes, _model): model_params["type_map"], model_params["new_type_map"], ) - self.model.fitting_net.change_energy_bias( - config, - self.model, - old_type_map, - new_type_map, - ntest=ntest, - bias_shift=model_params.get("bias_shift", "delta"), - ) + if hasattr(self.model, "fitting_net"): + self.model.fitting_net.change_energy_bias( + config, + self.model, + old_type_map, + new_type_map, + ntest=ntest, + bias_shift=model_params.get("bias_shift", "delta"), + ) + elif isinstance(self.model, DPZBLModel): + # need to updated + self.model.change_energy_bias() + else: + raise NotImplementedError if init_frz_model is not None: frz_model = torch.jit.load(init_frz_model, map_location=DEVICE) self.model.load_state_dict(frz_model.state_dict()) diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index 3b246a0ec2..63abccc75d 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -1,10 +1,27 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging +from typing import ( + Callable, + List, + Optional, + Union, +) +import numpy as np import torch +from deepmd.pt.utils import ( + env, +) from deepmd.pt.utils.utils import ( dict_to_device, + to_numpy_array, +) +from deepmd.utils.out_stat import ( + compute_stats_from_redu, +) +from deepmd.utils.path import ( + DPPath, ) log = logging.getLogger(__name__) @@ -50,3 +67,67 @@ def make_stat_input(datasets, dataloaders, nbatches): dict_to_device(sys_stat) lst.append(sys_stat) return lst + + +def compute_output_stats( + merged: Union[Callable[[], List[dict]], List[dict]], + stat_file_path: Optional[DPPath] = None, + rcond: Optional[float] = None, + atom_ener: Optional[List[float]] = None, +): + """ + Compute the output statistics (e.g. energy bias) for the fitting net from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + stat_file_path : DPPath, optional + The path to the stat file. + rcond : float, optional + The condition number for the regression of atomic energy. + atom_ener : List[float], optional + Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. + + """ + if stat_file_path is not None: + stat_file_path = stat_file_path / "bias_atom_e" + if stat_file_path is not None and stat_file_path.is_file(): + bias_atom_e = stat_file_path.load_numpy() + else: + if callable(merged): + # only get data for once + sampled = merged() + else: + sampled = merged + energy = [item["energy"] for item in sampled] + data_mixed_type = "real_natoms_vec" in sampled[0] + if data_mixed_type: + input_natoms = [item["real_natoms_vec"] for item in sampled] + else: + input_natoms = [item["natoms"] for item in sampled] + # shape: (nframes, ndim) + merged_energy = to_numpy_array(torch.cat(energy)) + # shape: (nframes, ntypes) + merged_natoms = to_numpy_array(torch.cat(input_natoms)[:, 2:]) + if atom_ener is not None and len(atom_ener) > 0: + assigned_atom_ener = np.array( + [ee if ee is not None else np.nan for ee in atom_ener] + ) + else: + assigned_atom_ener = None + bias_atom_e, _ = compute_stats_from_redu( + merged_energy, + merged_natoms, + assigned_bias=assigned_atom_ener, + rcond=rcond, + ) + if stat_file_path is not None: + stat_file_path.save_numpy(bias_atom_e) + assert all(x is not None for x in [bias_atom_e]) + return torch.tensor(bias_atom_e, device=env.DEVICE) diff --git a/source/tests/common/dpmodel/test_linear_atomic_model.py b/source/tests/common/dpmodel/test_linear_atomic_model.py index cc08a3b3dd..832d1de106 100644 --- a/source/tests/common/dpmodel/test_linear_atomic_model.py +++ b/source/tests/common/dpmodel/test_linear_atomic_model.py @@ -10,7 +10,7 @@ DPAtomicModel, ) from deepmd.dpmodel.atomic_model.linear_atomic_model import ( - DPZBLLinearAtomicModel, + DPZBLLinearEnergyAtomicModel, ) from deepmd.dpmodel.atomic_model.pairtab_atomic_model import ( PairTabAtomicModel, @@ -58,7 +58,7 @@ def test_pairwise(self, mock_loadtxt): ) dp_model = DPAtomicModel(ds, ft, type_map=type_map) - wgt_model = DPZBLLinearAtomicModel( + wgt_model = DPZBLLinearEnergyAtomicModel( dp_model, zbl_model, sw_rmin=0.1, @@ -151,14 +151,14 @@ def setUp(self, mock_loadtxt): zbl_model = PairTabAtomicModel( file_path, self.rcut, sum(self.sel), type_map=type_map ) - self.md0 = DPZBLLinearAtomicModel( + self.md0 = DPZBLLinearEnergyAtomicModel( dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25, type_map=type_map, ) - self.md1 = DPZBLLinearAtomicModel.deserialize(self.md0.serialize()) + self.md1 = DPZBLLinearEnergyAtomicModel.deserialize(self.md0.serialize()) def test_self_consistency(self): ret0 = self.md0.forward_atomic(self.coord_ext, self.atype_ext, self.nlist) diff --git a/source/tests/pt/model/test_linear_atomic_model.py b/source/tests/pt/model/test_linear_atomic_model.py index e0904097e3..adc682a41f 100644 --- a/source/tests/pt/model/test_linear_atomic_model.py +++ b/source/tests/pt/model/test_linear_atomic_model.py @@ -8,11 +8,11 @@ import torch from deepmd.dpmodel.atomic_model import ( - DPZBLLinearAtomicModel as DPDPZBLLinearAtomicModel, + DPZBLLinearEnergyAtomicModel as DPDPZBLLinearEnergyAtomicModel, ) from deepmd.pt.model.atomic_model import ( DPAtomicModel, - DPZBLLinearAtomicModel, + DPZBLLinearEnergyAtomicModel, PairTabAtomicModel, ) from deepmd.pt.model.descriptor.se_a import ( @@ -70,10 +70,10 @@ def test_pairwise(self, mock_loadtxt): type_map = ["foo", "bar"] zbl_model = PairTabAtomicModel( - tab_file=file_path, rcut=0.3, sel=2, type_map=type_map + tab_file=file_path, rcut=0.3, sel=2, type_map=type_map[::-1] ) dp_model = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) - wgt_model = DPZBLLinearAtomicModel( + wgt_model = DPZBLLinearEnergyAtomicModel( dp_model, zbl_model, sw_rmin=0.1, @@ -145,17 +145,17 @@ def setUp(self, mock_loadtxt): zbl_model = PairTabAtomicModel( file_path, self.rcut, sum(self.sel), type_map=type_map ) - self.md0 = DPZBLLinearAtomicModel( + self.md0 = DPZBLLinearEnergyAtomicModel( dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25, type_map=type_map, ).to(env.DEVICE) - self.md1 = DPZBLLinearAtomicModel.deserialize(self.md0.serialize()).to( + self.md1 = DPZBLLinearEnergyAtomicModel.deserialize(self.md0.serialize()).to( env.DEVICE ) - self.md2 = DPDPZBLLinearAtomicModel.deserialize(self.md0.serialize()) + self.md2 = DPDPZBLLinearEnergyAtomicModel.deserialize(self.md0.serialize()) self.md3 = DPZBLModel( dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25, type_map=type_map ) @@ -185,5 +185,23 @@ def test_jit(self): self.assertEqual(md3.get_type_map(), ["foo", "bar"]) +class TestRemmapMethod(unittest.TestCase): + def test_valid(self): + atype = torch.randint(0, 3, (4, 20), device=env.DEVICE) + commonl = ["H", "O", "S"] + originl = ["Si", "H", "O", "S"] + mapping = DPZBLLinearEnergyAtomicModel.remap_atype(originl, commonl) + new_atype = mapping[atype] + + def trans(atype, map): + idx = atype.flatten().tolist() + res = [] + for i in idx: + res.append(map[i]) + return res + + assert trans(atype, commonl) == trans(new_atype, originl) + + if __name__ == "__main__": unittest.main(warnings="ignore") diff --git a/source/tests/pt/model/test_pairtab_atomic_model.py b/source/tests/pt/model/test_pairtab_atomic_model.py index 0576f89910..322de51a2c 100644 --- a/source/tests/pt/model/test_pairtab_atomic_model.py +++ b/source/tests/pt/model/test_pairtab_atomic_model.py @@ -229,7 +229,7 @@ def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: ) model = PairTabAtomicModel( - tab_file=file_path, rcut=rcut, sel=2, type_map=["S"] + tab_file=file_path, rcut=rcut, sel=2, type_map=["H"] ) results.append( model.forward_atomic(extended_coord, extended_atype, nlist)["energy"] diff --git a/source/tests/pt/model/water/zbl.json b/source/tests/pt/model/water/zbl.json new file mode 100644 index 0000000000..cb5602d92d --- /dev/null +++ b/source/tests/pt/model/water/zbl.json @@ -0,0 +1,92 @@ +{ + "_comment1": " model parameters", + "model": { + "use_srtab": "H2O_tab_potential.txt", + "smin_alpha": 0.1, + "sw_rmin": 0.8, + "sw_rmax": 1.0, + "type_map": [ + "O", + "H" + ], + "descriptor": { + "type": "se_e2_a", + "sel": [ + 46, + 92 + ], + "rcut_smth": 0.50, + "rcut": 6.00, + "neuron": [ + 25, + 50, + 100 + ], + "resnet_dt": false, + "axis_neuron": 16, + "type_one_side": true, + "precision": "float64", + "seed": 1, + "_comment2": " that's all" + }, + "fitting_net": { + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "precision": "float64", + "seed": 1, + "_comment3": " that's all" + }, + "_comment4": " that's all" + }, + + "learning_rate": { + "type": "exp", + "decay_steps": 5000, + "start_lr": 0.001, + "stop_lr": 3.51e-8, + "_comment5": "that's all" + }, + + "loss": { + "type": "ener", + "start_pref_e": 0.02, + "limit_pref_e": 1, + "start_pref_f": 1000, + "limit_pref_f": 1, + "start_pref_v": 0, + "limit_pref_v": 0, + "_comment6": " that's all" + }, + + "training": { + "training_data": { + "systems": [ + "../data/data_0/", + "../data/data_1/", + "../data/data_2/" + ], + "batch_size": "auto", + "_comment7": "that's all" + }, + "validation_data": { + "systems": [ + "../data/data_3" + ], + "batch_size": 1, + "numb_btch": 3, + "_comment8": "that's all" + }, + "numb_steps": 1000000, + "seed": 10, + "disp_file": "lcurve.out", + "disp_freq": 100, + "save_freq": 1000, + "_comment9": "that's all" + }, + + "_comment10": "that's all" +} diff --git a/source/tests/pt/test_finetune.py b/source/tests/pt/test_finetune.py index 226fed3c65..d21a44acc7 100644 --- a/source/tests/pt/test_finetune.py +++ b/source/tests/pt/test_finetune.py @@ -17,7 +17,10 @@ DeepEval, ) from deepmd.pt.model.model import ( + DPZBLModel, + EnergyModel, get_model, + get_zbl_model, ) from deepmd.utils.data_system import ( DeepmdDataSystem, @@ -27,23 +30,48 @@ ) from .model.test_permutation import ( - model_dpa1, model_dpa2, model_se_e2_a, + model_zbl, ) class FinetuneTest: def test_finetune_change_energy_bias(self): # get model - model = get_model(self.model_config) - model.fitting_net.bias_atom_e = torch.rand_like(model.fitting_net.bias_atom_e) - energy_bias_before = deepcopy( - model.fitting_net.bias_atom_e.detach().cpu().numpy().reshape(-1) - ) - bias_atom_e_input = deepcopy( - model.fitting_net.bias_atom_e.detach().cpu().numpy().reshape(-1) - ) + if "use_srtab" in self.model_config: + model = get_zbl_model(self.model_config) + else: + model = get_model(self.model_config) + if isinstance(model, EnergyModel): + model.fitting_net.bias_atom_e = torch.rand_like( + model.fitting_net.bias_atom_e + ) + energy_bias_before = deepcopy( + model.fitting_net.bias_atom_e.detach().cpu().numpy().reshape(-1) + ) + bias_atom_e_input = deepcopy( + model.fitting_net.bias_atom_e.detach().cpu().numpy().reshape(-1) + ) + elif isinstance(model, DPZBLModel): + model.dp_model.fitting_net.bias_atom_e = torch.rand_like( + model.dp_model.fitting_net.bias_atom_e + ) + energy_bias_before = deepcopy( + model.dp_model.fitting_net.bias_atom_e.detach() + .cpu() + .numpy() + .reshape(-1) + ) + bias_atom_e_input = deepcopy( + model.dp_model.fitting_net.bias_atom_e.detach() + .cpu() + .numpy() + .reshape(-1) + ) + else: + bias_atom_e_input = None + model = torch.jit.script(model) tmp_model = tempfile.NamedTemporaryFile(delete=False, suffix=".pth") torch.jit.save(model, tmp_model.name) @@ -109,7 +137,8 @@ def tearDown(self) -> None: FinetuneTest.tearDown(self) -class TestEnergyModelDPA1(unittest.TestCase, FinetuneTest): +@unittest.skip("change bias not implemented yet.") +class TestEnergyZBLModelSeA(unittest.TestCase, FinetuneTest): def setUp(self): self.data_file = [str(Path(__file__).parent / "water/data/data_0")] self.data = DeepmdDataSystem( @@ -118,7 +147,7 @@ def setUp(self): test_size=1, ) self.data.add("energy", ndof=1, atomic=False, must=True, high_prec=True) - self.model_config = model_dpa1 + self.model_config = model_zbl def tearDown(self) -> None: FinetuneTest.tearDown(self) diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index bcb8c6c188..db69a1bcea 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -21,6 +21,7 @@ model_dpa2, model_hybrid, model_se_e2_a, + model_zbl, ) @@ -66,6 +67,7 @@ def test_trainable(self): torch.testing.assert_close( model_dict_before_training[key], model_dict_after_training[key] ) + self.tearDown() def tearDown(self): @@ -94,6 +96,22 @@ def tearDown(self) -> None: DPTrainTest.tearDown(self) +class TestEnergyZBLModelSeA(unittest.TestCase, DPTrainTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water/zbl.json") + with open(input_json) as f: + self.config = json.load(f) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_zbl) + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + + def tearDown(self) -> None: + DPTrainTest.tearDown(self) + + class TestFparam(unittest.TestCase, DPTrainTest): """Test if `fparam` can be loaded correctly.""" From 2d48d1f2d729e2c863bc37f9b57bbb7be822f5e1 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 7 Mar 2024 06:12:04 -0500 Subject: [PATCH 196/686] pt: improve nlist performance (#3425) 1. use inv_ex instead of inv. `inv_ex` does not check errors. We can assume the input is correct. 2. pass CPU box for `torch.arange`; 3. avoid torch.tensor. --------- Signed-off-by: Jinzhe Zeng --- deepmd/pt/train/training.py | 2 +- deepmd/pt/utils/nlist.py | 33 ++++++++++++++++++--------------- deepmd/pt/utils/region.py | 2 +- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 2a80956b9d..f066c4fe37 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -961,7 +961,7 @@ def get_data(self, is_train=True, task_key="Default"): batch_data = next(iter(self.validation_data[task_key])) for key in batch_data.keys(): - if key == "sid" or key == "fid": + if key == "sid" or key == "fid" or key == "box": continue elif not isinstance(batch_data[key], list): if batch_data[key] is not None: diff --git a/deepmd/pt/utils/nlist.py b/deepmd/pt/utils/nlist.py index cfc75d9438..56a062f1b8 100644 --- a/deepmd/pt/utils/nlist.py +++ b/deepmd/pt/utils/nlist.py @@ -27,14 +27,16 @@ def extend_input_and_build_neighbor_list( ): nframes, nloc = atype.shape[:2] if box is not None: + box_gpu = box.to(coord.device, non_blocking=True) coord_normalized = normalize_coord( coord.view(nframes, nloc, 3), - box.reshape(nframes, 3, 3), + box_gpu.reshape(nframes, 3, 3), ) else: + box_gpu = None coord_normalized = coord.clone() extended_coord, extended_atype, mapping = extend_coord_with_ghosts( - coord_normalized, atype, box, rcut + coord_normalized, atype, box_gpu, rcut, box ) nlist = build_neighbor_list( extended_coord, @@ -262,6 +264,7 @@ def extend_coord_with_ghosts( atype: torch.Tensor, cell: Optional[torch.Tensor], rcut: float, + cell_cpu: Optional[torch.Tensor] = None, ): """Extend the coordinates of the atoms by appending peridoc images. The number of images is large enough to ensure all the neighbors @@ -277,6 +280,8 @@ def extend_coord_with_ghosts( simulation cell tensor of shape [-1, 9]. rcut : float the cutoff radius + cell_cpu : torch.Tensor + cell on cpu for performance Returns ------- @@ -299,27 +304,25 @@ def extend_coord_with_ghosts( else: coord = coord.view([nf, nloc, 3]) cell = cell.view([nf, 3, 3]) + cell_cpu = cell_cpu.view([nf, 3, 3]) if cell_cpu is not None else cell # nf x 3 - to_face = to_face_distance(cell) + to_face = to_face_distance(cell_cpu) # nf x 3 # *2: ghost copies on + and - directions # +1: central cell nbuff = torch.ceil(rcut / to_face).to(torch.long) # 3 nbuff = torch.max(nbuff, dim=0, keepdim=False).values - xi = torch.arange(-nbuff[0], nbuff[0] + 1, 1, device=device) - yi = torch.arange(-nbuff[1], nbuff[1] + 1, 1, device=device) - zi = torch.arange(-nbuff[2], nbuff[2] + 1, 1, device=device) - xyz = xi.view(-1, 1, 1, 1) * torch.tensor( - [1, 0, 0], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=device - ) - xyz = xyz + yi.view(1, -1, 1, 1) * torch.tensor( - [0, 1, 0], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=device - ) - xyz = xyz + zi.view(1, 1, -1, 1) * torch.tensor( - [0, 0, 1], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=device - ) + nbuff_cpu = nbuff.cpu() + xi = torch.arange(-nbuff_cpu[0], nbuff_cpu[0] + 1, 1, device="cpu") + yi = torch.arange(-nbuff_cpu[1], nbuff_cpu[1] + 1, 1, device="cpu") + zi = torch.arange(-nbuff_cpu[2], nbuff_cpu[2] + 1, 1, device="cpu") + eye_3 = torch.eye(3, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device="cpu") + xyz = xi.view(-1, 1, 1, 1) * eye_3[0] + xyz = xyz + yi.view(1, -1, 1, 1) * eye_3[1] + xyz = xyz + zi.view(1, 1, -1, 1) * eye_3[2] xyz = xyz.view(-1, 3) + xyz = xyz.to(device=device, non_blocking=True) # ns x 3 shift_idx = xyz[torch.argsort(torch.norm(xyz, dim=1))] ns, _ = shift_idx.shape diff --git a/deepmd/pt/utils/region.py b/deepmd/pt/utils/region.py index b07d2f73bf..9d811acb9b 100644 --- a/deepmd/pt/utils/region.py +++ b/deepmd/pt/utils/region.py @@ -21,7 +21,7 @@ def phys2inter( the internal coordinates """ - rec_cell = torch.linalg.inv(cell) + rec_cell, _ = torch.linalg.inv_ex(cell) return torch.matmul(coord, rec_cell) From 09bd522fbec7a42f26886d0daed1853203372a35 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Fri, 8 Mar 2024 01:23:51 +0800 Subject: [PATCH 197/686] Support DPSpin for AtomicModel (#3301) This PR support DPSpin for AtomicModel: - [x] `Spin` base class to handle spin-related information. - [x] `SpinModel` implementation in pt, which can generally take any backbone model, with input and output process for output like `energy`, `dipole` and etc. - [x] `SpinEnergyModel` implementation inherited from `SpinModel` for `energy` calclulation. - [x] Input improvement, now DPSpin model can take real `spin` as input with `vitual_scale`, which can support much more flexible input. (Also with dataset support for `spin` input.) - [x] `EnergySpinLoss` class to process `energy`, `force_real`, `force_mag`, `virial`. - [x] Add `protection` for environment calculations. TODO: - [x] Support `forward_lower` for `SpinModel`. - [x] Add `data_stat` with general `exclude_types` for descriptors and fittings. - [x] Add examples and UTs. - [x] numpy model - [x] Support multi-task training for `SpinModel` models. Will fix in next PRs: - tf consistency test (?) --------- Signed-off-by: Duo <50307526+iProzd@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/backend/pytorch.py | 4 +- deepmd/dpmodel/descriptor/se_e2_a.py | 8 +- deepmd/dpmodel/descriptor/se_r.py | 6 +- deepmd/dpmodel/fitting/ener_fitting.py | 1 + deepmd/dpmodel/model/__init__.py | 4 + deepmd/dpmodel/model/model.py | 58 +- deepmd/dpmodel/model/spin_model.py | 394 ++++++++++++ deepmd/dpmodel/output_def.py | 72 ++- deepmd/dpmodel/utils/env_mat.py | 14 +- deepmd/dpmodel/utils/exclude_mask.py | 9 + deepmd/dpmodel/utils/nlist.py | 2 + deepmd/entrypoints/test.py | 84 ++- deepmd/infer/deep_eval.py | 19 +- deepmd/infer/deep_pot.py | 28 +- deepmd/pt/infer/deep_eval.py | 174 +++++- deepmd/pt/loss/__init__.py | 4 + deepmd/pt/loss/ener_spin.py | 245 ++++++++ .../pt/model/atomic_model/dp_atomic_model.py | 19 +- .../atomic_model/pairtab_atomic_model.py | 2 +- deepmd/pt/model/descriptor/dpa1.py | 8 +- deepmd/pt/model/descriptor/dpa2.py | 10 + deepmd/pt/model/descriptor/env_mat.py | 20 +- deepmd/pt/model/descriptor/repformers.py | 17 + deepmd/pt/model/descriptor/se_a.py | 6 + deepmd/pt/model/descriptor/se_atten.py | 17 + deepmd/pt/model/descriptor/se_r.py | 14 +- deepmd/pt/model/model/__init__.py | 51 +- deepmd/pt/model/model/dp_zbl_model.py | 7 +- deepmd/pt/model/model/spin_model.py | 560 ++++++++++++++++++ deepmd/pt/model/task/ener.py | 2 +- deepmd/pt/model/task/fitting.py | 6 +- deepmd/pt/train/training.py | 28 +- deepmd/pt/train/wrapper.py | 24 +- deepmd/pt/utils/env_mat_stat.py | 19 +- deepmd/pt/utils/exclude_mask.py | 11 +- deepmd/pt/utils/multi_task.py | 6 +- deepmd/pt/utils/nlist.py | 3 +- deepmd/pt/utils/stat.py | 16 +- deepmd/tf/descriptor/se_a.py | 7 + deepmd/tf/descriptor/se_r.py | 5 + deepmd/tf/infer/deep_eval.py | 4 + deepmd/utils/argcheck.py | 36 +- deepmd/utils/data.py | 2 +- deepmd/utils/spin.py | 199 +++++++ .../spin/data_reformat/data_0/set.000/box.npy | Bin 0 -> 4448 bytes .../data_reformat/data_0/set.000/coord.npy | Bin 0 -> 46208 bytes .../data_reformat/data_0/set.000/energy.npy | Bin 0 -> 608 bytes .../data_reformat/data_0/set.000/force.npy | Bin 0 -> 46208 bytes .../data_0/set.000/force_mag.npy | Bin 0 -> 46208 bytes .../data_reformat/data_0/set.000/spin.npy | Bin 0 -> 46208 bytes examples/spin/data_reformat/data_0/type.raw | 32 + .../spin/data_reformat/data_0/type_map.raw | 2 + .../spin/data_reformat/data_1/set.000/box.npy | Bin 0 -> 4448 bytes .../data_reformat/data_1/set.000/coord.npy | Bin 0 -> 46208 bytes .../data_reformat/data_1/set.000/energy.npy | Bin 0 -> 608 bytes .../data_reformat/data_1/set.000/force.npy | Bin 0 -> 46208 bytes .../data_1/set.000/force_mag.npy | Bin 0 -> 46208 bytes .../data_reformat/data_1/set.000/spin.npy | Bin 0 -> 46208 bytes examples/spin/data_reformat/data_1/type.raw | 32 + .../spin/data_reformat/data_1/type_map.raw | 2 + .../spin/data_reformat/data_2/set.000/box.npy | Bin 0 -> 2360 bytes .../data_reformat/data_2/set.000/coord.npy | Bin 0 -> 23936 bytes .../data_reformat/data_2/set.000/energy.npy | Bin 0 -> 376 bytes .../data_reformat/data_2/set.000/force.npy | Bin 0 -> 23936 bytes .../data_2/set.000/force_mag.npy | Bin 0 -> 23936 bytes .../data_reformat/data_2/set.000/spin.npy | Bin 0 -> 23936 bytes examples/spin/data_reformat/data_2/type.raw | 32 + .../spin/data_reformat/data_2/type_map.raw | 2 + .../se_e2_a/{input.json => input_tf.json} | 0 examples/spin/se_e2_a/input_torch.json | 90 +++ .../tests/common/dpmodel/test_output_def.py | 156 ++++- source/tests/common/test_examples.py | 3 +- source/tests/common/test_spin.py | 172 ++++++ .../consistent/descriptor/test_se_e2_a.py | 19 + .../tests/pt/NiO/data/data_0/set.000/box.npy | Bin 0 -> 4448 bytes .../pt/NiO/data/data_0/set.000/coord.npy | Bin 0 -> 46208 bytes .../pt/NiO/data/data_0/set.000/energy.npy | Bin 0 -> 608 bytes .../pt/NiO/data/data_0/set.000/force.npy | Bin 0 -> 46208 bytes .../pt/NiO/data/data_0/set.000/force_mag.npy | Bin 0 -> 46208 bytes .../tests/pt/NiO/data/data_0/set.000/spin.npy | Bin 0 -> 46208 bytes source/tests/pt/NiO/data/data_0/type.raw | 32 + source/tests/pt/NiO/data/data_0/type_map.raw | 2 + .../tests/pt/NiO/data/single/set.000/box.npy | Bin 0 -> 200 bytes .../pt/NiO/data/single/set.000/coord.npy | Bin 0 -> 896 bytes .../pt/NiO/data/single/set.000/energy.npy | Bin 0 -> 136 bytes .../pt/NiO/data/single/set.000/force.npy | Bin 0 -> 896 bytes .../pt/NiO/data/single/set.000/force_mag.npy | Bin 0 -> 896 bytes .../tests/pt/NiO/data/single/set.000/spin.npy | Bin 0 -> 896 bytes source/tests/pt/NiO/data/single/type.raw | 32 + source/tests/pt/NiO/data/single/type_map.raw | 2 + source/tests/pt/model/test_autodiff.py | 86 ++- source/tests/pt/model/test_deeppot.py | 3 +- source/tests/pt/model/test_embedding_net.py | 15 +- source/tests/pt/model/test_ener_spin_model.py | 420 +++++++++++++ source/tests/pt/model/test_forward_lower.py | 177 ++++++ source/tests/pt/model/test_null_input.py | 22 +- source/tests/pt/model/test_permutation.py | 94 ++- source/tests/pt/model/test_rot.py | 136 +++-- source/tests/pt/model/test_smooth.py | 106 ++-- source/tests/pt/model/test_trans.py | 62 +- source/tests/pt/model/test_unused_params.py | 11 +- source/tests/pt/test_dp_test.py | 136 ++++- source/tests/pt/test_init_frz_model.py | 6 +- source/tests/pt/test_loss.py | 215 ++++++- source/tests/pt/test_stat.py | 4 +- source/tests/tf/test_deeppot_a.py | 4 +- 106 files changed, 3959 insertions(+), 373 deletions(-) create mode 100644 deepmd/dpmodel/model/spin_model.py create mode 100644 deepmd/pt/loss/ener_spin.py create mode 100644 deepmd/pt/model/model/spin_model.py create mode 100644 deepmd/utils/spin.py create mode 100644 examples/spin/data_reformat/data_0/set.000/box.npy create mode 100644 examples/spin/data_reformat/data_0/set.000/coord.npy create mode 100644 examples/spin/data_reformat/data_0/set.000/energy.npy create mode 100644 examples/spin/data_reformat/data_0/set.000/force.npy create mode 100644 examples/spin/data_reformat/data_0/set.000/force_mag.npy create mode 100644 examples/spin/data_reformat/data_0/set.000/spin.npy create mode 100644 examples/spin/data_reformat/data_0/type.raw create mode 100644 examples/spin/data_reformat/data_0/type_map.raw create mode 100644 examples/spin/data_reformat/data_1/set.000/box.npy create mode 100644 examples/spin/data_reformat/data_1/set.000/coord.npy create mode 100644 examples/spin/data_reformat/data_1/set.000/energy.npy create mode 100644 examples/spin/data_reformat/data_1/set.000/force.npy create mode 100644 examples/spin/data_reformat/data_1/set.000/force_mag.npy create mode 100644 examples/spin/data_reformat/data_1/set.000/spin.npy create mode 100644 examples/spin/data_reformat/data_1/type.raw create mode 100644 examples/spin/data_reformat/data_1/type_map.raw create mode 100644 examples/spin/data_reformat/data_2/set.000/box.npy create mode 100644 examples/spin/data_reformat/data_2/set.000/coord.npy create mode 100644 examples/spin/data_reformat/data_2/set.000/energy.npy create mode 100644 examples/spin/data_reformat/data_2/set.000/force.npy create mode 100644 examples/spin/data_reformat/data_2/set.000/force_mag.npy create mode 100644 examples/spin/data_reformat/data_2/set.000/spin.npy create mode 100644 examples/spin/data_reformat/data_2/type.raw create mode 100644 examples/spin/data_reformat/data_2/type_map.raw rename examples/spin/se_e2_a/{input.json => input_tf.json} (100%) create mode 100644 examples/spin/se_e2_a/input_torch.json create mode 100644 source/tests/common/test_spin.py create mode 100644 source/tests/pt/NiO/data/data_0/set.000/box.npy create mode 100644 source/tests/pt/NiO/data/data_0/set.000/coord.npy create mode 100644 source/tests/pt/NiO/data/data_0/set.000/energy.npy create mode 100644 source/tests/pt/NiO/data/data_0/set.000/force.npy create mode 100644 source/tests/pt/NiO/data/data_0/set.000/force_mag.npy create mode 100644 source/tests/pt/NiO/data/data_0/set.000/spin.npy create mode 100644 source/tests/pt/NiO/data/data_0/type.raw create mode 100644 source/tests/pt/NiO/data/data_0/type_map.raw create mode 100644 source/tests/pt/NiO/data/single/set.000/box.npy create mode 100644 source/tests/pt/NiO/data/single/set.000/coord.npy create mode 100644 source/tests/pt/NiO/data/single/set.000/energy.npy create mode 100644 source/tests/pt/NiO/data/single/set.000/force.npy create mode 100644 source/tests/pt/NiO/data/single/set.000/force_mag.npy create mode 100644 source/tests/pt/NiO/data/single/set.000/spin.npy create mode 100644 source/tests/pt/NiO/data/single/type.raw create mode 100644 source/tests/pt/NiO/data/single/type_map.raw create mode 100644 source/tests/pt/model/test_ener_spin_model.py create mode 100644 source/tests/pt/model/test_forward_lower.py diff --git a/deepmd/backend/pytorch.py b/deepmd/backend/pytorch.py index 676694172b..fb7d30e994 100644 --- a/deepmd/backend/pytorch.py +++ b/deepmd/backend/pytorch.py @@ -29,8 +29,8 @@ @Backend.register("pt") @Backend.register("pytorch") -class TensorFlowBackend(Backend): - """TensorFlow backend.""" +class PyTorchBackend(Backend): + """PyTorch backend.""" name = "PyTorch" """The formal name of the backend.""" diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index a068a2e366..531aa09f3a 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -111,6 +111,8 @@ class DescrptSeA(NativeOP, BaseDescriptor): exclude_types : List[List[int]] The excluded pairs of types which have no interaction with each other. For example, `[[0, 1]]` means no interaction between type 0 and type 1. + env_protection: float + Protection parameter to prevent division by zero errors during environment matrix calculations. set_davg_zero Set the shift of embedding net input to zero. activation_function @@ -149,6 +151,7 @@ def __init__( trainable: bool = True, type_one_side: bool = True, exclude_types: List[List[int]] = [], + env_protection: float = 0.0, set_davg_zero: bool = False, activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, @@ -169,6 +172,7 @@ def __init__( self.resnet_dt = resnet_dt self.trainable = trainable self.type_one_side = type_one_side + self.env_protection = env_protection self.set_davg_zero = set_davg_zero self.activation_function = activation_function self.precision = precision @@ -192,7 +196,7 @@ def __init__( self.resnet_dt, self.precision, ) - self.env_mat = EnvMat(self.rcut, self.rcut_smth) + self.env_mat = EnvMat(self.rcut, self.rcut_smth, protection=self.env_protection) self.nnei = np.sum(self.sel) self.davg = np.zeros( [self.ntypes, self.nnei, 4], dtype=PRECISION_DICT[self.precision] @@ -378,6 +382,7 @@ def serialize(self) -> dict: "trainable": self.trainable, "type_one_side": self.type_one_side, "exclude_types": self.exclude_types, + "env_protection": self.env_protection, "set_davg_zero": self.set_davg_zero, "activation_function": self.activation_function, # make deterministic @@ -406,7 +411,6 @@ def deserialize(cls, data: dict) -> "DescrptSeA": obj["davg"] = variables["davg"] obj["dstd"] = variables["dstd"] obj.embeddings = NetworkCollection.deserialize(embeddings) - obj.env_mat = EnvMat.deserialize(env_mat) return obj @classmethod diff --git a/deepmd/dpmodel/descriptor/se_r.py b/deepmd/dpmodel/descriptor/se_r.py index 2dbf495d14..3128a28493 100644 --- a/deepmd/dpmodel/descriptor/se_r.py +++ b/deepmd/dpmodel/descriptor/se_r.py @@ -106,6 +106,7 @@ def __init__( trainable: bool = True, type_one_side: bool = True, exclude_types: List[List[int]] = [], + env_protection: float = 0.0, set_davg_zero: bool = False, activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, @@ -133,6 +134,7 @@ def __init__( self.precision = precision self.spin = spin self.emask = PairExcludeMask(self.ntypes, self.exclude_types) + self.env_protection = env_protection in_dim = 1 # not considiering type embedding self.embeddings = NetworkCollection( @@ -150,7 +152,7 @@ def __init__( self.resnet_dt, self.precision, ) - self.env_mat = EnvMat(self.rcut, self.rcut_smth) + self.env_mat = EnvMat(self.rcut, self.rcut_smth, protection=self.env_protection) self.nnei = np.sum(self.sel) self.davg = np.zeros( [self.ntypes, self.nnei, 1], dtype=PRECISION_DICT[self.precision] @@ -305,6 +307,7 @@ def serialize(self) -> dict: "trainable": self.trainable, "type_one_side": self.type_one_side, "exclude_types": self.exclude_types, + "env_protection": self.env_protection, "set_davg_zero": self.set_davg_zero, "activation_function": self.activation_function, # make deterministic @@ -333,7 +336,6 @@ def deserialize(cls, data: dict) -> "DescrptSeR": obj["davg"] = variables["davg"] obj["dstd"] = variables["dstd"] obj.embeddings = NetworkCollection.deserialize(embeddings) - obj.env_mat = EnvMat.deserialize(env_mat) return obj @classmethod diff --git a/deepmd/dpmodel/fitting/ener_fitting.py b/deepmd/dpmodel/fitting/ener_fitting.py index de41bebf6d..3a0e9909b9 100644 --- a/deepmd/dpmodel/fitting/ener_fitting.py +++ b/deepmd/dpmodel/fitting/ener_fitting.py @@ -63,6 +63,7 @@ def __init__( use_aparam_as_mask=use_aparam_as_mask, spin=spin, mixed_types=mixed_types, + exclude_types=exclude_types, ) @classmethod diff --git a/deepmd/dpmodel/model/__init__.py b/deepmd/dpmodel/model/__init__.py index dda174fa4e..cb796e6d35 100644 --- a/deepmd/dpmodel/model/__init__.py +++ b/deepmd/dpmodel/model/__init__.py @@ -16,8 +16,12 @@ from .make_model import ( make_model, ) +from .spin_model import ( + SpinModel, +) __all__ = [ "DPModel", + "SpinModel", "make_model", ] diff --git a/deepmd/dpmodel/model/model.py b/deepmd/dpmodel/model/model.py index 6f06785c56..3fdf5b802b 100644 --- a/deepmd/dpmodel/model/model.py +++ b/deepmd/dpmodel/model/model.py @@ -8,10 +8,16 @@ from deepmd.dpmodel.model.dp_model import ( DPModel, ) +from deepmd.dpmodel.model.spin_model import ( + SpinModel, +) +from deepmd.utils.spin import ( + Spin, +) -def get_model(data: dict) -> DPModel: - """Get a DPModel from a dictionary. +def get_standard_model(data: dict) -> DPModel: + """Get a standard DPModel from a dictionary. Parameters ---------- @@ -30,6 +36,7 @@ def get_model(data: dict) -> DPModel: fitting = EnergyFittingNet( ntypes=descriptor.get_ntypes(), dim_descrpt=descriptor.get_dim_out(), + mixed_types=descriptor.mixed_types(), **data["fitting_net"], ) else: @@ -41,3 +48,50 @@ def get_model(data: dict) -> DPModel: atom_exclude_types=data.get("atom_exclude_types", []), pair_exclude_types=data.get("pair_exclude_types", []), ) + + +def get_spin_model(data: dict) -> SpinModel: + """Get a spin model from a dictionary. + + Parameters + ---------- + data : dict + The data to construct the model. + """ + # include virtual spin and placeholder types + data["type_map"] += [item + "_spin" for item in data["type_map"]] + spin = Spin( + use_spin=data["spin"]["use_spin"], + virtual_scale=data["spin"]["virtual_scale"], + ) + pair_exclude_types = spin.get_pair_exclude_types( + exclude_types=data.get("pair_exclude_types", None) + ) + data["pair_exclude_types"] = pair_exclude_types + # for descriptor data stat + data["descriptor"]["exclude_types"] = pair_exclude_types + atom_exclude_types = spin.get_atom_exclude_types( + exclude_types=data.get("atom_exclude_types", None) + ) + data["atom_exclude_types"] = atom_exclude_types + if "env_protection" not in data["descriptor"]: + data["descriptor"]["env_protection"] = 1e-6 + if data["descriptor"]["type"] in ["se_e2_a"]: + # only expand sel for se_e2_a + data["descriptor"]["sel"] += data["descriptor"]["sel"] + backbone_model = get_standard_model(data) + return SpinModel(backbone_model=backbone_model, spin=spin) + + +def get_model(data: dict): + """Get a model from a dictionary. + + Parameters + ---------- + data : dict + The data to construct the model. + """ + if "spin" in data: + return get_spin_model(data) + else: + return get_standard_model(data) diff --git a/deepmd/dpmodel/model/spin_model.py b/deepmd/dpmodel/model/spin_model.py new file mode 100644 index 0000000000..5b31b64fdf --- /dev/null +++ b/deepmd/dpmodel/model/spin_model.py @@ -0,0 +1,394 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + List, + Optional, +) + +import numpy as np + +from deepmd.dpmodel.model.dp_model import ( + DPModel, +) +from deepmd.utils.spin import ( + Spin, +) + + +class SpinModel: + """A spin model wrapper, with spin input preprocess and output split.""" + + def __init__( + self, + backbone_model, + spin: Spin, + ): + super().__init__() + self.backbone_model = backbone_model + self.spin = spin + self.ntypes_real = self.spin.ntypes_real + self.virtual_scale_mask = self.spin.get_virtual_scale_mask() + self.spin_mask = self.spin.get_spin_mask() + + def process_spin_input(self, coord, atype, spin): + """Generate virtual coordinates and types, concat into the input.""" + nframes, nloc = coord.shape[:-1] + atype_spin = np.concatenate([atype, atype + self.ntypes_real], axis=-1) + virtual_coord = coord + spin * self.virtual_scale_mask[atype].reshape( + [nframes, nloc, 1] + ) + coord_spin = np.concatenate([coord, virtual_coord], axis=-2) + return coord_spin, atype_spin + + def process_spin_input_lower( + self, + extended_coord: np.ndarray, + extended_atype: np.ndarray, + extended_spin: np.ndarray, + nlist: np.ndarray, + mapping: Optional[np.ndarray] = None, + ): + """ + Add `extended_spin` into `extended_coord` to generate virtual atoms, and extend `nlist` and `mapping`. + Note that the final `extended_coord_updated` with shape [nframes, nall + nall, 3] has the following order: + - [:, :nloc]: original nloc real atoms. + - [:, nloc: nloc + nloc]: virtual atoms corresponding to nloc real atoms. + - [:, nloc + nloc: nloc + nall]: ghost real atoms. + - [:, nloc + nall: nall + nall]: virtual atoms corresponding to ghost real atoms. + """ + nframes, nall = extended_coord.shape[:2] + nloc = nlist.shape[1] + virtual_extended_coord = ( + extended_coord + + extended_spin + * self.virtual_scale_mask[extended_atype].reshape([nframes, nall, 1]) + ) + virtual_extended_atype = extended_atype + self.ntypes_real + extended_coord_updated = self.concat_switch_virtual( + extended_coord, virtual_extended_coord, nloc + ) + extended_atype_updated = self.concat_switch_virtual( + extended_atype, virtual_extended_atype, nloc + ) + if mapping is not None: + virtual_mapping = mapping + nloc + mapping_updated = self.concat_switch_virtual(mapping, virtual_mapping, nloc) + else: + mapping_updated = None + # extend the nlist + nlist_updated = self.extend_nlist(extended_atype, nlist) + return ( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping_updated, + ) + + def process_spin_output( + self, atype, out_tensor, add_mag: bool = True, virtual_scale: bool = True + ): + """Split the output both real and virtual atoms, and scale the latter.""" + nframes, nloc_double = out_tensor.shape[:2] + nloc = nloc_double // 2 + if virtual_scale: + virtual_scale_mask = self.virtual_scale_mask + else: + virtual_scale_mask = self.spin_mask + atomic_mask = virtual_scale_mask[atype].reshape([nframes, nloc, 1]) + out_real, out_mag = np.split(out_tensor, [nloc], axis=1) + if add_mag: + out_real = out_real + out_mag + out_mag = (out_mag.reshape([nframes, nloc, -1]) * atomic_mask).reshape( + out_mag.shape + ) + return out_real, out_mag, atomic_mask > 0.0 + + def process_spin_output_lower( + self, + extended_atype, + extended_out_tensor, + nloc: int, + add_mag: bool = True, + virtual_scale: bool = True, + ): + """Split the extended output of both real and virtual atoms with switch, and scale the latter.""" + nframes, nall_double = extended_out_tensor.shape[:2] + nall = nall_double // 2 + if virtual_scale: + virtual_scale_mask = self.virtual_scale_mask + else: + virtual_scale_mask = self.spin_mask + atomic_mask = virtual_scale_mask[extended_atype].reshape([nframes, nall, 1]) + extended_out_real = np.concatenate( + [ + extended_out_tensor[:, :nloc], + extended_out_tensor[:, nloc + nloc : nloc + nall], + ], + axis=1, + ) + extended_out_mag = np.concatenate( + [ + extended_out_tensor[:, nloc : nloc + nloc], + extended_out_tensor[:, nloc + nall :], + ], + axis=1, + ) + if add_mag: + extended_out_real = extended_out_real + extended_out_mag + extended_out_mag = ( + extended_out_mag.reshape([nframes, nall, -1]) * atomic_mask + ).reshape(extended_out_mag.shape) + return extended_out_real, extended_out_mag, atomic_mask > 0.0 + + @staticmethod + def extend_nlist(extended_atype, nlist): + nframes, nloc, nnei = nlist.shape + nall = extended_atype.shape[1] + nlist_mask = nlist != -1 + nlist[nlist == -1] = 0 + nlist_shift = nlist + nall + nlist[~nlist_mask] = -1 + nlist_shift[~nlist_mask] = -1 + self_spin = np.arange(0, nloc, dtype=nlist.dtype) + nall + self_spin = self_spin.reshape(1, -1, 1).repeat(nframes, axis=0) + # self spin + real neighbor + virtual neighbor + # nf x nloc x (1 + nnei + nnei) + extended_nlist = np.concatenate([self_spin, nlist, nlist_shift], axis=-1) + # nf x (nloc + nloc) x (1 + nnei + nnei) + extended_nlist = np.concatenate( + [extended_nlist, -1 * np.ones_like(extended_nlist)], axis=-2 + ) + # update the index for switch + first_part_index = (nloc <= extended_nlist) & (extended_nlist < nall) + second_part_index = (nall <= extended_nlist) & (extended_nlist < (nall + nloc)) + extended_nlist[first_part_index] += nloc + extended_nlist[second_part_index] -= nall - nloc + return extended_nlist + + @staticmethod + def concat_switch_virtual(extended_tensor, extended_tensor_virtual, nloc: int): + nframes, nall = extended_tensor.shape[:2] + out_shape = list(extended_tensor.shape) + out_shape[1] *= 2 + extended_tensor_updated = np.zeros( + out_shape, + dtype=extended_tensor.dtype, + ) + extended_tensor_updated[:, :nloc] = extended_tensor[:, :nloc] + extended_tensor_updated[:, nloc : nloc + nloc] = extended_tensor_virtual[ + :, :nloc + ] + extended_tensor_updated[:, nloc + nloc : nloc + nall] = extended_tensor[ + :, nloc: + ] + extended_tensor_updated[:, nloc + nall :] = extended_tensor_virtual[:, nloc:] + return extended_tensor_updated.reshape(out_shape) + + def get_type_map(self) -> List[str]: + """Get the type map.""" + tmap = self.backbone_model.get_type_map() + ntypes = len(tmap) // 2 # ignore the virtual type + return tmap[:ntypes] + + def get_rcut(self): + """Get the cut-off radius.""" + return self.backbone_model.get_rcut() + + def get_dim_fparam(self): + """Get the number (dimension) of frame parameters of this atomic model.""" + return self.backbone_model.get_dim_fparam() + + def get_dim_aparam(self): + """Get the number (dimension) of atomic parameters of this atomic model.""" + return self.backbone_model.get_dim_aparam() + + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return self.backbone_model.get_sel_type() + + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + If False, the shape is (nframes, nloc, ndim). + """ + return self.backbone_model.is_aparam_nall() + + def model_output_type(self) -> List[str]: + """Get the output type for the model.""" + return self.backbone_model.model_output_type() + + def get_model_def_script(self) -> str: + """Get the model definition script.""" + return self.backbone_model.get_model_def_script() + + def get_nnei(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + # for C++ interface + if not self.backbone_model.mixed_types(): + return self.backbone_model.get_nnei() // 2 # ignore the virtual selected + else: + return self.backbone_model.get_nnei() + + def get_nsel(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + if not self.backbone_model.mixed_types(): + return self.backbone_model.get_nsel() // 2 # ignore the virtual selected + else: + return self.backbone_model.get_nsel() + + @staticmethod + def has_spin() -> bool: + """Returns whether it has spin input and output.""" + return True + + def __getattr__(self, name): + """Get attribute from the wrapped model.""" + if name in self.__dict__: + return self.__dict__[name] + else: + return getattr(self.backbone_model, name) + + def serialize(self) -> dict: + return { + "backbone_model": self.backbone_model.serialize(), + "spin": self.spin.serialize(), + } + + @classmethod + def deserialize(cls, data) -> "SpinModel": + backbone_model_obj = DPModel.deserialize(data["backbone_model"]) + spin = Spin.deserialize(data["spin"]) + return cls( + backbone_model=backbone_model_obj, + spin=spin, + ) + + def call( + self, + coord, + atype, + spin, + box: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, np.ndarray]: + """Return model prediction. + + Parameters + ---------- + coord + The coordinates of the atoms. + shape: nf x (nloc x 3) + atype + The type of atoms. shape: nf x nloc + spin + The spins of the atoms. + shape: nf x (nloc x 3) + box + The simulation box. shape: nf x 9 + fparam + frame parameter. nf x ndf + aparam + atomic parameter. nf x nloc x nda + do_atomic_virial + If calculate the atomic virial. + + Returns + ------- + ret_dict + The result dict of type Dict[str,np.ndarray]. + The keys are defined by the `ModelOutputDef`. + + """ + nframes, nloc = coord.shape[:2] + coord_updated, atype_updated = self.process_spin_input(coord, atype, spin) + model_predict = self.backbone_model.call( + coord_updated, + atype_updated, + box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + model_output_type = self.backbone_model.model_output_type() + if "mask" in model_output_type: + model_output_type.pop(model_output_type.index("mask")) + var_name = model_output_type[0] + model_predict[f"{var_name}"] = np.split( + model_predict[f"{var_name}"], [nloc], axis=1 + )[0] + # for now omit the grad output + return model_predict + + def call_lower( + self, + extended_coord: np.ndarray, + extended_atype: np.ndarray, + extended_spin: np.ndarray, + nlist: np.ndarray, + mapping: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + do_atomic_virial: bool = False, + ): + """Return model prediction. Lower interface that takes + extended atomic coordinates, types and spins, nlist, and mapping + as input, and returns the predictions on the extended region. + The predictions are not reduced. + + Parameters + ---------- + extended_coord + coordinates in extended region. nf x (nall x 3). + extended_atype + atomic type in extended region. nf x nall. + extended_spin + spins in extended region. nf x (nall x 3). + nlist + neighbor list. nf x nloc x nsel. + mapping + maps the extended indices to local indices. nf x nall. + fparam + frame parameter. nf x ndf + aparam + atomic parameter. nf x nloc x nda + do_atomic_virial + whether calculate atomic virial + + Returns + ------- + result_dict + the result dict, defined by the `FittingOutputDef`. + + """ + nframes, nloc = nlist.shape[:2] + ( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping_updated, + ) = self.process_spin_input_lower( + extended_coord, extended_atype, extended_spin, nlist, mapping=mapping + ) + model_predict = self.backbone_model.call_lower( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping=mapping_updated, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + model_output_type = self.backbone_model.model_output_type() + if "mask" in model_output_type: + model_output_type.pop(model_output_type.index("mask")) + var_name = model_output_type[0] + model_predict[f"{var_name}"] = np.split( + model_predict[f"{var_name}"], [nloc], axis=1 + )[0] + # for now omit the grad output + return model_predict diff --git a/deepmd/dpmodel/output_def.py b/deepmd/dpmodel/output_def.py index ac41513246..cbebb4908a 100644 --- a/deepmd/dpmodel/output_def.py +++ b/deepmd/dpmodel/output_def.py @@ -125,6 +125,8 @@ class OutputVariableOperation(IntEnum): """Derivative w.r.t. cell.""" _SEC_DERV_R = 8 """Second derivative w.r.t. coordinates.""" + MAG = 16 + """Magnetic output.""" class OutputVariableCategory(IntEnum): @@ -142,6 +144,10 @@ class OutputVariableCategory(IntEnum): """Virial, the transposed negative gradient with cell tensor times cell tensor, see eq 40 JCP 159, 054801 (2023). """ DERV_R_DERV_R = OutputVariableOperation.DERV_R | OutputVariableOperation._SEC_DERV_R """Hession matrix, the second derivative w.r.t. coordinates.""" + DERV_R_MAG = OutputVariableOperation.DERV_R | OutputVariableOperation.MAG + """Magnetic part of negative derivative w.r.t. coordinates. (e.g. magnetic force)""" + DERV_C_MAG = OutputVariableOperation.DERV_C | OutputVariableOperation.MAG + """Magnetic part of atomic component of the virial.""" class OutputVariableDef: @@ -176,8 +182,10 @@ class OutputVariableDef: If the variable is defined for each atom. category : int The category of the output variable. - hessian : bool + r_hessian : bool If hessian is requred + magnetic : bool + If the derivatives of variable have magnetic parts. """ def __init__( @@ -190,6 +198,7 @@ def __init__( atomic: bool = True, category: int = OutputVariableCategory.OUT.value, r_hessian: bool = False, + magnetic: bool = False, ): self.name = name self.shape = list(shape) @@ -208,6 +217,7 @@ def __init__( raise ValueError("a reduciable variable should be atomic") self.category = category self.r_hessian = r_hessian + self.magnetic = magnetic if self.r_hessian: if not self.reduciable: raise ValueError("only reduciable variable can calculate hessian") @@ -271,6 +281,7 @@ def __init__( self.def_derv_r, self.def_derv_c = do_derivative(self.def_outp.get_data()) self.def_hess_r, _ = do_derivative(self.def_derv_r) self.def_derv_c_redu = do_reduce(self.def_derv_c) + self.def_mask = do_mask(self.def_outp.get_data()) self.var_defs: Dict[str, OutputVariableDef] = {} for ii in [ self.def_outp.get_data(), @@ -279,6 +290,7 @@ def __init__( self.def_derv_r, self.def_derv_c_redu, self.def_hess_r, + self.def_mask, ]: self.var_defs.update(ii) @@ -324,12 +336,16 @@ def get_deriv_name(name: str) -> Tuple[str, str]: return name + "_derv_r", name + "_derv_c" +def get_deriv_name_mag(name: str) -> Tuple[str, str]: + return name + "_derv_r_mag", name + "_derv_c_mag" + + def get_hessian_name(name: str) -> str: return name + "_derv_r_derv_r" def apply_operation(var_def: OutputVariableDef, op: OutputVariableOperation) -> int: - """Apply a operation to the category of a variable definition. + """Apply an operation to the category of a variable definition. Parameters ---------- @@ -401,6 +417,31 @@ def do_reduce( return def_redu +def do_mask( + def_outp_data: Dict[str, OutputVariableDef], +) -> Dict[str, OutputVariableDef]: + def_mask: Dict[str, OutputVariableDef] = {} + # for deep eval when has atomic mask + def_mask["mask"] = OutputVariableDef( + name="mask", + shape=[1], + reduciable=False, + r_differentiable=False, + c_differentiable=False, + ) + for kk, vv in def_outp_data.items(): + if vv.magnetic: + # for deep eval when has atomic mask for magnetic atoms + def_mask["mask_mag"] = OutputVariableDef( + name="mask_mag", + shape=[1], + reduciable=False, + r_differentiable=False, + c_differentiable=False, + ) + return def_mask + + def do_derivative( def_outp_data: Dict[str, OutputVariableDef], ) -> Tuple[Dict[str, OutputVariableDef], Dict[str, OutputVariableDef]]: @@ -408,6 +449,7 @@ def do_derivative( def_derv_c: Dict[str, OutputVariableDef] = {} for kk, vv in def_outp_data.items(): rkr, rkc = get_deriv_name(kk) + rkrm, rkcm = get_deriv_name_mag(kk) if vv.r_differentiable: def_derv_r[rkr] = OutputVariableDef( rkr, @@ -420,9 +462,22 @@ def do_derivative( atomic=True, category=apply_operation(vv, OutputVariableOperation.DERV_R), ) + if vv.magnetic: + def_derv_r[rkrm] = OutputVariableDef( + rkrm, + vv.shape + [3], # noqa: RUF005 + reduciable=False, + r_differentiable=( + vv.r_hessian and vv.category == OutputVariableCategory.OUT.value + ), + c_differentiable=False, + atomic=True, + category=apply_operation(vv, OutputVariableOperation.DERV_R), + magnetic=True, + ) + if vv.c_differentiable: assert vv.r_differentiable - rkr, rkc = get_deriv_name(kk) def_derv_c[rkc] = OutputVariableDef( rkc, vv.shape + [9], # noqa: RUF005 @@ -432,4 +487,15 @@ def do_derivative( atomic=True, category=apply_operation(vv, OutputVariableOperation.DERV_C), ) + if vv.magnetic: + def_derv_r[rkcm] = OutputVariableDef( + rkcm, + vv.shape + [9], # noqa: RUF005 + reduciable=True, + r_differentiable=False, + c_differentiable=False, + atomic=True, + category=apply_operation(vv, OutputVariableOperation.DERV_C), + magnetic=True, + ) return def_derv_r, def_derv_c diff --git a/deepmd/dpmodel/utils/env_mat.py b/deepmd/dpmodel/utils/env_mat.py index 5fb4ac4107..0c2ca43c40 100644 --- a/deepmd/dpmodel/utils/env_mat.py +++ b/deepmd/dpmodel/utils/env_mat.py @@ -33,6 +33,7 @@ def _make_env_mat( rcut: float, ruct_smth: float, radial_only: bool = False, + protection: float = 0.0, ): """Make smooth environment matrix.""" nf, nloc, nnei = nlist.shape @@ -53,8 +54,8 @@ def _make_env_mat( length = np.linalg.norm(diff, axis=-1, keepdims=True) # for index 0 nloc atom length = length + ~np.expand_dims(mask, -1) - t0 = 1 / length - t1 = diff / length**2 + t0 = 1 / (length + protection) + t1 = diff / (length + protection) ** 2 weight = compute_smooth_weight(length, ruct_smth, rcut) weight = weight * np.expand_dims(mask, -1) if radial_only: @@ -69,9 +70,11 @@ def __init__( self, rcut, rcut_smth, + protection: float = 0.0, ): self.rcut = rcut self.rcut_smth = rcut_smth + self.protection = protection def call( self, @@ -120,7 +123,12 @@ def call( def _call(self, nlist, coord_ext, radial_only): em, diff, ww = _make_env_mat( - nlist, coord_ext, self.rcut, self.rcut_smth, radial_only + nlist, + coord_ext, + self.rcut, + self.rcut_smth, + radial_only=radial_only, + protection=self.protection, ) return em, ww diff --git a/deepmd/dpmodel/utils/exclude_mask.py b/deepmd/dpmodel/utils/exclude_mask.py index 360f190e13..ff668b8153 100644 --- a/deepmd/dpmodel/utils/exclude_mask.py +++ b/deepmd/dpmodel/utils/exclude_mask.py @@ -24,6 +24,12 @@ def __init__( # (ntypes) self.type_mask = self.type_mask.reshape([-1]) + def get_exclude_types(self): + return self.exclude_types + + def get_type_mask(self): + return self.type_mask + def build_type_exclude_mask( self, atype: np.ndarray, @@ -75,6 +81,9 @@ def __init__( # (ntypes+1 x ntypes+1) self.type_mask = self.type_mask.reshape([-1]) + def get_exclude_types(self): + return self.exclude_types + def build_type_exclude_mask( self, nlist: np.ndarray, diff --git a/deepmd/dpmodel/utils/nlist.py b/deepmd/dpmodel/utils/nlist.py index 657d6ecee2..1aa1820495 100644 --- a/deepmd/dpmodel/utils/nlist.py +++ b/deepmd/dpmodel/utils/nlist.py @@ -69,6 +69,8 @@ def build_neighbor_list( ) assert list(diff.shape) == [batch_size, nloc, nall, 3] rr = np.linalg.norm(diff, axis=-1) + # if central atom has two zero distances, sorting sometimes can not exclude itself + rr -= np.eye(nloc, nall, dtype=diff.dtype)[np.newaxis, :, :] nlist = np.argsort(rr, axis=-1) rr = np.sort(rr, axis=-1) rr = rr[:, :, 1:] diff --git a/deepmd/entrypoints/test.py b/deepmd/entrypoints/test.py index efc75e31a7..ccf8b1da1e 100644 --- a/deepmd/entrypoints/test.py +++ b/deepmd/entrypoints/test.py @@ -298,6 +298,9 @@ def test_ener( ) if dp.get_dim_aparam() > 0: data.add("aparam", dp.get_dim_aparam(), atomic=True, must=True, high_prec=False) + if dp.has_spin: + data.add("spin", 3, atomic=True, must=True, high_prec=False) + data.add("force_mag", 3, atomic=True, must=False, high_prec=False) test_data = data.get_test() mixed_type = data.mixed_type @@ -311,6 +314,10 @@ def test_ener( efield = test_data["efield"][:numb_test].reshape([numb_test, -1]) else: efield = None + if dp.has_spin: + spin = test_data["spin"][:numb_test].reshape([numb_test, -1]) + else: + spin = None if not data.pbc: box = None if mixed_type: @@ -335,6 +342,7 @@ def test_ener( atomic=has_atom_ener, efield=efield, mixed_type=mixed_type, + spin=spin, ) energy = ret[0] force = ret[1] @@ -347,26 +355,50 @@ def test_ener( av = ret[4] ae = ae.reshape([numb_test, -1]) av = av.reshape([numb_test, -1]) - if dp.get_ntypes_spin() != 0: - ntypes_real = dp.get_ntypes() - dp.get_ntypes_spin() - nloc = natoms - nloc_real = sum([np.count_nonzero(atype == ii) for ii in range(ntypes_real)]) - force_r = np.split( - force, indices_or_sections=[nloc_real * 3, nloc * 3], axis=1 - )[0] - force_m = np.split( - force, indices_or_sections=[nloc_real * 3, nloc * 3], axis=1 - )[1] - test_force_r = np.split( - test_data["force"][:numb_test], - indices_or_sections=[nloc_real * 3, nloc * 3], - axis=1, - )[0] - test_force_m = np.split( - test_data["force"][:numb_test], - indices_or_sections=[nloc_real * 3, nloc * 3], - axis=1, - )[1] + if dp.has_spin: + force_m = ret[5] + force_m = force_m.reshape([numb_test, -1]) + mask_mag = ret[6] + mask_mag = mask_mag.reshape([numb_test, -1]) + else: + if dp.has_spin: + force_m = ret[3] + force_m = force_m.reshape([numb_test, -1]) + mask_mag = ret[4] + mask_mag = mask_mag.reshape([numb_test, -1]) + out_put_spin = dp.get_ntypes_spin() != 0 or dp.has_spin + if out_put_spin: + if dp.get_ntypes_spin() != 0: # old tf support for spin + ntypes_real = dp.get_ntypes() - dp.get_ntypes_spin() + nloc = natoms + nloc_real = sum( + [np.count_nonzero(atype == ii) for ii in range(ntypes_real)] + ) + force_r = np.split( + force, indices_or_sections=[nloc_real * 3, nloc * 3], axis=1 + )[0] + force_m = np.split( + force, indices_or_sections=[nloc_real * 3, nloc * 3], axis=1 + )[1] + test_force_r = np.split( + test_data["force"][:numb_test], + indices_or_sections=[nloc_real * 3, nloc * 3], + axis=1, + )[0] + test_force_m = np.split( + test_data["force"][:numb_test], + indices_or_sections=[nloc_real * 3, nloc * 3], + axis=1, + )[1] + else: # pt support for spin + force_r = force + test_force_r = test_data["force"][:numb_test] + # The shape of force_m and test_force_m are [-1, 3], + # which is designed for mixed_type cases + force_m = force_m.reshape(-1, 3)[mask_mag.reshape(-1)] + test_force_m = test_data["force_mag"][:numb_test].reshape(-1, 3)[ + mask_mag.reshape(-1) + ] diff_e = energy - test_data["energy"][:numb_test].reshape([-1, 1]) mae_e = mae(diff_e) @@ -385,7 +417,7 @@ def test_ener( diff_ae = test_data["atom_ener"][:numb_test].reshape([-1]) - ae.reshape([-1]) mae_ae = mae(diff_ae) rmse_ae = rmse(diff_ae) - if dp.get_ntypes_spin() != 0: + if out_put_spin: mae_fr = mae(force_r - test_force_r) mae_fm = mae(force_m - test_force_m) rmse_fr = rmse(force_r - test_force_r) @@ -396,16 +428,16 @@ def test_ener( log.info(f"Energy RMSE : {rmse_e:e} eV") log.info(f"Energy MAE/Natoms : {mae_ea:e} eV") log.info(f"Energy RMSE/Natoms : {rmse_ea:e} eV") - if dp.get_ntypes_spin() == 0: + if not out_put_spin: log.info(f"Force MAE : {mae_f:e} eV/A") log.info(f"Force RMSE : {rmse_f:e} eV/A") else: log.info(f"Force atom MAE : {mae_fr:e} eV/A") - log.info(f"Force spin MAE : {mae_fm:e} eV/uB") log.info(f"Force atom RMSE : {rmse_fr:e} eV/A") + log.info(f"Force spin MAE : {mae_fm:e} eV/uB") log.info(f"Force spin RMSE : {rmse_fm:e} eV/uB") - if data.pbc: + if data.pbc and not out_put_spin: log.info(f"Virial MAE : {mae_v:e} eV") log.info(f"Virial RMSE : {rmse_v:e} eV") log.info(f"Virial MAE/Natoms : {mae_va:e} eV") @@ -437,7 +469,7 @@ def test_ener( header="%s: data_e pred_e" % system, append=append_detail, ) - if dp.get_ntypes_spin() == 0: + if not out_put_spin: pf = np.concatenate( ( np.reshape(test_data["force"][:numb_test], [-1, 3]), @@ -497,7 +529,7 @@ def test_ener( "pred_vyy pred_vyz pred_vzx pred_vzy pred_vzz", append=append_detail, ) - if dp.get_ntypes_spin() == 0: + if not out_put_spin: return { "mae_e": (mae_e, energy.size), "mae_ea": (mae_ea, energy.size), diff --git a/deepmd/infer/deep_eval.py b/deepmd/infer/deep_eval.py index de964b88b9..065982a870 100644 --- a/deepmd/infer/deep_eval.py +++ b/deepmd/infer/deep_eval.py @@ -57,7 +57,9 @@ class DeepEvalBackend(ABC): "energy": "atom_energy", "energy_redu": "energy", "energy_derv_r": "force", + "energy_derv_r_mag": "force_mag", "energy_derv_c": "atom_virial", + "energy_derv_c_mag": "atom_virial_mag", "energy_derv_c_redu": "virial", "polar": "polar", "polar_redu": "global_polar", @@ -71,6 +73,8 @@ class DeepEvalBackend(ABC): "dipole_derv_c_redu": "virial", "dos": "atom_dos", "dos_redu": "dos", + "mask_mag": "mask_mag", + "mask": "mask", } @abstractmethod @@ -262,9 +266,13 @@ def get_has_efield(self): """Check if the model has efield.""" return False + def get_has_spin(self): + """Check if the model has spin atom types.""" + return False + @abstractmethod def get_ntypes_spin(self) -> int: - """Get the number of spin atom types of this model.""" + """Get the number of spin atom types of this model. Only used in old implement.""" class DeepEval(ABC): @@ -317,6 +325,8 @@ def __init__( neighbor_list=neighbor_list, **kwargs, ) + if self.deep_eval.get_has_spin() and hasattr(self, "output_def_mag"): + self.deep_eval.output_def = self.output_def_mag @property @abstractmethod @@ -518,6 +528,11 @@ def has_efield(self) -> bool: """Check if the model has efield.""" return self.deep_eval.get_has_efield() + @property + def has_spin(self) -> bool: + """Check if the model has spin.""" + return self.deep_eval.get_has_spin() + def get_ntypes_spin(self) -> int: - """Get the number of spin atom types of this model.""" + """Get the number of spin atom types of this model. Only used in old implement.""" return self.deep_eval.get_ntypes_spin() diff --git a/deepmd/infer/deep_pot.py b/deepmd/infer/deep_pot.py index e955a3ed65..bc0bfc9599 100644 --- a/deepmd/infer/deep_pot.py +++ b/deepmd/infer/deep_pot.py @@ -70,6 +70,25 @@ def output_def(self) -> ModelOutputDef: ) ) + @property + def output_def_mag(self) -> ModelOutputDef: + """Get the output definition of this model with magnetic parts.""" + return ModelOutputDef( + FittingOutputDef( + [ + OutputVariableDef( + "energy", + shape=[1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + atomic=True, + magnetic=True, + ), + ] + ) + ) + def eval( self, coords: np.ndarray, @@ -162,7 +181,7 @@ def eval( natoms_real = natoms atomic_energy = results["energy"].reshape(nframes, natoms_real, 1) atomic_virial = results["energy_derv_c"].reshape(nframes, natoms, 9) - return ( + result = ( energy, force, virial, @@ -170,11 +189,16 @@ def eval( atomic_virial, ) else: - return ( + result = ( energy, force, virial, ) + if self.deep_eval.get_has_spin(): + force_mag = results["energy_derv_r_mag"].reshape(nframes, natoms, 3) + mask_mag = results["mask_mag"].reshape(nframes, natoms, 1) + result = (*list(result), force_mag, mask_mag) + return result __all__ = ["DeepPot"] diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index bf6a5b0306..b8031993c0 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -124,6 +124,9 @@ def __init__( self.auto_batch_size = auto_batch_size else: raise TypeError("auto_batch_size should be bool, int, or AutoBatchSize") + self._has_spin = getattr(self.dp.model["Default"], "has_spin", False) + if callable(self._has_spin): + self._has_spin = self._has_spin() def get_rcut(self) -> float: """Get the cutoff radius of this model.""" @@ -182,9 +185,13 @@ def get_has_efield(self): return False def get_ntypes_spin(self): - """Get the number of spin atom types of this model.""" + """Get the number of spin atom types of this model. Only used in old implement.""" return 0 + def get_has_spin(self): + """Check if the model has spin atom types.""" + return self._has_spin + def eval( self, coords: np.ndarray, @@ -240,14 +247,20 @@ def eval( coords, atom_types, len(atom_types.shape) > 1 ) request_defs = self._get_request_defs(atomic) - out = self._eval_func(self._eval_model, numb_test, natoms)( - coords, - cells, - atom_types, - fparam, - aparam, - request_defs, - ) + if "spin" not in kwargs or kwargs["spin"] is None: + out = self._eval_func(self._eval_model, numb_test, natoms)( + coords, cells, atom_types, fparam, aparam, request_defs + ) + else: + out = self._eval_func(self._eval_model_spin, numb_test, natoms)( + coords, + cells, + atom_types, + np.array(kwargs["spin"]), + fparam, + aparam, + request_defs, + ) return dict( zip( [x.name for x in request_defs], @@ -280,6 +293,7 @@ def _get_request_defs(self, atomic: bool) -> List[OutputVariableDef]: for x in self.output_def.var_defs.values() if x.category in ( + OutputVariableCategory.OUT, OutputVariableCategory.REDU, OutputVariableCategory.DERV_R, OutputVariableCategory.DERV_C_REDU, @@ -399,6 +413,82 @@ def _eval_model( results.append(np.full(np.abs(shape), np.nan)) # this is kinda hacky return tuple(results) + def _eval_model_spin( + self, + coords: np.ndarray, + cells: Optional[np.ndarray], + atom_types: np.ndarray, + spins: np.ndarray, + fparam: Optional[np.ndarray], + aparam: Optional[np.ndarray], + request_defs: List[OutputVariableDef], + ): + model = self.dp.to(DEVICE) + + nframes = coords.shape[0] + if len(atom_types.shape) == 1: + natoms = len(atom_types) + atom_types = np.tile(atom_types, nframes).reshape(nframes, -1) + else: + natoms = len(atom_types[0]) + + coord_input = torch.tensor( + coords.reshape([-1, natoms, 3]), + dtype=GLOBAL_PT_FLOAT_PRECISION, + device=DEVICE, + ) + type_input = torch.tensor(atom_types, dtype=torch.long, device=DEVICE) + spin_input = torch.tensor( + spins.reshape([-1, natoms, 3]), + dtype=GLOBAL_PT_FLOAT_PRECISION, + device=DEVICE, + ) + if cells is not None: + box_input = torch.tensor( + cells.reshape([-1, 3, 3]), + dtype=GLOBAL_PT_FLOAT_PRECISION, + device=DEVICE, + ) + else: + box_input = None + if fparam is not None: + fparam_input = to_torch_tensor(fparam.reshape(-1, self.get_dim_fparam())) + else: + fparam_input = None + if aparam is not None: + aparam_input = to_torch_tensor( + aparam.reshape(-1, natoms, self.get_dim_aparam()) + ) + else: + aparam_input = None + + do_atomic_virial = any( + x.category == OutputVariableCategory.DERV_C_REDU for x in request_defs + ) + batch_output = model( + coord_input, + type_input, + spin=spin_input, + box=box_input, + do_atomic_virial=do_atomic_virial, + fparam=fparam_input, + aparam=aparam_input, + ) + if isinstance(batch_output, tuple): + batch_output = batch_output[0] + + results = [] + for odef in request_defs: + pt_name = self._OUTDEF_DP2BACKEND[odef.name] + if pt_name in batch_output: + shape = self._get_output_shape(odef, nframes, natoms) + out = batch_output[pt_name].reshape(shape).detach().cpu().numpy() + results.append(out) + else: + shape = self._get_output_shape(odef, nframes, natoms) + results.append(np.full(np.abs(shape), np.nan)) # this is kinda hacky + return tuple(results) + def _get_output_shape(self, odef, nframes, natoms): if odef.category == OutputVariableCategory.DERV_C_REDU: # virial @@ -427,6 +517,7 @@ def eval_model( coords: Union[np.ndarray, torch.Tensor], cells: Optional[Union[np.ndarray, torch.Tensor]], atom_types: Union[np.ndarray, torch.Tensor, List[int]], + spins: Optional[Union[np.ndarray, torch.Tensor]] = None, atomic: bool = False, infer_batch_size: int = 2, denoise: bool = False, @@ -435,6 +526,7 @@ def eval_model( energy_out = [] atomic_energy_out = [] force_out = [] + force_mag_out = [] virial_out = [] atomic_virial_out = [] updated_coord_out = [] @@ -447,11 +539,15 @@ def eval_model( if isinstance(coords, torch.Tensor): if cells is not None: assert isinstance(cells, torch.Tensor), err_msg + if spins is not None: + assert isinstance(spins, torch.Tensor), err_msg assert isinstance(atom_types, torch.Tensor) or isinstance(atom_types, list) atom_types = torch.tensor(atom_types, dtype=torch.long, device=DEVICE) elif isinstance(coords, np.ndarray): if cells is not None: assert isinstance(cells, np.ndarray), err_msg + if spins is not None: + assert isinstance(spins, np.ndarray), err_msg assert isinstance(atom_types, np.ndarray) or isinstance(atom_types, list) atom_types = np.array(atom_types, dtype=np.int32) return_tensor = False @@ -471,6 +567,16 @@ def eval_model( coord_input = torch.tensor( coords.reshape([-1, natoms, 3]), dtype=GLOBAL_PT_FLOAT_PRECISION, device=DEVICE ) + spin_input = None + if spins is not None: + spin_input = torch.tensor( + spins.reshape([-1, natoms, 3]), + dtype=GLOBAL_PT_FLOAT_PRECISION, + device=DEVICE, + ) + has_spin = getattr(model, "has_spin", False) + if callable(has_spin): + has_spin = has_spin() type_input = torch.tensor(atom_types, dtype=torch.long, device=DEVICE) box_input = None if cells is None: @@ -486,9 +592,20 @@ def eval_model( batch_coord = coord_input[ii * infer_batch_size : (ii + 1) * infer_batch_size] batch_atype = type_input[ii * infer_batch_size : (ii + 1) * infer_batch_size] batch_box = None + batch_spin = None + if spin_input is not None: + batch_spin = spin_input[ii * infer_batch_size : (ii + 1) * infer_batch_size] if pbc: batch_box = box_input[ii * infer_batch_size : (ii + 1) * infer_batch_size] - batch_output = model(batch_coord, batch_atype, box=batch_box) + input_dict = { + "coord": batch_coord, + "atype": batch_atype, + "box": batch_box, + "do_atomic_virial": atomic, + } + if has_spin: + input_dict["spin"] = batch_spin + batch_output = model(**input_dict) if isinstance(batch_output, tuple): batch_output = batch_output[0] if not return_tensor: @@ -500,6 +617,8 @@ def eval_model( ) if "force" in batch_output: force_out.append(batch_output["force"].detach().cpu().numpy()) + if "force_mag" in batch_output: + force_mag_out.append(batch_output["force_mag"].detach().cpu().numpy()) if "virial" in batch_output: virial_out.append(batch_output["virial"].detach().cpu().numpy()) if "atom_virial" in batch_output: @@ -519,6 +638,8 @@ def eval_model( atomic_energy_out.append(batch_output["atom_energy"]) if "force" in batch_output: force_out.append(batch_output["force"]) + if "force_mag" in batch_output: + force_mag_out.append(batch_output["force_mag"]) if "virial" in batch_output: virial_out.append(batch_output["virial"]) if "atom_virial" in batch_output: @@ -539,6 +660,11 @@ def eval_model( force_out = ( np.concatenate(force_out) if force_out else np.zeros([nframes, natoms, 3]) ) + force_mag_out = ( + np.concatenate(force_mag_out) + if force_mag_out + else np.zeros([nframes, natoms, 3]) + ) virial_out = ( np.concatenate(virial_out) if virial_out else np.zeros([nframes, 3, 3]) ) @@ -573,6 +699,13 @@ def eval_model( [nframes, natoms, 3], dtype=GLOBAL_PT_FLOAT_PRECISION, device=DEVICE ) ) + force_mag_out = ( + torch.cat(force_mag_out) + if force_mag_out + else torch.zeros( + [nframes, natoms, 3], dtype=GLOBAL_PT_FLOAT_PRECISION, device=DEVICE + ) + ) virial_out = ( torch.cat(virial_out) if virial_out @@ -592,13 +725,14 @@ def eval_model( if denoise: return updated_coord_out, logits_out else: - if not atomic: - return energy_out, force_out, virial_out - else: - return ( - energy_out, - force_out, - virial_out, - atomic_energy_out, - atomic_virial_out, - ) + results_dict = { + "energy": energy_out, + "force": force_out, + "virial": virial_out, + } + if has_spin: + results_dict["force_mag"] = force_mag_out + if atomic: + results_dict["atom_energy"] = atomic_energy_out + results_dict["atom_virial"] = atomic_virial_out + return results_dict diff --git a/deepmd/pt/loss/__init__.py b/deepmd/pt/loss/__init__.py index d2f6ab9e52..9c8bbc9a2a 100644 --- a/deepmd/pt/loss/__init__.py +++ b/deepmd/pt/loss/__init__.py @@ -5,6 +5,9 @@ from .ener import ( EnergyStdLoss, ) +from .ener_spin import ( + EnergySpinLoss, +) from .loss import ( TaskLoss, ) @@ -15,6 +18,7 @@ __all__ = [ "DenoiseLoss", "EnergyStdLoss", + "EnergySpinLoss", "TensorLoss", "TaskLoss", ] diff --git a/deepmd/pt/loss/ener_spin.py b/deepmd/pt/loss/ener_spin.py new file mode 100644 index 0000000000..b94acf26ea --- /dev/null +++ b/deepmd/pt/loss/ener_spin.py @@ -0,0 +1,245 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + List, +) + +import torch +import torch.nn.functional as F + +from deepmd.pt.loss.loss import ( + TaskLoss, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.env import ( + GLOBAL_PT_FLOAT_PRECISION, +) +from deepmd.utils.data import ( + DataRequirementItem, +) + + +class EnergySpinLoss(TaskLoss): + def __init__( + self, + starter_learning_rate=1.0, + start_pref_e=0.0, + limit_pref_e=0.0, + start_pref_fr=0.0, + limit_pref_fr=0.0, + start_pref_fm=0.0, + limit_pref_fm=0.0, + start_pref_v=0.0, + limit_pref_v=0.0, + start_pref_ae: float = 0.0, + limit_pref_ae: float = 0.0, + start_pref_pf: float = 0.0, + limit_pref_pf: float = 0.0, + use_l1_all: bool = False, + inference=False, + **kwargs, + ): + """Construct a layer to compute loss on energy, real force, magnetic force and virial.""" + super().__init__() + self.starter_learning_rate = starter_learning_rate + self.has_e = (start_pref_e != 0.0 and limit_pref_e != 0.0) or inference + self.has_fr = (start_pref_fr != 0.0 and limit_pref_fr != 0.0) or inference + self.has_fm = (start_pref_fm != 0.0 and limit_pref_fm != 0.0) or inference + + # TODO need support for virial, atomic energy and atomic pref + self.has_v = (start_pref_v != 0.0 and limit_pref_v != 0.0) or inference + self.has_ae = (start_pref_ae != 0.0 and limit_pref_ae != 0.0) or inference + self.has_pf = (start_pref_pf != 0.0 and limit_pref_pf != 0.0) or inference + + self.start_pref_e = start_pref_e + self.limit_pref_e = limit_pref_e + self.start_pref_fr = start_pref_fr + self.limit_pref_fr = limit_pref_fr + self.start_pref_fm = start_pref_fm + self.limit_pref_fm = limit_pref_fm + self.start_pref_v = start_pref_v + self.limit_pref_v = limit_pref_v + self.use_l1_all = use_l1_all + self.inference = inference + + def forward(self, model_pred, label, natoms, learning_rate, mae=False): + """Return energy loss with magnetic labels. + + Parameters + ---------- + model_pred : dict[str, torch.Tensor] + Model predictions. + label : dict[str, torch.Tensor] + Labels. + natoms : int + The local atom number. + + Returns + ------- + loss: torch.Tensor + Loss for model to minimize. + more_loss: dict[str, torch.Tensor] + Other losses for display. + """ + coef = learning_rate / self.starter_learning_rate + pref_e = self.limit_pref_e + (self.start_pref_e - self.limit_pref_e) * coef + pref_fr = self.limit_pref_fr + (self.start_pref_fr - self.limit_pref_fr) * coef + pref_fm = self.limit_pref_fm + (self.start_pref_fm - self.limit_pref_fm) * coef + pref_v = self.limit_pref_v + (self.start_pref_v - self.limit_pref_v) * coef + loss = torch.tensor(0.0, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE) + more_loss = {} + # more_loss['log_keys'] = [] # showed when validation on the fly + # more_loss['test_keys'] = [] # showed when doing dp test + atom_norm = 1.0 / natoms + if self.has_e and "energy" in model_pred and "energy" in label: + if not self.use_l1_all: + l2_ener_loss = torch.mean( + torch.square(model_pred["energy"] - label["energy"]) + ) + if not self.inference: + more_loss["l2_ener_loss"] = l2_ener_loss.detach() + loss += atom_norm * (pref_e * l2_ener_loss) + rmse_e = l2_ener_loss.sqrt() * atom_norm + more_loss["rmse_e"] = rmse_e.detach() + # more_loss['log_keys'].append('rmse_e') + else: # use l1 and for all atoms + l1_ener_loss = F.l1_loss( + model_pred["energy"].reshape(-1), + label["energy"].reshape(-1), + reduction="sum", + ) + loss += pref_e * l1_ener_loss + more_loss["mae_e"] = F.l1_loss( + model_pred["energy"].reshape(-1), + label["energy"].reshape(-1), + reduction="mean", + ).detach() + # more_loss['log_keys'].append('rmse_e') + if mae: + mae_e = ( + torch.mean(torch.abs(model_pred["energy"] - label["energy"])) + * atom_norm + ) + more_loss["mae_e"] = mae_e.detach() + mae_e_all = torch.mean( + torch.abs(model_pred["energy"] - label["energy"]) + ) + more_loss["mae_e_all"] = mae_e_all.detach() + + if self.has_fr and "force" in model_pred and "force" in label: + if not self.use_l1_all: + diff_fr = label["force"] - model_pred["force"] + l2_force_real_loss = torch.mean(torch.square(diff_fr)) + if not self.inference: + more_loss["l2_force_r_loss"] = l2_force_real_loss.detach() + loss += (pref_fr * l2_force_real_loss).to(GLOBAL_PT_FLOAT_PRECISION) + rmse_fr = l2_force_real_loss.sqrt() + more_loss["rmse_fr"] = rmse_fr.detach() + if mae: + mae_fr = torch.mean(torch.abs(diff_fr)) + more_loss["mae_fr"] = mae_fr.detach() + else: + l1_force_real_loss = F.l1_loss( + label["force"], model_pred["force"], reduction="none" + ) + more_loss["mae_fr"] = l1_force_real_loss.mean().detach() + l1_force_real_loss = l1_force_real_loss.sum(-1).mean(-1).sum() + loss += (pref_fr * l1_force_real_loss).to(GLOBAL_PT_FLOAT_PRECISION) + + if self.has_fm and "force_mag" in model_pred and "force_mag" in label: + nframes = model_pred["force_mag"].shape[0] + atomic_mask = model_pred["mask_mag"].expand([-1, -1, 3]) + label_force_mag = label["force_mag"][atomic_mask].view(nframes, -1, 3) + model_pred_force_mag = model_pred["force_mag"][atomic_mask].view( + nframes, -1, 3 + ) + if not self.use_l1_all: + diff_fm = label_force_mag - model_pred_force_mag + l2_force_mag_loss = torch.mean(torch.square(diff_fm)) + if not self.inference: + more_loss["l2_force_m_loss"] = l2_force_mag_loss.detach() + loss += (pref_fm * l2_force_mag_loss).to(GLOBAL_PT_FLOAT_PRECISION) + rmse_fm = l2_force_mag_loss.sqrt() + more_loss["rmse_fm"] = rmse_fm.detach() + if mae: + mae_fm = torch.mean(torch.abs(diff_fm)) + more_loss["mae_fm"] = mae_fm.detach() + else: + l1_force_mag_loss = F.l1_loss( + label_force_mag, model_pred_force_mag, reduction="none" + ) + more_loss["mae_fm"] = l1_force_mag_loss.mean().detach() + l1_force_mag_loss = l1_force_mag_loss.sum(-1).mean(-1).sum() + loss += (pref_fm * l1_force_mag_loss).to(GLOBAL_PT_FLOAT_PRECISION) + + if not self.inference: + more_loss["rmse"] = torch.sqrt(loss.detach()) + return loss, more_loss + + @property + def label_requirement(self) -> List[DataRequirementItem]: + """Return data label requirements needed for this loss calculation.""" + label_requirement = [] + if self.has_e: + label_requirement.append( + DataRequirementItem( + "energy", + ndof=1, + atomic=False, + must=False, + high_prec=True, + ) + ) + if self.has_fr: + label_requirement.append( + DataRequirementItem( + "force", + ndof=3, + atomic=True, + must=False, + high_prec=False, + ) + ) + if self.has_fm: + label_requirement.append( + DataRequirementItem( + "force_mag", + ndof=3, + atomic=True, + must=False, + high_prec=False, + ) + ) + if self.has_v: + label_requirement.append( + DataRequirementItem( + "virial", + ndof=9, + atomic=False, + must=False, + high_prec=False, + ) + ) + if self.has_ae: + label_requirement.append( + DataRequirementItem( + "atom_ener", + ndof=1, + atomic=True, + must=False, + high_prec=False, + ) + ) + if self.has_pf: + label_requirement.append( + DataRequirementItem( + "atom_pref", + ndof=1, + atomic=True, + must=False, + high_prec=False, + repeat=3, + ) + ) + return label_requirement diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index 807f8433e5..cad1e1cc88 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import copy +import functools import logging from typing import ( Dict, @@ -204,9 +205,23 @@ def compute_or_load_stat( # descriptors and fitting net with different type_map # should not share the same parameters stat_file_path /= " ".join(self.type_map) - self.descriptor.compute_input_stats(sampled_func, stat_file_path) + + @functools.lru_cache + def wrapped_sampler(): + sampled = sampled_func() + if self.pair_excl is not None: + pair_exclude_types = self.pair_excl.get_exclude_types() + for sample in sampled: + sample["pair_exclude_types"] = list(pair_exclude_types) + if self.atom_excl is not None: + atom_exclude_types = self.atom_excl.get_exclude_types() + for sample in sampled: + sample["atom_exclude_types"] = list(atom_exclude_types) + return sampled + + self.descriptor.compute_input_stats(wrapped_sampler, stat_file_path) if self.fitting_net is not None: - self.fitting_net.compute_output_stats(sampled_func, stat_file_path) + self.fitting_net.compute_output_stats(wrapped_sampler, stat_file_path) @torch.jit.export def get_dim_fparam(self) -> int: diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index 215bb25de5..19a67fc8ff 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -230,7 +230,7 @@ def compute_or_load_stat( """ bias_atom_e = compute_output_stats( - merged, stat_file_path, self.rcond, self.atom_ener + merged, self.ntypes, stat_file_path, self.rcond, self.atom_ener ) self.bias_atom_e.copy_( torch.tensor(bias_atom_e, device=env.DEVICE).view([self.ntypes, 1]) diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index 1b32467540..21275317dc 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -3,6 +3,7 @@ Callable, List, Optional, + Tuple, Union, ) @@ -55,13 +56,14 @@ def __init__( temperature=None, return_rot=False, concat_output_tebd: bool = True, + env_protection: float = 0.0, type: Optional[str] = None, # not implemented resnet_dt: bool = False, type_one_side: bool = True, precision: str = "default", trainable: bool = True, - exclude_types: Optional[List[List[int]]] = None, + exclude_types: List[Tuple[int, int]] = [], stripped_type_embedding: bool = False, smooth_type_embdding: bool = False, ): @@ -72,8 +74,6 @@ def __init__( raise NotImplementedError("type_one_side is not supported.") if precision != "default" and precision != "float64": raise NotImplementedError("precison is not supported.") - if exclude_types is not None and exclude_types != []: - raise NotImplementedError("exclude_types is not supported.") if stripped_type_embedding: raise NotImplementedError("stripped_type_embedding is not supported.") if smooth_type_embdding: @@ -102,6 +102,8 @@ def __init__( normalize=normalize, temperature=temperature, return_rot=return_rot, + exclude_types=exclude_types, + env_protection=env_protection, ) self.type_embedding = TypeEmbedNet(ntypes, tebd_dim) self.tebd_dim = tebd_dim diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index a80cc4a445..fb792a51e2 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -3,6 +3,7 @@ Callable, List, Optional, + Tuple, Union, ) @@ -77,7 +78,9 @@ def __init__( repformer_update_style: str = "res_avg", repformer_set_davg_zero: bool = True, # TODO repformer_add_type_ebd_to_seq: bool = False, + env_protection: float = 0.0, trainable: bool = True, + exclude_types: List[Tuple[int, int]] = [], type: Optional[ str ] = None, # work around the bad design in get_trainer and DpLoaderSet! @@ -175,6 +178,9 @@ def __init__( repformers block: concatenate the type embedding at the output. trainable : bool If the parameters in the descriptor are trainable. + exclude_types : List[Tuple[int, int]] = [], + The excluded pairs of types which have no interaction with each other. + For example, `[[0, 1]]` means no interaction between type 0 and type 1. Returns ------- @@ -205,6 +211,8 @@ def __init__( tebd_input_mode="concat", # tebd_input_mode='dot_residual_s', set_davg_zero=repinit_set_davg_zero, + exclude_types=exclude_types, + env_protection=env_protection, activation_function=repinit_activation, ) self.repformers = DescrptBlockRepformers( @@ -236,6 +244,8 @@ def __init__( set_davg_zero=repformer_set_davg_zero, smooth=True, add_type_ebd_to_seq=repformer_add_type_ebd_to_seq, + exclude_types=exclude_types, + env_protection=env_protection, ) self.type_embedding = TypeEmbedNet(ntypes, tebd_dim) if self.repinit.dim_out == self.repformers.dim_in: diff --git a/deepmd/pt/model/descriptor/env_mat.py b/deepmd/pt/model/descriptor/env_mat.py index 4e6ffb7785..e89e7467d3 100644 --- a/deepmd/pt/model/descriptor/env_mat.py +++ b/deepmd/pt/model/descriptor/env_mat.py @@ -8,7 +8,12 @@ def _make_env_mat( - nlist, coord, rcut: float, ruct_smth: float, radial_only: bool = False + nlist, + coord, + rcut: float, + ruct_smth: float, + radial_only: bool = False, + protection: float = 0.0, ): """Make smooth environment matrix.""" bsz, natoms, nnei = nlist.shape @@ -25,8 +30,8 @@ def _make_env_mat( length = torch.linalg.norm(diff, dim=-1, keepdim=True) # for index 0 nloc atom length = length + ~mask.unsqueeze(-1) - t0 = 1 / length - t1 = diff / length**2 + t0 = 1 / (length + protection) + t1 = diff / (length + protection) ** 2 weight = compute_smooth_weight(length, ruct_smth, rcut) weight = weight * mask.unsqueeze(-1) if radial_only: @@ -45,6 +50,7 @@ def prod_env_mat( rcut: float, rcut_smth: float, radial_only: bool = False, + protection: float = 0.0, ): """Generate smooth environment matrix from atom coordinates and other context. @@ -56,13 +62,19 @@ def prod_env_mat( - rcut: Cut-off radius. - rcut_smth: Smooth hyper-parameter for pair force & energy. - radial_only: Whether to return a full description or a radial-only descriptor. + - protection: Protection parameter to prevent division by zero errors during calculations. Returns ------- - env_mat: Shape is [nframes, natoms[1]*nnei*4]. """ _env_mat_se_a, diff, switch = _make_env_mat( - nlist, extended_coord, rcut, rcut_smth, radial_only + nlist, + extended_coord, + rcut, + rcut_smth, + radial_only, + protection=protection, ) # shape [n_atom, dim, 4 or 1] t_avg = mean[atype] # [n_atom, dim, 4 or 1] t_std = stddev[atype] # [n_atom, dim, 4 or 1] diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py index 3e8bf72f77..a908d2e057 100644 --- a/deepmd/pt/model/descriptor/repformers.py +++ b/deepmd/pt/model/descriptor/repformers.py @@ -4,6 +4,7 @@ Dict, List, Optional, + Tuple, Union, ) @@ -24,6 +25,9 @@ from deepmd.pt.utils.env_mat_stat import ( EnvMatStatSe, ) +from deepmd.pt.utils.exclude_mask import ( + PairExcludeMask, +) from deepmd.pt.utils.utils import ( get_activation_fn, ) @@ -83,6 +87,8 @@ def __init__( set_davg_zero: bool = True, # TODO smooth: bool = True, add_type_ebd_to_seq: bool = False, + exclude_types: List[Tuple[int, int]] = [], + env_protection: float = 0.0, type: Optional[str] = None, ): """ @@ -114,6 +120,9 @@ def __init__( self.act = get_activation_fn(activation_function) self.direct_dist = direct_dist self.add_type_ebd_to_seq = add_type_ebd_to_seq + # order matters, placed after the assignment of self.ntypes + self.reinit_exclude(exclude_types) + self.env_protection = env_protection self.g2_embd = mylinear(1, self.g2_dim) layers = [] @@ -211,6 +220,13 @@ def dim_emb(self): """Returns the embedding dimension g2.""" return self.get_dim_emb() + def reinit_exclude( + self, + exclude_types: List[Tuple[int, int]] = [], + ): + self.exclude_types = exclude_types + self.emask = PairExcludeMask(self.ntypes, exclude_types=exclude_types) + def forward( self, nlist: torch.Tensor, @@ -233,6 +249,7 @@ def forward( self.stddev, self.rcut, self.rcut_smth, + protection=self.env_protection, ) nlist_mask = nlist != -1 sw = torch.squeeze(sw, -1) diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index c4b2c772f8..e17b7c5d54 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -79,6 +79,7 @@ def __init__( precision: str = "float64", resnet_dt: bool = False, exclude_types: List[Tuple[int, int]] = [], + env_protection: float = 0.0, old_impl: bool = False, type_one_side: bool = True, **kwargs, @@ -95,6 +96,7 @@ def __init__( precision=precision, resnet_dt=resnet_dt, exclude_types=exclude_types, + env_protection=env_protection, old_impl=old_impl, type_one_side=type_one_side, **kwargs, @@ -249,6 +251,7 @@ def serialize(self) -> dict: "embeddings": obj.filter_layers.serialize(), "env_mat": DPEnvMat(obj.rcut, obj.rcut_smth).serialize(), "exclude_types": obj.exclude_types, + "env_protection": obj.env_protection, "@variables": { "davg": obj["davg"].detach().cpu().numpy(), "dstd": obj["dstd"].detach().cpu().numpy(), @@ -310,6 +313,7 @@ def __init__( precision: str = "float64", resnet_dt: bool = False, exclude_types: List[Tuple[int, int]] = [], + env_protection: float = 0.0, old_impl: bool = False, type_one_side: bool = True, trainable: bool = True, @@ -336,6 +340,7 @@ def __init__( self.prec = PRECISION_DICT[self.precision] self.resnet_dt = resnet_dt self.old_impl = old_impl + self.env_protection = env_protection self.ntypes = len(sel) self.type_one_side = type_one_side # order matters, placed after the assignment of self.ntypes @@ -539,6 +544,7 @@ def forward( self.stddev, self.rcut, self.rcut_smth, + protection=self.env_protection, ) if self.old_impl: diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index db9202c7fc..051c66385c 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -4,6 +4,7 @@ Dict, List, Optional, + Tuple, Union, ) @@ -26,6 +27,9 @@ from deepmd.pt.utils.env_mat_stat import ( EnvMatStatSe, ) +from deepmd.pt.utils.exclude_mask import ( + PairExcludeMask, +) from deepmd.utils.env_mat_stat import ( StatItem, ) @@ -61,6 +65,8 @@ def __init__( normalize=True, temperature=None, return_rot=False, + exclude_types: List[Tuple[int, int]] = [], + env_protection: float = 0.0, type: Optional[str] = None, ): """Construct an embedding net of type `se_atten`. @@ -96,6 +102,7 @@ def __init__( self.normalize = normalize self.temperature = temperature self.return_rot = return_rot + self.env_protection = env_protection if isinstance(sel, int): sel = [sel] @@ -106,6 +113,8 @@ def __init__( self.split_sel = self.sel self.nnei = sum(sel) self.ndescrpt = self.nnei * 4 + # order matters, placed after the assignment of self.ntypes + self.reinit_exclude(exclude_types) self.dpa1_attention = NeighborWiseAttention( self.attn_layer, self.nnei, @@ -249,6 +258,13 @@ def get_stats(self) -> Dict[str, StatItem]: ) return self.stats + def reinit_exclude( + self, + exclude_types: List[Tuple[int, int]] = [], + ): + self.exclude_types = exclude_types + self.emask = PairExcludeMask(self.ntypes, exclude_types=exclude_types) + def forward( self, nlist: torch.Tensor, @@ -284,6 +300,7 @@ def forward( self.stddev, self.rcut, self.rcut_smth, + protection=self.env_protection, ) # [nfxnlocxnnei, self.ndescrpt] dmatrix = dmatrix.view(-1, self.ndescrpt) diff --git a/deepmd/pt/model/descriptor/se_r.py b/deepmd/pt/model/descriptor/se_r.py index 5a4920b0e6..ff922e0649 100644 --- a/deepmd/pt/model/descriptor/se_r.py +++ b/deepmd/pt/model/descriptor/se_r.py @@ -64,6 +64,7 @@ def __init__( precision: str = "float64", resnet_dt: bool = False, exclude_types: List[Tuple[int, int]] = [], + env_protection: float = 0.0, old_impl: bool = False, trainable: bool = True, **kwargs, @@ -81,7 +82,9 @@ def __init__( self.old_impl = False # this does not support old implementation. self.exclude_types = exclude_types self.ntypes = len(sel) - self.emask = PairExcludeMask(len(sel), exclude_types=exclude_types) + # order matters, placed after the assignment of self.ntypes + self.reinit_exclude(exclude_types) + self.env_protection = env_protection self.sel = sel self.sec = torch.tensor( @@ -253,6 +256,13 @@ def __getitem__(self, key): else: raise KeyError(key) + def reinit_exclude( + self, + exclude_types: List[Tuple[int, int]] = [], + ): + self.exclude_types = exclude_types + self.emask = PairExcludeMask(self.ntypes, exclude_types=exclude_types) + def forward( self, coord_ext: torch.Tensor, @@ -302,6 +312,7 @@ def forward( self.rcut, self.rcut_smth, True, + protection=self.env_protection, ) assert self.filter_layers is not None @@ -362,6 +373,7 @@ def serialize(self) -> dict: "embeddings": self.filter_layers.serialize(), "env_mat": DPEnvMat(self.rcut, self.rcut_smth).serialize(), "exclude_types": self.exclude_types, + "env_protection": self.env_protection, "@variables": { "davg": self["davg"].detach().cpu().numpy(), "dstd": self["dstd"].detach().cpu().numpy(), diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index cd53f0a6b3..8e4352e60c 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -22,6 +22,9 @@ from deepmd.pt.model.task import ( BaseFitting, ) +from deepmd.utils.spin import ( + Spin, +) from .dp_model import ( DPModel, @@ -41,6 +44,40 @@ from .model import ( BaseModel, ) +from .spin_model import ( + SpinEnergyModel, + SpinModel, +) + + +def get_spin_model(model_params): + model_params = copy.deepcopy(model_params) + # include virtual spin and placeholder types + model_params["type_map"] += [item + "_spin" for item in model_params["type_map"]] + spin = Spin( + use_spin=model_params["spin"]["use_spin"], + virtual_scale=model_params["spin"]["virtual_scale"], + ) + pair_exclude_types = spin.get_pair_exclude_types( + exclude_types=model_params.get("pair_exclude_types", None) + ) + model_params["pair_exclude_types"] = pair_exclude_types + # for descriptor data stat + model_params["descriptor"]["exclude_types"] = pair_exclude_types + atom_exclude_types = spin.get_atom_exclude_types( + exclude_types=model_params.get("atom_exclude_types", None) + ) + model_params["atom_exclude_types"] = atom_exclude_types + if ( + "env_protection" not in model_params["descriptor"] + or model_params["descriptor"]["env_protection"] == 0.0 + ): + model_params["descriptor"]["env_protection"] = 1e-6 + if model_params["descriptor"]["type"] in ["se_e2_a"]: + # only expand sel for se_e2_a + model_params["descriptor"]["sel"] += model_params["descriptor"]["sel"] + backbone_model = get_standard_model(model_params) + return SpinEnergyModel(backbone_model=backbone_model, spin=spin) def get_zbl_model(model_params): @@ -87,7 +124,7 @@ def get_zbl_model(model_params): ) -def get_model(model_params): +def get_standard_model(model_params): model_params = copy.deepcopy(model_params) ntypes = len(model_params["type_map"]) # descriptor @@ -120,12 +157,22 @@ def get_model(model_params): return model +def get_model(model_params): + if "spin" in model_params: + return get_spin_model(model_params) + elif "use_srtab" in model_params: + return get_zbl_model(model_params) + else: + return get_standard_model(model_params) + + __all__ = [ "BaseModel", "get_model", - "get_zbl_model", "DPModel", "EnergyModel", + "SpinModel", + "SpinEnergyModel", "DPZBLModel", "make_model", "make_hessian_model", diff --git a/deepmd/pt/model/model/dp_zbl_model.py b/deepmd/pt/model/model/dp_zbl_model.py index cacf59c16c..fdf9334119 100644 --- a/deepmd/pt/model/model/dp_zbl_model.py +++ b/deepmd/pt/model/model/dp_zbl_model.py @@ -92,15 +92,16 @@ def forward_lower( model_predict["atom_energy"] = model_ret["energy"] model_predict["energy"] = model_ret["energy_redu"] if self.do_grad_r("energy"): - model_predict["force"] = model_ret["energy_derv_r"].squeeze(-2) + model_predict["extended_force"] = model_ret["energy_derv_r"].squeeze(-2) if self.do_grad_c("energy"): model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) if do_atomic_virial: - model_predict["atom_virial"] = model_ret["energy_derv_c"].squeeze(-3) + model_predict["extended_virial"] = model_ret["energy_derv_c"].squeeze( + -3 + ) else: assert model_ret["dforce"] is not None model_predict["dforce"] = model_ret["dforce"] - model_predict = model_ret return model_predict @classmethod diff --git a/deepmd/pt/model/model/spin_model.py b/deepmd/pt/model/model/spin_model.py new file mode 100644 index 0000000000..df2f48e2e4 --- /dev/null +++ b/deepmd/pt/model/model/spin_model.py @@ -0,0 +1,560 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import functools +from typing import ( + Dict, + List, + Optional, +) + +import torch + +from deepmd.pt.utils.utils import ( + to_torch_tensor, +) +from deepmd.utils.path import ( + DPPath, +) +from deepmd.utils.spin import ( + Spin, +) + +from .dp_model import ( + DPModel, +) + + +class SpinModel(torch.nn.Module): + """A spin model wrapper, with spin input preprocess and output split.""" + + def __init__( + self, + backbone_model, + spin: Spin, + ): + super().__init__() + self.backbone_model = backbone_model + self.spin = spin + self.ntypes_real = self.spin.ntypes_real + self.virtual_scale_mask = to_torch_tensor(self.spin.get_virtual_scale_mask()) + self.spin_mask = to_torch_tensor(self.spin.get_spin_mask()) + + def process_spin_input(self, coord, atype, spin): + """Generate virtual coordinates and types, concat into the input.""" + nframes, nloc = coord.shape[:-1] + atype_spin = torch.concat([atype, atype + self.ntypes_real], dim=-1) + virtual_coord = coord + spin * self.virtual_scale_mask[atype].reshape( + [nframes, nloc, 1] + ) + coord_spin = torch.concat([coord, virtual_coord], dim=-2) + return coord_spin, atype_spin + + def process_spin_input_lower( + self, + extended_coord, + extended_atype, + extended_spin, + nlist, + mapping: Optional[torch.Tensor] = None, + ): + """ + Add `extended_spin` into `extended_coord` to generate virtual atoms, and extend `nlist` and `mapping`. + Note that the final `extended_coord_updated` with shape [nframes, nall + nall, 3] has the following order: + - [:, :nloc]: original nloc real atoms. + - [:, nloc: nloc + nloc]: virtual atoms corresponding to nloc real atoms. + - [:, nloc + nloc: nloc + nall]: ghost real atoms. + - [:, nloc + nall: nall + nall]: virtual atoms corresponding to ghost real atoms. + """ + nframes, nall = extended_coord.shape[:2] + nloc = nlist.shape[1] + virtual_extended_coord = ( + extended_coord + + extended_spin + * self.virtual_scale_mask[extended_atype].reshape([nframes, nall, 1]) + ) + virtual_extended_atype = extended_atype + self.ntypes_real + extended_coord_updated = self.concat_switch_virtual( + extended_coord, virtual_extended_coord, nloc + ) + extended_atype_updated = self.concat_switch_virtual( + extended_atype, virtual_extended_atype, nloc + ) + if mapping is not None: + virtual_mapping = mapping + nloc + mapping_updated = self.concat_switch_virtual(mapping, virtual_mapping, nloc) + else: + mapping_updated = None + # extend the nlist + nlist_updated = self.extend_nlist(extended_atype, nlist) + return ( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping_updated, + ) + + def process_spin_output( + self, atype, out_tensor, add_mag: bool = True, virtual_scale: bool = True + ): + """ + Split the output both real and virtual atoms, and scale the latter. + add_mag: whether to add magnetic tensor onto the real tensor. + Default: True. e.g. Ture for forces and False for atomic virials on real atoms. + virtual_scale: whether to scale the magnetic tensor with virtual scale factor. + Default: True. e.g. Ture for forces and False for atomic virials on virtual atoms. + """ + nframes, nloc_double = out_tensor.shape[:2] + nloc = nloc_double // 2 + if virtual_scale: + virtual_scale_mask = self.virtual_scale_mask + else: + virtual_scale_mask = self.spin_mask + atomic_mask = virtual_scale_mask[atype].reshape([nframes, nloc, 1]) + out_real, out_mag = torch.split(out_tensor, [nloc, nloc], dim=1) + if add_mag: + out_real = out_real + out_mag + out_mag = (out_mag.view([nframes, nloc, -1]) * atomic_mask).view(out_mag.shape) + return out_real, out_mag, atomic_mask > 0.0 + + def process_spin_output_lower( + self, + extended_atype, + extended_out_tensor, + nloc: int, + add_mag: bool = True, + virtual_scale: bool = True, + ): + """ + Split the extended output of both real and virtual atoms with switch, and scale the latter. + add_mag: whether to add magnetic tensor onto the real tensor. + Default: True. e.g. Ture for forces and False for atomic virials on real atoms. + virtual_scale: whether to scale the magnetic tensor with virtual scale factor. + Default: True. e.g. Ture for forces and False for atomic virials on virtual atoms. + """ + nframes, nall_double = extended_out_tensor.shape[:2] + nall = nall_double // 2 + if virtual_scale: + virtual_scale_mask = self.virtual_scale_mask + else: + virtual_scale_mask = self.spin_mask + atomic_mask = virtual_scale_mask[extended_atype].reshape([nframes, nall, 1]) + extended_out_real = torch.cat( + [ + extended_out_tensor[:, :nloc], + extended_out_tensor[:, nloc + nloc : nloc + nall], + ], + dim=1, + ) + extended_out_mag = torch.cat( + [ + extended_out_tensor[:, nloc : nloc + nloc], + extended_out_tensor[:, nloc + nall :], + ], + dim=1, + ) + if add_mag: + extended_out_real = extended_out_real + extended_out_mag + extended_out_mag = ( + extended_out_mag.view([nframes, nall, -1]) * atomic_mask + ).view(extended_out_mag.shape) + return extended_out_real, extended_out_mag, atomic_mask > 0.0 + + @staticmethod + def extend_nlist(extended_atype, nlist): + nframes, nloc, nnei = nlist.shape + nall = extended_atype.shape[1] + nlist_mask = nlist != -1 + nlist[nlist == -1] = 0 + nlist_shift = nlist + nall + nlist[~nlist_mask] = -1 + nlist_shift[~nlist_mask] = -1 + self_spin = torch.arange(0, nloc, dtype=nlist.dtype, device=nlist.device) + nall + self_spin = self_spin.view(1, -1, 1).expand(nframes, -1, -1) + # self spin + real neighbor + virtual neighbor + # nf x nloc x (1 + nnei + nnei) + extended_nlist = torch.cat([self_spin, nlist, nlist_shift], dim=-1) + # nf x (nloc + nloc) x (1 + nnei + nnei) + extended_nlist = torch.cat( + [extended_nlist, -1 * torch.ones_like(extended_nlist)], dim=-2 + ) + # update the index for switch + first_part_index = (nloc <= extended_nlist) & (extended_nlist < nall) + second_part_index = (nall <= extended_nlist) & (extended_nlist < (nall + nloc)) + extended_nlist[first_part_index] += nloc + extended_nlist[second_part_index] -= nall - nloc + return extended_nlist + + @staticmethod + def concat_switch_virtual(extended_tensor, extended_tensor_virtual, nloc: int): + """ + Concat real and virtual extended tensors, and switch all the local ones to the first nloc * 2 atoms. + - [:, :nloc]: original nloc real atoms. + - [:, nloc: nloc + nloc]: virtual atoms corresponding to nloc real atoms. + - [:, nloc + nloc: nloc + nall]: ghost real atoms. + - [:, nloc + nall: nall + nall]: virtual atoms corresponding to ghost real atoms. + """ + nframes, nall = extended_tensor.shape[:2] + out_shape = list(extended_tensor.shape) + out_shape[1] *= 2 + extended_tensor_updated = torch.zeros( + out_shape, + dtype=extended_tensor.dtype, + device=extended_tensor.device, + ) + extended_tensor_updated[:, :nloc] = extended_tensor[:, :nloc] + extended_tensor_updated[:, nloc : nloc + nloc] = extended_tensor_virtual[ + :, :nloc + ] + extended_tensor_updated[:, nloc + nloc : nloc + nall] = extended_tensor[ + :, nloc: + ] + extended_tensor_updated[:, nloc + nall :] = extended_tensor_virtual[:, nloc:] + return extended_tensor_updated.view(out_shape) + + @staticmethod + def expand_aparam(aparam, nloc: int): + """Expand the atom parameters for virtual atoms if necessary.""" + nframes, natom, numb_aparam = aparam.shape[1:] + if natom == nloc: # good + pass + elif natom < nloc: # for spin with virtual atoms + aparam = torch.concat( + [ + aparam, + torch.zeros( + [nframes, nloc - natom, numb_aparam], + device=aparam.device, + dtype=aparam.dtype, + ), + ], + dim=1, + ) + else: + raise ValueError( + f"get an input aparam with {aparam.shape[1]} inputs, ", + f"which is larger than {nloc} atoms.", + ) + return aparam + + @torch.jit.export + def get_type_map(self) -> List[str]: + """Get the type map.""" + tmap = self.backbone_model.get_type_map() + ntypes = len(tmap) // 2 # ignore the virtual type + return tmap[:ntypes] + + @torch.jit.export + def get_rcut(self): + """Get the cut-off radius.""" + return self.backbone_model.get_rcut() + + @torch.jit.export + def get_dim_fparam(self): + """Get the number (dimension) of frame parameters of this atomic model.""" + return self.backbone_model.get_dim_fparam() + + @torch.jit.export + def get_dim_aparam(self): + """Get the number (dimension) of atomic parameters of this atomic model.""" + return self.backbone_model.get_dim_aparam() + + @torch.jit.export + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return self.backbone_model.get_sel_type() + + @torch.jit.export + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + If False, the shape is (nframes, nloc, ndim). + """ + return self.backbone_model.is_aparam_nall() + + @torch.jit.export + def model_output_type(self) -> List[str]: + """Get the output type for the model.""" + return self.backbone_model.model_output_type() + + @torch.jit.export + def get_model_def_script(self) -> str: + """Get the model definition script.""" + return self.backbone_model.get_model_def_script() + + @torch.jit.export + def get_nnei(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + # for C++ interface + if not self.backbone_model.mixed_types(): + return self.backbone_model.get_nnei() // 2 # ignore the virtual selected + else: + return self.backbone_model.get_nnei() + + @torch.jit.export + def get_nsel(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + if not self.backbone_model.mixed_types(): + return self.backbone_model.get_nsel() // 2 # ignore the virtual selected + else: + return self.backbone_model.get_nsel() + + @torch.jit.export + def has_spin(self) -> bool: + """Returns whether it has spin input and output.""" + return True + + def __getattr__(self, name): + """Get attribute from the wrapped model.""" + if ( + name == "backbone_model" + ): # torch.nn.Module will exclude modules to self.__dict__["_modules"] + return self.__dict__["_modules"]["backbone_model"] + elif name in self.__dict__: + return self.__dict__[name] + else: + return getattr(self.backbone_model, name) + + def compute_or_load_stat( + self, + sampled_func, + stat_file_path: Optional[DPPath] = None, + ): + """ + Compute or load the statistics parameters of the model, + such as mean and standard deviation of descriptors or the energy bias of the fitting net. + When `sampled` is provided, all the statistics parameters will be calculated (or re-calculated for update), + and saved in the `stat_file_path`(s). + When `sampled` is not provided, it will check the existence of `stat_file_path`(s) + and load the calculated statistics parameters. + + Parameters + ---------- + sampled_func + The lazy sampled function to get data frames from different data systems. + stat_file_path + The dictionary of paths to the statistics files. + """ + + @functools.lru_cache + def spin_sampled_func(): + sampled = sampled_func() + spin_sampled = [] + for sys in sampled: + coord_updated, atype_updated = self.process_spin_input( + sys["coord"], sys["atype"], sys["spin"] + ) + tmp_dict = { + "coord": coord_updated, + "atype": atype_updated, + } + if "natoms" in sys: + natoms = sys["natoms"] + tmp_dict["natoms"] = torch.cat( + [2 * natoms[:, :2], natoms[:, 2:], natoms[:, 2:]], dim=-1 + ) + for item_key in sys.keys(): + if item_key not in ["coord", "atype", "spin", "natoms"]: + tmp_dict[item_key] = sys[item_key] + spin_sampled.append(tmp_dict) + return spin_sampled + + self.backbone_model.compute_or_load_stat(spin_sampled_func, stat_file_path) + + def forward_common( + self, + coord, + atype, + spin, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + nframes, nloc = coord.shape[:2] + coord_updated, atype_updated = self.process_spin_input(coord, atype, spin) + model_ret = self.backbone_model.forward_common( + coord_updated, + atype_updated, + box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + model_output_type = self.backbone_model.model_output_type() + if "mask" in model_output_type: + model_output_type.pop(model_output_type.index("mask")) + var_name = model_output_type[0] + model_ret[f"{var_name}"] = torch.split( + model_ret[f"{var_name}"], [nloc, nloc], dim=1 + )[0] + if self.backbone_model.do_grad_r(var_name): + ( + model_ret[f"{var_name}_derv_r"], + model_ret[f"{var_name}_derv_r_mag"], + model_ret["mask_mag"], + ) = self.process_spin_output(atype, model_ret[f"{var_name}_derv_r"]) + if self.backbone_model.do_grad_c(var_name) and do_atomic_virial: + ( + model_ret[f"{var_name}_derv_c"], + model_ret[f"{var_name}_derv_c_mag"], + model_ret["mask_mag"], + ) = self.process_spin_output( + atype, + model_ret[f"{var_name}_derv_c"], + add_mag=False, + virtual_scale=False, + ) + return model_ret + + def forward_common_lower( + self, + extended_coord, + extended_atype, + extended_spin, + nlist, + mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ): + nframes, nloc = nlist.shape[:2] + ( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping_updated, + ) = self.process_spin_input_lower( + extended_coord, extended_atype, extended_spin, nlist, mapping=mapping + ) + model_ret = self.backbone_model.forward_common_lower( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping=mapping_updated, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + model_output_type = self.backbone_model.model_output_type() + if "mask" in model_output_type: + model_output_type.pop(model_output_type.index("mask")) + var_name = model_output_type[0] + model_ret[f"{var_name}"] = torch.split( + model_ret[f"{var_name}"], [nloc, nloc], dim=1 + )[0] + if self.backbone_model.do_grad_r(var_name): + ( + model_ret[f"{var_name}_derv_r"], + model_ret[f"{var_name}_derv_r_mag"], + model_ret["mask_mag"], + ) = self.process_spin_output_lower( + extended_atype, model_ret[f"{var_name}_derv_r"], nloc + ) + if self.backbone_model.do_grad_c(var_name) and do_atomic_virial: + ( + model_ret[f"{var_name}_derv_c"], + model_ret[f"{var_name}_derv_c_mag"], + model_ret["mask_mag"], + ) = self.process_spin_output_lower( + extended_atype, + model_ret[f"{var_name}_derv_c"], + nloc, + add_mag=False, + virtual_scale=False, + ) + return model_ret + + def serialize(self) -> dict: + return { + "backbone_model": self.backbone_model.serialize(), + "spin": self.spin.serialize(), + } + + @classmethod + def deserialize(cls, data) -> "SpinModel": + backbone_model_obj = DPModel.deserialize(data["backbone_model"]) + spin = Spin.deserialize(data["spin"]) + return cls( + backbone_model=backbone_model_obj, + spin=spin, + ) + + +class SpinEnergyModel(SpinModel): + """A spin model for energy.""" + + model_type = "ener" + + def __init__( + self, + backbone_model, + spin: Spin, + ): + super().__init__(backbone_model, spin) + + def forward( + self, + coord, + atype, + spin, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + if aparam is not None: + aparam = self.expand_aparam(aparam, coord.shape[1]) + model_ret = self.forward_common( + coord, + atype, + spin, + box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + model_predict = {} + model_predict["atom_energy"] = model_ret["energy"] + model_predict["energy"] = model_ret["energy_redu"] + model_predict["mask_mag"] = model_ret["mask_mag"] + if self.backbone_model.do_grad_r("energy"): + model_predict["force"] = model_ret["energy_derv_r"].squeeze(-2) + model_predict["force_mag"] = model_ret["energy_derv_r_mag"].squeeze(-2) + # not support virial by far + return model_predict + + @torch.jit.export + def forward_lower( + self, + extended_coord, + extended_atype, + extended_spin, + nlist, + mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ): + model_ret = self.forward_common_lower( + extended_coord, + extended_atype, + extended_spin, + nlist, + mapping=mapping, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + model_predict = {} + model_predict["atom_energy"] = model_ret["energy"] + model_predict["energy"] = model_ret["energy_redu"] + model_predict["mask_mag"] = model_ret["mask_mag"] + if self.backbone_model.do_grad_r("energy"): + model_predict["extended_force"] = model_ret["energy_derv_r"].squeeze(-2) + model_predict["extended_force_mag"] = model_ret[ + "energy_derv_r_mag" + ].squeeze(-2) + # not support virial by far + return model_predict diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index a11f6410a4..b593ddc3cc 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -162,7 +162,7 @@ def compute_output_stats( """ bias_atom_e = compute_output_stats( - merged, stat_file_path, self.rcond, self.atom_ener + merged, self.ntypes, stat_file_path, self.rcond, self.atom_ener ) self.bias_atom_e.copy_( torch.tensor(bias_atom_e, device=env.DEVICE).view( diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 22fb409cad..09f8563bfb 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -508,10 +508,10 @@ def _forward_common( assert self.aparam_inv_std is not None if aparam.shape[-1] != self.numb_aparam: raise ValueError( - "get an input aparam of dim {aparam.shape[-1]}, ", - "which is not consistent with {self.numb_aparam}.", + f"get an input aparam of dim {aparam.shape[-1]}, ", + f"which is not consistent with {self.numb_aparam}.", ) - aparam = aparam.view([nf, nloc, self.numb_aparam]) + aparam = aparam.view([nf, -1, self.numb_aparam]) nb, nloc, _ = aparam.shape t_aparam_avg = self._extend_a_avg_std(self.aparam_avg, nb, nloc) t_aparam_inv_std = self._extend_a_avg_std(self.aparam_inv_std, nb, nloc) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index f066c4fe37..6938db9b3c 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -25,6 +25,7 @@ ) from deepmd.pt.loss import ( DenoiseLoss, + EnergySpinLoss, EnergyStdLoss, TensorLoss, ) @@ -207,27 +208,31 @@ def single_model_stat( _stat_file_path, _data_requirement, ): - _training_data.add_data_requirement(_data_requirement) - if _validation_data is not None: - _validation_data.add_data_requirement(_data_requirement) if _model.get_dim_fparam() > 0: fparam_requirement_items = [ DataRequirementItem( "fparam", _model.get_dim_fparam(), atomic=False, must=True ) ] - _training_data.add_data_requirement(fparam_requirement_items) - if _validation_data is not None: - _validation_data.add_data_requirement(fparam_requirement_items) + _data_requirement += fparam_requirement_items if _model.get_dim_aparam() > 0: aparam_requirement_items = [ DataRequirementItem( "aparam", _model.get_dim_aparam(), atomic=True, must=True ) ] - _training_data.add_data_requirement(aparam_requirement_items) - if _validation_data is not None: - _validation_data.add_data_requirement(aparam_requirement_items) + _data_requirement += aparam_requirement_items + has_spin = getattr(_model, "has_spin", False) + if callable(has_spin): + has_spin = has_spin() + if has_spin: + spin_requirement_items = [ + DataRequirementItem("spin", ndof=3, atomic=True, must=True) + ] + _data_requirement += spin_requirement_items + _training_data.add_data_requirement(_data_requirement) + if _validation_data is not None: + _validation_data.add_data_requirement(_data_requirement) if not resuming and self.rank == 0: @functools.lru_cache @@ -268,6 +273,9 @@ def get_loss(loss_params, start_lr, _ntypes, _model): if loss_type == "ener": loss_params["starter_learning_rate"] = start_lr return EnergyStdLoss(**loss_params) + elif loss_type == "ener_spin": + loss_params["starter_learning_rate"] = start_lr + return EnergySpinLoss(**loss_params) elif loss_type == "denoise": loss_params["ntypes"] = _ntypes return DenoiseLoss(**loss_params) @@ -973,8 +981,8 @@ def get_data(self, is_train=True, task_key="Default"): input_keys = [ "coord", "atype", - "box", "spin", + "box", "fparam", "aparam", ] diff --git a/deepmd/pt/train/wrapper.py b/deepmd/pt/train/wrapper.py index a455041526..c1040fb9e3 100644 --- a/deepmd/pt/train/wrapper.py +++ b/deepmd/pt/train/wrapper.py @@ -141,8 +141,8 @@ def forward( self, coord, atype, - box: Optional[torch.Tensor] = None, spin: Optional[torch.Tensor] = None, + box: Optional[torch.Tensor] = None, cur_lr: Optional[torch.Tensor] = None, label: Optional[torch.Tensor] = None, task_key: Optional[torch.Tensor] = None, @@ -157,14 +157,20 @@ def forward( assert ( task_key is not None ), f"Multitask model must specify the inference task! Supported tasks are {list(self.model.keys())}." - model_pred = self.model[task_key]( - coord, - atype, - box=box, - do_atomic_virial=do_atomic_virial, - fparam=fparam, - aparam=aparam, - ) + input_dict = { + "coord": coord, + "atype": atype, + "box": box, + "do_atomic_virial": do_atomic_virial, + "fparam": fparam, + "aparam": aparam, + } + has_spin = getattr(self.model[task_key], "has_spin", False) + if callable(has_spin): + has_spin = has_spin() + if has_spin: + input_dict["spin"] = spin + model_pred = self.model[task_key](**input_dict) natoms = atype.shape[-1] if not self.inference_only and not inference_only: loss, more_loss = self.loss[task_key]( diff --git a/deepmd/pt/utils/env_mat_stat.py b/deepmd/pt/utils/env_mat_stat.py index cd2943e6a8..47e17e9eaa 100644 --- a/deepmd/pt/utils/env_mat_stat.py +++ b/deepmd/pt/utils/env_mat_stat.py @@ -4,6 +4,8 @@ Dict, Iterator, List, + Tuple, + Union, ) import numpy as np @@ -18,6 +20,9 @@ from deepmd.pt.utils import ( env, ) +from deepmd.pt.utils.exclude_mask import ( + PairExcludeMask, +) from deepmd.pt.utils.nlist import ( extend_input_and_build_neighbor_list, ) @@ -73,13 +78,13 @@ def __init__(self, descriptor: "DescriptorBlock"): ) # se_r=1, se_a=4 def iter( - self, data: List[Dict[str, torch.Tensor]] + self, data: List[Dict[str, Union[torch.Tensor, List[Tuple[int, int]]]]] ) -> Iterator[Dict[str, StatItem]]: """Get the iterator of the environment matrix. Parameters ---------- - data : List[Dict[str, torch.Tensor]] + data : List[Dict[str, Union[torch.Tensor, List[Tuple[int, int]]]]] The data. Yields @@ -139,6 +144,7 @@ def iter( # TODO: export rcut_smth from DescriptorBlock self.descriptor.rcut_smth, radial_only, + protection=self.descriptor.env_protection, ) # reshape to nframes * nloc at the atom level, # so nframes/mixed_type do not matter @@ -156,9 +162,16 @@ def iter( self.descriptor.get_ntypes(), device=env.DEVICE, dtype=torch.int32 ).view(-1, 1), ) + if "pair_exclude_types" in system: + # shape: (1, nloc, nnei) + exclude_mask = PairExcludeMask( + self.descriptor.get_ntypes(), system["pair_exclude_types"] + )(nlist, extended_atype).view(1, coord.shape[0] * coord.shape[1], -1) + # shape: (ntypes, nloc, nnei) + type_idx = torch.logical_and(type_idx.unsqueeze(-1), exclude_mask) for type_i in range(self.descriptor.get_ntypes()): dd = env_mat[type_idx[type_i]] - dd = dd.reshape([-1, self.last_dim]) # typen_atoms * nnei, 4 + dd = dd.reshape([-1, self.last_dim]) # typen_atoms * unmasked_nnei, 4 env_mats = {} env_mats[f"r_{type_i}"] = dd[:, :1] if self.last_dim == 4: diff --git a/deepmd/pt/utils/exclude_mask.py b/deepmd/pt/utils/exclude_mask.py index 6df8df8dd0..9ddae3a416 100644 --- a/deepmd/pt/utils/exclude_mask.py +++ b/deepmd/pt/utils/exclude_mask.py @@ -37,6 +37,12 @@ def reinit( ) self.type_mask = to_torch_tensor(self.type_mask).view([-1]) + def get_exclude_types(self): + return self.exclude_types + + def get_type_mask(self): + return self.type_mask + def forward( self, atype: torch.Tensor, @@ -46,7 +52,7 @@ def forward( Parameters ---------- atype - The extended aotm types. shape: nf x natom + The extended atom types. shape: nf x natom Returns ------- @@ -97,6 +103,9 @@ def reinit( self.type_mask = to_torch_tensor(self.type_mask).view([-1]) self.no_exclusion = len(self._exclude_types) == 0 + def get_exclude_types(self): + return self._exclude_types + # may have a better place for this method... def forward( self, diff --git a/deepmd/pt/utils/multi_task.py b/deepmd/pt/utils/multi_task.py index ae3933a101..5f06d93208 100644 --- a/deepmd/pt/utils/multi_task.py +++ b/deepmd/pt/utils/multi_task.py @@ -143,8 +143,12 @@ def replace_one_item(params_dict, key_type, key_in_dict, suffix="", index=None): ) for shared_key in shared_links: shared_links[shared_key]["links"] = sorted( - shared_links[shared_key]["links"], key=lambda x: x["shared_level"] + shared_links[shared_key]["links"], + key=lambda x: x["shared_level"] + - ("spin" in model_config["model_dict"][x["model_key"]]) * 100, ) + # little trick to make spin models in the front to be the base models, + # because its type embeddings are more general. assert len(type_map_keys) == 1, "Multitask model must have only one type_map!" return model_config, shared_links diff --git a/deepmd/pt/utils/nlist.py b/deepmd/pt/utils/nlist.py index 56a062f1b8..d37931b65a 100644 --- a/deepmd/pt/utils/nlist.py +++ b/deepmd/pt/utils/nlist.py @@ -107,6 +107,8 @@ def build_neighbor_list( assert list(diff.shape) == [batch_size, nloc, nall, 3] # nloc x nall rr = torch.linalg.norm(diff, dim=-1) + # if central atom has two zero distances, sorting sometimes can not exclude itself + rr -= torch.eye(nloc, nall, dtype=rr.dtype, device=rr.device).unsqueeze(0) rr, nlist = torch.sort(rr, dim=-1) # nloc x (nall-1) rr = rr[:, :, 1:] @@ -335,7 +337,6 @@ def extend_coord_with_ghosts( extend_atype = torch.tile(atype.unsqueeze(-2), [1, ns, 1]) # nf x ns x nloc extend_aidx = torch.tile(aidx.unsqueeze(-2), [1, ns, 1]) - return ( extend_coord.reshape([nf, nall * 3]).to(device), extend_atype.view([nf, nall]).to(device), diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index 63abccc75d..5e631d9412 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -11,6 +11,7 @@ import torch from deepmd.pt.utils import ( + AtomExcludeMask, env, ) from deepmd.pt.utils.utils import ( @@ -71,6 +72,7 @@ def make_stat_input(datasets, dataloaders, nbatches): def compute_output_stats( merged: Union[Callable[[], List[dict]], List[dict]], + ntypes: int, stat_file_path: Optional[DPPath] = None, rcond: Optional[float] = None, atom_ener: Optional[List[float]] = None, @@ -87,6 +89,8 @@ def compute_output_stats( - Callable[[], List[dict]]: A lazy function that returns data samples in the above format only when needed. Since the sampling process can be slow and memory-intensive, the lazy function helps by only sampling once. + ntypes : int + The number of atom types. stat_file_path : DPPath, optional The path to the stat file. rcond : float, optional @@ -107,10 +111,14 @@ def compute_output_stats( sampled = merged energy = [item["energy"] for item in sampled] data_mixed_type = "real_natoms_vec" in sampled[0] - if data_mixed_type: - input_natoms = [item["real_natoms_vec"] for item in sampled] - else: - input_natoms = [item["natoms"] for item in sampled] + natoms_key = "natoms" if not data_mixed_type else "real_natoms_vec" + for system in sampled: + if "atom_exclude_types" in system: + type_mask = AtomExcludeMask( + ntypes, system["atom_exclude_types"] + ).get_type_mask() + system[natoms_key][:, 2:] *= type_mask.unsqueeze(0) + input_natoms = [item[natoms_key] for item in sampled] # shape: (nframes, ndim) merged_energy = to_numpy_array(torch.cat(energy)) # shape: (nframes, ntypes) diff --git a/deepmd/tf/descriptor/se_a.py b/deepmd/tf/descriptor/se_a.py index 0e15ba13a8..4635554610 100644 --- a/deepmd/tf/descriptor/se_a.py +++ b/deepmd/tf/descriptor/se_a.py @@ -154,6 +154,8 @@ class DescrptSeA(DescrptSe): Only for the purpose of backward compatibility, retrieves the old behavior of using the random seed multi_task If the model has multi fitting nets to train. + env_protection: float + Protection parameter to prevent division by zero errors during environment matrix calculations. References ---------- @@ -182,6 +184,7 @@ def __init__( multi_task: bool = False, spin: Optional[Spin] = None, stripped_type_embedding: bool = False, + env_protection: float = 0.0, # not implement!! **kwargs, ) -> None: """Constructor.""" @@ -189,6 +192,8 @@ def __init__( raise RuntimeError( f"rcut_smth ({rcut_smth:f}) should be no more than rcut ({rcut:f})!" ) + if env_protection != 0.0: + raise NotImplementedError("env_protection != 0.0 is not supported.") self.sel_a = sel self.rcut_r = rcut self.rcut_r_smth = rcut_smth @@ -206,6 +211,7 @@ def __init__( self.filter_np_precision = get_np_precision(precision) self.orig_exclude_types = exclude_types self.exclude_types = set() + self.env_protection = env_protection for tt in exclude_types: assert len(tt) == 2 self.exclude_types.add((tt[0], tt[1])) @@ -1436,6 +1442,7 @@ def serialize(self, suffix: str = "") -> dict: "trainable": self.trainable, "type_one_side": self.type_one_side, "exclude_types": list(self.orig_exclude_types), + "env_protection": self.env_protection, "set_davg_zero": self.set_davg_zero, "activation_function": self.activation_function_name, "precision": self.filter_precision.name, diff --git a/deepmd/tf/descriptor/se_r.py b/deepmd/tf/descriptor/se_r.py index ba1a261390..9f88ebe37d 100644 --- a/deepmd/tf/descriptor/se_r.py +++ b/deepmd/tf/descriptor/se_r.py @@ -104,6 +104,7 @@ def __init__( uniform_seed: bool = False, multi_task: bool = False, spin: Optional[Spin] = None, + env_protection: float = 0.0, # not implement!! **kwargs, ) -> None: """Constructor.""" @@ -111,6 +112,8 @@ def __init__( raise RuntimeError( f"rcut_smth ({rcut_smth:f}) should be no more than rcut ({rcut:f})!" ) + if env_protection != 0.0: + raise NotImplementedError("env_protection != 0.0 is not supported.") self.sel_r = sel self.rcut = rcut self.rcut_smth = rcut_smth @@ -125,6 +128,7 @@ def __init__( self.filter_precision = get_precision(precision) self.orig_exclude_types = exclude_types self.exclude_types = set() + self.env_protection = env_protection for tt in exclude_types: assert len(tt) == 2 self.exclude_types.add((tt[0], tt[1])) @@ -776,6 +780,7 @@ def serialize(self, suffix: str = "") -> dict: "trainable": self.trainable, "type_one_side": self.type_one_side, "exclude_types": list(self.orig_exclude_types), + "env_protection": self.env_protection, "set_davg_zero": self.set_davg_zero, "activation_function": self.activation_function_name, "precision": self.filter_precision.name, diff --git a/deepmd/tf/infer/deep_eval.py b/deepmd/tf/infer/deep_eval.py index 45eda3392f..b9db0863b5 100644 --- a/deepmd/tf/infer/deep_eval.py +++ b/deepmd/tf/infer/deep_eval.py @@ -4,6 +4,7 @@ ) from typing import ( TYPE_CHECKING, + Any, Callable, Dict, List, @@ -693,6 +694,7 @@ def eval( fparam: Optional[np.ndarray] = None, aparam: Optional[np.ndarray] = None, efield: Optional[np.ndarray] = None, + **kwargs: Dict[str, Any], ) -> Dict[str, np.ndarray]: """Evaluate the energy, force and virial by using this DP. @@ -724,6 +726,8 @@ def eval( efield The external field on atoms. The array should be of size nframes x natoms x 3 + **kwargs + Other parameters Returns ------- diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 8bc9104b16..5e8db431f8 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -96,11 +96,35 @@ def spin_args(): doc_use_spin = "Whether to use atomic spin model for each atom type" doc_spin_norm = "The magnitude of atomic spin for each atom type with spin" doc_virtual_len = "The distance between virtual atom representing spin and its corresponding real atom for each atom type with spin" + doc_virtual_scale = ( + "The scaling factor to determine the virtual distance between a virtual atom " + "representing spin and its corresponding real atom for each atom type with spin. " + "This factor is defined as the virtual distance divided by the magnitude of atomic spin " + "for each atom type with spin. The virtual coordinate is defined as the real coordinate " + "plus spin * virtual_scale. List of float values with shape of [ntypes] or [ntypes_spin] " + "or one single float value for all types, only used when use_spin is True for each atom type." + ) return [ Argument("use_spin", List[bool], doc=doc_use_spin), - Argument("spin_norm", List[float], doc=doc_spin_norm), - Argument("virtual_len", List[float], doc=doc_virtual_len), + Argument( + "spin_norm", + List[float], + optional=True, + doc=doc_only_tf_supported + doc_spin_norm, + ), + Argument( + "virtual_len", + List[float], + optional=True, + doc=doc_only_tf_supported + doc_virtual_len, + ), + Argument( + "virtual_scale", + List[float], + optional=True, + doc=doc_only_pt_supported + doc_virtual_scale, + ), ] @@ -203,6 +227,7 @@ def descrpt_se_a_args(): doc_trainable = "If the parameters in the embedding net is trainable" doc_seed = "Random seed for parameter initialization" doc_exclude_types = "The excluded pairs of types which have no interaction with each other. For example, `[[0, 1]]` means no interaction between type 0 and type 1." + doc_env_protection = "Protection parameter to prevent division by zero errors during environment matrix calculations. For example, when using paddings, there may be zero distances of neighbors, which may make division by zero error during environment matrix calculations without protection." doc_set_davg_zero = "Set the normalization average to zero. This option should be set when `atom_ener` in the energy fitting is used" return [ @@ -241,6 +266,13 @@ def descrpt_se_a_args(): default=[], doc=doc_exclude_types, ), + Argument( + "env_protection", + float, + optional=True, + default=0.0, + doc=doc_only_tf_supported + doc_env_protection, + ), Argument( "set_davg_zero", bool, optional=True, default=False, doc=doc_set_davg_zero ), diff --git a/deepmd/utils/data.py b/deepmd/utils/data.py index 194c6b1e24..1e1d7c2251 100644 --- a/deepmd/utils/data.py +++ b/deepmd/utils/data.py @@ -491,7 +491,7 @@ def reformat_data_torch(self, data): if "find_" in kk: pass else: - if self.data_dict[kk]["atomic"]: + if kk in data and self.data_dict[kk]["atomic"]: data[kk] = data[kk].reshape(-1, self.data_dict[kk]["ndof"]) data["atype"] = data["type"] if not self.pbc: diff --git a/deepmd/utils/spin.py b/deepmd/utils/spin.py new file mode 100644 index 0000000000..38e8da48da --- /dev/null +++ b/deepmd/utils/spin.py @@ -0,0 +1,199 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +from typing import ( + List, + Tuple, + Union, +) + +import numpy as np + + +class Spin: + """Class for spin, mainly processes the spin type-related information. + Atom types can be split into three kinds: + 1. Real types: real atom species, "Fe", "H", "O", etc. + 2. Spin types: atom species with spin, as virtual atoms in input, "Fe_spin", etc. + 3. Placeholder types: atom species without spin, as placeholders in input without contribution, + also name "H_spin", "O_spin", etc. + For any types in 2. or 3., the type index is `ntypes` plus index of its corresponding real type. + + Parameters + ---------- + use_spin: List[bool] + A list of boolean values indicating whether to use atomic spin for each atom type. + True for spin and False for not. List of bool values with shape of [ntypes]. + virtual_scale: List[float], float + The scaling factor to determine the virtual distance + between a virtual atom representing spin and its corresponding real atom + for each atom type with spin. This factor is defined as the virtual distance + divided by the magnitude of atomic spin for each atom type with spin. + The virtual coordinate is defined as the real coordinate plus spin * virtual_scale. + List of float values with shape of [ntypes] or [ntypes_spin] or one single float value for all types, + only used when use_spin is True for each atom type. + """ + + def __init__( + self, + use_spin: List[bool], + virtual_scale: Union[List[float], float], + ) -> None: + self.ntypes_real = len(use_spin) + self.ntypes_spin = use_spin.count(True) + self.use_spin = np.array(use_spin) + self.spin_mask = self.use_spin.astype(np.int64) + self.ntypes_real_and_spin = self.ntypes_real + self.ntypes_spin + self.ntypes_placeholder = self.ntypes_real - self.ntypes_spin + self.ntypes_input = 2 * self.ntypes_real # with placeholder for input types + self.real_type = np.arange(self.ntypes_real) + self.spin_type = np.arange(self.ntypes_real)[self.use_spin] + self.ntypes_real + self.real_and_spin_type = np.concatenate([self.real_type, self.spin_type]) + self.placeholder_type = ( + np.arange(self.ntypes_real)[~self.use_spin] + self.ntypes_real + ) + self.spin_placeholder_type = np.arange(self.ntypes_real) + self.ntypes_real + self.input_type = np.arange(self.ntypes_real * 2) + if isinstance(virtual_scale, list): + if len(virtual_scale) == self.ntypes_real: + self.virtual_scale = virtual_scale + elif len(virtual_scale) == self.ntypes_spin: + self.virtual_scale = np.zeros(self.ntypes_real) + self.virtual_scale[self.use_spin] = virtual_scale + else: + raise ValueError( + f"Invalid length of virtual_scale for spin atoms" + f": Expected {self.ntypes_real} or { self.ntypes_spin} but got {len(virtual_scale)}!" + ) + elif isinstance(virtual_scale, float): + self.virtual_scale = [virtual_scale for _ in range(self.ntypes_real)] + else: + raise ValueError(f"Invalid virtual scale type: {type(virtual_scale)}") + self.virtual_scale = np.array(self.virtual_scale) + self.virtual_scale_mask = (self.virtual_scale * self.use_spin).reshape([-1]) + self.pair_exclude_types = [] + self.init_pair_exclude_types_placeholder() + self.atom_exclude_types_ps = [] + self.init_atom_exclude_types_placeholder_spin() + self.atom_exclude_types_p = [] + self.init_atom_exclude_types_placeholder() + + def get_ntypes_real(self) -> int: + """Returns the number of real atom types.""" + return self.ntypes_real + + def get_ntypes_spin(self) -> int: + """Returns the number of atom types which contain spin.""" + return self.ntypes_spin + + def get_ntypes_real_and_spin(self) -> int: + """Returns the number of real atom types and types which contain spin.""" + return self.ntypes_real_and_spin + + def get_ntypes_input(self) -> int: + """Returns the number of double real atom types for input placeholder.""" + return self.ntypes_input + + def get_use_spin(self) -> List[bool]: + """Returns the list of whether to use spin for each atom type.""" + return self.use_spin + + def get_virtual_scale(self) -> np.ndarray: + """Returns the list of magnitude of atomic spin for each atom type.""" + return self.virtual_scale + + def init_pair_exclude_types_placeholder(self) -> None: + """ + Initialize the pair-wise exclusion types for descriptor. + The placeholder types for those without spin are excluded. + """ + ti_grid, tj_grid = np.meshgrid( + self.placeholder_type, self.input_type, indexing="ij" + ) + self.pair_exclude_types = ( + np.stack((ti_grid, tj_grid), axis=-1).reshape(-1, 2).tolist() + ) + + def init_atom_exclude_types_placeholder_spin(self) -> None: + """ + Initialize the atom-wise exclusion types for fitting. + Both the placeholder types and spin types are excluded. + """ + self.atom_exclude_types_ps = self.spin_placeholder_type.tolist() + + def init_atom_exclude_types_placeholder(self) -> None: + """ + Initialize the atom-wise exclusion types for fitting. + The placeholder types for those without spin are excluded. + """ + self.atom_exclude_types_p = self.placeholder_type.tolist() + + def get_pair_exclude_types(self, exclude_types=None) -> List[Tuple[int, int]]: + """ + Return the pair-wise exclusion types for descriptor. + The placeholder types for those without spin are excluded. + """ + if exclude_types is None: + return self.pair_exclude_types + else: + _exclude_types: List[Tuple[int, int]] = copy.deepcopy( + self.pair_exclude_types + ) + for tt in exclude_types: + assert len(tt) == 2 + _exclude_types.append((tt[0], tt[1])) + return _exclude_types + + def get_atom_exclude_types(self, exclude_types=None) -> List[int]: + """ + Return the atom-wise exclusion types for fitting before out_def. + Both the placeholder types and spin types are excluded. + """ + if exclude_types is None: + return self.atom_exclude_types_ps + else: + _exclude_types: List[int] = copy.deepcopy(self.atom_exclude_types_ps) + _exclude_types += exclude_types + _exclude_types = list(set(_exclude_types)) + return _exclude_types + + def get_atom_exclude_types_placeholder(self, exclude_types=None) -> List[int]: + """ + Return the atom-wise exclusion types for fitting after out_def. + The placeholder types for those without spin are excluded. + """ + if exclude_types is None: + return self.atom_exclude_types_p + else: + _exclude_types: List[int] = copy.deepcopy(self.atom_exclude_types_p) + _exclude_types += exclude_types + _exclude_types = list(set(_exclude_types)) + return _exclude_types + + def get_spin_mask(self): + """ + Return the spin mask of shape [ntypes], + with spin types being 1, and non-spin types being 0. + """ + return self.spin_mask + + def get_virtual_scale_mask(self): + """ + Return the virtual scale mask of shape [ntypes], + with spin types being its virtual scale, and non-spin types being 0. + """ + return self.virtual_scale_mask + + def serialize( + self, + ) -> dict: + return { + "use_spin": self.use_spin.tolist(), + "virtual_scale": self.virtual_scale.tolist(), + } + + @classmethod + def deserialize( + cls, + data: dict, + ) -> "Spin": + return cls(**data) diff --git a/examples/spin/data_reformat/data_0/set.000/box.npy b/examples/spin/data_reformat/data_0/set.000/box.npy new file mode 100644 index 0000000000000000000000000000000000000000..1f72eb7185497167688c573cd800c4962932eca2 GIT binary patch literal 4448 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$d2099snmP)#3giN=-9J?f*y>aq#DCuDyv7@_Kj2RHyNH>F`%ka;_$(`= z=%8wS$4hpNn1ieKwtHtotnEbtckbc`iqG>{nYm#)i$mF|i4DcZA`YYGjE2u>`Wej^ qqvghEc{o~MjMk^4?S#>G)M)!~wEaBVFBt6?jP?sg`vn8lF8}}r5Jfux literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_0/set.000/coord.npy b/examples/spin/data_reformat/data_0/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..4b60ae0e0bb627d0fd55de43e9e6fb0ff0d79ad3 GIT binary patch literal 46208 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 literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_0/set.000/energy.npy b/examples/spin/data_reformat/data_0/set.000/energy.npy new file mode 100644 index 0000000000000000000000000000000000000000..8754b6dad25e9c00588bb4fb0f1eec06cf10e043 GIT binary patch literal 608 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$d20EHL3bhL411@nt-EA%>Qx2SwuTW$H(sxe0TxkoW8L#;-VK|j?U|!$4 zd|@CzW1{}di$J=!G@|aziIf9vPuH!C0rEfkRlJY~(kA~(L-zv3?JvwPCHiB3Z#x2{s}pjL@d4=_-~1av_86ANKYs~SKcmLJyb8#Fc>U?~6F~mL zujj*O0{QHcAB|#xbWF6U$$Fr;*CD}YeL()J+u~g2Ksv2+t%?(np3O9K?o%LbC;8e0 zWPg*7ie~pSO1d`KOkoYM2A*1O<)>3xVPq z;Vh0bfV6LbT%j{a-SF^mycS~t+Z3l`Unju?$1jtWjZB1GZ zcLBv+cM2Em0Mbj+9qv5`($=?EuV4Yn*91;_U<{;h+o&tt0*W`! z_>t%X`?!h8Ax}kN93CU#bq+SL~a4npLF)}g2LZFufYx^ a-up=z4zXGXC+BgMx?0MK}A+?nWdyc%4o<) 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

I(#)m-&!|)N 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 literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_0/set.000/spin.npy b/examples/spin/data_reformat/data_0/set.000/spin.npy new file mode 100644 index 0000000000000000000000000000000000000000..c426f1c7f67fdedfbed3ffbf275952f9945bc042 GIT binary patch literal 46208 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 literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_1/set.000/coord.npy b/examples/spin/data_reformat/data_1/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..fc51107998e65aa7835697b31c0147af716af723 GIT binary patch literal 46208 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 literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_1/set.000/energy.npy b/examples/spin/data_reformat/data_1/set.000/energy.npy new file mode 100644 index 0000000000000000000000000000000000000000..a0eecad8d83f473d4e04ddc002f83b296cef9188 GIT binary patch literal 608 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$d20EHL3bhL41Fo9sTMsTjnR39Vyw_*iiIf9(MK@fQ1M)Yp7>4KpX-$5W zAax*}^lQeun?U)c_l&e%fc)}e7lsZX9qr}A76zox91F6&1EkgX@|J!Bs{3>Ci9!I7 zueDu%+f^Xn?URvMK9C;cvQ3s?=b{M8H0O>bwM=bS$G+)(1 zw^h!uK=5_SZGRwtZGhv&dqCR%>{B*pAZ@8_{|Dqx_w`4(qk(i;SlZGSpt;-= zXX3nrkt{v5%hAbQ4>NdXUl;#{Xsyo&+StKN1h bfy5Onk2%Q#`4iTER@DO1=O0y@g5m)Hx3L=m literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_1/set.000/force.npy b/examples/spin/data_reformat/data_1/set.000/force.npy new file mode 100644 index 0000000000000000000000000000000000000000..ec4a05f8f25ee759ad2e8c1d6c86ff210d38911a GIT binary patch literal 46208 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 R@Da2HvBjU6Zf(kd{{sexSU&&& literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_1/set.000/force_mag.npy b/examples/spin/data_reformat/data_1/set.000/force_mag.npy new file mode 100644 index 0000000000000000000000000000000000000000..844df39b76e03bada75d4b57cba5c4f226c01596 GIT binary patch literal 46208 zcmeF4Wm{D5_x3RX1w=wjM5LqykrY^jbV_%3cgN7(9ny^`h=hVH5KvJ;l>DM#Qlf~U z0tWu)_ZIFaKDgb_W{!Q#%--`^>sZ&i&U5W7bzL=0!(AkPB*E-9w%*oW>|#vp;&vkJ z+)V6t?p{7#mTudtctt#oLznzPE#=hb{3nhY%k(lc>-+Zl+MC|HtFNp0hk> zj(!ONjRY}sF6MO9c`HtBQR@ooOJ^-!n)m=?m=Z4^!2_w}eydY>c>_P6rmMn7e-Jyx z`ar(P8^v^O#wJDlz*(53k$o-@N~-Jh_E-Djk<&c~Px<(R)3xjR$0!1zAt$t!?LTkS zam_EUWAy{YqOl)2O@Sa$u;Q$r?2Dn-zLBfN5zlv%e34q50%V_-V)uy!;3q~cm)$wV z(5>ty?ZO`nH>iI%^o^xr%dWONv`&uDp}t6XJ(7tqqx$>n0Y!+3Q-034nS;|;52?&x zJj#6PK5vqe3_E1^?p(VkfI?BKymJx0=z!L1r_XzW{w))>a$n-_I+XM|o;46PO&-if zTlheQTQ%Qgk{|K9lNps50`R~!iPOCC-ms7v{GZ@EUsO(%;O>7JjD-bDx#|giAaL)T zNttjU-eP;u(q5NbIK1yuSgyq&>7&&F?K&^G&NX!1%cBr@*5r<4G!$X0L$A-0WeU7JVNi4@Dj%-5 zh~zZgbcKGW<6$k8SrC{$`DFfACQNV(9`}#QLwY^d&#M`gu<+}<2m8BBu)4g<{y!lb z45vQNnab@6v>#`7^eG)-xYkQaY{da~wcB=;rDp+kSc6^>c{Xg-iW=6*x#9~?8$$xE z7wm`|i;n&9f}ZoaB6B7&NO?-`xQK!;m`9i!TzXvq`U?GalLzyl=|jE3LiwQ%%*RbjwcoDRWzb25 zH?(?-@%^#aU;CsBFvw1AiLO@;=d&E|S@Y$hhUo&!s#Y{MtvwxD=CX#4*RJe`Ub~*dP?uD)U zV}-!+X|5ikeNm$K?KzJCMXcHkHDBQi0Qx;%UZb4>m^Y9!vpUX$^ck@oA8QGqU9_aO zkfn=EMFn#&x&m?Vgg3!>A{ovkB!?A;2cw0Jt#5m_GX5}?3S&+sz#Ycg2`M!{IL4d( zIJ)y344DlG{(c{f*1nyGYyGc6;Z#UE|JNvpVM%FyuaSyP8AdIMKm1^Ue}wv7Xdr-8 z!?KKYB({F)9BLQQf$#|R)Ez1%P%eA*i{oekj_@BLSLW0PmK~!qJgWhrcX=59L=>PI zU-ewmupb1`pKISW8vv_b6U%Ld5%`8&_e8Tx78vq>c)&1{4mJHF2OeHZN28gyAg$mB ze@fSDHa7$T(rJ9&aE1Qk|1b0ZCThX3&?^g0pUhVeE6f0qlP#Yd){H@LZY}TZ=@59v zUczeZVhCw}LV7aR9AOad@;w{R1gXfA-O{~bXm9X~D*Hh;L~zCZ8eR^CscO~x@Qa;b`^t8=} zM5o9{yoVy-y>sT!nwKdUce>K=rFDn2xto`gg7e^oCVgP@qj0FMdl{d#?2M9j#k$II zL71`OtG-qlf#7^Zk*FfYb*uQ8Ki16a2+G#0gS8-G?fya}$_OTWiRJW1o5L5KrIu8o=KS)5S>`&N zJ13-67M_h_J14GsCF;Y=10MBndxOwH`$iOfs~dV|ja(@Z5Q9t~&8_1t5%`&c-BpPy z5S>??=3Cc|AnQy2)Z?}YbU(ejWbSbQ<|^$!GizrAeW&8&1n*Si;hTGoJ#Z|;SS3d9 zjJOm)iRb!*28Gb~zB}lGG67bPKbc+LjsxeSA!Lnzd z0*Y*&W|TkAgs&RTTT55-ae^{sl=f02Sn3Q3%H+g>ndO@GkhlbLA7qCl!Rl4IEI1%?;xIjA<$Arqpto!ru4PVK53)y&3+Vjy;IHn3S zIgG!y@)hEkqwiTMsszy6!(qVZ=z|yQbW)u?bMTIDg@IVH6AY_+k*<6DqEEZ|y^INW zEUaw2a@xrc(l?()Jj)Enuh}*~jmrG7Zm(RmXLTI>_NI&-T=YTbhI3!<1mqz5>it`( zOcv0m;!>I#=8CMj)KOe^ws_<0x0f3~q##+Faw~?|XDD)edLuDC|6l(9%MrNc7rn)j zqX{o^IO0e*HSx%K3Oe1RY4FX>Lyr7W7}SwfNBU%{fYI@o)Yqb+VE9ptyj&{;*2G3{ z4Q;6ag`EB1hx#B0@Jaiq)er_9_Z}VNGYf^am_F`&`!F!F7iF|P83A8~ex8Zu3xRFM zM`f#&;ShOVx%yR721M)DT(gf$1<`P$SH+BRAo`QfPLewqfc|af)etHb`H-IHN2T$g)4&H5x2DAMWZ|jvJvAmb3VVF7y z4_TfUXX;Qv!5LmU;lW_^&zY>+ZVAJqi<2atNg?2qbf~m7HXiX0<;!b#VsLL78UJIa z2v~f6BFEW14h53S4mZ3Hz;;``@_|7GE28DXeikoUQo-C96u{-5l#US^l+-Q z8(H0>kSabcrGSea9u7WwsK=y>BqAd(p74d^?67L}#rmW0emTmcYFri5^JHG!n~K3t zi@$WuBc=bs+Kh#C5_6gHULB# z#5P|Vxq;>Dy_OmIwm86lT)R*s5Jb6G9u}T(2hrBTy&T?Y_(q%+Tn&Z83OkE&!_HV%bgQw;ivT9e?B}JqeISD2;`y7V zE~q*ougLz00H^bn4IW(c0qd7+pm#kEd7lZE6?A4m#qE_XPwqrm%XxM!b0HeqWDe}y znkmQX+*qxvm%Z`m_si}T4Nh=jyG6}7fPe|)ndE!dz0v7SWW4Q&6TIsR91A26P}6qf z6zflKSIY{D-zQx~6Jg~&@7GfCKXn&QktJJjx=nw##VZ8oEOR8k&U@ga z_lHVfak)Y7jk?~@Is#_hZyUvHe)!|X_J3#36Ymq@HdARU0skAe+?lHN!~I2{S)GV| zFRfhPls#o>$eMM5XQt)f`u8v6Pcl+eZ#mf?hYpOE7Gc5ikQ@Fex_o~ue2U`{_Z{@b z3j$7^F6N=&d|li4WK}S9SqYLHZ?@`pqk0jgi5KKL@EDyo<| z2==L31b-sN&FPZ1Zl@d{j5T`ko&aHdHki6?d=G(zIH(Ajp7ZHd&0qeGb3EuF0 z?DHH~aWEPgTzc-F}!%0dBeznM8 zbf36+5O)P)T+X~Cr7dyai-Czr#VicFPPs*`^M&A~ZzFwnzZXomc5E|V3q(uTj|SqE z{^(Go%CB+B1MYV2`;wRKk1NhPr-Z)w{HuTdascq5!rpBv7o?Yvzi`+n7Bxh!EQ(I^ z;ggQ3fa@B{SabSv;RCg5%zZq~TAz6Z3NzjfN-^8xXEbZ>xfz0+(zN!&PlQpd{y*hN z)hyILoz^-iQ;mJbF)mZF2{<{ULuuOLK+K!{JiRx=@KBNALbA0y3Q-%=T(QW-Q#Hnt{muV&z`Vperu^5RsJ2ioz;) zdcV^g(x_eRLarNb51nHXHciz8*k;~7-L88c=*hHyJVB-A6uojao^|LUK=%)io;l_JY3b*MX|Wh!^h z4d!=?1ZAC$hC>?3woEY8^=PMpw$7y#gV|S4gSDOhM|MRF)9GA25Fp ze|~FS6X-SjrwT5(gHP*5AICs6m|QMvm~Xcs=4+WB+iRYk4*jiT?ZMvLNAE5athln|1-*-IyYAzB-3?{jXDH0&A#Bbx~Rs!<9pnxml zd04fy;%#{(0e(+p`cK|TKyJ@5T@T)TwDrp42XkE%zo+@)@k>Xv4_`<=r5AxoVhl@H z+^^upZgb1Y{ls-k1@#Xxo*-1;rqS&@s*4ZrU%OT-=!nJ)AA?$kBmd?9zsx_0P1x7+ zf@t(7A?K=*mPXP}rSn|gyTR@o`A&jSJ}PK-{`8$n#*cfR1xj3zL&H~35BWUKMJu(b zn%<=>G>gBo*qs~)ZRe&E9hAfHu8&EmgsL1mZ`S79ke$X9j#CQbDV5l8()~}2Um`ZL zB<>7Qdtp%TBdWLWYcN&odX8^Z2KL0|os8(tgf4A5MTyu*WOt4hUNo0S(R#hNrQv&U zXVG&1?VnNTJHuyk@7ZOTRrz$>&p`p&!!87EXGMe3_!jx|St($sm^7@FmB;TxedF_l zWMFDZ{w_sahjV&053#kFA;TB8L~Z?YxJEW$(jRUE`1~tcKEqJOsVgTYYo=tAR3_XK>3>3v}q2 zx0{sH!J|}BT+ullj#Rly(^EuY^!>M7gib3wF6kWdGbjRXlV_M-d#VJ&Gn#a*!b1P} z|I7Sar=Ur#GqDcNI-RHPXo>kv=A3UA>>)hVYq_*30`p#9?4KpI!SyY2B)s5=)zl>K%TWby0bk1S$Y33rpCGAE4~68Y>tHfw#82tb)hIhHb$;sYK1>2 zz8?}ubp(^i^CebSBGJDlE%LRwJ*Iz&-M=2~4ljSoTrew*fUs6lQEw3kI2j*(eZQp( z>J{J7s0oUKl}iPdB^$PIHB63>9^{GK;Vr7ZBvEklhD3YT4O@6i!Z$}t+@C$SYTk6Z zkEl0VyfLR(83Ug${9Nl0AnJhRA{6%&)j{Q&q#fhWT==jsl9c@_6&;@?RJ|_C1is-T zaV?_`kQVr9HFL}jy0)0PQi%Pj2pN{V%O@ScOti%B;RP2wKz(N@CL;V|1$qN&-GS{$E|^rv%$n`o`5gfYEPY&aDsZ6h_Dz9h1KOD!b{p~D5>D| z&6*?!@-x^p^<%GsWcGLOpZ6OuZ&G~tR8lJNrEYByhP2`6{;v23PCocrbPq%qn!u*C zzCrV00!(|UJ*OS1#}{d5yHhnXVY)VbdDAB2AWvdKYdu6^&1cBH{Fla!O_Y6^BRy&BQh&%v?kkMjm8|GUm$QWDA(Y?!JR zvbt zk#5_KC;}LDODr)668Hc99e?!>#6Gy9T18<6&zi@nG8Tj24@Ipo(@6r3cxXI{u1td6 zmvbqGtxMsO%ApLgy(xH*eW&pjw>OTC1SH;y^MQ?u!kEau0(6z)X4R~jGWwKNeNfl6CF70sW zaxwM7fc{rr)pB(ZXshkYI(#w=pD^UF z^GlnckMB7LeqTR4t|b%Ml^F=^QG3eHJTfKfu|1zX32?@SFSWz_PG$q?Ly2zh=@4YE zPJqdW1k~Yv?%81u&@DdBi9=DVgJE_M$C!tRT=FB%x(#)#zGf`{IC zLY&Vu-^~}eym>^uhSTx$%$S^)Vk(X>eRe$bBOksTwp|yuGY1-* zf4b7chKBxUm}_M^^l zCM$d3dYC&noRsCO-|Y{v??UBN`;#zuthsydR1$`#P}AK^2!((n-SgekLHL$SQ9rBN z3Pqn$?a;ptgzt6-vJ(?sP`45n$P7GiIjn2;nnygCTZhsJL`uQHfa-k~hb+_)?h9k) ziiiImtvwAcmVxIzUmX9_%)$#Cs~V=mVL&Gno0YQafmLr<#|R-pBTelAjG4oO-lUrse@Df-~1xXA>|}f#hjzMZ&-Q|CeKMu2_`etTD6%46Z<7$L9(hEf$_)|x_)$}Hx%#Zr_z&KmVvA7$>6i>fvEh4 z!(%_C5q2MiyiazOaC5(N_oo#X6u6aTu42WJr;Md(+U(N1WL8*wR_RW}Nyv?fqvz#*@uk`bg+r;Y!)bIxc;!3jsf*g@(O!DM`9iATlVb#(qT-5 z$9yHv4rm2J?i%YAqR4mB4|?c{s(vySNgKNGJu4^W+Mx(Mu$Qx?n$7cH{{PGTf2lfD zkow#k?^rGS^zizF&&giSBl`NbB$Gv%I8#$9;(&#xKNoX{_5v6fz8BYOi ze$~2zo=Na#%=D05UNBO+O7jduIIw=drm?FG~YJG#yS*Czmz&{X-OqPwN z?t5bE=D7INW*rQT3EThdT^TfxUlmklPsLE3F^0aq!GJ3_7wkV(g6V7Pgzw2w`1U<% zAc>#}a1>44xWF6+A***;Sq6QuzKalFy=;i?azj#13ej+nK1A)@6Ljf<>Kf~RIy^y-zQ^KvzYA()EAC(9kAR!jV%JzC{JI@~AhAB)p6dOE= z`nf+Q*Yfsw0cTy;ZvQ_n*b}Lyl%W>^gtp^t4IzH;yZOzCZJiTH(6marND=Vxugx#s zZ@tlB%Kk~SvNNbw-YHx;6^8lh6ml6%UYK{jPvHQu|1z~Jt*O`F9~C7wYg)8i@L2WZ zqc1gOz(1P4bopKqo`3jpzUo9i_LK|MY+aOtU1ra`xt$a7gLln`;fy>CJ`%kynOX>Z zcPZv`b{8TgsbG9qLL%~<9{R+1-3j*4o(=!SOhDsncB>J^-gtYDh<%czGpKQC<=0&b z!#9h!JdRxc=YRam_>&AZ=Zrk6(?q{@6JMhNqHdY)+ar>F{-9eINi%Pl0G*30I+c!@ zkW(wJ#ao^RiUK9a%Emmfr(LS6Ma32re4@B_-O2?!v2HGp>zX)s`gEGKM;I)>36kfp z^8-m;y>*WPZ9Jdk8j$}p2z(^&&nd3@!H>r6YZT%{S{%#t`IlKxm4ADlV-H|O#{Xr0}Bgc~X%nw?0m?R=pb>K?; z<$Y~;L$HINB>l#uKgu(_=5+SWfaW+UxGG+OoPnua74J(?cu=&Gmd67=Y0T_?CDn|M z;So=_Vr%iujDS*Sf(|T-xRg!)3_(Gy$N}{Qf23;6=*=3x0#$#6Dq4wpq07yeLoTL= z;~0w?#q4)4%t_!GmgC7mk0~ve9I8v0T#wUp&#!=pQG@Xn9dA5&{PUlEo$grGZ7}ir z#ue!4-QIJ^n1DXLof?%o;s5ggUk-q$PjlkNh(0PB1wzWb^)OI0R((mrBa4+R#QQ{lMT`4WRy`IuugthVyf8RsSQ%Y@XzLB;*7W+X9Rdk0BIz_p^j{C5|C5i(M(KV6!%pb=z%j7>Mkq*!6y)xabVNIo zYg_NAJRq*J@Xw#;5qRMM`yi{P8xU$jD6L6yVLti$%_lYWXnAX7aF{j$xO0b3b%cAs z;^P@I!LdmERMD3Hq0JL6&=6EB4h4g#v)|UXNd^AY(fxSngf?_k7}#v~Ie~m?`7w&y z;rN|@w&6RUE9`X)j$GPs2QL?oIRHD0W1@>$g-DSB^0fOz9 zPgCmo!aHwGs?1#x7~0FRT1C{Yc^~$ud~M?iiOWAHy^M%?FUn8u#$g}etvSVhd@c#j zna;`h23Vnl%gs5%r!kO8G2c$T;SEutQBS5Hha;2r$JZJy{?I{l+W+CxGSH$Si8ary zMCZFbMspwXK(O`u-BW47a4GW!=dpcPVenIb?2&I3SUnj##AxY(-C@5Z<9Gt$V}{&` z?USCEl>F#T^%4OoGb^X#t#ZI7py`f!tqbsRy}#k`#RK2}SZb;#^@sc3En%CNJaM+I z=EfYO2X0@b(b#MafC|N8&wxiB=vVZxxp^lOJr~ItyN=d@soa#~BbOMQ5z?B{>-WI$ zx;JsZJN;nS#}A&niLbxcu}U{i2gB&vwAK9|S3&k=y$=~l<-h#@mjghGp{^>yFbWw1 zTc7;c9fa+9vUh}UTfykC*{{7j#J&c0HC=6UL&j5`cPJ*zP`YLZ&K^z2nmx3CdEyD<@&SN!BiD+{5QUHfw zF0utCo>TQmMNVJO>a*5Xz^U_8;J|@cEGqT9qPjppt_)F)6$fXOXb^5XL}Gzv<{SOa z2V;U z_i=QbQi+7Vv=cAH3SB^-?WJRIwiWCRsoR&AO@ORN_Ir(@oq*2nM~PUW`M>_pU*=!P z|6FmYw>ao44p7~1^#?E6>67nPoPl1sSY5T{A{Pfcph>d_!hnk5XX>Ztm$lN}|DFC7>+l?Hx zQNUuHAl^{u4H@x8aRf#wxOJ2H{t)r}Bd2#}tIiR1$qU-Fyev{6p{hTa8yx|G;`tdm z;0rNp?2HfRO`Jw}lRF0mS zk`>1q64T0ExdeQ?^!tI@Jx`SSHr8>FSseGhsY)%;440GL3e!PFeC_Dn=UCS=9Yv{wD zq>f9E)Lii4D-Ev9=}=@Ece?hLMF(1<%_#CS0`Sb@Zc#m=KYXOWWc%J@cjTH3$u(FH zK;NN{9WHz^7@l*cJXFgAOS@!WC>}1r+A4)uUdbG67pACHQS*X|yNe#ANp5&B5zV5$ z66>zc!*&EG1K6kD9&t%C1BETf_X@v=!=oNaM%z7tcyz<0c9z8gWfK$kG-fBjt0S&Y zZr*lBhILOr$yhrWA5+{X7#9ZK2PZjI?F}&d(`<@;qBow|>ngE-A_LlDw&NL99S}-4 zOqhfmA!q0J>#&t1usqX5JyVo`;lumB7AD(4tjOtkS?^FF?^$0__BFuAM=SW!6g{zt zZy>s9ArtW1e``m>op9rVN`yAL0>=?U=3a~hAACH1o7Egkeya@C?zKWsr>!3ZvjlM6 z)gS|(-T&1;f0_T#;~l0?U4qf`S}gO8yi|GJ{KYnnkS`?2cy9J$2Y>>)h}U$L?J2;)=t{Ve+JsllefSBM?#M5rl;4*Yz#wR!EU-OfRyB*w1`k5x2Av zfFv(1e7>7oqN15wzFUV6INXRHvCs{}*6CprImR@MJ0iY)vpW~Isoqd@Th+kzk>*m{ z*TLW$b77wSX903-`N|l^hr_;6Eq59>JLt^5OgVQj7*}@I?uNUE!?iinxOWwHP@>qN zH76O2-&!obl8Q6$G49k_p--f%SdWfnrg7V%PP_MBou6OZJ&wKWntnk zyb|{&92g!nT<+m=0GdDbhAh#+$YOSWsdy$FHv4>fN!1)+^PJzfR(&w~w0#J2|3RGF zwb{|--(U*VHE!vuF8}(Ue;I$0B2kt$%?ov)I%Lz&oEHJ{qo4CBeJbF{l3ic-^=x?g zsjl3&G7bAEj6X@8iG$a9yH|?pav;j2JYM*&H%NZFX&-DC50u9?1;dyMfwsy2_55NI zKHTkYyIs~;IQVp z((|z#41Mc{;e$sbR;PnTFJkkrh0PLm z2V64eSnye_!SHfdZRy1_c(!x7t6#g|U;h8g0ib14+gMK20sG98S>Cge!S7|9Z>eQN zalwXfp=H(yW^^6iQl=|lnrJiK_J0w$uHh_A=5qy7Da~A2J=2g@FyqNDl3b!rzsEYS zISiLRSe#D0=?J^ty2yBw5d9-;-HpOW!ttQ-i9m`GNBGWQ;qm@P981d@9>&AbWxJpDB1s@@B)h;= zUVt7huL5_kse)9i7^`_AQRkM&F}-gp7rnnIm(y?%=jT~z1{7>p!vRw}{x=ugQFl*> zeovkbOt6U`>;|Gg!J%U)AmWu6@SOaNZ{I(bGo$PX^P-F&=3=K<7*1O}8;Vz9B1GG~;_9Hwg? z8Fl=Lhm06$g2h-MQidwUl!W?&1;?P>&X@=Ih?Gd$Zv~^DG0{hBffNB z4?q@g!I-1=9(a)K#?@M5}f2 zEy@jIpL0rcNlGyYX#aR`A1VvLpfsUw1$_@(&l{iiuS*1p(?4I?(&eMvljfbvY)9Zr zgHRs*d=LaG55`ateZm`$=*vure)+VeB-d-UMBnq5T0Xw10OU<5j~`C+_?Q3xGXJNd zL|vc1)Wgkp^Lot|VIZgaLr21g=u3H9@=(S!9^P2X%Cqrj!LR*OYi$b|IFd!{FU@X< zzbYi2kiQ6oOH>?Gsi@E?gklXzVq+Fp%(&CnJP@a?{e6>%P}-u&@X znK7AIxUi=%NoMAmRJ3%Kq!9vdHBb2u|H&; z$USxZQ6_w;)L2sCsKVLfN97&dO2Llw(`_$0T`+&6vYRxNfY0U%!r7h$0dq1{lh8mU za#f_xezDC!((*Q0f>R{S8X5;5xgC##4%a(2P84AC3C6=9nF_5huNlP#8A4C#yf~$K zC>|?*TeW@DAK2uj=#MsNL4EPUW#tSnlsK|+lF`={=6i*v^?&Msf)e-s(=G&j!w{<1 zJor!l{Fm`3QI|Gwe|n_|(+lnQmx&QzZT?b3uw^!UXmLMXmS2K%GtS~d^O%$7HOyh2I5izUTQQeF_H7U!I;%sU7j1 zzs*j?SqFH|J9&bo-WT?<$MCYzdZBOUdHFAv?r^1lop4Hk0L-V!{j7-l1lAKN4kw8| zPA3tcf)$l?9D7K`%l||Yx9?r$8x%9aV&3=C^ybCbBsNfyTa^l|=CT^cwosp+T&A=;rPpX_{ptkt?zTV^qZ_mv5>)E=aOj7H2^d*Z?1+PJw?%&K*ruZh$7HKbOEiZ6 zW_OEZ@Bk$$soRD>(^2r7$++3ISja0C67B!LBroU*zxW^7!@@ZcwNyej|oySd@oGcvTUCXN8RRLc6`2q** z6JW)cM??Qr4w%~+jj);;Vd80{a*Ep)a6o@l^^LAKbWgwd*8C<6c|#v?tzUHqCEwQ4 zdxgX~h^)^vj-If@JJJFw)1QQp43Aa)3d?~RdD*O+Hwux0yY9=Vsy%4a%Ff#(x zt(S(5Y{fxdoZ(s8@oXT|erVa`toM)qzs!FQW!9yDxd05;da)+hoCF38pM%Y{@{!Ud zy|&}27jiIHk4NcbLht&in0E6LoOV;BB1qSU?4WLy8y{I#^eh+2g!SQc80PoLc+z1x@zHGY4T zMHh*_9Xf_T0y&|0K_rsw*Hj?v=J=*Q%u=&8mAT{?hoAKgn%Yef&ZN zp8NJ6-jR)g7suo#tsSiaE!4@L%@gN@4lx?K3e`$Q$}=-#p(jgG=(oY4Kdo87IT&8#bGZ<|&>54TyH$e@3va%cK6Ze$o;I!L zQEu>HG(!2}pISV+t;)o8Fajv5M$aS%#-VfH+OmUwIfl?(jA-tShr{&v!KdZVYil#LRm;^7JaU)THqD4uO;>T{_YeZt%|2+!oY!$a`&8 zL1{&cp%9}VD)5*>4fM4uLY(a!z{6Vi=9qsL7ObXrN9McD{c z<7IuzE~kLiyF9g@ySd;_#JH zjQhQ1;(1TI;Y%i=&9Y4b_QX6k7(GZlpJuACUCRlpS-&P+(}_f~xQ%*wVt;u0ocerz zkqdI^crEg1MdGobw;XXNm2omZ+iHa9vn&uBH?!c&!m>z4s#}54&@C7wnbKkTum1VV z0WeV;$2$~CTrVi`Wci`;-*lwv|l}?wrEDcm69~Tch z!M6m82RgW+U!ceVL3r~#g4|JV%zcblk43vj#ZuJ+ZgAB^r|lcI1OEp8H};XO@u;TAZ`K9CQ9e?x$C2 z-Ut4xfBrK6{x0XOo`3Pep|6iy5;@#pSnT_rfv7Mza(?PluJ8LE#bCP3^%e`@6D0az0aqfdmOvGl+6tI3phSJYCf9e$0nAdG$$? zow6`J=-|VS+E`fcXfIIL4@Lc3rvfF(($IO4VzzIuG_d7#x=40dp?;(Jk1s^OZye2U zU>l8vj>4eu7T!#xzxRGUgS`Yv51z`Oqh-Pguvv6(w?O`wds}L8(WsLfK~lk$jC4Zm z%w&J;AmgX--2Tw3|MLG|=3npDMfzKYfsoP5WVCnH1-R2zbC2u|1N~jnpD(rf!!*q^ zfdyG&|1WxN|J-^AoSht2{#xb*)-1^~{P(Qkj{<9u*F}E_`NQ-6;>7@H+&Q54A;|^! zKio5;3kiW&epHlZaRE@fpVVcEJ{^{RU3*6n8v|TZ=A|PV+3-vCc-N<=BCvKw^Vom> z<)D9Ms9xN{w<@MrB4U)qS1Wh`5Mij18~21nHNZWjzpocy$MgC zFUr`OX3bFsgBQb($IP!?VOWRn_neD2=JJWTyfzO2KauDURvS*xGr-z*=SU2(pFglc z(di6WR#MI_77BV@v(e@cB5=;9T0uRUsBhLIp)nv3Ah0k@|C+NeTHn_Y*fkXdLW864 z@|RtqW%QHZIMGL3DgE6;T#$epNw*#gYI>pf0M@Qm#De#pS^8g=nRt@z>r07}LKHj1 zSa{yw5oUVtt-gMejc?XQEo@9I@q~g1n=kQsc5k;2KH=vlAg7{$xr>As9?=!YpeBE4 zzh52ovLFnfJ)5^Xd(Z=|tF?IweSG0*p6CG+`~8P64iWVYYTNZW62$#B!TQ+j+#XM; z|JAD#cp?&19y;0U^+e*HP_6j7gG9gM<~q~3I8kSC;^P*dU<4#S`YkI=^r>`8+Qsc5 zu4C=|8qa7(_il>NRG2JZ|`JM}fI0O4~$lA95+FXM8X zb>Mv%*w@a!O9@v6>2K*3yc#~ZP%~q7=vf$ezRK1w4pD<6WiHhBFZ-aVi^Mf4l5h~o z;}#iKjl=_?v!!FgZaBo#7*jxeZtTTr_Jbu9ZooQ3vWMoZ2M)!Gzj5G;_*eh`xit}A_!!5>g=yxmDyqeg@!{d6jw;x9%iQq43(K!b^ zH23b;+KUKa+miQ>Q?kXGvQ}&B0(%@tu{yf(k^oi>zq=-gpTo<2jx<}&5kp7bw-gcQ zbh`IHD`1+o!S0IO!&5f)n0IGh``Bv&d^;kg=BsOie3iM1g!o)OUB_@vY|zy|5xPm*gMb%M?0r39CV2qfjP6JTGq0#fdZ zRSm{?a9b3k5+v654Iibe7k&m}dEPZ%rI(ISs=`xq^>GB+-};gEW!M@H>`Yy_ciIU` z6l^a)?~Op2s8^ZeF4n-|CoQsh$q6*78{g4wM&L*fiH8lPHK+vnni_>UKnG)c<~4x` zlvD3&a=&i{ekW!h-)^>t3!i0_wm*mC`Jma+{uXoCj4ORnZtV;SHTU2584%;rb>Z_F zW$Sy;0yMf;v&9~$}H@M?xMII`j zO`Nw@GtW7rg1s-aetHx2av3k+^G=~36kgE@kF?N5>u-iPDzdi3EDZrlk z&rcjzEAW=nE#IDvLtxr*g!ZQZeqOi@)z-WFIwXL(&MUd(d_*vtLPw17-TMcl@x%P?sQxN zME`!g$u39M$B~%*-Oovgr~~>OyGG%CGXtJ^P4kO37GR6`O#%L=d02Mo6Ys@%1=#(v zF?-X%3yb;426rF-r~m)U_>-jGefjHtRSb~PF*rAtq~W><4dr!)5ct3LuKb(I_iLL9 z8B&PIkPw;;DP%`7l|!Z^GLIQMgPG$PGtYCTqC_fDLhaHZ@<~EU6GfRyGDXz;to8m8 z{qXJAz3#K0v(|I%``P!t_r9)$yz-T(JUWbvCA@8jAVY$=#a&6_ym~l!OH848JakCJ zvzldQ;zeHeh2`*j1L^aia6IgIzeH1#$V8#vC1>Lao%QouO_+qO3I4@YCL1L(70g-Fq95{Q z!d%zK+2oI8kagy_;W_Jxtb62dy=3c?~cF=cG znvGYGidENmGdSh_kU6vHxWWY|XkouPB`iimt%BImce+8y5I8VX+I9lB{P#64Uo7F@ z`S&jez|OT=(+5`NVpjdh)X_butFYm^rn}!T@C*_rOSh*v|WI#^-xaz?@}?5D24T}HpYpOWJB&~7|I{qz4Ey%-h-B!L z;$xTfDi-|={PFm%kA+GBL_Di#-e|+XpdqtQ){5r%yWrJ)8jm}qJXG);9H-;W>!~@s zi7vS0tE_u{8ORDxyAR#-;M9A`)t6 zn=kPwy2Hl9$j7hmfu^S7Mb<0f@Ri@HYKNQ)OuoOHeznd8ITW}qsg#$2iv(BM=!twN zQWFV_AODB{zYKpSkquYPKZh9NK^CRW*@W)Z36oma%P&lTZ~f->d=(?)3F=8*P@`j6 zN^lITloDF}4*mL#$pTAnkgSup6ZcxBE%mz;IKcDjzJ*_I=C~(u=vlQ@3_6e8*kTjC6Z&1g~>&W%wz%yMqAaM zej@ZDp8VMLtM4Ly@V#AhP@jR`0&Y#lVH6CMX(xwC)F7j<>=BO%QQs+X&-^c+0)0`X zmw3MrxDu9Gu-=?Xhz{*Kzt~TMtJU2Pi^nq{sJLZ%7grYqBapl(}v_gy6g^jCWxwu$h^ z%SVprYN!W*t@DI}YE>9qI4a<7*cO9t9~js3Z?l4KsbzNeCYy4>8d4j|oGV9Mq@ex&v~F`7ka2GO-|nUx!>{KnpO3K^g69 z0!jW*yiJvPkgzw`%WBYg&A~fiH_5o1z=M90PIyQp+{%+z6q_;wy`v}3`k|jQ4tPhR-f?a{pJ!(HBi-8IVvIkonDVU`af`$z!FKQ6*5=qt znO(XY=#OGNTV&m>BC(W{_UC|=Ir7||+aE{pqdYJZWO`f@iKBySo60EW_-lS;ZZ^{& z%@?;uxf1myI~HZ>I1O{$RKnqOhu}w%4YYjylIU+X;c^ghtu#kr5dr1ai~cAbZL!(7 znBb%140qeV*&L_We$w@h^hc3e>fGq5NIdh;bEb0`04CW zPLT_^E}Pbksd}RYiQCzNm~Y`IbI|#r?gcV_ziAPou6Qf=c(J%mC@5zdb#@TzzczWl zS84LXG4`Y#jh7kVcl&W+gn}0&tJic18+)PT^S&QN#tfk4Jnr{TJq_93H-7PSMd7-( zrpDye#JSmUIp@++CX~E6eKc0q2@bRiKb~J5j05g?Z|1E%n!oC@(7);pLibcvww5!n zchkX+*gzlTlV-}$67>d|pJU_>HwHdxl42X#O4R9Xed>aWJfUqcRw>6h66N{7ELm;! z!1evh&7)_$;cZ9Ph2&EVytC2Ln^)ThCrUjI?`rUbs9~Y|3MrA;+C8$Mk>P>rGi2S? z5l=|vjyiNZoZze0HE8ZT>4Evf63Ta`;^7r*HM)pa;uciB5$Wdk5C4A||3YJZ3QVtQ z=ypF)fADG~=c`-_{@)8BdT8~&XEz*HsIz|s%^x;;r&!*}r3NqBE9zPwT zgB6>}&eSk{cs9UO;k}1}0!dApt(!{W{7^+e+84sF)}_puo-iDjA8(FG4gtS&EU`z4 zI%au!h+KI%4ev*04u88A3Hog;nHKwvL3oZq5%i;B#iwXlvD!$;cpY}`elrQK-wi5r z9-`n&4v_SFPlqr5Y`<68=tI_$M|PMp1tQo!npPOoG5ctJ-7#YwwEXE_D{nx7c9Bw* zMI(ae%Jf~`xnLcXu4d7=XH0=KuSHf4D>{-I%>y()>L7a^*FyExbm&*JvnjcCGeyUK31^S3reFvvDXnfMPE8f`*YvD+^h2s|9=_(zK4I% zLaRMc7hfmyUI~KXgo0-!1P>K=?ZEX>js(;Y&XJfmrGn;GizIeJKd3I)BS!CQ3Q8!Q zW*Q%egwt!34=~yL!ka~7%cpjpU~l@)<_3w-mzxz9Npg#UlyGy6esv1?8JpbE%SUX$}tQITM|#!B|Nvk!zX7xr-8Pe6rLwal#qZ)cA%&xZa7 zS&;u>qF*f23)x5X6;_^x!uwSFWOq6R9(Q+ryYki#&JUV8g$6{RG!MmKJR$(A|8wsGA_t}=P}Qr-aOp@^jfe*Sr#g)@aJ#&76>oY z8cXHg2I6RkiSL)aRAf13z*Iey12sqbZO8YdqwkNBfoGRP@XolT58r1m*s^|M!nQnw z*v*HedKM_SMM3^nQ$id>CRUsLT)KeSPq{V7pOaBwnBUud&KERhzCP$_kHEfYMv@)D zv+|c;|Ns2sMYVUz@Q69V+wjj7tQ*K+E5GF3UlB;u?;d9RRqLZ+W>3DzL=cTVV_TO7X0_@VDU?EKK zqKZHG+Wxp3SXlsjCL+i8yk|hGu=3mfSr-^}Y1%t@o(jfH8In9~p`g9}c51l%A$-fE zXYr-@H1HZ7f$u`aVAoJSslww5oi|wHpqK)0{}}a2?hAvd;FugfLT4-DCi!~{ITTkM z%jrHtM1A($;>?*~8&rK?{P6>w=-2lxulwOj=oC2#6-}tQq5B$ft8XkcG~O-#{yEAF2+dWCoFfPCjrmWu6QJsePY*Uvz|#%793+ zCLM>RSRQ;^LxJtOl?jhB9N}5qv_u18U*ps)u1tLIvv-;uig&ZA6{O#B7 z{ze9?D=n-#>5jmv_@jQS79C?5%AK!?{{6@1d;h4>9N{zb2hB)gefiXxMX@O|6s5Ay z4#zn{fNA);d0C?F+ImCr*d!S~t{gboa?TOdh1OrGJV3`T?7pTDHw6OV7f>@kj*zltW5PWYdn??aa*LNCX^ zI{#FqBOU^ecS)*rs4-HQd%{dX4cTZ{ql=E1e7Gt+gYYA1t#riYHyLe>3dC2ZIO5pw zNvlaESMqx!~b8#e?d--j*6=WUO(RW@pJ(lGXoA52`bcH7kg+ttBxD(oK(MN&|!9Yyn7;u~ug9qS&) z507l5;vY+msSoeeL65ynfHjxk%gz2X#4AEY=QT3@=R4Fvv&&MbmsoFkQo-RT9~J93 z^7(#kQ-|O+9PBPdbez8aJ!Bu@U(-cRdrO`=%(@oN?>5R<`$YbKWoRvnylo`&(oZ^ZLO$#BQA;QmoT3ceB}-4_w35PfY|r>{FZLDdOM6L$A>>^J)O zno}wpU1fg9R|wNUIsQfbpedm{_NsB495Ejj%DIz8{09{x?9*Qw5qe}QiR~dTi2H|( z2V=GhLo_JoJry0;M#I2mdRyp4+Q0q(F9(3)tk_<^#8}ubS$p$<>0#vjajcJG69nP; zHhmZD;Wsi_KrCoF3pA1ifwff z`7x+xsPanDraNRKDk-q`F{y(3qewyq1A{cis6GGPS|$M}^@eI~ALT zR=8K_r8vJLd3UXQ5ROdV{Q&yKSkK4dw(J>;K?i%Z_=AstcvEzpg=Yvp8yr8i*b z13oJG_epS&Q!#zfDi}*{+8*Ce^lzMKIXo=etONaPDHq$d1CaIAp4K9Xa(wjePnJV6 z1E)E=*e|sc`d}1IvoEXSaG>(nFtqVT+8b2u%H1b_MU|Ia_3e6U?Y zwS^3OI(u5DK1Kp-UeMvr#auXbVBc7qUk0itYW1oQr$YJ}UHW>-cyJB8zUtw00=8^t z@w>PxAKLd{nQ=%Df+w3Knge&6VcZLuKR1O!fLA({f<(3*X6&@X$!joEY zK)yO8>ZnM8;}wp_%g-{vF2H$)D=`m1VQzS6<54Icf3`(pI~CPl-aCC#%MWiWwYN=V zdgIs0=(dd$=h0E4)287}DN-eQ-hCqOc~e-OIOatexVj-FGhux?vW@l1DLpiWY%Zte z&ii3_jQR0aC$2zTn{p>bTGbXf*^S;D?}|i`$C{1RJHj!_N~!sFGX=doHlLLp3Bx~` zQk5+8Dfr~V#}+5egTUmZ#Bs}$j6ww&kE+srvHd|#%>s7<(CjL{%G6dMx#F4Tt&KVV z_W!>efm>cQP8Qqy14(Pg)EV1ktY~`AA-XpaSZ`~t-MHEw0~UvqSBQR>!F4Z}_lchd z_r_2y%QNn9ve$IKq-Y8@c86A+3d;uHx$R?;A5K86{0yt0Z5pzD_NNMKc!1i4%{Mij zT_MkUA8-0VG8XZC+TVA_0aiVEJpaAi83cS{e(V)X!EFZ1HYQ5+IL*cXgqK8?YN5Yn)^bfeX$z zy86a^kt;2Dgh?<7Y!>!BZBvVe!`VlBm~Vw*S$+cZ^W8bH;h6B=pNi+XB{yUMJnid01ygQ9*>eKM`cH4ZJITyI!J>SryNCye-T^lr_X*eQ#!B!~2 z87S`!N7rx%!0JIS2l`zPtU1P1+1#51jh`1*E5cv zj!->*)2Zqf8HTH0eSbAq0<{7`ZLa5XAT;)@hnZ3)Y?!v>KAYeU1)B1@;wB-md}w#B zL`XWsh%w??a|6LcD)dR`Ru33IWEgsYB@_l)`*q8V0zl?Z;H$K1X9#QYXFm3m0_J9J zb8j9Mz*avtZkW#liEBAKIk6lx<`PP-ND_LebE2dAdQq^bIJvfK)(ZmlcwH{c6MS@8 z_q;5*9Pq`r7b;5iXK+`C^_67X0{kPaS8B@X4bK{W)@<^L$3g3v_#K7ecp|H!h<(HX zcDoH#o7~L7vgbw*_K(J4Q-RTz&J`DQ7jrQETH}q(tF=e`Yvb_ke(4X@9xjBAyiD}O zWp@-=Utn=yDxA<_qEc+@9DrnfPmC)(7a56J^^B@~)b;ST-Q?s85oZr-6}!b_o}9yi cvfWv9kMm}A{TPCJ33&mOfzp5Y|I7IQKOJ0qh5!Hn literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_1/set.000/spin.npy b/examples/spin/data_reformat/data_1/set.000/spin.npy new file mode 100644 index 0000000000000000000000000000000000000000..1444e35c5fd898f232eeeb76afd5e57cc3e5cd35 GIT binary patch literal 46208 zcmeF)XE>IB{5NnEDJr93l!Tr<7;PShwMEr z^D?4@`*Z&P_k;WK{ltTQzh}SK@jl+4&-p!1j*g4#@={qtN%aOLg*}DKVIyNZL)*hr zoQI`vpE%6VdHA-q?LAxlJGZQDjf@GusBdX!3~pztZ(|Jq&nF?w&v{bf2tTJQ=l|z_ z+vb8=E_qgsBNOE;_5(>n*lvj{W~gHnNhtQOhk$DZ3veC;zpeA+`3U$_y#&h}@LgH* zVMoCEBNMyZzzrM*@`AvxwRK+D2cG;uW~3YZ#@=UFV!;hX)Db(lO^2a+FSsLJX45Nh zT&60%7d-86t@uyyty3s@(02lje$024M;w{rL6C&|=7At1*?qqe(;PA){w9?2kFcv#q1R zb;OpUU>{VMVEAJSoSkK}B9X6HD~^YAKr@T>)nssqkEi+Hfu9~MKS}t~t5o3uaCJ5= z?-=msP9!&O@Z_$)(Qv)kJLdk2*WfWW?HQwr@m{=W3Mzs+OtVxc8 z=$nceOD*Id{rUtQ$ojzt4Hz+)}Uy@BcmjBUY{hDP4K&&r(cAX=_re#gJcE zP0hFn`Sgr;>U@yb_h=lg12=h6*DVV!94M|M2(IIucD)chL4S6yD!BW_SItDfLCJV) z4Y=wZN=)oY^FcL*8$97_+2=hWeJE=!?${B*Hf$0q-c*-P#tL_J6pCsGP|GAmtHJ~c zU)*fjB+NU86)jHXH-n$*^F1OAUOIf#9{O>t%yFt#@EpgR&T8Nx&Yx~bf=8D11;bnv zq?GSW^qfDhwJZj160Xg6J1Gnqo!mTm|JW2p)Tyr&?oDE~u}GU1$TQL3ehd5K;rXw9 zM83gn(7p-WDSz)eTrc+CT4}}tzM(h&zu^=%;+=`Q5#zY}=&L8=S`pYTBK34s`V0!r zWf&aSBVp1Za^{UKL->o?$wIy2QItiK|Mk1Z7C;&GPh8xgpunEMSG`dJ6MRJ6g>N~o0MG$L@` z{O-IgaJIO08zt~RJQUS2uuqAvY;_g5q!-&AZ}8cWsN}!bBd`h^h437>?(PFE9uec{ z>ak0QyKh7w8;iYXN$E3q7A5g-;T}K{9!ZCWo$FD8S=leQ%@;AGaR2eh)$M41ip5S#i zFK{5u2y}5!xv&rNpYEUnTX4sO#K~9M5s2&g%-tn$#g)8=0pNlvGb?YQN2V|C+Y0!9 zkKo3hz^)1bjv#?KB=cf_o8hT?WUakIEt=kka$}6zUV$^lC)b;UN4{ru7Xzp0{Z$_V zer|{Gd1G+axOz!RaK}%FSA)SB;O$;WVOWFy;Ig*b`5F^$^wvr@g}$ zN4WmF^I^DN9M=D2#T0y#2x%*v50=%CmUelTB{b`kmbf1yO5ak3*T)aD zKo5GIamE(*M466P3ipEFy{mpF20ZhSs+k$MFLUbg)#fss>)PHx_|vcLiq_zd_fROU zfoJF`9g7E7JW;0t=Yzz~gunR({@kG73id~b64^xVg4=WD4wArAnEme(Jq{z@zjVNj zxO^UOhq1{zPsu%jLtB#P0M{B#FD<+)4_jwQJNVxs{H}~KFPam zIs*B)>Iv3($j>V(%cw&>J?bUPAb9=mvs!WB>mzTs5zjx0y zaI78p)bqgpS#)W>WaYv6e%!Cw|3+@p0AAGg4TXLr$ZNrv0Qp$vYT0X$|KM@sE9`@W zMtpX|K3F_iPZRdU&+jDCkAW++H7$Jr?-TzkqzSIi+ucsg75C)(L(Ju4GHxRBDnfhD z54QFZM-7*hXTIylHfmVXnY9QeELmN7JPA!p2SJ;W`<8@ z;@}U+&s62XQ@#dmC;Zy;O_j&Nw;xtFR0fYcr(Gci&T;ue(wZma63o(8xWJKZ;xk|>(QrWK0gFc-(%(tLUjyeQ+3DD005ZiX$x^I&qSG^;GdiJq=^+X+9r z^Y-EM;0vu%`=!7i6eJ7@fuCY3H@L40}kEej*_2h1jCI5UgM z|2_X3{QRq3c8yr#El1Te)t|2%UqICvEc+JkM`OIZ^z%G;0wZ-P;SvulbmqZ7ap-o8 zf-@#lUr__M?dO>v0bgCBah(P)J9|5r@WeCPGJn9QA91>lgS%?hvJviV^sS}(e|omR z90T`JeR_lFe^c)h(+hqdS2Xp3bMTrTC;YzAms_pipV^gOlEG^LzD`J;VG z(W;P-QM~`61M)RN?Ury|c)+t~?0mMb_knh_naCHFud3V|+!fTV_E^f7tMx&kwRVKj8ZA~Z$m$onK zU4#4v-~NAoJSVB-gc55fPA>SceO!JP1#&$&xoSRyT9r-(T>)2~qUs+7XI)!hC;USD z?1BQg9K&=Nk^d0#tL88G_+#F)s^G?rmeeHhcuoIT6c$6shC-2A9o+n(P$=OsKlgnh zdbmeJBhmNb&%E>4+J%N*C2yTQJ%R<)9IhNcG=uqKyOcCoJ5kv?YFfx+jYsDsAdj`_sjeu4 z?=|HJB)svUAnh7>=z(vGXFSGG;9Bsfvztj+=6L{RP{<(Oar_ZKk(c-xstV_TixTy3 z5*X`^V z?KvDt$sL8G8vgtKzrkODVRa9Yj^-f#nf6B6MU8}>e@{+4sXK;Fadd<>UK_;ME|i?w zr%pn;ZnOfGx5iKg&7YP_;C;=rd74*As7kXXn+;s`+|Vw{SF!mGm~kZ0Q|#TW!m zN2ZB3hy2RS*~w(cM`znTF$ez>>vby^d{Y={Z5rGr|Au!uxMy%j_855NR3kg#o4>pr zW!D=+%^!54T)>5`JBQ1`>*Z{ZT!Os%!G|lQj-jL$=^l5oX!O4e7m#;`Zj#7)=-$$o6>`U-xYN``j^e98A{^ha-JF#@>w1^v`ml}WH!T+FnU589I%>@-wNOXlcQvZdfWG@;Z@+>n0(;EAc~Cu~iHn zro3CBC|!x^hg$e!vgfcw{o?y|Qa9Rs?4a0l$Uocj%W4|(R-tZliq=Su!z$Zg4Bu;_fju=oo z4?W)R&929_!Ow@A>pj3xmy^WF@jEf7lKJ_u<=9pf8=8GBLTDaUUM?tM;H^iU62!t%>0wc*^06?PK82c%Rv{ zfg@jG%5ZRA zug&qcTLqtdWp#o9oJ&*jWj=V?*5~R&O&wUco-|taz8}eJ_pqg~krCf>Rly0!i`hPT zxdZZ^nj@57A%AtjWquu;_FLz1X7B}9d?FA0ox*v6Z7(X2UWex9J>Vy`qN5AJ1DXf+ zZ3kzQ`OQn@cdBrRVsJ+TO8T|H3T)8k)3Gn76A#U&-zi?0!wjZHevjL1k;xSIhn2LO zcxvz_`wsmDeBu#bFU^YzJX^16%>j9S3h_oFpCQ+|Z3%iPs-+xYF5X?+Bmn(b)nSeF z2V9=_#^`SF%d*icun&IGx?kWgxc^zpAU5##Ji4w$;NP=Y^QfWUwI(PZ&JFiUR0|RA zbJHtk0rI0755BR2*9iSVW#Aip`~Ugz-J=iQ?bcerFD9)!z0_my_ew^^A&X?B{DC%C zHF6%;6xE%|+5H23$#6PQNck0+eB$ER1^KO2AFcQxZ|-&TZyMwiL^~<>fSdRu-W%Xu z*}<>rEt3%{QOsISo5M3Z+_&(DcA|+jze~;%jfl5(SAgvCdF;bIRGRy_4)4C|(DdnT zD;|vfe3wryjEamweM^PzU~X9EXFW4`!QYjMZb1 z!5S6cphYqs{#^Ko3A{un&qp8JZjOt27Q8x2T#x?Q97@=9LblAW4~dx9@z?X@;X_7G z{=ju1J%-on+K{IfyG~aL`EQ1H6^!7r`)r%dz{~2I_IHCH-b+2hzIPrqMJYZXe%OdU zljxeFeOhsIN{cixx2^J>uP@|(aN!Z;?F-IW#W+L( z`H>L4EDi8)m5#$N!2bm2n-fk|Uj5Pt+(m6$-(&DWJ*{u6(Bnq0%C7`Ixb^0bU~sKN zO#^G-y4p4I>fmCY%S8d;5tI+EZGj$qDEgTxIJKX33W)wDxUN|KhJ+O{P_S#-&J?_om-do9jS^eWkRL>@22zq#m6-&6FM^m=VeLwU( z*?%fiqKbqxjvR0$n~Y-Lu%im~!-GhWv-Hvg1DO*X{r|@KL)5tpHVO(}7N&bD?ESiqh*`yO+hb?HGM;oCh zu%>JuC-krxyGPML&-|*V2njqMQzmeOdz#s&af9>47jyT4Q;eL9-wXaMjb#3Zx(EBs zi$@QG-_sJIh5gakN!QmbkeAjvFFXeB9k6zh2YinKl`R|iKmP!3@ByHyvD^DX8qr!* z#+!ml5)w$dJpJ2x3e)I_{<_znfsB^rT`i{i(B}F^3sLtutoT*<(%Zky$g{51YtO4` zWVh$RbP)A8KBDiKbq{*}To5Wwg`R7g8N$oZQ|0?qBmi8e%ky?1_^D$b`+C6HpVfZu z4{1O@`rfg}Q#T_Q#(*CMdh=M>$9AhPD{A1X;W?6qzGsPz;&q@S$g3SKZCko*QbUd`BX9Nh8CfGgo| zS7NsluF}g%E|65M=yNX;Vn_TvT5Z-d8N-~8kic#95WdOtX?u3Pan zX(ybf(MXpaGlHKRYTt32n?>$|6{3NVzgMlqPVD1D$yNyaBk$C^2R*>2Ug+i!bBjl= z2o8d8@a_NS$8)=L&6<6L-xv6Dm$&ZaD571ByPthx97R2(@!erEgR|!@-hJHOhnmzF zuPap!qD{3{U5wy52X1N*9zJCvsSJKE#WS`hlZ2(;{Qmk-y$Wrb^-&M!o<8= zpA0QC^o0DJ;ho7#kbhZhCC3I%nXGB+3vP4PD)1_}RE=Z9FU1)wuQuP98!?DNVuj1d zC;QQ+$q)3z+=)=3^{0>*e>5qo2Ki@l)j%-3eemD9{U*D>gUVhV zC49^EoM;vBR!*+zz2L_rn`*qlhb}!$fa}8JOS5-(gTK$)=1k;!`TJ88!MVTIpJ4$% zU*i?v1^#fK`zBRzw-o+Ia9yY*nZF0F3zxI>=Meq5JC%Zoo}XtTJ)eLV<(W(o`}lcj z$uWcPxc%ra(eE1)6iMX&`3G==51{*s2(4d0ITq@WmdPI_BaS0?x2}I0z!S#a2W7W~ z;`+agnw9hm=;XaB9s|1X`1>F=iy`C>DM{6qK%VR4Ir}BZOUJcHyMg!g$^R+_FL4aJ z`x|_>*5M0=@4B$Ry27hVH^x!Z^}_a&r;}LganD?7T^Cm9I^XqVjf85Rq?(&dk?=G3 z7kBSK&ybR2HqlQ$M=L!IJz61|o<#qh8&rx#;0cd|p09!1ZgD>28&Hlyd3I>QT)Zc_ zRe#gx0W_wp^aJw9_s{!Z(1UX#sdg@Y8bD_^?FxOD+m7^>r=4%o&S9P_*34Z^U5Ho8 z@X~j;LG;D6UF%TpBre^@aW_M27+nr$dh-~1oSDaRF!b~uQIGlqJ^dYe%(uWrO#G9H zez~@Ve4?K%Te0ov?`*Uz`Xk?wJd3&OE}y^XFoJl+=8xGxK4{C=1fu6~>5$rH*mHw# z|35$eg2r_g8KYUuZ)X{ir9O(T?NavPU8zEXl;*3_;52bhPvwHUJ3Vzg4gT?>LZ__S zJZ{K)yE$)9BQo$ivAq9bGg@2jlsFA}G1c<*bjXtmqTk(sd=+zNF5ztZ#x7@r*EER> zUjW}@rCCMv|5bP&Pxy=30xSz|Kc!wsxTXx76tRy1?e-i5ZhlVOpYZ6VBd7Af)3qC0 zG{6TW-;qul&7y-6)-8qVqqso(ERD=c6;|-9TB50$KnC~U<|&p=;@9n;X`FTI@&BER z{5&NGp&vhZ;LHivg}+_3xN`=2{-g-{rGa0jdz5PcKDckvW6gUEMfD1?6;MpzNgapF zn#Tt4px~$RFDE9^TglSB<_#m5zvY#HQNRGc>~Rny=+W-n6A0IZgXRq_#GvPJ+KD|y zAv35Zk=o5SehjY_I9+1v`TpPc{|){MoaK1!dvvA?(@=?~hN{h?{TcoGItPtVI6 zO5o32jlv?px0V_;90qr5bQzZgU*(Z3AUxECUhycnpzQ$;eel-X9-^_}0fmWwg~996 z?+tsFmSB5riz&6EW2o(~wU&U)EV5xM-g_GIH9`}+h`G+bZi+`BZ?gYkEzuvo%jsPd zxaC#7g@fSLM>!niz}Z7?NrZ!MId{1782HNWT)C4oU5K%k^yszPEEXU){^m;<#Db2F zQS#v2o_=eD>;IG$;skFQEggiph`JUR7H|&>0v`-*`~!2b z`bgBp3*cd2oJT^y-#HGg5WbLnR3$aU5?5;7TAL)z;-<%vS+2PwSU4{@iP$ID*NTgn z8=Eszv=8!8RK|y(2+r$$jxrKlybRzRuzmm4`>;Q%T=6(81Aa2K>jgEqjghkGTW}`F`UolT z*BrNl2=7c%Tea{?9k4I7T8*Wh*+evit6-_Aa6ybXMVZ~s3({>_dd zyG@xr*iHX1{|!qrVi&w3duLZS&dbl@-U5F0r$y5h@O_`(r>KKF@5;_O0vcZ%&hJ+>_JNxzrQOv8XI=`5(*hr8KP+<*Jd`Vh z(-_=2&$_}HoZQnE&IpdUhCdMfiQ{LM^uZ1JFFg63(Sz%?s`=Hyb2+JcOu)}(xmoSW z>_Li?fmgtB;G*PiaCBzsd1{Mp8?u%BbIi_Z6z>_F;pP>aK{NGk(<+dEk+qy)2zj=o zjKl|!r?ppODOMOlS4C{CQ)|aDeQhP**pV^x#*JwsSU26>G%=O6FuAI9ZwDJ5$VQ$3*0NK=A|R} zKmP!3@Bwg|vYNkStVYgJXLJqANAaZFr}&+xW|8%T{m$dd11R2$OS88E-rv2Az1T`; z2;C}^rWiNvM|txH1db@o!Oyw2EIpViMw(Z)Xa`GB#~481i;}B`p%* z8HL;OCZLC(A?=|HI8E3z#Rc%}*H0h*2LF)y>bN&Jm5a|7CGfEGxPJ|t6sRxZ3Qqqr zwkotQ4hN>8&nne3Sf)+lqLJe`TBl&xC%rs?&+Hw&GFdT&X1DJVg*-M;f7WyaoFOOv za25DecIV4m;8l_4EF$1Wv<~5LU1%R`{YwY%J!^mb*udHD-zY2wAIa@9)(8K2z2+4k z`1>`0`r6m~lim0m@0+Ng4~@8A zw7SC{eC?5PXBha+<{yu;z{~IU{jdd}ucDlN1|D|*gi#IP(AlR{_e>zK#->{24|)EiR2HJ&_+D6zDfmFx zZYxvpk|RQ`f#8D!w~L6m)b3Y!1Hippyr&AmnR3rb=At>&`5-{UqM#p1kh3Ip?i8Z) zoLt9Ttmn{eby35IlijF!S8ztrheqV=n3QJ)uDi?B26|BH5p$+5;FrI*1v!EHN@r`n z0+Cz2o^&8sl+^Td z(x%AhLYJ1Hm;$`+evx!m**^ujCi*-}1t&l8a1#fQO1jn_5B@2QJBe_Fji!!+e-xBy z^8)8d=s%bO?&(so89e3@uO^oK?5 z59!*8!)0znn(6PWnq1qQ{|rFQplHZt^1q!iOy$jlgwb%5@Qn5b%L- zytfH2In7)^h&iplljo$qRt#9Zg_^i0X~ zP}W)`+oJYp@gn@bz)c@ELGU;cnYlyYF7p$Un;@_JNxzC8eBp6ygcNvY&?Yff@W&b_ z8BT(4Gw0}W1m}$3r!WIODt0|GL|*^mqsi;whgc3zGn;=a)9t$>NCont3m;3j9B9M=%eBFjrt|PQC~xP_GBlxk^JDKA<$BR~ zWf%1`uJC>R^rV&tYb}-yewYgTqk6Iiy9oHxQr@Ed;I0yhUi07 z?ep%k*X~DEBfqw^!1rktT1-Xj^%AS9zn5=^ym_0&US7z{YIDV1guJr6%9m|0x5H(; zMHKvopiXW%xT^P7aT;(^Q+u&EIN4B4wg{ZfGBR}tdaBwCmw3PpFYdmg0shZFfE#=O z%a^Bl`mCoheA)*^(XSS)HK4Lkx;}_*-WRvBbeU_l|cT}Zkok%@HRF>-x2W3t`Eiwz@I9$ND)1rMzx${-~!K- zvz)3M@WAk{(`3{`wd35+nh^9LMk66D zcNjTuQp;|YCL=e!kmpZ-v7c7GgRv zBm_MV?_GG@Q5}Mrv=+B=v~3g{tPK!byBR(^mPk*&LRP z3efm6QIBr9=?b{B^kV-f?nAuLGiUnT=P>jrdMu+I(DS|Z>y|&VGkDYQga9$=bgXNX zArY|azt=xE`0=Mm8fU`xjooODt|RtGOg>H(Y&+f2KZobaNB%m1M~HtZZ3l0^F{ecM z$YtJ-gd1FqC~E~j#J%oHc$JpC0paHcn(}{w+tJJQ`~>%RJmpU~^Pgv`o#3ZO?>;B; z-Ya~Z#N3ZUoOwk5=ZRZ%|J(nV%?X@u+VN#C_%6#dIie?yf6TxUT<_{`-!AZ&A)ZG> z{!ON%CiG+e>G}6Xrzuj)P}}*#)%>Ch zh?e(I!Yq9S{^{|=aD`t^(?iVN===Y#&wriu|a-bD|DtlCGx*QuA~$F!haGHh@Q#=-0ha&#h>Fs2f;~F zDsS7t8?vJWAAy_72_LCtnLwYe@7UkKKaMZTUhlHt?8h-h4LYztdiYYlp&oqr_+Mp1 z@WboVj?>_>S2{DGA2)QJ*=7bVXOpZs4&K2(6OajhyGdu4DL7(Q>YW7txaLw{3$Fg7 z&({r{(eKvfSjtIsE6}L0>3%;px~XbtF)@loe`oc>{wViV#5%E$cf{>BI3G;eMyb*X zeus6tWgB?VHQK{Y;Q#vbx6#+Xc=%a>z@-{o!>!-(if;zh(WJ=N+#1ITjl06Rzz?;k zRz!ib>GbXY2_B?l=eG}haYFlDGZ)@-Iap-VOB?)IeL(2uj~Gq~Gtjia~mrD3e+e&WPk zH*EUh-z%$J7YZ0w$I%!0h@-raH*awAj)nY-Vp2KmgKp0$W*-E<=<#Tj@P>1rXB)x$ z>%OH3zimNAu9Lsw45#t*c5RxTgX3sWxMB&;4gKh#lZ=PFj&$#@7RYyhGB`&#&HJRb zci@((@amL-8Kk)$RlZ0!W-`WOqG2dP9hXLTvo!D|uf`>&14XlH=cGws^123WU z-XjL?TCa2(UX#TRzFa{>&rWtB_2b}FJ9do1T#SEZ{epdP=9Y^QQbdp5&g(>v_zL9^ zk(b%@d>5PpqDiVaK=iENztchV>x=w7P3)uXE+tRw!%|JYLG=9V&)-I0f4RGTyS8l~ ze)`?kQc-sTbM+KTvTKZ^8TxWSn*p{g-H2K~H`unNoNN~#_R!JL9_F9?6SqVeo z?S?;4g0Ro?oACO~`8!mDmvtwQ%I(5bn2X&n6zHpihZj@GsDO)PF(q+=TL=a<68Q{A z=hLtcQhu6M40CbN&cdr7Qb~y6eCYU+^Ehf0Rh@pQ){k$rXeF9L-uRZO(M8D12$>r0 zhx}mR37#?ipUBzvgA}45Bf8PUf#%N!urYZjt1hvR^0!PlH+0fgxEsz5FMgc%f^$Y& z6rZ&b`>?ye?}l^!@A=>0FMvxTYfmIof8ZE>8uPV)IkbKH(dE_hpLp+`l}q<|;OF>X zvhSYmm_!%DokT^oM)2pj|d|&swOBxPS>A84C?mIrVhc1WwX%-LHTR2chHsg_KZW~^aVH~?ZbsJ+H z8H=xPlA>rH#8=rrWyL2U~~=oS5!HtcY{l5pD{iCAPsMeHj5SkCu=Hd6Z?o$S-7V`-hi|!O5}IQdox21 zGU{0)^|+*=d;O(VnsVP!Mhd4}RSg;a$(9=MQ*Xzni*^G?Zug=Ku6yn=Kz=~vT>5Ls zpa1Qua~<;Ur;;O~A8q|e$9xAoqSn-j@ROhDwTQe|ld^afcu!LAUM=ugfu=8v;KRpu zEq??Tq&l6g2VN$7HdttR7G+Z%2t?z}*k#L)XL_!?hq|okWTruwR^xTyuG*Gx(lVun_pWo29Z_XkgWemOJ(a-8e zhXoHsFoNscZ{D}lG9L{#9UnUnuIdr2J2Nzg`mT(-gb{grMj{d0H(%K0v2Zb_etKow4`GmO{hUai`cCF7D%Y1C1tmu9*s~casf7f$%XbyLkP(AFi$j1l0 z%rE@@+>K={#Cc&)d~lD@1U2L<(`&D8hWuk0?T5ReM`&LFRTJd9!g9?7!MS`H$P4Aip`D5F^W`g) z2JL6Z&{3n9H*{2O=-O~t1kv+++stPH$kztyxcr5DWHF;TIAW}N_(+I6iqC6QTCT*_ zVaGU0%lSSMGG7`qTuOw`hh~gWbc+6g3p_*(;vs)dMDO-7$TLliJY9!;MMJBHy$kF{IEo+#w7*JHR|EYd?e0dsM0ppiD51BxjYDJJ}w{-zJB z;C*@t^u#{&$=-qff(JLvM12D%*;m{ogR?a2bshxwd*yZNJNS#iG$z8s zIioX%FGu51)9agvo*MDhqB(Hu^KzC=fG-R- z=8b_@WO>q`1*dEA{?ZKo?Yd3&H2BYVM`dQdwxF)�Wtr4|Gv3!pSmg9t~VNGL@zn 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 literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_1/type.raw b/examples/spin/data_reformat/data_1/type.raw new file mode 100644 index 0000000000..d9664c7a22 --- /dev/null +++ b/examples/spin/data_reformat/data_1/type.raw @@ -0,0 +1,32 @@ +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 diff --git a/examples/spin/data_reformat/data_1/type_map.raw b/examples/spin/data_reformat/data_1/type_map.raw new file mode 100644 index 0000000000..7eca995c31 --- /dev/null +++ b/examples/spin/data_reformat/data_1/type_map.raw @@ -0,0 +1,2 @@ +Ni +O diff --git a/examples/spin/data_reformat/data_2/set.000/box.npy b/examples/spin/data_reformat/data_2/set.000/box.npy new file mode 100644 index 0000000000000000000000000000000000000000..4e817ccff59b0654c0a1d033d884d4a22b3ec4f7 GIT binary patch literal 2360 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%ohB^wCnmP)#3giN=-9J?f*y>aq#DCuDyv7@_Kj2RHyNH>F`%ka;_$(`= z=%8wS$4hpNn1ieKwtHtotnEbtckbc`iqG>{nYm#)i$mF|i4DcZA`YYGjE2u>`Wej^ TqvghEc{o~MjMk^LZYKZ$TE@=j literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_2/set.000/coord.npy b/examples/spin/data_reformat/data_2/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..aa515d0b6e8b86de9fe880a1b3ec9c5ccc8161b6 GIT binary patch literal 23936 zcmeI)dsK~C8vyW=OLS67N21iJ=tvjcL&N*z-u=|bMX@!lUvlZ{77inlZ@)W#%=brYP2U>dlG*3K^Q^u0df&C)wb$Oyex6^t zqpJgVl@ueC5y4sS&0iP7nak#Uvtc$zpUv427_vFUGr%J-WW6^XU*hS<_eOC(&ojsy z{hwiKtk0ffp{vi1V*ignCS#Ca`?!{p@e>rmUY*DLDN91FRg;S4n~nmuWaq1?D1Ok> z*KX=*Y1q1v(k)xAM=hJuT(3Mq6TD`Nh3_I1z`b;0Z7UkzCZ1v>9nFGKPuot`%7IYt z{?2%}98KYdfXLU(axM7&es#f_ObNA&nWbug#*g&6tvCV2ooAN­IB90k5jU8oW@ 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 literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_2/set.000/energy.npy b/examples/spin/data_reformat/data_2/set.000/energy.npy new file mode 100644 index 0000000000000000000000000000000000000000..cd4efe3b554213af05151778822ccce7d6db06c1 GIT binary patch literal 376 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 K#9DMp7yk#F+7>zh literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_2/set.000/force_mag.npy b/examples/spin/data_reformat/data_2/set.000/force_mag.npy new file mode 100644 index 0000000000000000000000000000000000000000..14b73ffb54c66edbd1b4a179dd7b885bfd5fe566 GIT binary patch literal 23936 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 dict: type_one_side, excluded_types, precision, + env_protection, ) = self.param return { "sel": [9, 10], @@ -59,6 +61,7 @@ def data(self) -> dict: "resnet_dt": resnet_dt, "type_one_side": type_one_side, "exclude_types": excluded_types, + "env_protection": env_protection, "precision": precision, "seed": 1145141919810, } @@ -70,6 +73,7 @@ def skip_pt(self) -> bool: type_one_side, excluded_types, precision, + env_protection, ) = self.param return CommonTest.skip_pt @@ -80,9 +84,21 @@ def skip_dp(self) -> bool: type_one_side, excluded_types, precision, + env_protection, ) = self.param return CommonTest.skip_dp + @property + def skip_tf(self) -> bool: + ( + resnet_dt, + type_one_side, + excluded_types, + precision, + env_protection, + ) = self.param + return env_protection != 0.0 + tf_class = DescrptSeATF dp_class = DescrptSeADP pt_class = DescrptSeAPT @@ -127,6 +143,7 @@ def setUp(self): type_one_side, excluded_types, precision, + env_protection, ) = self.param if not type_one_side: idx = np.argsort(self.atype) @@ -172,6 +189,7 @@ def rtol(self) -> float: type_one_side, excluded_types, precision, + env_protection, ) = self.param if precision == "float64": return 1e-10 @@ -188,6 +206,7 @@ def atol(self) -> float: type_one_side, excluded_types, precision, + env_protection, ) = self.param if precision == "float64": return 1e-10 diff --git a/source/tests/pt/NiO/data/data_0/set.000/box.npy b/source/tests/pt/NiO/data/data_0/set.000/box.npy new file mode 100644 index 0000000000000000000000000000000000000000..1f72eb7185497167688c573cd800c4962932eca2 GIT binary patch literal 4448 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$d2099snmP)#3giN=-9J?f*y>aq#DCuDyv7@_Kj2RHyNH>F`%ka;_$(`= z=%8wS$4hpNn1ieKwtHtotnEbtckbc`iqG>{nYm#)i$mF|i4DcZA`YYGjE2u>`Wej^ qqvghEc{o~MjMk^4?S#>G)M)!~wEaBVFBt6?jP?sg`vn8lF8}}r5Jfux literal 0 HcmV?d00001 diff --git a/source/tests/pt/NiO/data/data_0/set.000/coord.npy b/source/tests/pt/NiO/data/data_0/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..4b60ae0e0bb627d0fd55de43e9e6fb0ff0d79ad3 GIT binary patch literal 46208 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 literal 0 HcmV?d00001 diff --git a/source/tests/pt/NiO/data/data_0/set.000/energy.npy b/source/tests/pt/NiO/data/data_0/set.000/energy.npy new file mode 100644 index 0000000000000000000000000000000000000000..8754b6dad25e9c00588bb4fb0f1eec06cf10e043 GIT binary patch literal 608 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$d20EHL3bhL411@nt-EA%>Qx2SwuTW$H(sxe0TxkoW8L#;-VK|j?U|!$4 zd|@CzW1{}di$J=!G@|aziIf9vPuH!C0rEfkRlJY~(kA~(L-zv3?JvwPCHiB3Z#x2{s}pjL@d4=_-~1av_86ANKYs~SKcmLJyb8#Fc>U?~6F~mL zujj*O0{QHcAB|#xbWF6U$$Fr;*CD}YeL()J+u~g2Ksv2+t%?(np3O9K?o%LbC;8e0 zWPg*7ie~pSO1d`KOkoYM2A*1O<)>3xVPq z;Vh0bfV6LbT%j{a-SF^mycS~t+Z3l`Unju?$1jtWjZB1GZ zcLBv+cM2Em0Mbj+9qv5`($=?EuV4Yn*91;_U<{;h+o&tt0*W`! z_>t%X`?!h8Ax}kN93CU#bq+SL~a4npLF)}g2LZFufYx^ a-up=z4zXGXC+BgMx?0MK}A+?nWdyc%4o<) z8DF1&;rsmUdHwRd-JaLE&UMb?aX&d)S2T4@NcQ>f3psCV=VRl2UW)bnWqXP9ysYQ# zJ-mIrt=ugSMRJ?&E0XX}9;zB__zrDk;Xz%NoY||9r@E$_y#!AeV&vF?vRYRUs7Ypg=%Awj z9REE%y?I9nm=1oqEXD1M9PQ59Q_P_-OxY>0ZR-Pf3*EHVdQEV)_mS0*fiOQzBL|A-eWk>2^1#d5i=&DC2)QDuG!F?@XV*1&vB*_wljHen|y)xcRi~Fpa+)4&N#uf3ai&4VDsY#WvwMcAJ-1+%Nz#tL!ETj+*f6 zYFnLYs}(ko(2)H+n*ii17pcd4b>P+HxAXL34p`uLu8i!U9Q-~KU*6Pg3DnI#Jby1m zU|Jx3_o$sUR%#m@)%6a<|0J$Ag>`#D80Ah14IvtZzVm237;u3Ex-==~FkQITd$jyZ zhcQr`NdGMnrUYstw=0Ui7z00*2E~nVGl=VwR1|UM0(V-$CiP#D$e#N2^a*zzWO$ls zdz|SCof+vAZT6nSUwmxYBmmch|1&N zv~W$yksPG-1lP~6bReacb8W;&4OoUwR=)Xn1xwo9UJ{-3q0RE+*>`h1aPr6J(db2c zc)4t7eCL=g@CNhOl6(jS&*A#MA>mlG5%mw(r*{NhPx?DO_hMn;R!(3rLlkZ@`FMP} zrUv1W&FQacWN=l~crh>07zP|jD%X5;0IW%iFZjs9jpwXmbYBe6);{_`%eoJqQCKnc zysn7ib1Qsr0+-z1)P)4?Z)6Rg%Pby4Yl-Qs^SA}CC8 zzj{{sDlkS*eHpZ~hLg0{h{seku^_-_N1G%HoByO3RsB;0)%K0_GXv^y?H6+@X^b+o z9_rQ0atgvt{~sg^j@RKL*sB;m@db^Uqz6KIQQ)nsCFrws6W7^(d>q|+0$!>nf5xZM z;optoyi>w8KqU$ca7rR0z1H>1Et>O57>foOCP zm(0jgkHXxayQ*V!so+~s<*8{JgXR!SA>eNWew&+IDnm&?81x&Pawvw7u2W|I99d9E zj4{1OtN|765sQ4NH z4YYLSoezai>#?&_G2W0u={=*?k%$*2N=WB<^YIWHPigtlTc8}Ua{X;-Ego3$CbpB* z;=6Uxu6TxcIP>=-nFnhD5+A0ypROy$Lz5fHP6w`|>W4gU$>%|+bZ=88uiqBtC(e?> zZWhYCjxwnB_Jo4#YsU|k`=KCffOmY61&(iZ9`~wB0N+3j)#u8QaNPHd5_f45e$pCy zAvzKZeRZjK9G%0EitK0yUr+!<7&D*g5ebF^?;H~t@}lvthh-@{X9)cFWV=zOJPw>r z2y6tB`+>-5rbO9?sknSjxTAGnHu76@4iv-`!9TuZF1@v3s7aT$q_CWZ^mhG33+)ov z?s#oiqHzsG9`k&uK5L9$4;pmOXjp+uEBE#Nh5*e9#}Bc;wZ!>#fw_U?>u9dkze|ym zgNq*9`=jM^;BJ`pmwowWc&`1?2hAVeuwR(w?86Wr$c_*Dv-8UtR3F_fDW6Kf7_*zh zQJRS`RHEe7JsS&7-|OyA&*fs$)3v9wheKeIo3GhOF&x4q{5z&^9h zUnVcjN6L3khQ1QBkWFt0#X3DPHTsEfo^=kAxgOy-``iN_@1wICVNb;p!EgI)6H>5s zN6QrIO;#u@7#r_P2*JoIy!no#Hi@eJqsqmqh{ zLUgRWwl?g0oUeQ<1TtB{z)I z5C0W7uD;&;E`<`c$`)%V+N|pvr7tHyO-|K#;Il})ncPnr*`5h!%k)$;4b!nicl=#x zy&VdOOlJ*#v4yAYH+`*gg5YZ?Huv9-0oST`9U4z!;bpSe%WQ6VUg;P(@$n z_iMS(pzQJfsAxK9_zfkz`5A`KzNi%X?TdpYBfWx?-;_ZAoUA8G#^a@+6OK#I>cA)P zwz{E50ur8l4>p=_2IfxV%4-90ppb4h@y)XUI$dAi%lI3GCkkEb^*qBtXL#r08!{Ih zN^xRHxD*8irwSVO+53Z*rj83ox(9ORemQIL*9%)5>K7bR5<&X4m!?m*J(8J!qf)yY ziGCa@e;+6vgw#N<<&-f4K>4-KRyA{I`OP0XVrz%4h8{KdJ!O$_%%J2{kP<$lqYe!2 zGK2tbLK@3YEx38z_|hD22G&lX%<+HM@Jyp`_=^V?c!odBpm8=F?|cs_=jhX~xN z!)hY%S(#y4(#!#c-V-ZaSz_V%6)J*%Zwk=7e!A7&9gCEOIwm`r8Q@ZwZ+dq<6&NE6 z1c$*F$<_bW;6O0m(-itM@K*{hu{rrjqz2;mFWZ#LD#0k}_}_=F9S_X^D<1iwI0Rcs z8P9BF#erC|PrbmZ6`X&3dOC>Q8k(|+H4}-+_?YoR&{e5xsAC{`>g2&3{Lbja^Pg8R z$nqN479KK%z626=J9%|bK4{Ffb~GQ3`;?9zkn_bC)|rRev-k4DD-GvNIzDJOAe@72 zS5%yFrlqdl!BY#I8pD%^MQS2bVxbI9u{a zkGLE4nydTqXTp&4!d6AzPvV-U`1_>f{|IbU@7_Mz86}Lvb@t?=biiZTkn?So8BUF( zqZ3&N;i{TbWODf@B0JTvyn6Xpf`nR?W?%JN!U>}Zvc=K~;<3k9le76g5Ie`}jHf<# z60$q=Nt4+_2oZidWzq^Y1XHJ5*Z05bA_`Jvdw)IFNi0ggyv9}BN`$(B-0|ry;@qoK z!p~N=eMUV4|5Ka8I5n>C7J@P}`;Sh@PG&NHydL z+=_im+|usd()8FQMp5OL6Q^ehQb9{y&I#o3+|Q6sGLjt1$XMl$$A2XLna{G(X#7q5 z*)Lypk=RQVzHKFXGwUtkPDu5eZsQW-Gs`~d@AtkD-YSt(YW2P+Y~&x$Wv2Z}_)dM- zgsgKv9`R=(-N^q&*nViEo+X(CqaM2T5|=1}mS=^ct^E_h zCW4ZJWBd~_AT~S2Iiib@*Pge{^`U`yZnF9fV?{f0|Cei7LPhh0dDRa!LjOJx?i7bo zuY3F^#x?Nr(;cA1spLFK8)*{UY}HVqDg8xAn%7y&q@E)PdSC6Me{`RypM6Gx#QF#E zDc45Ki~I%RpQ!qiqPKq#X{f~9Z0~gvUL~&#N>+~%z1?5GekObnemfkK=4xFf42hnu z6^<=H)#hkXLeq8B$gukHv!WZQU4)f}DV(A2nmC_~XEtcej+UQ(ZVBAHMavnJst~-s zCb&)+hz-wZKWbhvLYJ3@Cz!<(Ah*%|asgWn3@a!(aR*djL*wqwmR@0uz;OJ5oFLNbe@iTkMoZ6DP`NeF3Sfve)VSg+nz>{Z6!_M$;(AU+wi8io-FJ>D^E4|(F@r<9~g$S z6+&pskzp@$OZ>aybK2p4Fsj^7=Q68HL63CJl>3@A=q_Modygs#n>@Gr1X>!g{y8~A zU)^0S4sdm%EJ5rn(i0`zkHQcpmlJm`*dXP%&Ic{FQ6NZPbl}sM3gip^x>+2M1R>jj znbRi~V9Wd#?+BG0jyE;y98k*u_2)h{!7nr6@Rr#KJ-IhtIq~1^s+bJ;w3f;II<6Wr z_dnEOHMGJzvW?`Wx#DQ&>hQ(snI(4fLt#HOW&NY zHi4pwb}ONqc1ZtxWrzQ*Czv*rRgq_AfvfzhxWSSrtoFg#;&cO2TseuS(9#8_Z`jv zffdv_5txI)G$f~6ZdBlIF^^5lSU3b8@%|g=n~$Q$bccm;NRh4${}F0TQpI%}|@d$6NZ6p4wZt}VVT z0dR?W_@#xaH@1p=3oWV&g4v3fyTfA@pgmqTFgDb)>H^Jtc3*)|BKJV_JxJuN|N)pc{xt#H_8HAb`WYAmZc^POxc9Zrw5a0JL?bcoYJao~>;&)|J$_({R3R^2Tbl3V(y-gPi!Imj_W6l{!Ap@C z^2bD4qb3mlj_|Njj7P%xV@m3^_Eyk7x)}YZEfJP@WY%YPop6QCP{^jqA3oNQcZ(}o zVUKRq^gqsMT-Dc*@lDQ0;}c?|Etam}*FV>`y&ej)B`aDVqy6w*^+Mxi{;QZ-bTR87 zeI%?nDk}2Ir{YaWmHtXwji)u02$y$~v45+1RyE=QWQ&K5mP>@eBu-stosPty>3`W> zo8eILv0GMPYX%iV1gHtxd(|F`ZLcy00yhcc;XK%XTidYGANvB93BqriVqDaN za%iLWBkz5xDWnsOO%|X4raWxP2f~t}tdL$~i6j_2lGR0}2$2}CZ@rO!DjLg<`_9n8GKM3?8qELnp50k-MM3k{mcMm{5aTKB$DODKC*CAgaAl}slVn8? zeNPmI+ljwky%ddRbt2{BL6)G>&EImAp%8*!Nh#!rq(O$?SMK^nPyB9ISv;Q*fd!Xk z)#UeiLB~QVwf6uoJow|%y1VWRzQX51>pBY2<+|f4^xlHff&QDcKP~Ye&t?Xp&kHPn z-gCKX>xqwPn%_OwPJs_a+~-cv814eSxk7M?WXs!Luc>s3hwl&{YSOZq}}JWrcW_GotOOj5Tf4d%o%ZiY zRFEr7m_AmMxgG`dS?9JY7^6VXl}yXuUIA-m!t?pK!*E?qDctCmB{n=J$aSXJA!BU~ zdrYSn25AdA%qKO} zn6u=MsKT^i=5>c8f0XflE}O6BkNv+(5;MvCkwamm-6uW^Z85IJRVfZjqWS1RED(>p zWT|iQ$b;*W4}Wjf`#?or(#Z<2MVouoxT(#K0Yc; z;t%Usb-AX)5ij&w__b%>0Ey3$hrFiCU|>wtGfu%2_{=|ssXxiWdx}55_V4(jo&V0i zM-EZ2CUjw?o&6GKB+fZ;|!Q4(H6~m~f zlJtvn@JIw9{_TI);mxWP*@8-qatSS z*P+3qN-1h310rT39?;&+gbQ?^L)27!kke7@NOD#vay1KzxN21b@eGsf*=f27^Exk<0 z7i9R6V4DI$F0~hBk2u4D#D8t(YIGj|q2_t(eligF(mbdO&SnF%LJzxe zSUhwLyJcGVmcxj%!1_%x8~E9HmGN?^3sjOXZp&ONflaa3i{JP3$b~6R27@L$9O-Gi z=(!mNX`NSUxAOM(&8Eoi=A#(Mex%GFC|?N%LPCKgyAfD(j{D*HDt6?s^ql)p5Cq4W zG(A6OJECgs=i!Rj00@66()F-#Pq)%re;hTO4M_<#j$OXq_?T^URq(tb?6RK`JV{}X z-p6D6bXk0Gw9ovL{Y5KeX|hQ@p&A2U#%4b?NTfmn|CalCQYDCF_?f=2osBgz5&~Ze z)A3R7_s=V5uOMN_c=l^-7RKx3F+3Hr$Fd~phJTSE==9jYneJmg*#+ z`q%RJk}RaA@JkuKq>hqo0@SaBBf&5F1s2`#0s5V1+WRVHP<_gvzA`f&)RInLKW?gw z)+ax`5Ip4yb8M-ZdcSJ%FYhFi!QdU>u=MD7tX+g1CLI6$d>#&4FPk;mS^R)TPozR{ zQwzETLj3cHHz5CYljjS?9PF_^!jM?l2s=8Hb)L^+FkPl5%0xT{&RX6)b2BIxZyxw} zX|y5^^3TP%4Su`|<$on49GX4%@_Au{Cs!P(3sTMn4+kLQ?Z_mtcpFH}ywvd8Jq1fA z{bIG;gYZJi+0d-aYnWl&eWsy06+2#*4K-f1f_$|ivY=EqG`3qEyGN&qi$lA&9aVYZ zZ)xy1jrt^Li2uZSH#Z)+>c1!6x>JY)&-CAR4yU5m83nJbOVPlZQ`RVuKwIy%dGKc;K@;9|Mx-sd3s^gINE6hc)aGOgO ztG-yN#?Cy zF#d(#S>om`NL*7)q!5inqSM*8D(DA!L%ZK%PwAoZA;xAJk2tIquxDcxHb9lv=%LKr zjPDN`ydWlY;r9xolLqS*P+uUNZkIE)xjmhF67x;m$^KbBQIm+W4oL}g zA7fxE;!4%(RUa%ow$jBjnTMk-M<3owzlL{B-x|d3*$LL@IA zbX7ia2Tr?Ir+wFr1Rb}1jAl2Jz<|Eerr59)pFg%OD>S@VX_0YVr8)4p^ee7rOw$VOP+-UHths zc;H5c(WZh+G@LjY@UE%B3$sL;;sshF@lShVwm_o^5cz^0trjQZzpQ3YW6MC0o_(6z zv6%tkDLf}hS7Y&)R7$g!Y5?*FA1{^n3q#8dn`;4WJv~f14LeWO7L44B#Fv70 zw;O5X@Y}DOcNY%_<7C^ru1_9bu;)FVzAv7KzhA|*at1g9EmfP@H){`M{}YkH^F9+S zKPP{q^A(2tx9)7)Zk{lyGbENCZ4VUWLvtH(nNSkTyLSJc53nEfxsrKY2F6Ypi@kZ0 z3ysSRu@lD%AYZ`o&8LAJ$eH^>5w+-uZ+>-hmNjTYFw4Qw9`HocmE@OxUK#N5sqIO& z3m*9AVZsOcWPMyN>0b+pOa-#Az^6wSL!eNH@G5>#09_*22mhtSfkgeSH!U-^_@lYv z!qj8{>{N^s4?ao2Y~zxPQd+*SDKADQ{iYDxYN!^vueqRGxr8V^rlXtx zqVS^jn0Y`9W$zQhwPF(u*4)czc$N)c8%r@2O}pqSC_8uu4IgCMJ!dX*-d~Xs^skQycE{N zKl#}gaSn`2&V;0vI-)u!dpjdh9{;$=TRsmWhlTm3#{zLd@N3^by4f$Wkdj1op zM&!lzOSDH}RZ^Mjk9|H!;W5z4ZR(1o8bP%K7QS$&e5o$cDiWR^W?TO0kO115-|n`^ zMB>+2`oTBpmM~T~@=}7%0xlkMPfv*O!E}#%%Ccg<@P&hR?H%zFcn~_wv(IG0q;)$} zMVLPPB=Ruw?3zO7jQz%al560t)WyF-Vuy-@;@s>DlIX^?(cyU~9zR&m^UYA&;m7&? ztqVuoF)%~6WLL=nmIW`st|Oq z1Z#MuCSV2o_sD$vco^RlAvxozjnOBHNfWu;kwoe9w{EIEy(;i3m(As1JX@#svVKJz zb@0#iUv0O+WB+*T0qdL73Byr#@OPT)FX34>oPBB1H+U!rJ(oYrH^q2h!Aftn7*h!<$pPl#^mgppeyw#d(+gVrSiY`|c3^Pd2b0&QZsReYZrOa2YLGd0 z>}KAR+puso@*O`#41DyDN^tQu#Okzfg-k(Sc-8M2{d;V{;eLm|WkSu^@U`=#_VISu zcjFy@g;oU4_1=p*VV4ZzK|zz(Up8ajNjp!Tj2MiJI55{q7m1v&_I9Bq9>kgNsE{8| z!_yyAJQKaLP;q~Sogh;omKs=4US4(qgTFC+Uv-L6-g4Y*ye}7gl+K23`Z!~p*F&<- zd)_$FQ|fbJ)Cg|ObdT~?gZ|-^P}Haa{t&NB*?n3NpqYSJ=7*Se@~KTY`v5Y%bimbHjF$_rrvrvdBrD%}5E{FGD#KkV8MPO2h?iu^;a9py` zv12z1zyQ0a0hSH^P`OrOPd90a9$Xx*6nO~{{4Fm0I7=|>p&9RmnZuEBJEk%H;X|A_ zNjoR2(uveS%Vt4U39^(f?1{uM%rxRMG0W0Hi?WXHuqQUKbEIW^{<$aaMl{f=Ok`mO z_jlKH(wk_bD*7xiCms3X2-nHNV}UY?;z)|o4P0RA&Qe{|#D||>hsN&dAXCxWUNvw0 zk)dwAXRFKt)7$dO-i-%ipz?_c#+QnabiuVosk{P@G#OiMNR&Y2JNGFrNge!q=K_3Hi!ODy&FE6JOugZzfjXfv>x`_R) z+TrRzdrEwPS=tIdW+^g_ZluCWj?hV-PjZ-iIk}Pc=XGp#y86rgbTK|@Zz!{&ZO5Qp z?K@Ev4}qy*wf@GJdiX$HHhaRb9B#aenAiNB3H17fA4{%f;I{&kn^Uy~!0c_X;ItA6 z!m(ll`LS6*_n@guzcdhIQl)#fNfR*QfEU~6)nqvTr)Q(&+@8O|A5$O75CPltcDpI$ zDfrWI-+uCcdO)w5cHvaK7wkAt{5Q2f52EvaHtdhO0k0p9I~P9;LT9t}+KO*k&>uY# z6DL~%Pa|%ZmNBH@)B(qH@0`7GKlOIw^NFjVu5j~r+EOAEUMh)P{FMe3k0@%A!+e0$ z=T)#&p&t5JkK6CE#GvO^o-}8tADaCzpt$`b7$5c;o4huO$9P`2;jYyjv!E9j~H z6alwKs`5`h%!Px$N8U`>L}Oa^E5f9g07RXYIbss#2cz4({16+41RNNuxf%(L!Ly#F zKfFNr-lom+jySX@|A=BEaexVRUz)QY#lX^1-&0;Y9)gvl+gtUcU|(n7kjd@q@Gh)? zGqyVuhs3Joqgd5JKgD|1UDpM6q3yU-Y9w$*H?ahC7(w!QuEnXAC{S?tr$!w2M8p4P zpNU?+2!}6OP%|2_0?W~!kS~dK1jTqI6-6fz;DwPpBay>~KUq3QD1d0lT-!K83=N**G!o@s$!dq^Xig zUn~(kewNtv(Fo&8kU*4HJ}a7szOeR^yGyi)Rc761Zx7|yewEIzkin@QfgbKKVazXa zJN}T13ahAoMilc?gWt8Ye<&mwv7*rXP%4$HR$2uIeAXA`x5Yi=fQ5rc}-kW-tYWm3vfpM)f@9K z%9tH^?5p$dj~s4J|yL1p&RvO#zqqFpwsWgp(tlH z$u(LK6&3S6M>yCpxG`7lxRfYxUd&EU8W6B9t<)k8un-{&)YFf;AkfBUNogj|h)f2`I(eTZdZ_reXNQt;dsIPZt`@9EdfWz3K#DI?N@ z+hOl~8dD9ih2Y&&bVB4OEuhR!N5fju0n6`(ZhsdHL(P10x*zK<5%VVgeT~#^2YX+2hQj52Noj3mnjv<&EOEz**y^wW zuMN`Pr<1e;v-r|SscpePhry;5ZuCxC2 zk^sW{{%GH2bNo&=s7iKD6@FZIRz2%|1%EGH6c6kR2dxaPvY&55;T@4lSUX+?t38Hp z5QX(n%Pyjay37Rm{Cdou`srgP8++d*4JLhPOAw-01Sw^UpGR_5h{G}WP6nem9OQu^iQ1{!eQ~(mR57LLtOsm{7S~JXoMHWmx8TSN)6wDfeMKsU66x_Yx507+Wkwze1E1j7=_{9*k z3XUM6!C622!T|I}T=^$xwZUG^An@kfG7RJOBt`~D!9mKMs0y{c`XM|}c~WbSvr~9a zArxT>%tN*tdN7IBG(w8{z_o9fxq*I(XX?(S?P`*>@sc-^bRG!>@zZ?oVrcL(yWUoR< z_KW`Kib{LB)(atiaX&0jmZLgtPm7CB`9AReWyhu{xBGT?Rgt$LBIi-IKRQyp;nCZ2 zz~ZXSyFp%8Fie}hu6mCLEoycWnl|(Xx9~U9gLMR`C}rg~<*>h#ZVjaC~+I@*t$@~Xn`>Br*ox*@Q^ ztuhz?-VSbTGpm%Q1n>DE%xQT}J|GuY-NLq~bM{Y96=E{Lj z@xHUR-_$`;T*Zyzlq|G~JW4&IZw50?Uyq7-TOmEanCK@dFMOy-&vK7e9RfdyOoteX z;nJJc=>x_FaO4wXK;UZwq>yW4SoL&37Lkvc7gBAp!Oo)8kii#Umoj)?rMAH<19#u% zhbLj=qVHmTn+dd*4#bS@QM36CUE&? z3NS{402Rwf%R6xluwn#T>&2g-3H_jr1uz8lUp`HDr)=mG1G0m+u6 zSPYW&SP*Nogs{Xr$I~`y(Ir#G`Q=a*>=>tuM%2nfcgGs(2Z}HdifAL+k!3^wXy42l zRW4NjClkwW=7Zx^@#35^(OA>CUSj+r8b(=T7;EBUK;n`}p4LVlme5YQwNNy{9mpTu z{@eg6BPGmt{F+cVL1{qby9pS_*1Aiax51qF14h3iBC$%xWVgjO0);kT_0$LN^~*o) zREr|RLG3U*pHiDQJi8+)M)o@j$1emm-{CBS{p1P4M88O-!AC0v+*a^Zvh&>jJ<>mUe!tG6CfjLVc~3-7)&7pJ6S9 z72ds#4jcS_;KMr@uxS~Gfv*b_XnC^m{qkSRVwXs`;hvK(8MNmcldcwRiYGz;`8b;s zGf}v1aV#hAs2OfLzWeu_npv6U|JH>~C;n!qTCV#CbN-Z+#jB92g zJ!3Iu=$ylu8*aTkwniZB{GH)Zl`35Bop)%r;sK?z9QB?@?15?j5?w5l2{f9M`PFi9 zgOtVtQMW#Or1-|w&hcCY58qB;HaKLAoJk~&En3C+Leb=Ry|p#EC)1ApEq8#)_Uk?z zO!8ngHnAqQeg-@D2M+kZ_(*uFu)=b&#t~f!mAXbzc6dAf_B?TqE6ECZ&}#8W3}Q>_ z4|euT!H-1ouQ{yLDBbwY>Bt7aCDry(u_Z205Y+XcBawiur1^cx1^P&F)j%aP*%UJ< z3YJ$3_WZoIdq13qx$);OYPGJ53i$bpad)n-7BDT{7c~haU;%~Z z(hWL}GmA(6+M82R&t344H$Z!mL+L)PcA(ASyG(ge5X1h87%ONA!ucV!oB(eZIAR%V z(pF*#Z@ztF5r1U}@6TNS`YzK3T(gW$bSxO7w?BiQiH;@AxujOEELwxfUB{?JQacF$ zR=oD!>?Ark?F#yDY2$Jq`7E235Q@<(9#x@~1y!@SIRPJ86dWxn<4%!9vi%iz^2J0^ zaA|m(Dc2ev*Z%vWeNh&2IPM3HbgIFD2N8Ai-_`NaP1pXK=enpl@$&cjeO`RFCHJ>5-Jqe8C5ryT3fE*_&6X zv1~WV-&H~~?TOz1wsfHT^ve&A#|-zlwdThW=7P{pb7n4y-2i7_dL0Q?Is?w0iJWI< zE~Dc6ftb@%_ORJUQnj{XQX#H4-aFlE)TU-W$> zRGqi-%iypD_KXL@2c9USz0ZS=lQ*M5Wh3=xW1cm>fDF=~~<{%*5|09oD)cnYh|G%Xj&BIn+O&)jD5c164V?6K)oUpwWCW ziu08Ra9#=HJjS7mbn&A9Ns{y8C7r{}7Zxn>mTT0hS`H^Tq$_e-*4G4shriAgF7D}i zwBD{KZm1#A--{*Q(VP*uQcmEq;s-2A__w{pNDP9h2!@m1JWiE8WoN z5(>iA15eIBmcF8Ub3E~A zzVU5dG`Ksog|Ky-;N`+Q^pj64_jt>tQ#q49_|Hh~!#JxCu*kJYeIAX0^Ut3=|5Q)} zeL`KcC&TyV6(V{n-1hX3*{GLnnonXd?6IoDfv4_3JgV0Dl{Xmut47Jvh$6s7xn(X# z8VbsH4_vW{v%%r%M`51JLEy`W z?L6{B^6*a&FFrCxa@*6<9%|9RaBE}Ix+WO2&sM)wvpNAb6sKJW46JY@bZOz6s5MOW zY|V{6(}9+h56&YebirnZj7qjG7Ls!2C2v|}Kpp%0d6u9ET<|ihkXDz&x(|9k)slFi z-a~OSDAE=PmfdAtt8ySol3V#`#1=9bUumYf=pn=1Ew1YNXmq*ez1ICy8Ae9+9b3mE zvG2EUM^~p9E_WJjxofin2UP)2n4SqfJnf~nc0m_o!+TsSE+}Ks@1dEw5=;DibZmE* z)f{s;GAB&}R55k`?N*_eS0M1{gW$L<5Bv~w?QbxT7Dfr4snOgphbz3}UxNR5Vc*kR zFYFHMWA*5}GG;k**rc5zJH5wKxELNN|5l=a!8-w5na7pI^GCRsP_TReY>=kK*_ z)$MnIocg;&NIVI3Cg9>NV~i|}Es`Iet`ddsuLr2aUqBn-ijg&vbLiP2S;x>^0AXT* z2XcbqU{OCcqR>4b@5D7Vxdr-x<2d84t^T=DVHiF7FO9khrj976rUCV?*K+-)jzD* z-u1VG?6@E%)U3Ovs=MMRDQEGBd#aFGL2Lb!)ea1!MVFnsXz&z88d`NWd(4j%b8_M@;)qCz!!K8->la zOaQCQ_cg&bbZDmdaaVlcEKquG@hr-mhdg@#w4>7Do&H zTH*D?hveC8H?l9m^oCjaWN9LN+^$(i<}{4Zv;CY?6$tAy*GBS_Bhaf-k@DnwJ1pr; zEjxY55xATOw9m?C0$=Xx{)HT0(0QvoFFxmok9|JvlI?Mi=l=Qhr7c@PkqWmh*|7wa zI;XEI^hOsnWM@x2S#^gY$)!-`YJ1d|IzlQen+fjc_|F~*^Mu*T@J`qCKsd4gRQrRg zHSmWjxRlkS3G98TTjoeB;b@Pc0>v96JWV71*}6dt)B+8%|9S0kbEP7o=l6WYq@g-XakPY@C8*x-;SQ z(Sb1R3CGbNf`uIp5ok`h;mf-aiC14y3pRf+g(h3Y?u3Wgc&><~kl=R>BUw|g{U-NE zLq&7ZtK0$j`km3_>V=2+an&tlMMVP|zyERI;*fz~Z*oN1uj1uQ?issw$R1&^LX2-1mD=-jMw!kOQmbH;o9lJ&0???2HxxWFj-!@9yAPWufH zrZ0uS>3iZgI`_w7-5;{CcHcy#CeaX|I4J{sm5P2_Iu5W=VVIB|VT+Sr&p69f?{U>% zqp^A+|Dca`1;@og_ zh^q4c_@5sRhK?67#3f?Gk((0zSFAv{wcm5%(`7hPo=0)`uQmL&qV2eqXay$zybBh` zjev@Bl}e9A4x!zA{{_2vXB&n z4X?9hc&7#6QPNmk;~r;{VC!W2y*L27(n4J~-YWvfi^b)4kCMQh=0lfRZ#qaBH;Hub zhQOb-5QgJ(4)B#WK!r!z9lqqsQ7L~3fyegt4-;j)flAr<#p+#cYL1C+(-jQ6o(aq4lH3mSX}j;S&+e*#KvVMY8L}iaS!5#X>yLt#8~d6?TkNpp%J_i#IKqccy!@(3#0N&~oEF0`O5?SKKPGSYE1-Yz(SV9FD}3qj z@u$g6AJCp8?Rr{u4T^f}%}4Y@p!IpCIem#M#Gb6}``v#Tm51-LG|Wq(XUMP44nb%9 z%~&bOv1o+XKixlAM4|{$Rl_0VI;qezIWYR;P9QkNdv4!5OBLpAN)_5`?eV!o3y z*dF%%+`IOKn4kSG|6pIe>yC~ZsgC<-g)w`qJT&`5IQ-?edMndRR)&fXCs4`sOjX{y)9uy`gX%tivloCM)`ot`3P2WU4L(-;k}pQ<*N$zxaK`~zy3ue z&PsM!9hmR}haxq7@2CV+&tmZ){SyZVOgIF*`;2g9?FX053emIpT}VqK=zzu|2QKjM ztppop>(j5#L}0fhe~y4r1}2Q2|Mz3g7DONFf8{^sh?PzTswZr%0Tv6*-_9f;%xj&~ zc-#VYnLo9~J%>Tl?pM_}x;Fgt@(xKM=QZSBF|M?m4h9~9pRs%Cqv3!T`Q8(q)mZIi zc{ok73ifoAyeM1g0QbG#1(aK@Xj0SuPJ%ui0>=%SPyfuu05y7kMZ*+On3jJ2gftt? zDDOJw3YVa2)W7oXU!mCbu$1}7zd{t6G;#_LE=T1P9|n7d6JazaCH(hnF_bjb?C2$x zqfvfylaN_CJY&{+J@={-S5qGDY5g+-vuo;Zg-Qt+LGdO(&@dmFU#Yxf%@l_}j%VLo z;B|zz+5&B-h<+Bg(%e38rDQBmjCpIYR~OGMC=@l6sbllF=r{+V1C9U4{`YXx7jM76 z)lffc0>%|ye$)Sm{MUaa8o@t<;Mwn_LY>YCw4D7~{8}*rSQTb}QNG$5JiO$PZ7UiH z4!?}-M2lka_~QM=#vBV=3>xO&Z&raRJez0RiGDKosCM2d;W%K9*njR1Sv1P-4z(W2 zc7_A|hno4AVzAqU--T1a3>DKif0{hY#R7G;Z!C(DKp(82vSBWb=g*x!aXc;>|2E_w zC!Yu-&RMT0C8r`$v`;-%*w~Ng4TLR(x@V)@#lM4>9f({WYvUnJS{o>Nl+E1ZycCHMtrk*(V^P_F#Nl;*Ffkv~sfH$)!C96lRo-j`SU7P#$>4%2oNT^TN)=%NT3(kQS06Tj zPKuM9r72QCIg!<&=G}%0XS?78O$M3;NZqmLx{iM)5(Aer!ojG9{n!)Y_xiTt_UiSJ z7j&>3QLWVQfj`|&dTL^NV3yQPp|D|xT=XgbUM9ps#+ECu=_wcdxY@1HF=qpOXNBLq zKCcDLYjjVSf`Z^u;IcHipe5AQwTPV}^9J2FUngwXBEao#=+V6jrjR!O>FsxqXfT5Y z2e7<`B6e!QN50rVy!LyWU>>4ZC@8lwx~&DjxVg5%2CVV^ota26Sy%XXab!VsOar!0g`pB9x>gP1Mjd5$23wW6r8(pMq7&)QlH#>euqa5 zXDj%&_vl{5@STb!$6$M0S(42ilKeDyIKz=~n>z~j$`5tjsv&Zs9`-bM{ET7Vaw5P^ z)d7zjbN)U+djhOUB;#U-kD_$k<%)DVe>i#i%`Ssa0FWD9?cB7}gv#(Iqv{)xuv@Q3 z&9&bOvZ)S#Qlm{F@-R|WJog>3!(sA5?QKnD_8@V@_qurF<$T{gaz%Iw0ni8hn{ZzJ1`Y z9ma(uS>3iU#0TaD99DArs7d!^o0iQ6Z!j?x(R|cH>bsIr?viJ5kGv#pXt)#H&G-So z$TVP4B0urK2|*lwPgZ;J06kF2|7wbTNKNF?eA3yfXrSW7o#SCWcJPTxguKsA5dI@s zTn{3A`7174?VWtm7~G(H>}abP^6^)G@*X;ef)O!NA9ab`!@~z5Lse|h#&M7I)g30- zsT956VW*6q{qGM=y`%&8^t@!&K}o!1^{An{ivwhQZItw^HwM*YMjZUt{tiap7p$Jz z)yAIXHNK`2cBs+ss2OP%gh$iHS2Hx_@L3IeXVb6qxI^u7ry!0CmK@(6eEC-i>!SwS zZhj%fsfP5LGpr0)-{5v@;KoYo8qKsUk-K%;w@6ae$zRfiI`)Er5HYY9XCk3ySZ~J#4$B z2lo^nkc&2)25Uj?e~<2*!J@#g8Lr@S*&4DR)P|H2h;e^T?p{EDG)~?*2FTTT3{+Rek zi$ebtW&ARP(fMMU+pz`)SR71Euv)%|*1;S*M~9?9g?78Mbbt%Ytk!pfD~@5y6Y4(S zA!(#oCiUU9kU@d%IJTN6|G`|^CSCGdC3v!DtmV8MDdBHvM8}AVz}`%qn+at60PdR4dlP~%ocpx|m{XyL)2hsR$Q^qj@u zBSt3+J;h!9)T5a2)VBbn!Xiw^Vg{ZRB#8LmkUyQz!W- z)X}FKpG*z_ZStExhm>@2XQeLbJ!c%A-#0+3Rh$fd^hu=Mk5b^@ug9WOgidxVTk`y_ zu{Bh#aC;uL(#K8)v-CMeWaSqZpZ0GH$B zLIXYF7=CM1LaW0Qo1;D%rc^|r`g3lz)8z(W=30CC=$aW|hym|mr3UQ!PwhhtUl&$5 zE~-eer@<4ZcC{~DiAWN*vUw;Y8%w`_Rx(xdB6w3(wPg+-Sn}<{=%Yq4i2p37#eKs9 zDEyM;=>yzw=1K(FNRc_pBbPvx6qRpb{~CAJ_``&2H;r3Dc0-RzM~T=cIZ~H?}$8Q09D% zz$-iG5s#i9e?<5~nB`BuI((#3UEa3BJU%f0CYBX zKXvj@2zXYTNH0wV!&vpL?7_qcw3n6TiYm0g3rCml&>qUc1F~!IDZ?6$th?59toZ`P z=Ke!1KK9`2r)|zAi*j9#7T(dZd5H6h``2&F=gQ zLgpWrs@?ybLcRwfscaX_VW{=R;cqdPkdbrtC{fsieKX^(`4UDTKNIYCtt}XiwjSAn zL#ntmdcOZTi8sV7N-<@J_=EaGWCucAztu3=wYP)XF2qZQvXoN8i%B(9JAOpW00%o ziQmP*6kNQYaY9p64evFw{p|RZ59wk|&kigGL-RVx)ORUo$a!yA(J=3gy$N+{W8sOA z5%~N{q<1;cbacM^-4X#wcCS`fdt&h9<5J2=^?Z13JNRkygfYIf)z$GJdYhzof<14} zW`M%qYcpc?Iw;I~Qzw|*0)>QWwvR_-L860H{auPIVC}zsf^Rq-O_cjS-r(+rJEwN? z9;H>|arw6&#psFedxq*IV}u`(lUTT7-0TYJ`)>%c6S}B-(r;DD^chF$l;2G_s4y_W$ zg>zG?9W739f|!pq{35caY%SUWrwIQ3b>w~buTR;y8;Oe6*^0!OOV z9$$i#Sb230#R%k_i-|V!c1Mw(!58LHCMbAWs-Y~1$lJE4Nbfks0O=IdGUIp*ymzg4 z_U{SAk1uBjZ}Rw}@Zs>zDZ=lu);j-p<5e1@Zkw1lT=7Brisf~!SQT7iy+GP?*$Usk zmRZ@PFhuvZyY-sH=eX#fIA&`R1s0nR=GRI+@J>JTe&0_sl9}?I19?2Q#GHBdt2Jp5 z3dEI1-Xyy($=a5A68<^BMm@9mVu4y_$(b3))*R%YAS=@tbv8TLgR{ ziT#yU9gkeLaX`O^qAth@gy+;_#sQ#ATJ zoX)82U21kgB^2t?XDpWfrNKw(zX2@N;kaP_eQ~EZ7e}|wn)L>Uz^`8K7UR+G~PWl8Z6AWv*Cv7R7X(ChU#33ej3rJM{t0!pK4~K6WjxFql z!Sb{9S0mnvFw5;M*kzFp%zsUL|M~`i2E?D5i%gfXu zuN`3^ecb$4jw9v`J-OWUHVkfEO!f90Fu?c8Oaa5!9WcqwfO_$^2WsW~PUbe1#t%Rd zU>9S6>XWS*^*^HEqC!_>^j8BgasSxABj=C4DxbI1du;IGTF#tJHDEM-pX67)OibZ0 zXpZkN$B#=Ie0vp*F*NM@0TSYQE39_$zQv9i(tn5_Q=-v^$-FSxY&RQt;vvql8DN92 zW@ihIB&fmVx*MbE`LVd$7u^1RI}|JiUAU#n{LzCF>?%{nAaH~tT=p&gSI&2+$mNpp;Z8=MB2|px4FR9ykfJoViJrBngaAxoRlr-cFc`O0K z`&dl=IisnSf6yAD(F>!JJc4WSPDG=syTBEr%ftWf2yx@;|Cf!g;vXLJ?;jqS z0f!ZBsmqEvaQHscB@;3PZr^jdhYK8#x&GIWM<)%j%{6y()JYoyojzED`jex;};mi(No^WSJG@F*-OI+b7H)XDQ_7b@`rLU)tERv~UOl16oy@WqzZ0pmtT@Zoy zVd7_-qr8z>!v7iF9Yc^==%^Ko_d3N&3oBF7u-)nZVyCn^=MrHqlc&` z)EZ2bADCvt?YNA&0uSPPea&+ByigFl(xPl>c;W@7Ylh+r?}~H!vgQ0%Pd={ zNWf37(-r%548fbct+m(94LO5?)E-T^ffYrmZbmvU9$nq{FJ?Up78XQl9Vr^{-eC&1 z#s)ta*yBH^r=5tq!|C;fPaROqu~Pl+?N)3sqx^Q5Q5NarX7+LR#G>Ox$T3w&f^Ie& z-jh5LkUY%3sKw}ug;hIeT4-Z2|NFRP9O3hm{kt(wEg1r7ToVWQr(+^OPI%$P= z${M)lu8T7f9&C39{lFFHJHHctLQU;r9fxN;uyZufSe$c51bbLUVsQ^u@#EX*FfrS3s*vf zJHeCMWt>xX!uy3^t~?p^#Op4ST|$z&uqYjL;$^-Gv<$s#bJ@8DTR8!7{2v^lEP#pR zcRZo{@6)4Ul>eI0X2KzRukBj| z2bAJMj0pK1MJ&y*yiyJpM?iSah+7VwI!Ah+!B9;POSLjrNPGq z(FEtzS>31Ga7U5gs=bn>YP80EGws_7f)|l5K)0q>AQ4>odJE1f#-mBa*Xua(sidPwHa-pK) zgyD2^0SfgE^3XBaLLX-^OZ2cGu=Um!%k8FO9Mz^r!+C3B&i)fG$Zv&vx<<9`J}rUq zE5a0#B$*&~uGi$l84a+VQg}u+n2Z5)XA|ygei{t5{qox(#S#28$H$YvKD3bTLa{U-ZJ(*w1g*129(1NX?V4m^YFdv0pKR;{GEd6TeiM& z$hS|kLUBR!djp1{AX&X#Rdrh&dn}yIqMQQZ0hvY#i@Y81*Wc*;@gJdYG58iA9m&En z#;(vRB6q3iaOgMXiXHBZ>DyaV7DIPpNuBsg2K?*iGdtfxaHUN0_T=`KK*!UQVXXRj z(CfOrt-DS16RI!cwV$s7Z->U8rx$t`r zPo8&5gw&@(5-Q=r;6VFhPwNB1UzYkxNyg;~GIsXyCqN6{#dCRj+XbNC!Sb7Yev+`8 zq8UXK-3(KXSMJ1l^`rk%!+)i1ZTMu-%h<%a4#~be8GKiK6YMUWs%zu719I4BU3LEo z{5>h**8t=9qI`KSNYgTVm&@5ugd>FOa6a(GZ?y-p53z8*YI zL-;zfRF2fT{;hc7pIyJYxj*hC$!JgOc7nq*FWRYzWRPQbkDc3&g3pW_#Vm2ed68r! zqLRiN`_7b~ohdTG3!bC1EiIq&X{yZ{xM^lInu1;kq8m_V-Ed`@$-p3 z@P0>aG?wKYdV!?n=zJ=Wz!%#8QZ2D4lulm5It?@L-lAmcBlHXnDV=p5S*#R!k)Fb# zgzA5f9WUNvf^Ruh-WyR!!R|(HT={QXkd79*>X$_HZI5)dXU|^5HhBkXR`Lks;`Yt2 zCG^=_i+0+{oBDW|C+G1hO&IZ9@90Un9tQjm6L|w}2P2F5$h8kY!|@SAZ|Ki&1ULG_ z?D*-?a9k=qHmiAnc;2|_-TiOe8SGk{n?&k@!RpPo`n2u{+~`|Zi+Eyz>r|HpSWIm3 z^`!Y7E0* zDa7^cx~TD|p}LC6AMW>t7?qik5mMj@kx7QWX-D($p^S2ZJ!t{{a_PK((VP)|linn4 z@Ce}3KYw!fT!}#!#=>rv18GS5C_TJ|IFFE08VKrC#v<7*lCze?{B&rax7h<}SETYG zAt(8q0VeC8H7BWufwyRCX1Z1%&8J>8)wq;^5R0y;1#wQUUD8-0$x6h8ZjPeOTz9-B z)a3Wyk}H0z6BQ&sa0{|6{oeFtHo|v5-N(m9+lc#ae{w2g4beM!Ts?WA3YkI_!~?Vk zvE1ldm9>8^{=Vy*ID0J*sb?2uJE8)?F)RCiGQnp$$k5M~Mw~ypr!0kzj=1BmbpkDM zHySm+y)G&>PlnjPT}(1I*`U3A=Rw^}E<~h=yC3~>2Dtz8smt?CL9)YKKkg1>gQl;Y zN{x&UUUAl`_%P)T^S8(vM-F78vy#hm`V4;vqIXW{R1L<>ycAW+=^W^vm(@4X$$%?9 zB_z{y1+ZfHA&izW9oW8GveUXQ?_C%aQ6H?`MY( zQDBxm)1vI-M{qdqrEqfw!HcPTvrl;mj(;Sb;4=qzxbR19dZ*tI1d~n)9&hwTi#+Le zir-4GCZaM%QWb^t4?DhwC8R@)p>BB8fheH@*&3}f$1NW-) z$}{9CH=ldKp&W(F{6|#4_+6Luf%il{ zMMXyOgIy9D)!9bHsSrJHyNS-~ekZgmDXv^3^8WV)*NmhIA5weFkA+*)2gq8cSNlIl zz@2tU>7iv?M9z0F&tMiZ%_YZPQslv!k0AVOBM6w@Et2>|%c98vM-uPb641+DXI}2Y zfL0Ib7F2vm@VWfG$%rj0s1s0jHmWuS%`Ki)z8Zpi^rPD0oDn}pPF&&Bcp`+J9Fu$I z(izeIvF4XwEgBHKe%j^bXBoKdydA}H;E!Z(C2wkPenOeSC;m*PQ*L~HdxF#_?3^iJ4?wU7t_o)O% zzCJ)*D#$?aOR0*=mi9r&ze_*&eQF={)^6a)xH~Y|?eSgNE2CskjAg;InYB-{WAmU4 zS$LBSJ-L`2P?n4k`SNqluXuS==V;(Ik>|~{k{5GwdCDz ztqOCz?TO1^rzFqFNUEh%ts znEEEE63856DJ^y6gdZjKP6nqn@Nv(2>To3~w0xm0DK5Ho^jHl!kLAmF;)z? zKPXY_s3g&TEx)*D{P2IsbpQ1bKF!z9@JJJfhtWR^j2M;Bm3!|qUPEpi`=qdAdyw$A zqnT=de)%?dO5XA&1c0T7oP~O$5y#x=-YIkB zOSK!UORnN&agzMcT1~#2p)+ut=WNMq7P&0|Ick*&kp9Uan`)rmO;yb&u(+s zHrRE0cYe*t3cSsdmA|~vMlse_v|;vwdYj)Oih?$ncmDh5wA+zTl`8O+g^Ji?AzR3W zY6LGKW{-$M8*wgXO(>`@O@}u_51Zs!{IQHg>PZ5n3#9Qpu(@1j49g~Ex0Sawfi2=e#(+wNXIuuTH!qdtU;BpGfwsR<@$*z@0F9&k9JI(7D?iFobEL zlh;bqLc!xGMK{fi9@yrN-DLiKFcyKPH%th@1^db1-Pw+*^3NalDV-$SEgX5XzL zO{ss+)k|0419{^fMdZdJOY^6phPLqW8Kd>rBZ6Rfi7ch*r4qp-=qg-n$VR^G1MjDW z6Cv{W^~3)dlYzd7-E!K+1@arKc}G|L(D3+YQuGLi@dug0s*O&lqJ6?Q<&Fp(x-dUJ zdC&keCkCefRJ!3(>g1((aTENQF!zI;N*(%GDsIS(B|+LDhU=`Px-ezF(0rex2(tHd zeh~RZ@I+`0yULYhks!sSCw^4AL zRHl~N*&WZsHGk)G@r0Ij+N4EOCp^QBbYY5Cm`^hO$w5mN%|40`HxoLTY{I99Lo41G zf8_A)yR(G;@@eh+9#KNK@e8o|z0L{y(;SSSs!JnH`apKiwmp8Z?9KP{H^+U@C|Zi-KYnSCR>R4`d0n7!1<(fsO*sRF_OUw5R>E zFM&!Gn-o562$4$Rk?o4y7jOM>{%F_(eKBK*_qE?Y@$@nlf(9hUgs9N0wC^dP4ADIG~`Pi;cCiof{PyI39jWb1edFKxaF}b zR%;DOO{+P>;_qLdQf9SrAv894o8K08j$CmV%T&VW--OLoK?(c}^C)~2T#?^ScI8jG z1t{M>l4@>ek55+K(Z0zx0!`LS88`xO&ZUj% zd**m)HSfmZR(qroiWE5Hxd=xqBUkX{ki^-3i zer%1vQSb5QC!z1X$%*_iUuOk6wVx*at=u8T%*=Gmo$wKASnVFn8G>bM+UGzyfNziA zOjhJ6f!loL@LsUTGlMc8H%lWR#QKTAp};eEyqOf!W(39Fs8q3_YV2@Q!VkD0{a{A{yG<>2Bl_SQ16nYBa z`e2Ei-&_R0Xj3pL)e5$3dKez@xJ}BB*mu&^e87S{ z9;?59>?eI;0SDFOrp`_nqxwL^^uiS@DCITkz9r{~-lq%n$WrY=EbK8|8t+MXp&>Jr z{Mi(5G-^{)Na`Z*prd@ym=%($scbc!FoWx-*AC6|XhY12=U!|(?m*#E+bU1^3F!{0 zH9N%f{e;7vE7rEocu#q*dzium(kMw2Ic7C+jPKL)f|EMH7)O`Vt$h;NXeabE+ucFU z!$Z-pH?mytR2kcz=U-#6q{hfd>t;HhKby5u zN*fP37E;f;$s@sPH&>u~Aq;(`M-3?UJ3|ZKRGrHyZ&0S?I?_^~N$3%x9t$7xK$?L~ zFIKGV(CSIGpx4x?%Y$7SF}S7O4s5;A_ST{YONf;$LH0qzo^R5hhwVEjfr*( zP2_pDY4_}-4D8(}d@_-`i=3_h#c*o(eS0&%+Xn)w?rytpTWP{U}S?lw}^0CW8n_{0(BXS%y zZ0zg5_9PHK_wldcH0R`iQmI<-9Jwd_)zNP2qDsey z9i!<3#+kTx-SFISj0&dCSL(MIXW+iIhRv;-09=pUIlU#Bff^r^c_LVY;iTRxFZmTU zu)qF{wpv#n*~=~zsH%9O#=*bo&zvn`bpM^5NunIo|J(P~T@8NV;s{lpCv>sb)00}pp_*(ELwBJruon&2HtY=nt^+2I zh0q$#R*rn@>oLI_|HbrJ_E}-es|f$hwpc{^<&y5b>3HSmO|b`LK|tB`NiWSR6t{Od z4Mi0#kx{uZ_qUh`n#L6~5FH($;VM3)QqBnv6v%&@6S@DtWg`ZG;NQzUzqntX2dDIhv@bfnF>pHIN1~-F zZqU2Cn?*!nStzCKcp35gMmfLKIFtgN#hYB*wFx->f+1Sve4u%z|`>7NAib$#GwD!iMp4oUK zLu@&kIc$VC-MWoZ`3IRdj>KToUmJT@p(@yLAv5Mp^F-m<%zf1rzUbO#+^|?NBX*qI7H5U42ivKJ(5k2?n_0=Kq1Zb&xwv$ifD0yNxJ7RUSfx>6AGp1b@X6mA~ z^0oBg-R)#Xt#jePa3;KBoGKdj2mTE{P)KRdx7@UAZ*BP9?dZn8L#h!;YP&(!0XFhn>}54{#dV`9o=7jfwQ(%44zmAb9Co_wQ*V%s%k^v(989 z{A2sDaLCODSMG+|XQl_E*K?^25oRkKh(F$O|7sELWYQ+Es9Ryf+XA+ms@_m{(^Teq zOauhcbd!m35&Hley9H|)JV2^RDZ-#Q3d$BTFZNva!w0Pz$y~Yi7$;04FJY38=7m(5 zx6aDp9lwFJe_!KaAF~cqtga#M6T+~%K93FL=#6!@{~WBb&9KdOEh z!G4!>6#*Cv9Dz6buKT&c`PhAV=;;l9x&xydI=RsBM)46Fbw26_p4+A}%7D<xmj0{{7I9ha?SJkDYN8VE>D{Hc>hqF! zk7pEAB&uI!nTvtEqt3oti6t0hZlz8>cpbKk3*)G+-0j#DTI5335!r>B>qjr_pCMde;NQ`i(wD;0p@4d)7|FO)&A z?Qv71Z|T@vH7HzfuZuXs@4QU(6XZS!d{!a}MaoN81Up1L;LPceWZ{Y`*v}QAvS>=^ z<6Wm)u4gzP?SVI?75ZtABRtI2v9AH{NS;*|YAME1&Ax)2@*sS-u^|cQsyM!J z`+FSlycBi4X%&u@cFoMO{|&)`y7rRRz#3$KWGHX?q8D!ozcA!|8;QG1yd6H4wwM6# zo4jcvF)6_4=QEE86pHYmTYVk}in*;ODh@tf;h6s$Oi&1R_*`zUU<8*@{{KxJ&|3sa#2 zw3aZRqY8|{=&-ylT|VNyvS}qaxRrpvwg32P7wW^liyWqr{hGj^!W~l=dKI;z7iw#> zHSnHoJ{OsSI#R8Oa(dDb{?zO946jW6@W>7YN!wf?<{ZCqWa6AWxyK<3+k<7jLMGn>rzFtXK&xDnAPZe8_Jjnio zMH4UFfVKI`^CfhKUvhX+UOyH>GvB#nlg8m1m33?(7s00wGMAP*$&Bu{4Q}&kIT+HZ z^pxgh4A9HFpSHJ21I_moR8LK9QRrI!J@;={fp6QG-}JFJYLwZ@A1y4!k+y^$-Q7Hp z_+XIEY7mWVzADG|i+jM8B>8z!#cY%}-TdT7+km@HPwn!LMPj+XP_h(t45|vcTF+31 z!U-OgjKh5ONdEA&;$~73RF{nP++Zw+cP&ljC*{0gi|VJ~^Kzn(Y~)vShhc=^olokW ztt`dN$kfv;BnxJ)*wo_-LFYFtqgl9 zwwIE+N?>WTrg`&ZIj$6+ZDnOAIQiMCSFNJs@Gphoud~YmK=N-|M=UH0B~ELFQ>!S*RA4Ry;_2!$>3(-5sz*zV`GoW^Puxkn^;0%IP}*oM=>vt$#OZ_l*~#?SJds2|~Bw^7S2Xugw6yl7^4tlNF#Z_IS@{ zz6dyPZ~pp=Vjlb$v6Hyp8w6A;J+f1+W;nW^vQfZ18b#VjG*zq05KNWKe!dCAoRep* zV}~74{c^#~*VbZO+7znLNlHMj8K0#A;(1!F9q{SUE^$s(AiE#3=mnN5UQhl>JHm(Y zQ|}f25q;!lm*Kql|APX1QRb19NlzDB0*avWPzf-)f2sGFv4%!mFTSn0{691Su z4CrJS8P|!enK_h}K;*RpEkk)Dz*qH?&i83MU|7=p=|Vi$-V0i! zJ(P<9E&j^OBm~dZHE_Bq@IyK(a`0x;2>N4>E$6Y1g}L}eq4Ecne>zN*>zeC7&4E)t zQ(kISd*Hw9SpVzf**Jbak#0JZ@LJP6`yB&;Z1b?NNkj_#(z|Fe zR+0gAYQuMvoq|B<0K0~lKsLBC&l~&kWkXf-l~rFu4=lfL;6kmH4-GOpC+xm^5$AKW z-4fbJ$he(s?7CYFm$rG6AMeWq|5khX0UmGgq?}fD?J5HsO3{+N5s{$ryy|H5(IUA0 zwInZ&h1ef??}N{StRP6RI@Ulh5r(z(N6O=W5wRLqs))W8tQOVmNKUa zPj00J79Ohw+u#-MTcdTzxwG}#gV+PYPUcnGvXY4RoclXZM5KXRTj#|apVQI0Q}GwF zbHf#(<0lC>z5F8vL~eV(7;AhczVTpYI2Y`JzAg$J zuEd_l)5|yY|IGJ}s66(-b-HEWqejHOU6p{*dAUf; zySktes&EZXu?!~oi8>Ly{MCWV5G$}_`AIKv&lG>Jwu$TykA?AHhDzeW(eSY9P|e|$ z7})ekN^jfm4L7-O4GI3thM za#2__{pBr7v^WYo(6c+x`ar6`w6lKYb(q}iyvh+#iGCV`#w-%IU{|hkF>59rHM{78 zxR*jvC~xe->u#b?MYpcl%b*EYvv~Y(C{_Rgnp`Sk%|O|ieDlR)nJ}XwGdravcMZJ6)k@bA4)9Mr3OV{U!Bs1 z#ajc^uk8N1^(p`qr$2Uaq<$G>Wz&i|;cN(-adLl?vvbgQrjyI6Hxh2K7mmHicLLnY z71})!i7ijpWILk$FhQMtsq083CUPt+?t-2vbjisQJB{ke-FXV}hFSQFgS1{~V(O}F8k~z2^F0qI{RwWG_QKR3DhXsSbbqnL zUW$4)mHvP8<1zL`udT?F2$(3?-MCZ`4VoN(k3@d40NX<@Q>%08c+r8rqjo?ZpNLaU zY$)&m<;C7r6>krC5PR%;vs4()2bPacw;RLVB74V(C*DwV*tf^gI{`FDdi*|p^v8F4 zv_3*Nd{C)qB0-JlGs?@DOx4_SLej>XdFK;XLAC1d^EXY`P>ShuSmRw!Ts`c&JlYh2 zE4THPgSNtf#Nd8r*xx+ZRTlPFV#&bcPrg?fRuv&rpo?Cfa2Z~Feuu4Aqz~v66ql_- z^I`Ong%@?S4+(zwhkM48TIq0o#V7kcX(_nAblOt6IteE7aZ8!3N33x`&VU!5XrNB`;I7N*0+gicoUas4jAF;1#K6ZJ?J6(}}dUw)GV z!*}mJko^(?-c-jY=^QgF+M#T%-i&k})_eBBo_zKyMo?)BDVWE!WJ(C6p=yP_N zNpgYyAj7N5cR|2E(MUl@{I6e+>?b83i@@Wi??*&DbntSR{=PNtC|r6L$bVz2qhz?67Gw_*~~ z>u5>=fq21GU#~=bR^JiSofnOt&`s$AWg?t9{;uQd%RX51`?VFb+l8*va-_MwH6Z#ks7nWjhppIiN~YYW$!Z+J!}njOVbChGm*bFh;!_74rbV1#RSeE z+zAK^Yx41c?E>zX$0lvzO|A86lSeKn(!G1i#iWiDM~$tc^xfc?G&g^Udn7#4_*}F^ z^akXIZteTBmICS)&HF}WeL(9$^QW>zJ4A|i)Kk?&-=K4;Q8Tyz1OzyImzyyLNayf+?&GRsUTd+)uEkxf=c_TGET-ZPsJ zvW27}QaFjyP?DmORi6q)g@#go_wW1v^T+eNUeD{k&$-TZy)UolCMh{+$jwNRM{puX z*mIX}I%FZ!_4J~@{mDS3JZ(84dK*^OY0Qm`njwIh;=~Qo>*$c?m(}JOgCd&GWd^Ke z@iX(c0f({-q*dr5J@)qul)dgW-%Zwq=sC^IYf%or zYe76ORMIm@Y zB8k3-@x4)L=5F}tZKr|XEo=PN@k#7EQz>T5EE?rpO2;7ML00+nQj}<{jauAH2J(sB zLkj{Ykt?k=`i@^X_Q~{6>?V4{HP#>AiM`?QHJNfScFY4PREtzp2_Jdvzxu#l{|k63 zS>zoLStkUI1kbgfDTc@|18ur;VVJB);-{AqjJV+2#2{4y2~owa&!cTHLZ))E)IAtp zv~_*=Kah-{OapQ|7_-5^{4?qJJkdMrMPlzu5dusB6eVv`Qz76jBgxLFjs`Aoxy_g5XRv1e@R?hA^4oQRD3zU{VhV7*Fh5VMI*3L zm_^fX*oD%^)MpO2MIqh(Z}v25rO14+dg;S#3JivcNc=f z`FP3iXYu##YV_*W&|TX+g@4T?zw4)Z!BOM2eoA6r=Loj_UbjhbcwnISj`AyMA7-&c^7~1pzOa(-0W# zc!6dq1Z%Wj960wd6lPtNjixCJ;EZC>P#mQ;l+Q*+T;E}Yfe8D)(LNSD`^aQ<5A6v! zKbe&_&drG$#s40(&>sPOVnuyZDh;D|V$NH7Rl-8-;E5rADO^z<$;KCkFlR(JGU%ZN zjT|p8dFxBUK&!|jc4dM$U7=dKtJ{m0eL8#o7@t9A_dnkjKl|g;2?ge7E8IwR_W?=M zt1P5VkWUyiwg7<=O0&H#YH;pB=V`ie1z5Ymzn96_9?hueIj;0t;xUJsQ)M$mUkuYb z(Jb{8INImk-9YSd(pO|WZl^{9i%L~Q>vMVF%t^2e);Ue|u6_D2%VP^qzW?H>%k_nj zvD~cl&xroa%?9<^Jz6*?7uI>G#1V47C>k3^h2eeC$WO1B%uyw2CiUMw8?>9a%qFH9 z0kJp6o~r$)09FI)%hkKyusFSWjZY*DPCWiG`f4x&jP$lfKIi#i8nb(Jzp6QIvCgwP z2@^bDrh9W&D;01@ujYv5XCvTm@~x`br-OUqJC=@3#$dwFnbiH!$#90INU25E7qTx7 zwoB8R!`6>CR5=eE(Ojyep}5N&IU7^1KD;f3yq7>w(bx%}+^cNXR^|o9QL_-qS5g>! z%5CVgQY^&(E!uBC7Y55@yQ`)bjnTOMhMUW{DF_R2z3rKlf|o+H42M1t^Q2*q6?cpR z{MtP9?&b(DcxXOL_7(MjKOy_iJd(?SmcGbnuD_1hx}J6_pU_|W%j!Qxw&a40z`M_{ z69_(I%I5+}4JSNcE5@E3sD&Q~yPfVf=)rER$;-zQ_V_aY>(u?H8nER&5?02bfaDuu zt9CSAaM;Y{(6#t#b;KFj2?Tx%S zb6gqM;tYTF_O_R-36;Zy`4^dM zvGu5!d4AV>_8g8D(8s zZ*%KU_1E9BmS4Qc$AzjF{JHN+K$j5|rR&nb`10W8CL-UxI=kVmG#G+OQ;oBAZWo~7 zd6CRD!5Z93pc>nAycS&(a??%_dL~<6S_NHq77~b4CBxk|JpN^+H|9@0x@z>dbDY&C z{E}}YUblsV;a;k_UXrVXUn_E^e6t&5hEO{u-?F_)23O zi-+S+cUTVT!{01j{T+j1H2+@n_!+^$PU7qj>G#aQ<8aPpjkgH4W`FuAaCM=Sg;o9s zqIYzqD_$+8ssxmp%6`=AG@#BX>z7>3c3juHaFi;k1Iu$-_RtJphSA$53&*4~FlTXK z=HUC=_=sc7mE_X^=u})|WUtSMCznmOZ%G(p9L1@-`~LdEe$8{P4}{O)LvNLZ!4t%s zP0C~ZzL)5O?_T`#y{Q=gbZ#4nD3zf`*S?(&wQ4*9Gf@-c!N?yvTp?AbgB5z*AZxw+IFTAQZ z)rmab8mpCYLJ(Yet;DmpwHnnuLMLpe-0|RtN;fh&Gf1b?^_e|)1|PRJ?q)0p!|Kgb z+Cgiv_}aiZR5K?Ol7t?$L`)Mo!B*2FnZA*zl+?*zQ0{@_*#+0@eJ`EZ2oj)52VIhgSbnil;T}A5Tlhs-b z%@3Q8{*#Q>*UKy)8k|P;3zpY%dCy@}?nj&M$YLDQ8!=3IV}_R$=ad@1w%}K_hpPvP zJ^V6fCm+9bI7}boy?&OW5ZB@?ZbszQp^s;V6Wd4usONid{CU^_1rLYA$qq(9f~?c# z;UXuPeaYQ^AT<%F|Gv7+eL5YO-(OrF`5q0_X_~>Z1kdaDfu|_>E)eCGZL3l)6~b5h zlIp;7Noczy_{5Am4ED?QzZv~$iC(R=rT-m>M44=p2zjdveC>TRJ-DnJ_0MoHCH^eH zd)tvY?+Kk${P=lAnti8n{`U!P=|^73Y&mm$amx(sMGy2ecG_UiK;>hJ{ei%qJ;5R% z>4NjhQ;cuDH!7 zG+wvmfFF2-X|pa9J;=iC>tbV>=va``^coW2=D8*DGyd6d=Y>Umxlsag+OPyp6Z@v- z|BrXwkzm5}HlkP92KQ9={;UjeM0u998)HZO@MUaYnZpwoTvRr9x)%-UOsU z=M~Ke<>h#I>`*tIxfX{4WZIhSlEhs7kGaZ}#0Py0Wa^$KXW^Ljc0-?SFrMcz-Z)G6 zrgmEgSNE~HVzNO00FQVk$ha8K1TcAn!2(Y`TZ#kRvhk1Ak&i~QIXm_Ae13EwRW56; zvO+SwD>A?IBk|Zk!^$yh9rW4Ve@Xl|AJJDwW=xdZPJ@G+aAM)_+!x$n#H6-IC`~g>1v!<=R9WEUk6;!HZ6Z%hv{+{Z9@BwIEJN>ML z;AL<>m{wxtA$Swv;gc*?0hrQh^nJ(O0W(7#O=Fq;uu)61RaZF`_qTjKcFLs$NT0K+ z|9n#kV`F7~KPD@&Rdd78*C!SfGtEe^eM$jF+w~j1e=6X;j>-6W_A|iO^_|7CFc00% zX`_>DCH_kAd_dO_imanM^X~sF@rDwo{)Jc3;N{Hdd1Kd;cw85qJ64zl9!_)X8^?^n zC0dcpKPD62Wd&GA5YM~R?6Ii}(0~!i;&RytEXlIAf7PUH9Mb!dm1@i+wieo{ZO31HR=A zq~cFUU)}IJ15|sYw-`TLkD0c^ z_L>Sbc{jFU`kyZfi&swwQyU;d@2li$uS8sUdfiQ*;1MLgD-duQFbC}1qaPLHhTk>f zZ>K&YIAvt%6SqrK@rjN19ox4x*z&@ILAOF5x?S>iNDYa8-u0Tb1wT1ZS67_*uSX0D z!2H$aRxQ{wHn+OXYz@)mR6ENJ$$0S78yn_TQ(Veoko1qL#nFE~v<@t~(6~Dn*HO-e zE*$kdkHb7M%i&M1?h$vOJ`vJoQ*4Xg8DbGW1P^m-kYt>e;5v@U&{D||eOw!-Oeya~ z$KdVp57YURF8GSXM1R6w85lu^>i#+KmKi%pJUNZCuW z%X7sGopkE|E7GV!?+$&#%06w3Q+;#E975sUkDQY+%<1^mI?(q|43U#L(Rq#IuQk?_ zNGhZeJ^|6@oTaW0ry;&QwAkmWE3lC&{CSm=hD&UMS-Q>PNcJj14HR^+UM2HAN0AP4 zW{l-&m}ugVvHMYB5mmgnee5YoH=qp!3+khNJ{aLMMCQWd&@wZ8|p_rwAY42b70V z$)T&PU-qhyG>nj~&ObMoAoyVy`&T(M;l<|88O~BcTu7)5<$V+eLNw2XUY=1V?gQ3m zdDk`I3+JS>_9G@}zJGm(jgAXCE?<8&T+Bf9znB>vizGPoFM38EzCVB&3zqY%XGu_E z`J*=bG$#lqUTcw=V}XnA%{`i2ABWPZuf=ur0_Lo?oJyaR!bv)Jn#;cxz&`2rs8x$5 zmfj2RS@Yt>w)s4Bnhg#7d7JWJVjeeCRJ@AV^JaZWHl1hmx+OdEr+?}Q@nVNVO=TG& zq?a(vI9aOLX5iNyTyTaD6K7VG}^3Zo%LFZ~c4~UQ8?g**14t65OXPf2wVn<5`XIVy1n% zm~FklW6LE8R%0!Cb*%>QWR$CjI#?bi9P`DZ3N-KzOSw{Fpa>QeKiZq(?uvqPMJg`c z$MISF2Zm;Q4Ll<0XuN1t11OQvSXXJ?NCDL1Kc?cS-HS!Et@@eECb&--zJ&;T7WeY z`z|$A*<(-1>OSQ!ymU>t4f{j!4u_b}{nrWTHR`3%G$0A{ zoSOl;r6FiOY-zu32pJxV!qyvE*&SNNYBc za~D(CH9n|^_KfCUHf;pgf7{+xNzetQZ#O*q)@WH&3*Hdq`u4P0 z6B|k_en0)?yamkKi|^rm=#F%r&)zTAvjb_>yYE)F2>o@x^vKEN(SPN7StCl*ZC=T!R%A!)y`h_t&d0>z80wm!l?{Az1sgw+%&Thf%N z%nm`v-087=wjit)m&sn?l?S0dGsln(DLi*l^wNtnF=$-4hc0=)7r|G;-sit<@s^!w zSL8Wa^!jIUBk?B-6sAw5yN>ap;9B2c^kr$ZP@h& z`bl1J(FEe^lzXbxwIPbBCvBNV8pW<9|GW$|H5OY>T~WtBSDVV;9JYs72c9e{Dx8AA1Iurp z(jNz(`P)z3I<#;iKfXbFTod;+X+Ci_Rl=IpGJ^%$u@nd zLkspByV5?_wnH1ar3w0K5m@Q*XY@a73CAD3)%%@ehap35v!%9%_$Ok^|BH$&2I@w| zQ3hH-a|exl8m|dl(iga-6QK{D?C0x~^vzJNys>L@#{>7Q&sFriG{a?bQN`gR2M|kj zoYp0{xKa0lp6khL0QcaZzQZfFz_Rd9ODKsOoZHq!w9lVFV?)>K(h zKg^`elt;9sLAaop09#lRd>!0-;p`)V6I|sIXCI;f*#Hj-}nH>$VIFGB{ zUZD|U+sT4M>OW%C%(^w#>q6tZL2~wUz_|IIF<@WE>x;GPPssxWVD+Dtq*vt z1cp(0c;aRy|9!8gndr8{TXXVH6tb`MX!gt4!_+IPzrTk{Fs(OC=@g+S$*+&;S1*~M z=cyk%b;j<<@!$5=hZ;g>c=kpl_$a|AJW3wQX-ecz`*vB$#%|)1PG9^%nGWO@a-)*$ zsp#~-fjJfB>v-3VlO1r{l2Z4DMF@PYzMJ&G-33X?@9?^P3B}nw?JB`e3ph1N6I6FC z83Mxp)Bbt49C3X(@yRjI39s4b(XxLO_|94zs1W59J(`E zGH(|_s2Pu#aV5bo7l~EBeLe3ElV;c@&rrriiaUn!RR>O+&FyH;uM*Jt5wm3i%fvTRyi? z!+uNW-|-@$m_ANj71d-9JDc*G+jhbD-1*l;?_eOf>FM7c-RF*%n(i@OeP)XPrq)eg zl^NpV&|%uPa3;9xm36lwTN8~n3?D&WK&rmxw~PF8*8i_jYpMHzTE6rx05z@oD1ZPKp#jozuWaEB%R=*rp0Mw6rgr6 z+0mBxaI9^vek@~YfrSSzd~?w>K0rxn-6M8brjkkyewl%XLO$xIOeMg(i?_6b zgbss6%4@BFL1kzeyZfL?N)eSjMwn+g?BT)Rk_5hojyOg){K-Tp4y2!b4mA}EK*b`t zuf+68iCpr~fAm})k`5pO_ zkWWFyp!(|=&e7*x;Ihxe{y%G!s{KjeQYwY zJrgJkFgx4!#uIs-dRpBsiXiTaWS1&}eUU6gkK}@S0{Xp@5~4Trfr-u4CrhNsC{DwF zt8U5`hL<=sugy9^$tkj-x-n(=sv$mN)~W$tC5@%Iw?wf1{_P<8PAOct;de!3$_Xcq zYCf$aHAin}O$q08N%Vii<8Ig{0`58YEK}YS{WMNWazWy(aLcgeUW+3WkclZ>J+I6H zNfPGD+)_lZw#2HSrjrc#pV}vAFt9V^^>F*5%&<7z+1YD)jza|Ir)-y3UyI_vM$0>~ z8>Ud)?@k^zXNKMTXPuq>T!CBdVw&cv7cmF*zAeg7fH-NP<5P=FSSa!T$plU;{CWOg zNQ(yA`eh^v39(>&fB&uF79l89_BfEp?F4B#tRrVX@_@2d%c1xqJoug6twd{A80jLl zxCAaAMNXBI@%OVtku5VtiSz*reqc+;ioe4K6ZKZ#R3m8NpzDe#P3>FtAiJ3p;-6UY zwBV(*Yd<9L{75T@ZHy3Re5BerbDkSF=}(0S7s#WG|FKK4mjDbOXYgCmseqH+nssHI zE=d2c=u{Qem%AupIp@{yw_CsX8^#Cw3lrS`QzD0ZN0h~B`qzinj_f`uAQJ+TP7Y4h z_^;}s&dCKUu6*FN(YK#G{20dN3JXSQ>_w_4*Z4xkwefFN3`zAS34Gg{?zbdc8+uX2 zsq5AEOWjx1&~W}J8(dXu?fQ$AdD-T{9AnE7`O~p@Pg0B^?(|>^jh7u3V7?Lm% z_wqoMkCc>9d2#hEQqiJF7x~qInH_a13MEhJ`k8ef~^9(ybX86ypy;pg=5T4b_8dyW81+S}Q?QXR)g z*F=iVDkIqA6RSsmnH5rhsjrOA2|?(@@GOl04@_-8J2Up@1T@@v=kIO63(dKehi1DO zK|)USK4~i>u(fw4ten}0CypoL>1erg41@u{b5Nm;m4itvn?BqKzdc$v;5MbI2Oe9r#_hIJ3Hhi`}CzJ zWU&YOcGaJR@f1>~i&4%PJKM(@->i@0e8vU4UK-fd5#Q~6yAb54^RgYvIzje^Xcxal z7uf4Fo_B>PP+Ur2RA@_u?#aOe7uxOc(C%5x3U$COrcdk4R)$!OewQjgsG;p+DZ$TV z8Nl|9*}2Ig86Uj5$X*mzgjyB8Zq9d4p;uu|%)>{*=t0JtDsLl#XGq@(sR%e=w9m#y zVYv*(?s-?p5z2szYkWI>*Ww|i>BXieI1_w2s7!@`$Kn;qT`pzu3SobRCus04XfB@TK5i|PGQUr#C+FxVR~C?E{= zWY_DXbp3II?mzBuqF=S8@x=Imi8bcdUuO)6wE<=Q%QvUOY~bqO&cpF}5+EP-^}=6D zRamF`k*9qi0>2lO>&=h{<4u?HxZ8m0ozrtE1j^? z#%?)&DlM~hjYw+)KF5Z%Mf*=C?Z%q@O&^N=KYjQKmOh zAoO$FQJ)7NO)!5w#hRs^7oYS*GEY5}f-Q|#)Xr%D?@LT&ds=Bhn3r#D^{mA^K0js&~ zM}x_z&?6@`I5L zikNexDVGF;A^)A$A~sKmcyuP@cY-xkbUvsSaVGSF!^O|8ZF0kB#*QTwepzhP2>bW> zm;o64NI4^+`&M0ItYN$VGdKJ!v)pPH(n2AF!+(rORB+w9h3@k)Q6yb@M17fF2yOr4 zl*rm63GG?jqxFW1Lqhbtv#rhMSa$gCg-)~rhv#L~Q@J`QtR8TXLY3f7*&OW4sQ5lK zc>YG1*K2EV;S;uK?h=IP0f_@Ul;V*3uPHc(Ng5`B^1aI`B`|2TXHeqS!hbImB9E6^ z;CDS0C+7fmup4m8EsxoQQiD52ydCnWGow*vC#42oKBgpBI4WU4-tw{RrDHHXF_R=X zqYhL)zs9(Jso_WZdsMPbl2G80wXmA633c82{Y}r%#?NR114BmQ7k1%rwj5IPE&v3H^i!y>89Dj zeBk@u@U`FE1F2r0c(HOd4$J-ukW$fBK$M4ysl7-p;n(w;r+J)?A)h_EHHnj_DWha+2(u&fxfd0}5_a%NL5VEx zemS1md?i6gQh{v`*s`AOJr8ZULA;(eiTJ{0RJHFx0lNO8p6rbn!KxPbR_+6*;m~vX zj#my2fS=U34~A)iGUw|L8Gn;-cuj|*yN2Mu*_`9#_;?K#m4iT}B{-c^C|^i)1Vi&-B{Rk$eDnCfq{GC0&U-X~DYdH~ zVzM{IKkNyB29j6zGd{cH;x4tZTtPNm4ayC^S`v#z-cwy6Yv$;orMY_1+7ItHJal-| zM%-=JK4o1SvPAwT)F(#Wt>ONjF?Lh6bFg_Yzwt$=HKwS1l|8tp2%dBozoBUj#c@|X zjoNdT*z`|tE@{{V(KIu-d>czpQ zg3sKcs^LJP^=4KhJDSirzVH?ZdtvJp=k-I^gK)3QzIlWx9xXpKR)W31Il?gk*0aUWSs{JVzik11N4WZref)rK zC1ef1tbAoJg^V|P-@mEJg%7Si6j!7}@XU2-aU;)Dm>_*h_Mx{W>}u{`ciPN{!29B1mgtiq^^ly-}Jr9XL)c98s*AoVd~fB4PBah4Ti z6m1}xhT!e}@THq>k_O+KC)ehI8$M_-7?3J)!^!WzFP0}T;dX({Wm07k$SJZEG?C`U z#8VO(CJ9VHn#@_yrY3^cGhNnZwD!CD;nBI#!{<~(#B%!ZAfF@+fT<-C`oY)HW@)VQ0>jx6m_pBaSZ9qJaRi2gcXHZ&z^ zvBEfaZfJllEz{SbWQgx|cTmQzx4NyPwC-7zyrDUV=MHWWnd-9Q?PqSWBoB2{o+}NA zNLR1%8+!jBq(EJNI4ob5=A3%ruHb&2Nb5`md9iK{c_()c7vhF2z2 z2bSM=JTI~XV2?ZKlV_%b4_huK2a+401sLhgbQvigSenom-F~S3WstFtJvUsta|10r=f*bcK zuiM1a2 zt*M(Uqs5Bh6T>AHzQHg^%ko`#`DiSFIq zrcMTlLB%O~CPmmK5o5JK;(#s>?=4W#Mq-Hwj&JTsgG*phnnvi{BkmJP6!KB{jcGg6 zanlM(3dKCL>|&9n#>$63!U2j`Onp7S^N6trZ#Z@k+r#=cL&S&xESA9zyqm&EPz82Ww`dg#<$034GQD`()OYrZ@ zO}!ZiKcw~3I}96=hwzY(RJfpnJsJiYKRC+ZK-?q!+{CtB5gkpND`>sJ^Ndn)Rjebv zGPp|_@bDxmh3u08k*!@Z*hB>+2Y7EH5=L~+i=kHmg%7ENkF49x@xIj4lPE`oj8}rW{cL>E; z;a3);$th@d$!?u-HyUzfW*xT<9R#_2S2n>0bC?)nvoUQaJTzHiAFHHXL5tJBG-RI~ jCX9Trlcv`Os}cH_jjq-JT$N#*)u?;i!ecwsQ`m$Hrq?09-LM3EN zT12I!(k9>e{0rY-`r+ODHup8x+}CyAk8|#`JkPl^&Y0>MSg^AAvjmGdIr}-1#Ey%I zDY_gJJ0K$F;!7fv?0sx~NlwoHcU;rn%g>p4+|SLP;LQBKUry$Lh=QE>0TG(W|L13& zUi!Lq#y^6fJfVL8ohf+riMZoEHVW>MwXcilrb6wqhlcJOKNSD^RC1pv89%D(5BJ69 zLM`us!Im$nc*KiqTQ5lsa5dVk_@ST@S8 zIh1$#PB8TL(1Sy`Z6W8YN1pFOHlAg1r=25)z&o8?=~*ro;QHYb^_ZzQ4q6ECiz&Fl zoY38`5f40IX3;!7te=X4SGvU6i~XR&y(*ONnID`<%sJxvgNBD>tX5C42f+2I-IV_0 z{=o9@^H;CH_le9ak9%FwIJlAC_rE!MJkuX%4b1jCZgv8T5AEh<=Q7YGi#uCP z$QOD2_ID?J%EalP1#0I)OYqw{_KpzIBrK677`*DSLv2RG!P+Gvvk1qo2IY*J01R7=`&7AyDvYyt*QR3?~kb`MPsc@YSjX!a_wH{OHf!Y0{7Z z+e0th5fcnYdSmo?Ovwh7;oJ0c*HeIBbbFmDgMlq<{po8zX2HS214qtTrocjkz2~*f zWbCF@+}gh`5ym)7a+HKJ;Z4r_+z&faf&4oUf{#W)bYEoL<*6)aaS#>JvT=jH=#Y_g zM>=p_yU6o_hx(8Ize+y~OMZH=b9SRYNW1$fG~Fu)HHi(*v~?M%Hut%8A=nF2))7J- zoahjLbN;RUHVV?;$wXzm@`6USL*DmC=uo*2qKHprzzqAP}$b2 zTlSEE;#Q9N)rr0!VaO3AU0DXZw@T(TWfmZ7NzXnMa)F@_Jj9Dn(R_wnyUq^Vb`=JYb)((Bc6-C|iNka4F;!YdAcF6Jlv zco7Pf!#vyc#5|F6Q?<)Oc7J47R^ItwB?z`WG%Py$#1)kl%J4@w4aX{ObGIz`f!w9q zx?_)1@j$kOM}~YTdZ$TB2#=)xFw{$%euO$Fwh&B?OROugcSxM1^1PowBWLL!oeYt8D*zS6H87w~WOB!R6>r(Ks_C~WjiX@AW8{VKMIn5SkW7?!JYKV!~3UVC&$WhViO z1dC`r-9!v>=&I)N2}OQYE(TB89B=}YN-HjyT zG`Z#FNiGtuN#=}S!#pls?b5HWNW{N<`~T-pFFo)^c$@nL+`MJ;o0jY}82PQTTh%cJ zsq}M~vcip#TlC1au$DL&T&qYm4T;6OZ*qw;4*p2Hk-g1#2N7Iz%C4W{3c#Yp?WC&N zJhbl?wR9iwgxn7@Nu>EO{E?CT-Q6P}H`T{ok&5&NYs34s&L_igXE7Vb#gow|rPob4 zmH?tuP5o!l0eFhbP)|ul1?Sz}F1fMA!B_QYWsx1R*qC0#aX8Ws59R6{^$qlf7D1Vp zcI(Ng_@B!_zbiKL60xOyr?TC;K)Bf;!2fzjCPc2$Ry?q&0N=kdnBL}F3hFm^J_%|k z0^tPdN<_F9RzK2cQXV0LDd$B~jsJonSL?Z(q>UfO+sfau+#Leh<~uS7`wGZ=NB(-F9)&dgLT1S8kECu>{2W&QL2f7t>}9F4!zv=i~obeO=G z9UjoCQ1@BU%Nd@H^qsi5LPhH6wKoIX{9&-TC-wa}1B~74W~yC7u;`7gY{;}3gg4jq zTYJ%f{Z+?UrFtOh#B^o0HJQMaX?{G8KM<0a~_QDNe2*er1S< z{#?5$%|S!2_O^|p4+0@Nr0{%7i6OQ-k+p5rgK^gQk?{|Q5Mc1^(HT-S!c4)StCEjs zC{;CW%_0^A!##zM2HqLs(p$d9)1ASn)x)+)BrgPFJKO(zOEf~xb?2kg_JyKP$DyFK z#u$*GOB=}Dt_3472#|Nocy&--uMbNWam zIO*)Idz5bpcN|1D=0<~2On7*6+4o=&8|10^{@x1|#O3?w{3J|M&E&mT84jQNSu^98 znEWaWZrw7RjImqRTsWLd2VJtoH_xaj_%I&G@@&un*MUv^urD3j!j%*E5+b4M$%fz? zD$KkfymYbaL?iTgyi0cAE&{W(6@??g<(RPR&24>O2B_TrxfuW154fMl%&7*DQRnrh z*byjztJgoti?*uaQlsmEOPVBfz69r5*$5c8vBOyYFdaXeEXmv#jsfXgH@23z1tN-= zujzjrc;CJY?j|lhk+13b+wUil8PR8^p`tLsQ8uF3+qW)a+AF8dwa3GnAispSqZFlF5r| z@y-G~LT*ytd*wX5=^Aafy&r^ByRM@*ElW^O?CzSx%4FbvcnyL z-jGL&{;)lYaaG=(go_@>JvsS9P-3lu+6plkxHg1FEh`3K+6u=I%n*@Qt7)ja))yX0 zhWzoTyCF)rh+Wa4V(x45*zqn3kfn1~FGLbB$K~hePaFYgV=KOg{*ekE=j5xVE6D%+ z|6k_ce#-1&vz;5Pt+-OB&)Nj<-d`LXHqM26`hP-tbQAFAo-JEC-ju`6(ey3#rWN3r zyf?6rI~%`P1gWfKH9_L1^{s(NnRO%40@FA25JtG_8j}JkO9sr->#Gz-oU-u z=ad%sE75MYLvMF~23#LYm?wX2!aA{%G^?k@xcQy=uP^Mm@KHvkAkOkKcKW;$?^dor zt;i22wH4_Q#QBq!u2Y6Lc8uOJ$Sc9EZ`pGXuw;SMKrK1-coWK%DA2FRm*Wu2f0iuq z8K8E!=*zNc1HM-NS^mJg6#uAchRBX&f@N_5`{KQC?1}2KVVew;mAoH@X&sC6!yPZmVxNXDLVZ7g&)eVIU>3ET0T(XWAu?m@n%# z_FS}y`TpgeXCkCGOXVL9^1`e4Bs|!YQgQ20GYE2*K>a=b69+uXFz~$LCg(LdXmn{% zS~igi?HgO(KKRMN-{<0nFPi;}|Nk=nEVoq6+-Q0Jc(r_tWoS^|$Ak73H1Yqch+@ME3!t2ND3usmmS*+3>6&AoC<4=f#poriyJ(KJiItR<&)8bj%* zw12;bX8A%Do$sq=`#u2EU)m5|H}tmE+tm zTkw=ipKW9z!SBH%55#5TA>giLw+ksB4Ks@>7hUwBr|_Wahc+^_Ocg7cxjW;ggw9u| zuLq#AmG9igS4ALh{_JO$OaZ241e-1^JHYk6ZJXVda-gJ5XhLH?ANRSm+RxfXAYI=3 zTr__g`2MisRP>``uT7~OySgc?Yhd-r=c9pq;dt51A_3!0e{Fm!rvX|Ywj9nbtA>G= zwI#DW1^D5KxZ0F535v|0vb{VQ2Pa<{xES;2|MUNU*#I7r75<}rJc3VLlIg_*Dkw6z zYCgRn14epAc|INw$H6<>9_JrCfkl^EZt+Ycfg~y4XsFT^Wn;M2CLdDa*%t0iCO^VK zLc=^D-aZ}Q&ub>9S~&wx(8s5js?uSt?({InQaUm!-5kYcyg+!>E{fohOb}aKzjb51 z1r8ZkorFUk;4RNx;_X8LvxhHpgO7yc^4Yt)?DJHxmHpJ3jfqL1Ms602PM1PZmSy?n zuZ~}6b*3w78L-&4HSbAf9K^`!R+}Cx!16n6xrYd;xMk>_;~v?cw}m&6(Zwzozi*tm z8mXOzfuvpII_aLE{E(4WaF2p_JDS;^o({*>KX>M{ADKh>WCUqdj5l73AqHB-`D69O zp(}m$q;2Ffzy^BggnQO1#Z1_aSqCdiwpTz9I zVqqZwKNGPj(s^THd@xpC?6{9c?%?;uwX93q5AP&&Rv(%R`^W!Z=Ko;izVBZLBymFW z49~zw5QZGvzSi?>4(MjS6cu^x0T*l+E{0q(f}E2Vx_!cep?Y+cOu{D;eC7N0TVLM{ z9>1(o(3%T}HP?nR271oJEA}VD+&lbW+0||_H7FmnMss%-EqcLLx{yanlmS%6YKIt^ z1jFj_tihN-0`Q6kH`UGP!|V6nxhH$*FiTifuOkr#qlYRO1%;^)iybGojupe$oNs#5 zd2yJbvZ_{Z2t*$N3(x({IiP5g<-EVj9k)Lcx%ryK0Pj#UkLd6Q;13)w*dUUKmNhwi zeAnr)&Fn^Dj8Ht@H#r;p`n@mu9-0yz2{FJvg{R^R5)Ab4Yq#K#^#Gy7d%unLJEEMG zif{=b8?EM7YPCKRV5kO`cFWizbz8Wmjd&JPeC}{}_{nPfjPNoE-)D0SUc!l5&(K6Z9rxM|;Mf}4Qt{8B*qA}N5gmx z2~0w=JSNO_(LU_9S@Nwg5VG@-qyZ6j`|+MIaypGhsDs}Q#KD#6z~JJ#RH$E5nJ6qp zgTH+H|L0GC^x@coY_kqHJwIMsNC^>026Ajt1lS)k8IFa2!h^~JL0Z%DzcZ_Tl_k8mZ=|; z6TI`F7z);k4I4^kVfEh2EN%bgLD56L%R2jWp;uA>!qygI-zJuIt>Z*^o2L2Lnl~5A zABD;9i;IR{?g%Z?v2?sB^-fpC%v z0x$gqEdDrR)-;DVk39h`8E*8Mciymvagh+VFAvY85r1FT&B5C8q)FEB3vh*TNH-wP z6R#fH7}KR0h}jwJX}f<=Aad(lfewpwv^Os8YOtqb^&(~-rNqLQGY5KF2HZeirJ0~Q zn}J*Qo@i6bWBlX)FI!;0uev*%NeZODskvP^7=bh2Hmz`1=b_Jjn-eL9A<(4e_TxKM z9$KP4Z8r9xBDs32=X6>=I0(F?M>BN>Eq_v;1+f*OeK|R@)H@h7*vLw+XsW=MZ1Uwa z(H~{J&!1h~849cKT|RhN;TR~!_g`}73Pi1~URw2%$;eo6SCbw!I#MxI;o z?=Wifb&Wt_j^Fl){Y?FkXry?^LmEDH8G0P0B#%S9I~=XzsIXx;;uxO zkEuYnwAdzdnW+mNN$3;$lmXKB4eeks2tKS{R-I6A!Z@3R8^&oi@WORGgGDS7gp|76 zBeQ++&2gJxWrh(X>C|_f3}L_*PHFB`XHPV5Iq!b`ggxl27GL@Kt^_M$__ zTRFLZc-&5ax`~{IQer4*H}Z) zw#)!UsxQ2GSk2UBB!5t2`xF7cj|yy=i1_FK|1$n8&Wknue8dQx?$SQ7StSckWxSc& zJrx5(8QwFz%zQX|)$}%X9%db|T4sDWIv=EJO8G}pL-E4#eV_LE=U^uFc-y)KZ4_v; zl#&=Y0Nk-X<%+k%u~yGd;KE5F)F*8J>~>~1l+(?3c_>C;&(ciFRyHD-4L1#*HQWUo z`wiZ!1TygCc)G_UOJA5_)#N{yA_~c`TBX?jW9lshUbdGOd4r~d+s0>B43N|7csU~- zhijx(TE89jgWWpvE{AkuF~z%Qe-2+NTH87|jy+W5x;Iwo?wJX0LxzvD zUbp~lX2C|IjEqu)wh#3!8NlEAe(<$zG>WP|)K#e5js9=i?!Evzu;kirB5?cQ&5viB z2WDk4j?Q_dx}E`AYrm6}6$v=8i~R}DxF5P`lDHPj=uq^nivCk58jaJ$#r}KZkD{X5 zTwl8~;AX;;@8cnEKtCko6?oPTsC?#69)%{rM{bGn>u>!3<-dQ~0LJgGI^g)k4>S+z z&Ji3Oaf-k0K}#SV1cR-xDaK2t~qhjGzpWi-PwW z2-V^26=3=3N#NId0v!Lyh(l?n9(b$b<0DriVdk{$!SNh#2p86SRpCm*xfl)2gGa+5 zrRcR+N~Rb5An4g2kT{2Ww^K@cy#qkxbeq%mXz;>tIsq;==c1 z{?OnZyl%A59#vi`N90I^gW@6M1F2ztC~Q0Yt4-}JeEdf6ov>h_wBxEBCILkVzZI;0 zq(?!+=Id8DO(M}J?jHBi8(BE|L8#&Vmr%Gd&>lBd7l}_fmUUZwqVQVK*LBzA192fR zp^AM?0^Z`Ajii7#zR~bHCH%|`+N0#}+2095?;A~H>4Mpqu(w4==w}GLW-$tT85f0V zKijikm6T!4_RpHzBeP)lR964$vz7!0ges7sy$LtwwUiIHEXQ+TeI8x#xn3aJ zt8^dw^fm~7>8c9;Y@x^U8(O__T!d5zn3Wt9$*osp9AgtiYXzeTszXz9RvE}&S`J*bM zR?j^^*Kj1UA|(&S6Qgg@F$r(AZ&i%x4TS5D`)`mw1Rz6ZxUP_ef{pEuorji45X*9l zPjQnUzP&@+UcT&&D~cKUe0hPe&2PKRN0mTSkPzLY@sotCJIRfG*?u4}?)g@Zsl%)6 z36<{PCSY&vnE2ZpwxH>)wqc`OF7g-YUan#G`P6eF&JC0XLht;}w_imA@n$V?$iCU{ zAOC-ue~UvUlH4C7p(lGR5G4{oo>F$Eua$s}g=Y;@Gm_y{2aUwI6$=WBS{tLfb8woX zWa{kVjEsWqiOTCr;HH+u&QW46zMZZdZ*#B#gCDa`qbEb)a^_Rkj*cMQH&%D<&39Y) zpj6ge`;7*Ma%_rwE`*?J94AZf*#NM7p}D6f+Y9Kdk7Wjj=@@m>=*nW=G5DZyEwNoc z3~qFM`)<9*7d2!oZ#l$Af&b&>ZZ?Y~2)xm4%%M)fQ>9kc=HCr4zJ7gN_(2MCeZB8p zqC$p={K{2Tx+Hi_8*N1WBwW~8y;8C@2x10Jwko|(#Zx;3V%9y5z@N<`Cgb-rA?q~gLt?!Wx^FY~`g>7f>v`~_yb$|mQ&h1ow%SIFRSam3T?=_)5v+;F>+EPL!G zBUE?L@;$6nhQ)))3h$&cfcsg;CTgEQuF)=%tsQp3{MQ~8iAl#;#Gb`%5rW~@lUzYCF-h`wTR8gWwb_x6U%AKpbmsq^C#-sTq} zi+7Q2{)0DE8?xF6-z&zCC&XmU`}{$Ucfr;ATngNXoX@Z^h{UGnAH9DsGv^?!x*`6X zHKZ>IbuT?}M2Btj;*)zqVD$4qyg%6$2Dgn$bW_PFaLZk}@D>%uo^zE7_1lBLi`|m9 zv^O@|%$q=KDHJ4L_Z|qyfO6#A;>8(_?S3!PJ-5^0E7$gQ>R(dfNYFXVpN_*vj(<-7 z$;$+lihYJWM?yiW;U3rhm&O15|6k@`Uh#yJ;yyAwF7Ih zE6vHD-N^g7NDapn^sHFtL2$)ARKUJ`U~C2bV) ze*4mM7X_Q=Gov}X8L)B7^;@zuLomAW?ORV)9#&{QRHAgG!t`no^o)!E@tOY^R~N|m zr$}pq#RUf3PWr*)qC<_E1gDP)|K4Ap*flmSXS>km9qGWh5J|1$rNNYBj9Et;UQP2e}WS0qMHY|eL3 zON6BHVUdideh{1WeYp=qaPxW#`_bDZ*f0On=gNQ|l(}xM61*9LJhCFY&paVP<$oOS z;y31iWv#-VvCRc|dN?aHRX+|4HoTwo*0Fb2af&|0?6a1hjX0J~!z!Z_gXvOKu--PN5u_Up9apz)ZBoj^ z6Rlg`<=B>h;@4*3b%N(HXI|OXX;U$LI@^8k&~+ky;%mA6oH;Lf{+O^#T}B9O(M??5 z)I`S7nUi1b+{ws#{n%1+FAdawPn4`;>UUNYZj6|+yQ9hOjDZ{BNnpO?L|&d&J{k*o za9sulez{3o$Mq@+zTJ4PcC9Z0n`MT6J}4&OV!3s6SxW$ntxNpF8_b-i+J7%lgo1V3lk2xHGxa~#hGLDoNpM2)vXK!n5GDUx{k&Z)6K?-^WQ^jK z1IiaN<`#nknDe*S!gv_|U@rWFU2HcQ8ab493|)yu{MvRf^tBgoJv(D0YC8@w=e^N@@z#pDBc-ft0r{MHN)63R65^@D7eDwbu4cCXu zKO7Xfh-*3Sj&fM!qt2&(W0nRgthgjT*w0|*y*Bb>!QUjTzq7qZPA?Uz&b6LDJ>ZW^ zx|Q3NUG;ITOek$vX)1*Me9W10mWoQcJ#>*X2AEI!+UrmBhfmgwsyi|iI;na)Uq<`_xU*=zc z_s$bx!&A7n>qwa*U> z0H*_^vtQFFF!qL8-^i?=mp@y^2cUsl0x53C7kSLje-|0YFW*8nC-)lcc$8GysTK}3?-|niV`soB)?uEsiipxvxzt0q z|MkEAGX5;C#0})7BTmStu&>{~)*qxigBz2JePB@J{`pm^B^fMsdmFAC!DYw zbW2?H1Ea{2-={<}VdE6PV&V66oVwfeYAQAr)IzGy%gtqjLy+P#m^Q;rgFI2PI?TF$ z)mFjj5oerQyY&88gdK=%iR)X-m5Wu*9ED$%Okm%iwc{_hWkAyazcSUj0*yzN6DS)~ zF@$gLf(rjBBwHkGE!iH4k~8*O+r|l)KXCfISK|&4pL_Ie;#CY57Qgq`Bl)AoXM<*r zK1-0?QTS4R$_Hgs74bZ~E517!FkQ{ur*-zvG3Co9jo59K^`bYr06$F#&}>tTkmfOU zoNF!%32p7dz~S-F|NmtJ7!_D~F7`79?StKPYD}`>*L}u+_p1UCB2E|X;7Y|FxE1I< zRRVuhcx)?~^-iny=_PgDK#W&;>o)g25LP?lkVXQt|M^>M*qb>=8%2I}M*_;=-KiHKD&a#QZZo~sPm+oR zwtD>ozhhHyNs7{PcB%jykDA@tm=g=AM}o~)2jhV7<_12d9o$f%MvZcW0$c25&WEgpxMN`=ML63L=q_&V zV;<*FQG4N?jt5%9L5_1O|si3T%qq;MDK1qK^APU=tu+#d6sJS|aaN-C@o(?(Qf& z-1IyaSgE_CKWijHEz5K7$zxf-UgJ{tWJ4@aH`z|Fd`p0r8(${79wNZyyKmx_Uov2G zOgeWd#U1J|2YBBXBS6m2<%FZz3^=CIZlllV2EwLEc@uvEU@+qfd#iOgjN8dJyVWGY z1;YLePG@&~*tZfi(?-M4)4{W0XYG-A{bY7my&vvN-yFm~6^`W*PA)O6KA2sRrJTq{ z#veuVChl7zu-MPgDEO@pUTID$S#RKoC+V?LS3mpXMvfuLRlU|&pL!{vgFhB4@*{g~ zY^v~V8|l}!=qP+_VItkv>4SG$zs^7v!I$J&Kw4!LP3}$MQEZ@UhS* zzifXOe6>@yx4Ae5uf=k1ZQ5Lff)}4#8|@|j%YXke|G&+XLyPDa@qC2WFI}k|AV_}j zm5q&p*gxJ!HaCP~7@xS2`Ux*M_KNso#EGfrsq1}j@*@JDP#dK_+~q{$kmdG-mTYi$ zWxrFrIUMI%>;0-t2oUcjWPST71&-NTT-3bG+{>1&J!7rx1+lM(h;!aVV4t#)Gmfl7 z!70(E)bH8w!HSiCFJBduo!IzWh%FpR*DKyU+DZVQ;=Xv1Rth}ZGyG?IGy~0CP1&D> z2Le&y+j+l$^WZ00ED;-$2lqd<%{^qL;Yq>!kJ#ALaPv%|>5O$ScqguWt-9rgzE%$w zPFfN0eb?Qrq3C27E;AVBv5vrlL(%?8n*-o6Rp~u{9dmBr*oI7PR(H(OdJ4n#6g0`- zDt`7^Fm#J7k6+&Aj<4b$`tOY(V1$YRVZ5{v_`StW#b*@Z6la-zO+Iw z{4Y)}G54rQj@&2HEGT%+ch}43gSl|YLF3NnZy{LME$v*gBjum}|I7RzwW>aSbW1Rl z97bd5fLPca)jQOjLj{9@hJ(rGVc;gPwN=R6k+~1z(tP;Q0H{vzjW{Zv2q7&BLZ^PF zg5a`IA!#NIQi!UT9#aUg*b^%-#A5=%j*e_1&K}JAsmR7#WAQ-Zj@a?=svjyZJ3nw@ z&w_$;?L{lIQE;W+R_OYx6nM8=CRr)WA8xj*wp?fThc!KJQ6EdK;OI!?2CI1j7)+dB z+@==+*OI&*+u6p0%zsKx-rCdQ<}tOw1+y#+9Nq@&lf#*D{RjWSfn?NA@3P#i7lsd4 zsBJ_~NA&%9PtVenijH*>_qn@wTF!=} zkg*+KTdD&}?N8Ftp)%`HqC$Qq-r;yCv$5Uxgd?uJGL}9hK*bKTYTg|W%~3zBHd|sX z5%t@q{Y<-qv487)gB`yezSCIbe|nRQA@n(UcBUTNAmn6r+gE$+Vxgx_JN?tY{bl@F zUeB^0@EZ1qkACOdcSR>aXQ_+lo@d$E`9rU5b3-Zo$!ZE7v@C|M6aBZ<+|sddFJE^1 z!wR50x?gyyBMZ9Qo)*4J&BgjO!t)lXVBmOvY@Z;XAK2R3RB`2ya3S>@Za*CgZyudl zrMaC7f~qZ1Hdg{MeP2vQah*RH5~82QbGU%Ra7^ms%CorpR5DwW4O7ok_F2JW)Dy}| z?~lvpy5VuooGELj?)u=g@OTSXG8h;M_^n~f!((B)Lv82;Jak4xwF(%`W7xi~Id|vj4JegkyK+_-R?zMk^ zeK7|wr5!!ez?^G-?9K(n;R*PA^>#z0<+G^B;&_m}I~PK`Hx-lS%TVM{_HwXqHmW<1 z%ANd@{4f6b%LX8M?+lB&RuXbGY-#hD-sQ8{EDP$Ys>-bGQC1jM3GF@}Cw$>GRe@*=-RhHpTgXL_7zZ;Pf1;k8Ljmi`k82;~ z;Or4f?%2V2*!kulAJK!1?uYIVtbS0A7dLX;_HeUD`Ar7u<_!$koikBt{ z>09nD+)slyRueb0ePglRU_wxAIv=$!*RHSmQwGJ4)CN=iov>EqNsr;rTucg|bF@F0 z3ZBv@YC|9Sp%vpi=~`3vzxd}b^H1le|9sy|N8UoU>Y0}|@a=GXAMun3JraQ zd|xJ@Jw06OJ|Udvv>A5+PK&N&yk!P`hUFPw|h;E z^K%CHKHKB{NYnyHzb%x-G3S`Mn{V3GKPUr&h5l78=KSycj>Ue>q&Uu<6aiK&m-T-7MY0!raR;E4*9lt6DIOW%niUN*Z8>%|c=O zLsu-_6Q`-T6b^y8sYjk<+Tnus4TJBO{_TJM%lNbKa%#0?F9qVX_W8u=un^>p|14Qk zNr9OAhKaA3@$$l5-mb-#U?iOlEA}2BL4?$g4F~)KkZrBr%Agnx-+d1YUS{efE(=Hs z1o{zBRm(K9^D_aJmN)e`2zmiU|9qASp&0wiKb!i`<>2ZUj#Jw`i=cTtJAYFF6%7sU zc)#QiLcUvaE1uY`&k{IW>X;d-u@qbM*={|=@e@m_~&oqH&#ys(qf^v{P~Uk;oY+HHs7mp19= zxP*eK)adfFfP9dXATNjSv%}jAk5Fs3o9jp@);ZMQ#ng%GwC7!_^kBXpfsbN%Q}AE^_iyumYP-6US$`-N=$+BE zw6uYr>3`@-Le3!eMa4Zbp%4QFp7GddP(kg+na8iQf>FhfW7_7wIP_c{skhyN0VaAv z5^sG}@Z21?!n7I{#p)8f@-El_ul8X2>nR(!doc6fJR}t6dw3 zl<%Uf%{%NNdd9rr`yVgJv|;@qU=RsnB|KwTXO9GG**nXZ?nod+XuaJt+-ntf@Q1^Kr0UyzZ+pBlH*o#&_`m+w-{xPvtoDTD zD>t}Pq<&kXAR1>{X=6@TOwh(iy-p?48ESOiA2^-CV9up4ZW`A#K{gk;Yc7c{!1aCn z*)45mU7Zuvt_qmqCs(bYPiZ;Ky`GAGpMI60(i$aM;z|teRAbfsd^!M3`OIU~6moH` zcKU(-3$d`logVe(tSfwyED3FEj=~L$9ieQc#(1;(bhPa{H<+v}KKIZv8f%rBypFh= zpcQ+Z<4CA8bVb=}@yIaH)(PQD zoDJte?9tTQa6$k`=45l0~q-N9l#2Spc(75yEP#<&3NM)>pPCF(7?r z%Vd_1Dac=a`8A}{5qGog{Zgz&2mUp*v6*gTFqG~O*70;jf7`7|<}boQGjL5mdC2Tv z{`;2=;A(@{G0iQ87#dm^b<8puiCoX`+~~>%+gB{RH6O;JC)7LLjnRd@-g}zcW76Q^ zy~n$B&jsLtW8R~BzC@&6Fm7->K!sh0v-sGWC|LNg$XdSF6IT~>9ix^~py7?#y05YmDk`h)ibn9W~UXjrpnCkyG-U+$A`HAyXn$;()%#@kGH$(>7yuo z=O%XlpkWYmv_Je>mVhtxol3GWCWc@^n_Q8jQ2<;~owMN?BEq{{B0*uFf-$b<>Ztm2 zXRsgKbC&`jcB4a9{Ue7T1|DG$&MYldZxR(v2Z(I*Ld^3@# zRO?(yNW`q$E~K2d8F)P4PX5k!fvDa6{+4-&DwM_Sr%liZ$dWEpl*xQQpK$lXgQksP zP*G_UJ?WB)%XJe?L^m4dJL%At28|)B4LyYK5ivcc%r$K<0|O3k51Vg32?d05%eNQF z7_~r14lyf6ZvMN)=f03|yHTX64_h(L*DiioXlJ1NtDRh{`As2w^|udRJ1CeLAI5GX z$+TPjf+3CAB-CRc{rrfjv-r!m|9}4U|J|07FWl`6tuCvDulV`HX3I^=brN(C)0%N` ze(Z(TQV`-6!_l!q!aENv+II;Wc--# zIcf>qcs&^Q&pL^$-sKC=j&6N*#nJ$~{^Ohv(ar*EzKRoOu?`S?yRvaBi3mquSm!3n z1cTO^`kDY!7koPX!&CVj75iPE{kg~7DYI;xBT|DWK3LN z=2FAV6D+F?uI!s5Vrkvs{<895loU20z2R`i0P`bnJ5mHk(eD_m4VYSTM)c(}^S;D|w~vUKH|=|G#X3e|Rp5vLz6(Fp`~f=2-yb zFMYo@VQ&FC)4Q^HkLRHrVdUw>Lv(O8YcS2Mih}Ewv*mGX&f?Crk*j2tELd%9d@!@n z6>s-e=Tdg+;ET?^tlHN3P{P`8#<$%HCDyGDp5CmDc|9+R1NP;EzwPBksbU-4pI0YJ zZ9a)7SLy#q-jN9z+FV{wgWa&FK&yOOtrQOy&TUQEI;J`=-Ch598df zjyhfpVeSomckA>|D~KP~GAUlBK+q`{m)JZy)^{8XEqrZTCv}>6ryu?OGeq3piUz1HI?7?{l#xi26D^m2MaDCr5{+)$(I`qTv-JUR?& zw1YwN+eFgO4+IEI<903g2*L>YnBfCyVUSncS`&Pjz|<#v{bk=k#rbIMyEnB1V0CVi z_~k-Z*fRJoFH^!{1Y|@#0OMr};GKFbFH#JDmU> zor?aCU%TLIAG_#*zF^pQ+H;RPjR^af3vxewc7{)D9qLzAgyNoK!uw8$_@T3cnR)(8 zGYCx(-Sp>YDQ0uMSnt=BfXzaqPc6zl;YbcAX+(pLTH9rhQi_=IiGM&B?W!}RYZInM z$syQ2cdmr1i!5a3RB*!d_1E<1FcHDhln zaCj?=(hjvGND#g;&o60)i7nl78UtP+)u!}9;Yk2|G$f5bA0gx3;OEAf6{+xb(K55Y zo~b*&*1p`r|SLMxC|+g z5(%YDNktkcR4XBsdCZ(+NXBEHhjWaX=UJvi2z^i_TF6kL5oJn9WlkiLeE0KL^uyEs z1NMHs&c5#ZK6|Zey)US)sy| zReHci*3L)w+iXF}m3M!`R!dyW?isH%@OH|wJtQ~}Z>{w=gbHT>>y?2`yJzyzn#R}ci$gGo#Ek2mZqFyyfvAd#6&Z(k zEg9C7t-xj5Z_3!~Ysfru{jk*S6l5dEY#|R?;q;x6xXX-0-nHTzC9PpUEd9c-sTJW2 z8gxpLP1(+xt| zVd1!1S|ym`*75_g`u$lTCYAosTfz{@-4$FRMBQYu{!!Y{({Qjj?p;>cERO==7J_ln z4p_70IIBVl!Rge$+`nR{4zlFzJFd^nv6Le!^C*2Tv@62xbNsp38+-KT;*%JRETb8` zua*m%Q8~{wVy@wyvvPeeD-E%|IGAOE!x{R%R2Kihb11y1em#}x1YD10F?CSS#P+qY zBX>jf@ciiR%ZuyRL9NKu!Z9KXuf_7O)_%!_3=^yIsI+*{YxQEf^oQWHec39%{oV}_ zPK_G-z^MXYrIK)t&l6o`^f*oLI)g~b+h>J(mJq<*Zb8dMMgQ$Dr4PTc2G!K`y_Q9W zP*k9Gemch)>&}L!aQ@aq#)(qX(JCvLYeS)jB1Hbn?h%O+?_-!8rQTvWBnzKwdZ{za z|M>rZS^hM|x|3?zx;DrjcThK?JP0`HrVE?f++nw35?CzZ zC~?j>)zO@cYzxwAqNme-gYZM$Q8wKX~6p{(kRPSIB9-s!9q^#|Pa(-Xpe!5bejI^GrS+)Xz^Jb($c2K9y`a zWjk|#NpQHRM?Du*pN6T~v0I~{`vK8p;y%mZaNDWN6+r#iXMss`5CBHGORHY^@hIb? zmU;&yaTIO!vk!rcvU7U`{gd#;x~b4_E@GZ%wKt6~CmJ0OSaPS=-@-MMFY6(cD72~^ zXl?Kz&N+kNnvK7`v1!+BojM^$Tsoy7@FUj(Xh=E|;yZotSjB$7i(HP#w_V-lS4A{> zZ%QKFEWU;7u3={ab)t}Y?B|9pOrh{uRn~0ed@`CxWcjeC6ZJLHp#K|BOQG{*nihw{RPg7N?l6LrQ(w93I7KirD$%&^^~6QsqGHs-sn|+7^v@M%mqgS zaYIR@No%$qe3()69AAuvEh4;`EbVz1xKCtqS7$iXKbc-EDO87rE!64gEn%pzP2^AS zls>R{9BUe66$5ojhNP5#Ao71G9-q6V2lDc&?PqqV01Hb;7&KP=%)^<)E4!qdu zvFoZjXw%vS=iLv*^xNiz{F@CR@AuP*vyr}N8FQs-NFWYJ8Cb9G4f8|{=KaobC=}za zjklc7O$V~J_}x^(U(xr$wmeu~3>W0RJE(Dl-(qmzkM#w8^pP?&YOo81KjgZcV}VH! z`ntZ}$<-WsU;G-rk}ipw7k|bV+j;}sH<`EP(L#UWfK_IGN&NDk_eBqrQ2045DfQy4 z37(i0&(L}u0!uGm#V}?hKt<^f*`MR)pnqU%QcJWnEho5_SF(4o8?OY!keOLXb|G%n!8XA^kf*n)q-Z&f5Fv7nl1!_ZPuPTP>qRs(4 zJ0NU=R;EaOo%6M~j= zq<|S4D%i+|6jNQ3V0`S{ai8%J9IF&GZl@Caf5K34-|-|6kI7$X|LB3=g;Q(jW7A+& zJ~?LYtpx@*@m-$1V1#XV=$JP5`9cxZnEj=gGj#5$JGnk-fTRpvc{c_hC|j_gvCJa_ z3wi08thg?C)?eyqGNIx}m!ONfV^PQyp1Na7)esUMX^1FQ`{5;@8ikz%xASJ%)=eRw zw7|aB@_8Up_o4BV)>r0rK=UE(|EBwGpl)WA#wn^0uYF;RI2;{?%}N|c<2?xfg3s50 z?h78Mk9?L^j&>+js?oJZaIARTsy$A4=Ha}aN@%BX9=7}CJQUn+3B7`45|jPGXp?

I(#)m-&!|)N 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 literal 0 HcmV?d00001 diff --git a/source/tests/pt/NiO/data/data_0/set.000/spin.npy b/source/tests/pt/NiO/data/data_0/set.000/spin.npy new file mode 100644 index 0000000000000000000000000000000000000000..c426f1c7f67fdedfbed3ffbf275952f9945bc042 GIT binary patch literal 46208 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 literal 0 HcmV?d00001 diff --git a/source/tests/pt/NiO/data/single/set.000/coord.npy b/source/tests/pt/NiO/data/single/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..4060f0fc5368215559ee2bef69d35169d3207429 GIT binary patch literal 896 zcmbV{eJs>*9LIki^N`~nN^Muk9ovA9h}qCHH#V^Xdmu*eg$c9>rjB?s6w8^fV}i1RSQ742O6rhhff z0ph&-y5{q&K&V=!8OHut-`b7F1q?8p9&6B8K$q*tiK3V)hc1=H-b!ANBN&c-uM*Xf z=*P&nOO(v!+;GKMNEsh>8Bjj@K7CwDhiyMJj7%Ph z>Qwd55l_N_RDmL(Hx5EgKyIk^N_7r&BQa7m<+WzPk8Z-fk| zu4{rvX7F)jDr`QG0ww1z7lmNH_&GQ>0QYHH!haI`y^I*?%X}V`d=uyJabI^*uRaO) z+k?wL+X&2|u|tb@&7kDNKU1o?qo`-%XM2sp9u{==!tyivpx8<uRw#Jotp zC>hjIz$UZheu(ENBKNDpgyU>@a&7$DsI!UFYSvx8-=-((arLKNeU?4jGVoAqi8lc$+L~- z0T&``$D(k*?pdShG3+nzTK9LC2`o%ETuBOALO!;=p+iRQ@FFDl)u}=%w3;?G&|9c5 zNU_`h4s%)azPQF4)-cC^>AyXA37xUFU7xAm1TMFZ=U2RDg8OdIaDVLI%TFrqS8f20 zX!{zkP7`?2s~s2&Ttc#KDZKQ{T#yr5@e16R6LVQJ%~VL-TK+(Sxmege%fWp${nwEW b{GYXAs7Kl`ma{32W2Q#y*=I+w9Y0ds71TYuEi0=4M=k`;BO=Nl-pE%9|$$3UWSL6zw? zAFP&?zV=p!LAYn#=@TYCxGmahbw=|w?7QO=>Y26*WQ*4|M@;15m&NT)OJrQMZ$Bdb z%rPFB^vv|uY#|6%-tsy=<_cRg{d&Lu+#i(EMb9dzad>52a?Q=)9GpHsZE>3ujPFY{ zkv{kJXsv+s$Gu$8)vuVkPlm%6{q*YL5Aj&hmeJj@D;8RXkCy%Zga!0hj|a7833R{U z@eK>bz|L{2r~I)O6a$y;^?MXz3P+`hwM&H)o16}=i`hcDK{mCgC?(~ z^jBICzTxoAWe51sBc)VNkz9ae>Pc5}Bb@n>F=T6sL$O3V=jvM$mxfFxC{Bf<*OSRL z!&%?qb%S3nJ83<>c;01Uv&RjSjKc?zoz{fgzyzXhxc?O8N?8tyI3+aJ-Cpt9mY{{C<+$VaMfRRW){kea7uz$uIQ=T3y>|aD%cAVH zr_1dxw5Apq-zl|MyK(Mu{-1gK&z!$2e7I+}y`7?Ue)8r;_V!cXSR2<*+i%EbG;{NT zYWveC8men};_at@&6=72aoYZHhRoWy!W#S6UW${ZrbXJPzui=B5t_UIN~DOex@Cs_ zE7_x8o6ls~KmQytss4QVe$g$*G+7rE+b_#pQ73e>*xs*ax=H%a+Wkfb(JT*GE9`eX QP%1rdRc6mH3I;R;08ahYkN^Mx literal 0 HcmV?d00001 diff --git a/source/tests/pt/NiO/data/single/set.000/spin.npy b/source/tests/pt/NiO/data/single/set.000/spin.npy new file mode 100644 index 0000000000000000000000000000000000000000..88985f5d2c2a10f98160e21de8b8ab4694de2cba GIT binary patch literal 896 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$-ItrF%nmP)#3giN=ofD!%xH4YZ`_Ec&`QwA9`~PuW>zmE~Z2yDjFWMfY zzp}q`HLCF?P&_Dco$V~3c;(!kKW_rX*Phy4{23^oxO-jUWT5yFQJqKn_deRs%R2t@ zNAiRH#($TFtf;@be`;Q))m@-^duJ}acR=-iET^tb1*)&E+Bp9qQ2e^->QIn5@;k%6 z&j*StrZ3ET1Qh@Kz{TMgP`v$_Um?gn%rp6CUk8e}|9=+o7APK;sOdKeD84wAmE%eJ ztNpdCDZD^;+9&UrzyMTlpQve{ehVmWc1+G5Xudr!s{{woUVB#dSqhAkKkT>vbFsXo z@wxryoIZ74?Va`&zmx)=0o9-SE#vwHs6K0Jzurus`iM14_TK}FM_ri``Uxoh_OrxO zkocmEqwKeV;tEB|zup7IyOuHC0+}Dtv+Ej2{jTMPrB8w4oAu0oE&_@lH(0Fy9w>e} YB7YJv9PDrZ3GxGmr#-_c7|;*^0Gp!qKmY&$ literal 0 HcmV?d00001 diff --git a/source/tests/pt/NiO/data/single/type.raw b/source/tests/pt/NiO/data/single/type.raw new file mode 100644 index 0000000000..d9664c7a22 --- /dev/null +++ b/source/tests/pt/NiO/data/single/type.raw @@ -0,0 +1,32 @@ +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 diff --git a/source/tests/pt/NiO/data/single/type_map.raw b/source/tests/pt/NiO/data/single/type_map.raw new file mode 100644 index 0000000000..7eca995c31 --- /dev/null +++ b/source/tests/pt/NiO/data/single/type_map.raw @@ -0,0 +1,2 @@ +Ni +O diff --git a/source/tests/pt/model/test_autodiff.py b/source/tests/pt/model/test_autodiff.py index c32f202625..91fc3cabf6 100644 --- a/source/tests/pt/model/test_autodiff.py +++ b/source/tests/pt/model/test_autodiff.py @@ -7,11 +7,13 @@ from deepmd.pt.model.model import ( get_model, - get_zbl_model, ) from deepmd.pt.utils import ( env, ) +from deepmd.pt.utils.utils import ( + to_numpy_array, +) dtype = torch.float64 @@ -21,6 +23,7 @@ model_dpa2, model_hybrid, model_se_e2_a, + model_spin, model_zbl, ) @@ -59,34 +62,64 @@ def test( cell = (cell + cell.T) + 5.0 * torch.eye(3, device="cpu") coord = torch.rand([natoms, 3], dtype=dtype, device="cpu") coord = torch.matmul(coord, cell) + spin = torch.rand([natoms, 3], dtype=dtype, device="cpu") atype = torch.IntTensor([0, 0, 0, 1, 1]) # assumes input to be numpy tensor coord = coord.numpy() - - def np_infer( + spin = spin.numpy() + test_spin = getattr(self, "test_spin", False) + if not test_spin: + test_keys = ["energy", "force", "virial"] + else: + test_keys = ["energy", "force", "force_mag", "virial"] + + def np_infer_coord( coord, ): - e0, f0, v0 = eval_model( + result = eval_model( self.model, torch.tensor(coord, device=env.DEVICE).unsqueeze(0), cell.unsqueeze(0), atype, + spins=torch.tensor(spin, device=env.DEVICE).unsqueeze(0), ) - ret = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } # detach - ret = {kk: ret[kk].detach().cpu().numpy() for kk in ret} + ret = {key: to_numpy_array(result[key].squeeze(0)) for key in test_keys} return ret - def ff(_coord): - return np_infer(_coord)["energy"] + def np_infer_spin( + spin, + ): + result = eval_model( + self.model, + torch.tensor(coord, device=env.DEVICE).unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=torch.tensor(spin, device=env.DEVICE).unsqueeze(0), + ) + # detach + ret = {key: to_numpy_array(result[key].squeeze(0)) for key in test_keys} + return ret - fdf = -finite_difference(ff, coord, delta=delta).squeeze() - rff = np_infer(coord)["force"] - np.testing.assert_almost_equal(fdf, rff, decimal=places) + def ff_coord(_coord): + return np_infer_coord(_coord)["energy"] + + def ff_spin(_spin): + return np_infer_spin(_spin)["energy"] + + if not test_spin: + fdf = -finite_difference(ff_coord, coord, delta=delta).squeeze() + rff = np_infer_coord(coord)["force"] + np.testing.assert_almost_equal(fdf, rff, decimal=places) + else: + # real force + fdf = -finite_difference(ff_coord, coord, delta=delta).squeeze() + rff = np_infer_coord(coord)["force"] + np.testing.assert_almost_equal(fdf, rff, decimal=places) + # magnetic force + fdf = -finite_difference(ff_spin, spin, delta=delta).squeeze() + rff = np_infer_spin(spin)["force_mag"] + np.testing.assert_almost_equal(fdf, rff, decimal=places) class VirialTest: @@ -104,11 +137,12 @@ def test( # assumes input to be numpy tensor coord = coord.numpy() cell = cell.numpy() + test_keys = ["energy", "force", "virial"] def np_infer( new_cell, ): - e0, f0, v0 = eval_model( + result = eval_model( self.model, torch.tensor( stretch_box(coord, cell, new_cell), device="cpu" @@ -116,13 +150,9 @@ def np_infer( torch.tensor(new_cell, device="cpu").unsqueeze(0), atype, ) - ret = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } # detach - ret = {kk: ret[kk].detach().cpu().numpy() for kk in ret} + ret = {key: to_numpy_array(result[key].squeeze(0)) for key in test_keys} + # detach return ret def ff(bb): @@ -211,11 +241,19 @@ class TestEnergyModelZBLForce(unittest.TestCase, ForceTest): def setUp(self): model_params = copy.deepcopy(model_zbl) self.type_split = False - self.model = get_zbl_model(model_params).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelZBLVirial(unittest.TestCase, VirialTest): def setUp(self): model_params = copy.deepcopy(model_zbl) self.type_split = False - self.model = get_zbl_model(model_params).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinSeAForce(unittest.TestCase, ForceTest): + def setUp(self): + model_params = copy.deepcopy(model_spin) + self.type_split = False + self.test_spin = True + self.model = get_model(model_params).to(env.DEVICE) diff --git a/source/tests/pt/model/test_deeppot.py b/source/tests/pt/model/test_deeppot.py index 697ebb6411..68b1ff65d5 100644 --- a/source/tests/pt/model/test_deeppot.py +++ b/source/tests/pt/model/test_deeppot.py @@ -95,7 +95,8 @@ def test_dp_test(self): ).reshape(1, -1, 3) atype = np.array([0, 0, 0, 1, 1]).reshape(1, -1) - e, f, v, ae, av = dp.eval(coord, cell, atype, atomic=True) + ret = dp.eval(coord, cell, atype, atomic=True) + e, f, v, ae, av = ret[0], ret[1], ret[2], ret[3], ret[4] self.assertEqual(e.shape, (1, 1)) self.assertEqual(f.shape, (1, 5, 3)) self.assertEqual(v.shape, (1, 9)) diff --git a/source/tests/pt/model/test_embedding_net.py b/source/tests/pt/model/test_embedding_net.py index a1895718dd..63a3534c74 100644 --- a/source/tests/pt/model/test_embedding_net.py +++ b/source/tests/pt/model/test_embedding_net.py @@ -56,13 +56,22 @@ def get_single_batch(dataset, index=None): np_batch = dataset[index] pt_batch = {} - for key in ["coord", "box", "force", "energy", "virial", "atype", "natoms"]: + for key in [ + "coord", + "box", + "force", + "force_mag", + "energy", + "virial", + "atype", + "natoms", + ]: if key in np_batch.keys(): np_batch[key] = np.expand_dims(np_batch[key], axis=0) pt_batch[key] = torch.as_tensor(np_batch[key], device=env.DEVICE) - np_batch["coord"] = np_batch["coord"].reshape(1, -1) + if key in ["coord", "force", "force_mag"]: + np_batch[key] = np_batch[key].reshape(1, -1) np_batch["natoms"] = np_batch["natoms"][0] - np_batch["force"] = np_batch["force"].reshape(1, -1) return np_batch, pt_batch diff --git a/source/tests/pt/model/test_ener_spin_model.py b/source/tests/pt/model/test_ener_spin_model.py new file mode 100644 index 0000000000..2bd5c22aaf --- /dev/null +++ b/source/tests/pt/model/test_ener_spin_model.py @@ -0,0 +1,420 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import numpy as np +import torch + +from deepmd.dpmodel.model import SpinModel as DPSpinModel +from deepmd.pt.model.model import ( + SpinEnergyModel, + get_model, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.nlist import ( + extend_input_and_build_neighbor_list, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, +) + +from .test_permutation import ( + model_dpa1, + model_dpa2, + model_se_e2_a, + model_spin, +) + +dtype = torch.float64 + + +def reduce_tensor(extended_tensor, mapping, nloc: int): + nframes, nall = extended_tensor.shape[:2] + ext_dims = extended_tensor.shape[2:] + reduced_tensor = torch.zeros( + [nframes, nloc, *ext_dims], + dtype=extended_tensor.dtype, + device=extended_tensor.device, + ) + mldims = list(mapping.shape) + mapping = mapping.view(mldims + [1] * len(ext_dims)).expand( + [-1] * len(mldims) + list(ext_dims) + ) + # nf x nloc x (*ext_dims) + reduced_tensor = torch.scatter_reduce( + reduced_tensor, + 1, + index=mapping, + src=extended_tensor, + reduce="sum", + ) + return reduced_tensor + + +class SpinTest: + def setUp(self): + self.prec = 1e-10 + natoms = 5 + self.ntypes = 3 # ["O", "H", "B"] for test + self.cell = 4.0 * torch.eye(3, dtype=dtype, device=env.DEVICE).unsqueeze(0) + self.coord = 3.0 * torch.rand( + [natoms, 3], dtype=dtype, device=env.DEVICE + ).unsqueeze(0) + self.spin = 0.5 * torch.rand( + [natoms, 3], dtype=dtype, device=env.DEVICE + ).unsqueeze(0) + self.atype = torch.tensor( + [0, 0, 0, 1, 1], dtype=torch.int64, device=env.DEVICE + ).unsqueeze(0) + + self.expected_mask = torch.tensor( + [ + [True], + [True], + [True], + [False], + [False], + ], + dtype=torch.bool, + device=env.DEVICE, + ).unsqueeze(0) + self.expected_atype_with_spin = torch.tensor( + [0, 0, 0, 1, 1, 3, 3, 3, 4, 4], dtype=torch.int64, device=env.DEVICE + ).unsqueeze(0) + self.expected_nloc_spin_index = ( + torch.arange(natoms, natoms * 2, dtype=torch.int64, device=env.DEVICE) + .unsqueeze(0) + .unsqueeze(-1) + ) + + def test_output_shape( + self, + ): + result = self.model( + self.coord, + self.atype, + self.spin, + self.cell, + ) + # check magnetic mask + torch.testing.assert_close(result["mask_mag"], self.expected_mask) + # check output shape to assure split + nframes, nloc = self.coord.shape[:2] + torch.testing.assert_close(result["energy"].shape, [nframes, 1]) + torch.testing.assert_close(result["atom_energy"].shape, [nframes, nloc, 1]) + torch.testing.assert_close(result["force"].shape, [nframes, nloc, 3]) + torch.testing.assert_close(result["force_mag"].shape, [nframes, nloc, 3]) + + def test_input_output_process(self): + nframes, nloc = self.coord.shape[:2] + self.real_ntypes = self.model.spin.get_ntypes_real() + # 1. test forward input process + coord_updated, atype_updated = self.model.process_spin_input( + self.coord, self.atype, self.spin + ) + # compare atypes of real and virtual atoms + torch.testing.assert_close(atype_updated, self.expected_atype_with_spin) + # compare coords of real and virtual atoms + torch.testing.assert_close(coord_updated.shape, [nframes, nloc * 2, 3]) + torch.testing.assert_close(coord_updated[:, :nloc], self.coord) + virtual_scale = torch.tensor( + self.model.spin.get_virtual_scale_mask()[self.atype.cpu()], + dtype=dtype, + device=env.DEVICE, + ) + virtual_coord = self.coord + self.spin * virtual_scale.unsqueeze(-1) + torch.testing.assert_close(coord_updated[:, nloc:], virtual_coord) + + # 2. test forward output process + model_ret = self.model.backbone_model.forward_common( + coord_updated, + atype_updated, + self.cell, + do_atomic_virial=True, + ) + if self.model.do_grad_r("energy"): + force_all = model_ret["energy_derv_r"].squeeze(-2) + force_real, force_mag, _ = self.model.process_spin_output( + self.atype, force_all + ) + torch.testing.assert_close( + force_real, force_all[:, :nloc] + force_all[:, nloc:] + ) + torch.testing.assert_close( + force_mag, force_all[:, nloc:] * virtual_scale.unsqueeze(-1) + ) + + # 3. test forward_lower input process + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + self.coord, + self.atype, + self.model.get_rcut(), + self.model.get_sel(), + mixed_types=self.model.mixed_types(), + box=self.cell, + ) + nall = extended_coord.shape[1] + nnei = nlist.shape[-1] + extended_spin = torch.gather( + self.spin, index=mapping.unsqueeze(-1).tile((1, 1, 3)), dim=1 + ) + ( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping_updated, + ) = self.model.process_spin_input_lower( + extended_coord, extended_atype, extended_spin, nlist, mapping=mapping + ) + # compare atypes of real and virtual atoms + # Note that the real and virtual atoms corresponding to the local ones are switch to the first nloc * 2 atoms + torch.testing.assert_close(extended_atype_updated.shape, [nframes, nall * 2]) + torch.testing.assert_close( + extended_atype_updated[:, :nloc], extended_atype[:, :nloc] + ) + torch.testing.assert_close( + extended_atype_updated[:, nloc : nloc + nloc], + extended_atype[:, :nloc] + self.real_ntypes, + ) + torch.testing.assert_close( + extended_atype_updated[:, nloc + nloc : nloc + nall], + extended_atype[:, nloc:nall], + ) + torch.testing.assert_close( + extended_atype_updated[:, nloc + nall :], + extended_atype[:, nloc:nall] + self.real_ntypes, + ) + virtual_scale = torch.tensor( + self.model.spin.get_virtual_scale_mask()[extended_atype.cpu()], + dtype=dtype, + device=env.DEVICE, + ) + # compare coords of real and virtual atoms + virtual_coord = extended_coord + extended_spin * virtual_scale.unsqueeze(-1) + torch.testing.assert_close(extended_coord_updated.shape, [nframes, nall * 2, 3]) + torch.testing.assert_close( + extended_coord_updated[:, :nloc], extended_coord[:, :nloc] + ) + torch.testing.assert_close( + extended_coord_updated[:, nloc : nloc + nloc], virtual_coord[:, :nloc] + ) + torch.testing.assert_close( + extended_coord_updated[:, nloc + nloc : nloc + nall], + extended_coord[:, nloc:nall], + ) + torch.testing.assert_close( + extended_coord_updated[:, nloc + nall :], virtual_coord[:, nloc:nall] + ) + + # compare mapping + torch.testing.assert_close(mapping_updated.shape, [nframes, nall * 2]) + torch.testing.assert_close(mapping_updated[:, :nloc], mapping[:, :nloc]) + torch.testing.assert_close( + mapping_updated[:, nloc : nloc + nloc], mapping[:, :nloc] + nloc + ) + torch.testing.assert_close( + mapping_updated[:, nloc + nloc : nloc + nall], mapping[:, nloc:nall] + ) + torch.testing.assert_close( + mapping_updated[:, nloc + nall :], mapping[:, nloc:nall] + nloc + ) + + # compare nlist + torch.testing.assert_close( + nlist_updated.shape, [nframes, nloc * 2, nnei * 2 + 1] + ) + # self spin + torch.testing.assert_close( + nlist_updated[:, :nloc, :1], self.expected_nloc_spin_index + ) + # real and virtual neighbors + loc_atoms_mask = (nlist < nloc) & (nlist != -1) + ghost_atoms_mask = nlist >= nloc + real_neighbors = nlist.clone() + real_neighbors[ghost_atoms_mask] += nloc + torch.testing.assert_close( + nlist_updated[:, :nloc, 1 : 1 + nnei], real_neighbors + ) + virtual_neighbors = nlist.clone() + virtual_neighbors[loc_atoms_mask] += nloc + virtual_neighbors[ghost_atoms_mask] += nall + torch.testing.assert_close( + nlist_updated[:, :nloc, 1 + nnei :], virtual_neighbors + ) + + # 4. test forward_lower output process + model_ret = self.model.backbone_model.forward_common_lower( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping=mapping_updated, + do_atomic_virial=True, + ) + if self.model.do_grad_r("energy"): + force_all = model_ret["energy_derv_r"].squeeze(-2) + force_real, force_mag, _ = self.model.process_spin_output_lower( + extended_atype, force_all, nloc + ) + force_all_switched = torch.zeros_like(force_all) + force_all_switched[:, :nloc] = force_all[:, :nloc] + force_all_switched[:, nloc:nall] = force_all[:, nloc + nloc : nloc + nall] + force_all_switched[:, nall : nall + nloc] = force_all[:, nloc : nloc + nloc] + force_all_switched[:, nall + nloc :] = force_all[:, nloc + nall :] + torch.testing.assert_close( + force_real, force_all_switched[:, :nall] + force_all_switched[:, nall:] + ) + torch.testing.assert_close( + force_mag, force_all_switched[:, nall:] * virtual_scale.unsqueeze(-1) + ) + + def test_jit(self): + model = torch.jit.script(self.model) + self.assertEqual(model.get_rcut(), self.rcut) + self.assertEqual(model.get_nsel(), self.nsel) + self.assertEqual(model.get_type_map(), self.type_map) + + def test_self_consistency(self): + if hasattr(self, "serial_test") and not self.serial_test: + # not implement serialize and deserialize + return + model1 = SpinEnergyModel.deserialize(self.model.serialize()) + result = model1( + self.coord, + self.atype, + self.spin, + self.cell, + ) + expected_result = self.model( + self.coord, + self.atype, + self.spin, + self.cell, + ) + for key in result: + torch.testing.assert_close( + result[key], expected_result[key], rtol=self.prec, atol=self.prec + ) + model1 = torch.jit.script(model1) + + def test_dp_consistency(self): + if hasattr(self, "serial_test") and not self.serial_test: + # not implement serialize and deserialize + return + dp_model = DPSpinModel.deserialize(self.model.serialize()) + # test call + dp_ret = dp_model.call( + to_numpy_array(self.coord), + to_numpy_array(self.atype), + to_numpy_array(self.spin), + to_numpy_array(self.cell), + ) + result = self.model.forward_common( + self.coord, + self.atype, + self.spin, + self.cell, + ) + np.testing.assert_allclose( + to_numpy_array(result["energy"]), + dp_ret["energy"], + rtol=self.prec, + atol=self.prec, + ) + np.testing.assert_allclose( + to_numpy_array(result["energy_redu"]), + dp_ret["energy_redu"], + rtol=self.prec, + atol=self.prec, + ) + + # test call_lower + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + self.coord, + self.atype, + self.model.get_rcut(), + self.model.get_sel(), + mixed_types=self.model.mixed_types(), + box=self.cell, + ) + extended_spin = torch.gather( + self.spin, index=mapping.unsqueeze(-1).tile((1, 1, 3)), dim=1 + ) + dp_ret_lower = dp_model.call_lower( + to_numpy_array(extended_coord), + to_numpy_array(extended_atype), + to_numpy_array(extended_spin), + to_numpy_array(nlist), + to_numpy_array(mapping), + ) + result_lower = self.model.forward_common_lower( + extended_coord, + extended_atype, + extended_spin, + nlist, + mapping, + ) + np.testing.assert_allclose( + to_numpy_array(result_lower["energy"]), + dp_ret_lower["energy"], + rtol=self.prec, + atol=self.prec, + ) + np.testing.assert_allclose( + to_numpy_array(result_lower["energy_redu"]), + dp_ret_lower["energy_redu"], + rtol=self.prec, + atol=self.prec, + ) + + +class TestEnergyModelSpinSeA(unittest.TestCase, SpinTest): + def setUp(self): + SpinTest.setUp(self) + model_params = copy.deepcopy(model_spin) + model_params["descriptor"] = copy.deepcopy(model_se_e2_a["descriptor"]) + self.rcut = model_params["descriptor"]["rcut"] + self.nsel = sum(model_params["descriptor"]["sel"]) + self.type_map = model_params["type_map"] + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinDPA1(unittest.TestCase, SpinTest): + def setUp(self): + SpinTest.setUp(self) + model_params = copy.deepcopy(model_spin) + model_params["descriptor"] = copy.deepcopy(model_dpa1["descriptor"]) + self.rcut = model_params["descriptor"]["rcut"] + self.nsel = model_params["descriptor"]["sel"] + self.type_map = model_params["type_map"] + # not implement serialize and deserialize + self.serial_test = False + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinDPA2(unittest.TestCase, SpinTest): + def setUp(self): + SpinTest.setUp(self) + model_params = copy.deepcopy(model_spin) + model_params["descriptor"] = copy.deepcopy(model_dpa2["descriptor"]) + self.rcut = model_params["descriptor"]["repinit_rcut"] + self.nsel = model_params["descriptor"]["repinit_nsel"] + self.type_map = model_params["type_map"] + # not implement serialize and deserialize + self.serial_test = False + self.model = get_model(model_params).to(env.DEVICE) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/model/test_forward_lower.py b/source/tests/pt/model/test_forward_lower.py new file mode 100644 index 0000000000..32be3b62ad --- /dev/null +++ b/source/tests/pt/model/test_forward_lower.py @@ -0,0 +1,177 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import torch + +from deepmd.pt.infer.deep_eval import ( + eval_model, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.nlist import ( + extend_input_and_build_neighbor_list, +) + +from .test_permutation import ( # model_dpau, + model_dpa1, + model_dpa2, + model_se_e2_a, + model_spin, + model_zbl, +) + +dtype = torch.float64 + + +def reduce_tensor(extended_tensor, mapping, nloc: int): + nframes, nall = extended_tensor.shape[:2] + ext_dims = extended_tensor.shape[2:] + reduced_tensor = torch.zeros( + [nframes, nloc, *ext_dims], + dtype=extended_tensor.dtype, + device=extended_tensor.device, + ) + mldims = list(mapping.shape) + mapping = mapping.view(mldims + [1] * len(ext_dims)).expand( + [-1] * len(mldims) + list(ext_dims) + ) + # nf x nloc x (*ext_dims) + reduced_tensor = torch.scatter_reduce( + reduced_tensor, + 1, + index=mapping, + src=extended_tensor, + reduce="sum", + ) + return reduced_tensor + + +class ForwardLowerTest: + def test( + self, + ): + prec = self.prec + natoms = 5 + cell = 4.0 * torch.eye(3, dtype=dtype, device=env.DEVICE) + coord = 3.0 * torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) + spin = 0.5 * torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) + atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int64, device=env.DEVICE) + test_spin = getattr(self, "test_spin", False) + if not test_spin: + test_keys = ["energy", "force", "virial"] + else: + test_keys = ["energy", "force", "force_mag"] + + result_forward = eval_model( + self.model, + coord.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), + ) + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + coord.unsqueeze(0), + atype.unsqueeze(0), + self.model.get_rcut(), + self.model.get_sel(), + mixed_types=self.model.mixed_types(), + box=cell.unsqueeze(0), + ) + extended_spin = torch.gather( + spin.unsqueeze(0), index=mapping.unsqueeze(-1).tile((1, 1, 3)), dim=1 + ) + input_dict = { + "extended_coord": extended_coord, + "extended_atype": extended_atype, + "nlist": nlist, + "mapping": mapping, + "do_atomic_virial": False, + } + if test_spin: + input_dict["extended_spin"] = extended_spin + result_forward_lower = self.model.forward_lower(**input_dict) + for key in test_keys: + if key in ["energy"]: + torch.testing.assert_close( + result_forward_lower[key], result_forward[key], rtol=prec, atol=prec + ) + elif key in ["force", "force_mag"]: + reduced_vv = reduce_tensor( + result_forward_lower[f"extended_{key}"], mapping, natoms + ) + torch.testing.assert_close( + reduced_vv, result_forward[key], rtol=prec, atol=prec + ) + elif key == "virial": + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + result_forward_lower[key], + result_forward[key], + rtol=prec, + atol=prec, + ) + else: + raise RuntimeError(f"Unexpected test key {key}") + + +class TestEnergyModelSeA(unittest.TestCase, ForwardLowerTest): + def setUp(self): + self.prec = 1e-10 + model_params = copy.deepcopy(model_se_e2_a) + self.type_split = False + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelDPA1(unittest.TestCase, ForwardLowerTest): + def setUp(self): + self.prec = 1e-10 + model_params = copy.deepcopy(model_dpa1) + self.type_split = True + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelDPA2(unittest.TestCase, ForwardLowerTest): + def setUp(self): + self.prec = 1e-10 + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + model_params = copy.deepcopy(model_dpa2) + self.type_split = True + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelZBL(unittest.TestCase, ForwardLowerTest): + def setUp(self): + self.prec = 1e-10 + model_params = copy.deepcopy(model_zbl) + self.type_split = False + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinSeA(unittest.TestCase, ForwardLowerTest): + def setUp(self): + # still need to figure out why only 1e-5 rtol and atol + self.prec = 1e-5 + model_params = copy.deepcopy(model_spin) + self.type_split = False + self.test_spin = True + self.model = get_model(model_params).to(env.DEVICE) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/model/test_null_input.py b/source/tests/pt/model/test_null_input.py index eb8ff714e8..c8f4307d52 100644 --- a/source/tests/pt/model/test_null_input.py +++ b/source/tests/pt/model/test_null_input.py @@ -41,14 +41,9 @@ def test_nloc_1( cell = (cell + cell.T) + 100.0 * torch.eye(3, device=env.DEVICE) coord = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) atype = torch.tensor([0], dtype=torch.int32, device=env.DEVICE) - e0, f0, v0 = eval_model( - self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype - ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } + test_keys = ["energy", "force", "virial"] + result = eval_model(self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype) + ret0 = {key: result[key].squeeze(0) for key in test_keys} prec = 1e-10 expect_e_shape = [1] expect_f = torch.zeros([natoms, 3], dtype=dtype, device=env.DEVICE) @@ -70,14 +65,9 @@ def test_nloc_2_far( # 2 far-away atoms coord = torch.cat([coord, coord + 100.0], dim=0) atype = torch.tensor([0, 2], dtype=torch.int32, device=env.DEVICE) - e0, f0, v0 = eval_model( - self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype - ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } + test_keys = ["energy", "force", "virial"] + result = eval_model(self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype) + ret0 = {key: result[key].squeeze(0) for key in test_keys} prec = 1e-10 expect_e_shape = [1] expect_f = torch.zeros([natoms, 3], dtype=dtype, device=env.DEVICE) diff --git a/source/tests/pt/model/test_permutation.py b/source/tests/pt/model/test_permutation.py index fa97281718..8ec5c375fd 100644 --- a/source/tests/pt/model/test_permutation.py +++ b/source/tests/pt/model/test_permutation.py @@ -9,7 +9,6 @@ ) from deepmd.pt.model.model import ( get_model, - get_zbl_model, ) from deepmd.pt.utils import ( env, @@ -23,7 +22,7 @@ "type": "se_e2_a", "sel": [46, 92, 4], "rcut_smth": 0.50, - "rcut": 6.00, + "rcut": 4.00, "neuron": [25, 50, 100], "resnet_dt": False, "axis_neuron": 16, @@ -61,6 +60,31 @@ "data_stat_nbatch": 20, } +model_spin = { + "type_map": ["O", "H", "B"], + "descriptor": { + "type": "se_e2_a", + "sel": [46, 92, 4], + "rcut_smth": 0.50, + "rcut": 4.00, + "neuron": [25, 50, 100], + "resnet_dt": False, + "axis_neuron": 16, + "seed": 1, + }, + "fitting_net": { + "neuron": [24, 24, 24], + "resnet_dt": True, + "seed": 1, + }, + "data_stat_nbatch": 20, + "spin": { + "use_spin": [True, False, False], + "virtual_scale": [0.3140], + "_comment": " that's all", + }, +} + model_dpa2 = { "type_map": ["O", "H", "B"], "descriptor": { @@ -205,34 +229,46 @@ def test( cell = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) cell = (cell + cell.T) + 5.0 * torch.eye(3, device=env.DEVICE) coord = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) + spin = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) coord = torch.matmul(coord, cell) atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) idx_perm = [1, 0, 4, 3, 2] - e0, f0, v0 = eval_model( - self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype + test_spin = getattr(self, "test_spin", False) + if not test_spin: + test_keys = ["energy", "force", "virial"] + else: + test_keys = ["energy", "force", "force_mag", "virial"] + result_0 = eval_model( + self.model, + coord.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } - e1, f1, v1 = eval_model( - self.model, coord[idx_perm].unsqueeze(0), cell.unsqueeze(0), atype[idx_perm] + ret0 = {key: result_0[key].squeeze(0) for key in test_keys} + result_1 = eval_model( + self.model, + coord[idx_perm].unsqueeze(0), + cell.unsqueeze(0), + atype[idx_perm], + spins=spin[idx_perm].unsqueeze(0), ) - ret1 = { - "energy": e1.squeeze(0), - "force": f1.squeeze(0), - "virial": v1.squeeze(0), - } + ret1 = {key: result_1[key].squeeze(0) for key in test_keys} prec = 1e-10 - torch.testing.assert_close(ret0["energy"], ret1["energy"], rtol=prec, atol=prec) - torch.testing.assert_close( - ret0["force"][idx_perm], ret1["force"], rtol=prec, atol=prec - ) - if not hasattr(self, "test_virial") or self.test_virial: - torch.testing.assert_close( - ret0["virial"], ret1["virial"], rtol=prec, atol=prec - ) + for key in test_keys: + if key in ["energy"]: + torch.testing.assert_close(ret0[key], ret1[key], rtol=prec, atol=prec) + elif key in ["force", "force_mag"]: + torch.testing.assert_close( + ret0[key][idx_perm], ret1[key], rtol=prec, atol=prec + ) + elif key == "virial": + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + ret0[key], ret1[key], rtol=prec, atol=prec + ) + else: + raise RuntimeError(f"Unexpected test key {key}") class TestEnergyModelSeA(unittest.TestCase, PermutationTest): @@ -299,7 +335,15 @@ class TestEnergyModelZBL(unittest.TestCase, PermutationTest): def setUp(self): model_params = copy.deepcopy(model_zbl) self.type_split = False - self.model = get_zbl_model(model_params).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinSeA(unittest.TestCase, PermutationTest): + def setUp(self): + model_params = copy.deepcopy(model_spin) + self.type_split = False + self.test_spin = True + self.model = get_model(model_params).to(env.DEVICE) # class TestEnergyFoo(unittest.TestCase): diff --git a/source/tests/pt/model/test_rot.py b/source/tests/pt/model/test_rot.py index 19f671e619..a12bd063b4 100644 --- a/source/tests/pt/model/test_rot.py +++ b/source/tests/pt/model/test_rot.py @@ -9,7 +9,6 @@ ) from deepmd.pt.model.model import ( get_model, - get_zbl_model, ) from deepmd.pt.utils import ( env, @@ -20,6 +19,7 @@ model_dpa2, model_hybrid, model_se_e2_a, + model_spin, model_zbl, ) @@ -34,80 +34,102 @@ def test( natoms = 5 cell = 10.0 * torch.eye(3, dtype=dtype, device=env.DEVICE) coord = 2 * torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) + spin = 2 * torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) shift = torch.tensor([4, 4, 4], dtype=dtype, device=env.DEVICE) atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) from scipy.stats import ( special_ortho_group, ) + test_spin = getattr(self, "test_spin", False) + if not test_spin: + test_keys = ["energy", "force", "virial"] + else: + test_keys = ["energy", "force", "force_mag"] rmat = torch.tensor(special_ortho_group.rvs(3), dtype=dtype, device=env.DEVICE) # rotate only coord and shift to the center of cell coord_rot = torch.matmul(coord, rmat) - e0, f0, v0 = eval_model( - self.model, (coord + shift).unsqueeze(0), cell.unsqueeze(0), atype + spin_rot = torch.matmul(spin, rmat) + result_0 = eval_model( + self.model, + (coord + shift).unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } - e1, f1, v1 = eval_model( - self.model, (coord_rot + shift).unsqueeze(0), cell.unsqueeze(0), atype + ret0 = {key: result_0[key].squeeze(0) for key in test_keys} + result_1 = eval_model( + self.model, + (coord_rot + shift).unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin_rot.unsqueeze(0), ) - ret1 = { - "energy": e1.squeeze(0), - "force": f1.squeeze(0), - "virial": v1.squeeze(0), - } - torch.testing.assert_close(ret0["energy"], ret1["energy"], rtol=prec, atol=prec) - torch.testing.assert_close( - torch.matmul(ret0["force"], rmat), ret1["force"], rtol=prec, atol=prec - ) - if not hasattr(self, "test_virial") or self.test_virial: - torch.testing.assert_close( - torch.matmul(rmat.T, torch.matmul(ret0["virial"].view([3, 3]), rmat)), - ret1["virial"].view([3, 3]), - rtol=prec, - atol=prec, - ) - + ret1 = {key: result_1[key].squeeze(0) for key in test_keys} + for key in test_keys: + if key in ["energy"]: + torch.testing.assert_close(ret0[key], ret1[key], rtol=prec, atol=prec) + elif key in ["force", "force_mag"]: + torch.testing.assert_close( + torch.matmul(ret0[key], rmat), ret1[key], rtol=prec, atol=prec + ) + elif key == "virial": + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + torch.matmul( + rmat.T, torch.matmul(ret0[key].view([3, 3]), rmat) + ), + ret1[key].view([3, 3]), + rtol=prec, + atol=prec, + ) + else: + raise RuntimeError(f"Unexpected test key {key}") # rotate coord and cell torch.manual_seed(0) cell = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) cell = (cell + cell.T) + 5.0 * torch.eye(3, device=env.DEVICE) coord = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) coord = torch.matmul(coord, cell) + spin = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) coord_rot = torch.matmul(coord, rmat) + spin_rot = torch.matmul(spin, rmat) cell_rot = torch.matmul(cell, rmat) - e0, f0, v0 = eval_model( - self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype - ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } - e1, f1, v1 = eval_model( - self.model, coord_rot.unsqueeze(0), cell_rot.unsqueeze(0), atype + result_0 = eval_model( + self.model, + coord.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret1 = { - "energy": e1.squeeze(0), - "force": f1.squeeze(0), - "virial": v1.squeeze(0), - } - torch.testing.assert_close(ret0["energy"], ret1["energy"], rtol=prec, atol=prec) - torch.testing.assert_close( - torch.matmul(ret0["force"], rmat), ret1["force"], rtol=prec, atol=prec + ret0 = {key: result_0[key].squeeze(0) for key in test_keys} + result_1 = eval_model( + self.model, + coord_rot.unsqueeze(0), + cell_rot.unsqueeze(0), + atype, + spins=spin_rot.unsqueeze(0), ) - if not hasattr(self, "test_virial") or self.test_virial: - torch.testing.assert_close( - torch.matmul(rmat.T, torch.matmul(ret0["virial"].view([3, 3]), rmat)), - ret1["virial"].view([3, 3]), - rtol=prec, - atol=prec, - ) + ret1 = {key: result_1[key].squeeze(0) for key in test_keys} + for key in test_keys: + if key in ["energy"]: + torch.testing.assert_close(ret0[key], ret1[key], rtol=prec, atol=prec) + elif key in ["force", "force_mag"]: + torch.testing.assert_close( + torch.matmul(ret0[key], rmat), ret1[key], rtol=prec, atol=prec + ) + elif key == "virial": + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + torch.matmul( + rmat.T, torch.matmul(ret0[key].view([3, 3]), rmat) + ), + ret1[key].view([3, 3]), + rtol=prec, + atol=prec, + ) class TestEnergyModelSeA(unittest.TestCase, RotTest): @@ -174,7 +196,15 @@ class TestEnergyModelZBL(unittest.TestCase, RotTest): def setUp(self): model_params = copy.deepcopy(model_zbl) self.type_split = False - self.model = get_zbl_model(model_params).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinSeA(unittest.TestCase, RotTest): + def setUp(self): + model_params = copy.deepcopy(model_spin) + self.type_split = False + self.test_spin = True + self.model = get_model(model_params).to(env.DEVICE) if __name__ == "__main__": diff --git a/source/tests/pt/model/test_smooth.py b/source/tests/pt/model/test_smooth.py index bc1d26bffa..86e9ed94d7 100644 --- a/source/tests/pt/model/test_smooth.py +++ b/source/tests/pt/model/test_smooth.py @@ -9,7 +9,6 @@ ) from deepmd.pt.model.model import ( get_model, - get_zbl_model, ) from deepmd.pt.utils import ( env, @@ -20,6 +19,7 @@ model_dpa2, model_hybrid, model_se_e2_a, + model_spin, model_zbl, ) @@ -59,7 +59,7 @@ def test( ) coord1 = torch.matmul(coord1, cell) coord = torch.concat([coord0, coord1], dim=0) - + spin = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) coord0 = torch.clone(coord) coord1 = torch.clone(coord) coord1[1][0] += epsilon @@ -68,52 +68,63 @@ def test( coord3 = torch.clone(coord) coord3[1][0] += epsilon coord3[2][1] += epsilon - - e0, f0, v0 = eval_model( - self.model, coord0.unsqueeze(0), cell.unsqueeze(0), atype + test_spin = getattr(self, "test_spin", False) + if not test_spin: + test_keys = ["energy", "force", "virial"] + else: + test_keys = ["energy", "force", "force_mag", "virial"] + + result_0 = eval_model( + self.model, + coord0.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } - e1, f1, v1 = eval_model( - self.model, coord1.unsqueeze(0), cell.unsqueeze(0), atype + ret0 = {key: result_0[key].squeeze(0) for key in test_keys} + result_1 = eval_model( + self.model, + coord1.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret1 = { - "energy": e1.squeeze(0), - "force": f1.squeeze(0), - "virial": v1.squeeze(0), - } - e2, f2, v2 = eval_model( - self.model, coord2.unsqueeze(0), cell.unsqueeze(0), atype + ret1 = {key: result_1[key].squeeze(0) for key in test_keys} + result_2 = eval_model( + self.model, + coord2.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret2 = { - "energy": e2.squeeze(0), - "force": f2.squeeze(0), - "virial": v2.squeeze(0), - } - e3, f3, v3 = eval_model( - self.model, coord3.unsqueeze(0), cell.unsqueeze(0), atype + ret2 = {key: result_2[key].squeeze(0) for key in test_keys} + result_3 = eval_model( + self.model, + coord3.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret3 = { - "energy": e3.squeeze(0), - "force": f3.squeeze(0), - "virial": v3.squeeze(0), - } + ret3 = {key: result_3[key].squeeze(0) for key in test_keys} def compare(ret0, ret1): - torch.testing.assert_close( - ret0["energy"], ret1["energy"], rtol=rprec, atol=aprec - ) - # plus 1. to avoid the divided-by-zero issue - torch.testing.assert_close( - 1.0 + ret0["force"], 1.0 + ret1["force"], rtol=rprec, atol=aprec - ) - if not hasattr(self, "test_virial") or self.test_virial: - torch.testing.assert_close( - 1.0 + ret0["virial"], 1.0 + ret1["virial"], rtol=rprec, atol=aprec - ) + for key in test_keys: + if key in ["energy"]: + torch.testing.assert_close( + ret0[key], ret1[key], rtol=rprec, atol=aprec + ) + elif key in ["force", "force_mag"]: + # plus 1. to avoid the divided-by-zero issue + torch.testing.assert_close( + 1.0 + ret0[key], 1.0 + ret1[key], rtol=rprec, atol=aprec + ) + elif key == "virial": + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + 1.0 + ret0[key], 1.0 + ret1[key], rtol=rprec, atol=aprec + ) + else: + raise RuntimeError(f"Unexpected test key {key}") compare(ret0, ret1) compare(ret1, ret2) @@ -207,7 +218,16 @@ class TestEnergyModelZBL(unittest.TestCase, SmoothTest): def setUp(self): model_params = copy.deepcopy(model_zbl) self.type_split = False - self.model = get_zbl_model(model_params).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) + self.epsilon, self.aprec = 1e-10, None + + +class TestEnergyModelSpinSeA(unittest.TestCase, SmoothTest): + def setUp(self): + model_params = copy.deepcopy(model_spin) + self.type_split = False + self.test_spin = True + self.model = get_model(model_params).to(env.DEVICE) self.epsilon, self.aprec = None, None diff --git a/source/tests/pt/model/test_trans.py b/source/tests/pt/model/test_trans.py index b9affac3aa..359e91d8c8 100644 --- a/source/tests/pt/model/test_trans.py +++ b/source/tests/pt/model/test_trans.py @@ -9,7 +9,6 @@ ) from deepmd.pt.model.model import ( get_model, - get_zbl_model, ) from deepmd.pt.utils import ( env, @@ -20,6 +19,7 @@ model_dpa2, model_hybrid, model_se_e2_a, + model_spin, model_zbl, ) @@ -35,35 +35,45 @@ def test( cell = (cell + cell.T) + 5.0 * torch.eye(3, device=env.DEVICE) coord = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) coord = torch.matmul(coord, cell) + spin = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) shift = (torch.rand([3], dtype=dtype, device=env.DEVICE) - 0.5) * 2.0 coord_s = torch.matmul( torch.remainder(torch.matmul(coord + shift, torch.linalg.inv(cell)), 1.0), cell, ) - e0, f0, v0 = eval_model( - self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype + test_spin = getattr(self, "test_spin", False) + if not test_spin: + test_keys = ["energy", "force", "virial"] + else: + test_keys = ["energy", "force", "force_mag", "virial"] + result_0 = eval_model( + self.model, + coord.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } - e1, f1, v1 = eval_model( - self.model, coord_s.unsqueeze(0), cell.unsqueeze(0), atype + ret0 = {key: result_0[key].squeeze(0) for key in test_keys} + result_1 = eval_model( + self.model, + coord_s.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret1 = { - "energy": e1.squeeze(0), - "force": f1.squeeze(0), - "virial": v1.squeeze(0), - } + ret1 = {key: result_1[key].squeeze(0) for key in test_keys} prec = 1e-10 - torch.testing.assert_close(ret0["energy"], ret1["energy"], rtol=prec, atol=prec) - torch.testing.assert_close(ret0["force"], ret1["force"], rtol=prec, atol=prec) - if not hasattr(self, "test_virial") or self.test_virial: - torch.testing.assert_close( - ret0["virial"], ret1["virial"], rtol=prec, atol=prec - ) + for key in test_keys: + if key in ["energy", "force", "force_mag"]: + torch.testing.assert_close(ret0[key], ret1[key], rtol=prec, atol=prec) + elif key == "virial": + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + ret0[key], ret1[key], rtol=prec, atol=prec + ) + else: + raise RuntimeError(f"Unexpected test key {key}") class TestEnergyModelSeA(unittest.TestCase, TransTest): @@ -130,7 +140,15 @@ class TestEnergyModelZBL(unittest.TestCase, TransTest): def setUp(self): model_params = copy.deepcopy(model_zbl) self.type_split = False - self.model = get_zbl_model(model_params).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinSeA(unittest.TestCase, TransTest): + def setUp(self): + model_params = copy.deepcopy(model_spin) + self.type_split = False + self.test_spin = True + self.model = get_model(model_params).to(env.DEVICE) if __name__ == "__main__": diff --git a/source/tests/pt/model/test_unused_params.py b/source/tests/pt/model/test_unused_params.py index 36080c2bbd..a3c93cbe68 100644 --- a/source/tests/pt/model/test_unused_params.py +++ b/source/tests/pt/model/test_unused_params.py @@ -64,14 +64,9 @@ def _test_unused(self, model_params): coord = torch.matmul(coord, cell) atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) idx_perm = [1, 0, 4, 3, 2] - e0, f0, v0 = eval_model( - self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype - ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } + result_0 = eval_model(self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype) + test_keys = ["energy", "force", "virial"] + ret0 = {key: result_0[key].squeeze(0) for key in test_keys} # use computation graph to find all contributing tensors def get_contributing_params(y, top_level=True): diff --git a/source/tests/pt/test_dp_test.py b/source/tests/pt/test_dp_test.py index 095994f8ec..271b8f1082 100644 --- a/source/tests/pt/test_dp_test.py +++ b/source/tests/pt/test_dp_test.py @@ -2,6 +2,7 @@ import json import os import shutil +import tempfile import unittest from copy import ( deepcopy, @@ -13,59 +14,130 @@ import numpy as np import torch +from deepmd.entrypoints.test import test as dp_test from deepmd.pt.entrypoints.main import ( get_trainer, ) -from deepmd.pt.infer import ( - inference, +from deepmd.pt.utils.utils import ( + to_numpy_array, ) +from .model.test_permutation import ( + model_se_e2_a, + model_spin, +) -class TestDPTest(unittest.TestCase): - def setUp(self): - input_json = str(Path(__file__).parent / "water/se_atten.json") - with open(input_json) as f: - self.config = json.load(f) - self.config["training"]["numb_steps"] = 1 - self.config["training"]["save_freq"] = 1 - data_file = [str(Path(__file__).parent / "water/data/data_0")] - self.config["training"]["training_data"]["systems"] = data_file - self.config["training"]["validation_data"]["systems"] = [ - str(Path(__file__).parent / "water/data/single") - ] - self.input_json = "test_dp_test.json" - with open(self.input_json, "w") as fp: - json.dump(self.config, fp, indent=4) - def test_dp_test(self): +class DPTest: + def test_dp_test_1_frame(self): trainer = get_trainer(deepcopy(self.config)) - trainer.run() - with torch.device("cpu"): input_dict, label_dict, _ = trainer.get_data(is_train=False) - _, _, more_loss = trainer.wrapper(**input_dict, label=label_dict, cur_lr=1.0) - - tester = inference.Tester("model.pt", input_script=self.input_json) - try: - res = tester.run() - except StopIteration: - raise StopIteration("Unexpected stop iteration.(test step < total batch)") - for k, v in res.items(): - if k == "rmse" or "mae" in k or k not in more_loss: - continue - np.testing.assert_allclose( - v, more_loss[k].cpu().detach().numpy(), rtol=1e-04, atol=1e-07 + has_spin = getattr(trainer.model, "has_spin", False) + if callable(has_spin): + has_spin = has_spin() + if not has_spin: + input_dict.pop("spin", None) + input_dict["do_atomic_virial"] = True + result = trainer.model(**input_dict) + model = torch.jit.script(trainer.model) + tmp_model = tempfile.NamedTemporaryFile(delete=False, suffix=".pth") + torch.jit.save(model, tmp_model.name) + dp_test( + model=tmp_model.name, + system=self.config["training"]["validation_data"]["systems"][0], + datafile=None, + set_prefix="set", + numb_test=0, + rand_seed=None, + shuffle_test=False, + detail_file=self.detail_file, + atomic=False, + ) + os.unlink(tmp_model.name) + natom = input_dict["atype"].shape[1] + pred_e = np.loadtxt(self.detail_file + ".e.out", ndmin=2)[0, 1] + np.testing.assert_almost_equal( + pred_e, + to_numpy_array(result["energy"])[0][0], + ) + pred_e_peratom = np.loadtxt(self.detail_file + ".e_peratom.out", ndmin=2)[0, 1] + np.testing.assert_almost_equal(pred_e_peratom, pred_e / natom) + if not has_spin: + pred_f = np.loadtxt(self.detail_file + ".f.out", ndmin=2)[:, 3:6] + np.testing.assert_almost_equal( + pred_f, + to_numpy_array(result["force"]).reshape(-1, 3), + ) + pred_v = np.loadtxt(self.detail_file + ".v.out", ndmin=2)[:, 9:18] + np.testing.assert_almost_equal( + pred_v, + to_numpy_array(result["virial"]), + ) + pred_v_peratom = np.loadtxt(self.detail_file + ".v_peratom.out", ndmin=2)[ + :, 9:18 + ] + np.testing.assert_almost_equal(pred_v_peratom, pred_v / natom) + else: + pred_fr = np.loadtxt(self.detail_file + ".fr.out", ndmin=2)[:, 3:6] + np.testing.assert_almost_equal( + pred_fr, + to_numpy_array(result["force"]).reshape(-1, 3), + ) + pred_fm = np.loadtxt(self.detail_file + ".fm.out", ndmin=2)[:, 3:6] + np.testing.assert_almost_equal( + pred_fm, + to_numpy_array( + result["force_mag"][result["mask_mag"].bool().squeeze(-1)] + ).reshape(-1, 3), ) def tearDown(self): for f in os.listdir("."): if f.startswith("model") and f.endswith(".pt"): os.remove(f) + if f.startswith(self.detail_file): + os.remove(f) if f in ["lcurve.out", self.input_json]: os.remove(f) if f in ["stat_files"]: shutil.rmtree(f) +class TestDPTestSeA(DPTest, unittest.TestCase): + def setUp(self): + self.detail_file = "test_dp_test_ener_detail" + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + data_file = [str(Path(__file__).parent / "water/data/single")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_se_e2_a) + self.input_json = "test_dp_test.json" + with open(self.input_json, "w") as fp: + json.dump(self.config, fp, indent=4) + + +class TestDPTestSeASpin(DPTest, unittest.TestCase): + def setUp(self): + self.detail_file = "test_dp_test_ener_spin_detail" + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + data_file = [str(Path(__file__).parent / "NiO/data/single")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_spin) + self.config["model"]["type_map"] = ["Ni", "O", "B"] + self.input_json = "test_dp_test.json" + with open(self.input_json, "w") as fp: + json.dump(self.config, fp, indent=4) + + if __name__ == "__main__": unittest.main() diff --git a/source/tests/pt/test_init_frz_model.py b/source/tests/pt/test_init_frz_model.py index d156eddc41..223b28515d 100644 --- a/source/tests/pt/test_init_frz_model.py +++ b/source/tests/pt/test_init_frz_model.py @@ -92,8 +92,10 @@ def test_dp_test(self): ).reshape(1, -1, 3) atype = np.array([0, 0, 0, 1, 1]).reshape(1, -1) - e1, f1, v1, ae1, av1 = dp1.eval(coord, cell, atype, atomic=True) - e2, f2, v2, ae2, av2 = dp2.eval(coord, cell, atype, atomic=True) + ret1 = dp1.eval(coord, cell, atype, atomic=True) + e1, f1, v1, ae1, av1 = ret1[0], ret1[1], ret1[2], ret1[3], ret1[4] + ret2 = dp2.eval(coord, cell, atype, atomic=True) + e2, f2, v2, ae2, av2 = ret2[0], ret2[1], ret2[2], ret2[3], ret2[4] np.testing.assert_allclose(e1, e2, rtol=1e-10, atol=1e-10) np.testing.assert_allclose(f1, f2, rtol=1e-10, atol=1e-10) np.testing.assert_allclose(v1, v2, rtol=1e-10, atol=1e-10) diff --git a/source/tests/pt/test_loss.py b/source/tests/pt/test_loss.py index 484d62a3ad..dddc9af219 100644 --- a/source/tests/pt/test_loss.py +++ b/source/tests/pt/test_loss.py @@ -1,5 +1,4 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import json import os import unittest @@ -8,22 +7,27 @@ import torch tf.disable_eager_execution() +from copy import ( + deepcopy, +) from pathlib import ( Path, ) from deepmd.pt.loss import ( + EnergySpinLoss, EnergyStdLoss, ) from deepmd.pt.utils.dataset import ( DeepmdDataSetForLoader, ) -from deepmd.tf.common import ( - expand_sys_str, -) from deepmd.tf.loss.ener import ( + EnerSpinLoss, EnerStdLoss, ) +from deepmd.utils.data import ( + DataRequirementItem, +) from .model.test_embedding_net import ( get_single_batch, @@ -35,28 +39,17 @@ CUR_DIR = os.path.dirname(__file__) -def get_batch(): - with open(str(Path(__file__).parent / "water/se_e2_a.json")) as fin: - content = fin.read() - config = json.loads(content) - data_file = [str(Path(__file__).parent / "water/data/data_0")] - config["training"]["training_data"]["systems"] = data_file - config["training"]["validation_data"]["systems"] = data_file - model_config = config["model"] - rcut = model_config["descriptor"]["rcut"] - # self.rcut_smth = model_config['descriptor']['rcut_smth'] - sel = model_config["descriptor"]["sel"] - systems = config["training"]["validation_data"]["systems"] - if isinstance(systems, str): - systems = expand_sys_str(systems) - dataset = DeepmdDataSetForLoader(systems[0], model_config["type_map"]) - dataset.add_data_requirement(energy_data_requirement) +def get_batch(system, type_map, data_requirement): + dataset = DeepmdDataSetForLoader(system, type_map) + dataset.add_data_requirement(data_requirement) np_batch, pt_batch = get_single_batch(dataset) return np_batch, pt_batch -class TestLearningRate(unittest.TestCase): +class TestEnerStdLoss(unittest.TestCase): def setUp(self): + self.system = str(Path(__file__).parent / "water/data/data_0") + self.type_map = ["H", "O"] self.start_lr = 1.1 self.start_pref_e = 0.02 self.limit_pref_e = 1.0 @@ -66,7 +59,9 @@ def setUp(self): self.limit_pref_v = 1.0 self.cur_lr = 1.2 # data - np_batch, pt_batch = get_batch() + np_batch, pt_batch = get_batch( + self.system, self.type_map, energy_data_requirement + ) natoms = np_batch["natoms"] self.nloc = natoms[0] l_energy, l_force, l_virial = ( @@ -177,8 +172,8 @@ def test_consistency(self): self.limit_pref_v, ) my_loss, my_more_loss = mine( - self.label, self.model_pred, + self.label, self.nloc, self.cur_lr, ) @@ -192,5 +187,179 @@ def test_consistency(self): ) +class TestEnerSpinLoss(unittest.TestCase): + def setUp(self): + self.system = str(Path(__file__).parent / "NiO/data/data_0") + self.type_map = ["Ni", "O"] + self.start_lr = 1.1 + self.start_pref_e = 0.02 + self.limit_pref_e = 1.0 + self.start_pref_fr = 1000.0 + self.limit_pref_fr = 1.0 + self.start_pref_fm = 1000.0 + self.limit_pref_fm = 1.0 + self.cur_lr = 1.2 + self.use_spin = [1, 0] + # data + spin_data_requirement = deepcopy(energy_data_requirement) + spin_data_requirement.append( + DataRequirementItem( + "force_mag", + ndof=3, + atomic=True, + must=False, + high_prec=False, + ) + ) + np_batch, pt_batch = get_batch( + self.system, self.type_map, spin_data_requirement + ) + natoms = np_batch["natoms"] + self.nloc = natoms[0] + nframes = np_batch["energy"].shape[0] + l_energy, l_force_real, l_force_mag, l_virial = ( + np_batch["energy"], + np_batch["force"], + np_batch["force_mag"], + np_batch["virial"], + ) + # merged force for tf old implement + l_force_merge_tf = np.concatenate( + [ + l_force_real.reshape(nframes, self.nloc, 3), + l_force_mag.reshape(nframes, self.nloc, 3)[ + np_batch["atype"] == 0 + ].reshape(nframes, -1, 3), + ], + axis=1, + ).reshape(nframes, -1) + p_energy, p_force_real, p_force_mag, p_force_merge_tf, p_virial = ( + np.ones_like(l_energy), + np.ones_like(l_force_real), + np.ones_like(l_force_mag), + np.ones_like(l_force_merge_tf), + np.ones_like(l_virial), + ) + virt_nloc = (np_batch["atype"] == 0).sum(-1) + natoms_tf = np.concatenate([natoms, virt_nloc], axis=0) + natoms_tf[:2] += virt_nloc + nloc = natoms_tf[0] + batch_size = pt_batch["coord"].shape[0] + atom_energy = np.zeros(shape=[batch_size, nloc]) + atom_pref = np.zeros(shape=[batch_size, nloc * 3]) + self.nloc_tf = nloc + # tf + base = EnerSpinLoss( + self.start_lr, + self.start_pref_e, + self.limit_pref_e, + self.start_pref_fr, + self.limit_pref_fr, + self.start_pref_fm, + self.limit_pref_fm, + use_spin=self.use_spin, + ) + self.g = tf.Graph() + with self.g.as_default(): + t_cur_lr = tf.placeholder(shape=[], dtype=tf.float64) + t_natoms = tf.placeholder(shape=[None], dtype=tf.int32) + t_penergy = tf.placeholder(shape=[None, 1], dtype=tf.float64) + t_pforce = tf.placeholder(shape=[None, None], dtype=tf.float64) + t_pvirial = tf.placeholder(shape=[None, 9], dtype=tf.float64) + t_patom_energy = tf.placeholder(shape=[None, None], dtype=tf.float64) + t_lenergy = tf.placeholder(shape=[None, 1], dtype=tf.float64) + t_lforce = tf.placeholder(shape=[None, None], dtype=tf.float64) + t_lvirial = tf.placeholder(shape=[None, 9], dtype=tf.float64) + t_latom_energy = tf.placeholder(shape=[None, None], dtype=tf.float64) + t_atom_pref = tf.placeholder(shape=[None, None], dtype=tf.float64) + find_energy = tf.constant(1.0, dtype=tf.float64) + find_force = tf.constant(1.0, dtype=tf.float64) + find_virial = tf.constant(0.0, dtype=tf.float64) + find_atom_energy = tf.constant(0.0, dtype=tf.float64) + find_atom_pref = tf.constant(0.0, dtype=tf.float64) + model_dict = { + "energy": t_penergy, + "force": t_pforce, + "virial": t_pvirial, + "atom_ener": t_patom_energy, + } + label_dict = { + "energy": t_lenergy, + "force": t_lforce, + "virial": t_lvirial, + "atom_ener": t_latom_energy, + "atom_pref": t_atom_pref, + "find_energy": find_energy, + "find_force": find_force, + "find_virial": find_virial, + "find_atom_ener": find_atom_energy, + "find_atom_pref": find_atom_pref, + } + self.base_loss_sess = base.build( + t_cur_lr, t_natoms, model_dict, label_dict, "" + ) + # torch + self.feed_dict = { + t_cur_lr: self.cur_lr, + t_natoms: natoms_tf, + t_penergy: p_energy, + t_pforce: p_force_merge_tf, + t_pvirial: p_virial.reshape(-1, 9), + t_patom_energy: atom_energy, + t_lenergy: l_energy, + t_lforce: l_force_merge_tf, + t_lvirial: l_virial.reshape(-1, 9), + t_latom_energy: atom_energy, + t_atom_pref: atom_pref, + } + self.model_pred = { + "energy": torch.from_numpy(p_energy), + "force": torch.from_numpy(p_force_real).reshape(nframes, self.nloc, 3), + "force_mag": torch.from_numpy(p_force_mag).reshape(nframes, self.nloc, 3), + "mask_mag": torch.from_numpy(np_batch["atype"] == 0).reshape( + nframes, self.nloc, 1 + ), + } + self.label = { + "energy": torch.from_numpy(l_energy), + "force": torch.from_numpy(l_force_real).reshape(nframes, self.nloc, 3), + "force_mag": torch.from_numpy(l_force_mag).reshape(nframes, self.nloc, 3), + } + self.natoms = pt_batch["natoms"] + + def tearDown(self) -> None: + tf.reset_default_graph() + return super().tearDown() + + def test_consistency(self): + with tf.Session(graph=self.g) as sess: + base_loss, base_more_loss = sess.run( + self.base_loss_sess, feed_dict=self.feed_dict + ) + mine = EnergySpinLoss( + self.start_lr, + self.start_pref_e, + self.limit_pref_e, + self.start_pref_fr, + self.limit_pref_fr, + self.start_pref_fm, + self.limit_pref_fm, + ) + my_loss, my_more_loss = mine( + self.model_pred, + self.label, + self.nloc_tf, # use tf natoms pref + self.cur_lr, + ) + my_loss = my_loss.detach().cpu() + self.assertTrue(np.allclose(base_loss, my_loss.numpy())) + for key in ["ener", "force_r", "force_m"]: + self.assertTrue( + np.allclose( + base_more_loss["l2_%s_loss" % key], my_more_loss["l2_%s_loss" % key] + ) + ) + + if __name__ == "__main__": unittest.main() diff --git a/source/tests/pt/test_stat.py b/source/tests/pt/test_stat.py index 3a09f82baf..e69caad502 100644 --- a/source/tests/pt/test_stat.py +++ b/source/tests/pt/test_stat.py @@ -180,7 +180,9 @@ def my_merge(energy, natoms): .unsqueeze(0) .expand(energy[i][j].shape[0], -1) ) - return energy_lst, natoms_lst + energy_merge = torch.cat(energy_lst) + natoms_merge = torch.cat(natoms_lst) + return energy_merge, natoms_merge energy = self.dp_sampled["energy"] natoms = self.dp_sampled["natoms_vec"] diff --git a/source/tests/tf/test_deeppot_a.py b/source/tests/tf/test_deeppot_a.py index 9b4d64282f..f40b57c213 100644 --- a/source/tests/tf/test_deeppot_a.py +++ b/source/tests/tf/test_deeppot_a.py @@ -804,7 +804,7 @@ def test_convert_012(self): convert_pbtxt_to_pb(str(infer_path / "sea_012.pbtxt"), old_model) run_dp(f"dp convert-from 0.12 -i {old_model} -o {new_model}") dp = DeepPot(new_model) - _, _, _, _, _ = dp.eval(self.coords, self.box, self.atype, atomic=True) + _ = dp.eval(self.coords, self.box, self.atype, atomic=True) os.remove(old_model) os.remove(new_model) @@ -814,7 +814,7 @@ def test_convert(self): convert_pbtxt_to_pb(str(infer_path / "sea_012.pbtxt"), old_model) run_dp(f"dp convert-from -i {old_model} -o {new_model}") dp = DeepPot(new_model) - _, _, _, _, _ = dp.eval(self.coords, self.box, self.atype, atomic=True) + _ = dp.eval(self.coords, self.box, self.atype, atomic=True) os.remove(old_model) os.remove(new_model) From 268591cf8e03a8d23256efe711e5b3c3a4b71ce6 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 8 Mar 2024 03:24:49 -0500 Subject: [PATCH 198/686] pt: make get_data non-blocking (#3422) `to(DEVICE)` is cpu-blocking but `to(DEVICE, non-blocking=True)` is not blocking. This improves performance by at least 0.1s/100 steps. Before, `get_data` is blocking: ![1709698811097](https://github.com/deepmodeling/deepmd-kit/assets/9496702/b86b3928-41e7-46d3-8692-ca96b3a6475a) ![1709698811150](https://github.com/deepmodeling/deepmd-kit/assets/9496702/c4365203-3f3d-4de8-aae6-d8587f0e95a0) After, `get_data` is not blocking: ![1709698811122](https://github.com/deepmodeling/deepmd-kit/assets/9496702/d991c8f0-35c8-4b5d-822e-77af961e9b6e) ![1709698811169](https://github.com/deepmodeling/deepmd-kit/assets/9496702/a56160c2-78c7-4a44-aa96-1df0b520a60a) The subsequent blocking is `phys2inter` (via `torch.linalg.inv`). Signed-off-by: Jinzhe Zeng --- deepmd/pt/train/training.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 6938db9b3c..93afc38575 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -973,9 +973,11 @@ def get_data(self, is_train=True, task_key="Default"): continue elif not isinstance(batch_data[key], list): if batch_data[key] is not None: - batch_data[key] = batch_data[key].to(DEVICE) + batch_data[key] = batch_data[key].to(DEVICE, non_blocking=True) else: - batch_data[key] = [item.to(DEVICE) for item in batch_data[key]] + batch_data[key] = [ + item.to(DEVICE, non_blocking=True) for item in batch_data[key] + ] # we may need a better way to classify which are inputs and which are labels # now wrapper only supports the following inputs: input_keys = [ From fefc0e6cf64e3dcd6d78e9ce8707f4fc8c2a3b17 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 8 Mar 2024 03:25:01 -0500 Subject: [PATCH 199/686] pt: fix print_on_training when there is no validation data (#3423) #3405 changed results from `None` to `{}` but `print_on_training` wasn't revised. --- deepmd/pt/train/training.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 93afc38575..62bc5a4c97 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -1037,7 +1037,7 @@ def print_on_training(self, fout, step_id, cur_lr, train_results, valid_results) print_str = "" print_str += "%7d" % step_id if not self.multi_task: - if valid_results is not None: + if valid_results: prop_fmt = " %11.2e %11.2e" for k in train_keys: print_str += prop_fmt % (valid_results[k], train_results[k]) @@ -1047,7 +1047,7 @@ def print_on_training(self, fout, step_id, cur_lr, train_results, valid_results) print_str += prop_fmt % (train_results[k]) else: for model_key in self.model_keys: - if valid_results[model_key] is not None: + if valid_results[model_key]: prop_fmt = " %11.2e %11.2e" for k in sorted(valid_results[model_key].keys()): print_str += prop_fmt % ( From dabbd35cfcc0c75eff7e567c69f6c8b228e14cbe Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 8 Mar 2024 03:25:17 -0500 Subject: [PATCH 200/686] Consistent activation functions between backends (#3431) 1. add relu, gelu, gelu_tf, relu6, softplus, sigmoid, and linear to dpmodel; 2. add gelu_tf, relu6, soft6, softplus, and sigmoid to pt; 3. change gelu in pt from non-approximate to approximate. If one still wants to use the non-approximate version, we may consider to add a new key; 4. add linear to tf; 5. none in tf now returns `lambda x: x` instead of `None` to be type consistent; 6. support uppercase in all backends; 7. add consistent tests. Signed-off-by: Jinzhe Zeng --- deepmd/common.py | 10 +++- deepmd/dpmodel/utils/network.py | 59 +++++++++++++++++--- deepmd/pt/utils/utils.py | 20 +++++-- deepmd/tf/common.py | 13 ++--- source/tests/consistent/test_activation.py | 63 ++++++++++++++++++++++ 5 files changed, 146 insertions(+), 19 deletions(-) create mode 100644 source/tests/consistent/test_activation.py diff --git a/deepmd/common.py b/deepmd/common.py index 29d32111a8..c776975591 100644 --- a/deepmd/common.py +++ b/deepmd/common.py @@ -52,7 +52,15 @@ _DICT_VAL = TypeVar("_DICT_VAL") _PRECISION = Literal["default", "float16", "float32", "float64"] _ACTIVATION = Literal[ - "relu", "relu6", "softplus", "sigmoid", "tanh", "gelu", "gelu_tf" + "relu", + "relu6", + "softplus", + "sigmoid", + "tanh", + "gelu", + "gelu_tf", + "none", + "linear", ] __all__.extend( [ diff --git a/deepmd/dpmodel/utils/network.py b/deepmd/dpmodel/utils/network.py index feb3355e77..6206367b1b 100644 --- a/deepmd/dpmodel/utils/network.py +++ b/deepmd/dpmodel/utils/network.py @@ -10,6 +10,7 @@ datetime, ) from typing import ( + Callable, ClassVar, Dict, List, @@ -309,14 +310,7 @@ def call(self, x: np.ndarray) -> np.ndarray: """ if self.w is None or self.activation_function is None: raise ValueError("w, b, and activation_function must be set") - if self.activation_function == "tanh": - fn = np.tanh - elif self.activation_function.lower() == "none": - - def fn(x): - return x - else: - raise NotImplementedError(self.activation_function) + fn = get_activation_fn(self.activation_function) y = ( np.matmul(x, self.w) + self.b if self.b is not None @@ -332,6 +326,55 @@ def fn(x): return y +def get_activation_fn(activation_function: str) -> Callable[[np.ndarray], np.ndarray]: + activation_function = activation_function.lower() + if activation_function == "tanh": + return np.tanh + elif activation_function == "relu": + + def fn(x): + # https://stackoverflow.com/a/47936476/9567349 + return x * (x > 0) + + return fn + elif activation_function in ("gelu", "gelu_tf"): + + def fn(x): + # generated by GitHub Copilot + return 0.5 * x * (1 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * x**3))) + + return fn + elif activation_function == "relu6": + + def fn(x): + # generated by GitHub Copilot + return np.minimum(np.maximum(x, 0), 6) + + return fn + elif activation_function == "softplus": + + def fn(x): + # generated by GitHub Copilot + return np.log(1 + np.exp(x)) + + return fn + elif activation_function == "sigmoid": + + def fn(x): + # generated by GitHub Copilot + return 1 / (1 + np.exp(-x)) + + return fn + elif activation_function.lower() in ("none", "linear"): + + def fn(x): + return x + + return fn + else: + raise NotImplementedError(activation_function) + + def make_multilayer_network(T_NetworkLayer, ModuleBase): class NN(ModuleBase): """Native representation of a neural network. diff --git a/deepmd/pt/utils/utils.py b/deepmd/pt/utils/utils.py index f5a4cd84b6..10dcadadac 100644 --- a/deepmd/pt/utils/utils.py +++ b/deepmd/pt/utils/utils.py @@ -21,10 +21,16 @@ def get_activation_fn(activation: str) -> Callable: """Returns the activation function corresponding to `activation`.""" if activation.lower() == "relu": return F.relu - elif activation.lower() == "gelu": - return F.gelu + elif activation.lower() == "gelu" or activation.lower() == "gelu_tf": + return lambda x: F.gelu(x, approximate="tanh") elif activation.lower() == "tanh": return torch.tanh + elif activation.lower() == "relu6": + return F.relu6 + elif activation.lower() == "softplus": + return F.softplus + elif activation.lower() == "sigmoid": + return torch.sigmoid elif activation.lower() == "linear" or activation.lower() == "none": return lambda x: x else: @@ -42,10 +48,16 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: if self.activation.lower() == "relu": return F.relu(x) - elif self.activation.lower() == "gelu": - return F.gelu(x) + elif self.activation.lower() == "gelu" or self.activation.lower() == "gelu_tf": + return F.gelu(x, approximate="tanh") elif self.activation.lower() == "tanh": return torch.tanh(x) + elif self.activation.lower() == "relu6": + return F.relu6(x) + elif self.activation.lower() == "softplus": + return F.softplus(x) + elif self.activation.lower() == "sigmoid": + return torch.sigmoid(x) elif self.activation.lower() == "linear" or self.activation.lower() == "none": return x else: diff --git a/deepmd/tf/common.py b/deepmd/tf/common.py index b1872e72ed..0d59990a29 100644 --- a/deepmd/tf/common.py +++ b/deepmd/tf/common.py @@ -135,14 +135,14 @@ def gelu_wrapper(x): "tanh": tf.nn.tanh, "gelu": gelu, "gelu_tf": gelu_tf, - "None": None, - "none": None, + "linear": lambda x: x, + "none": lambda x: x, } def get_activation_func( activation_fn: Union["_ACTIVATION", None], -) -> Union[Callable[[tf.Tensor], tf.Tensor], None]: +) -> Callable[[tf.Tensor], tf.Tensor]: """Get activation function callable based on string name. Parameters @@ -161,10 +161,11 @@ def get_activation_func( if unknown activation function is specified """ if activation_fn is None: - return None - if activation_fn not in ACTIVATION_FN_DICT: + activation_fn = "none" + assert activation_fn is not None + if activation_fn.lower() not in ACTIVATION_FN_DICT: raise RuntimeError(f"{activation_fn} is not a valid activation function") - return ACTIVATION_FN_DICT[activation_fn] + return ACTIVATION_FN_DICT[activation_fn.lower()] def get_precision(precision: "_PRECISION") -> Any: diff --git a/source/tests/consistent/test_activation.py b/source/tests/consistent/test_activation.py new file mode 100644 index 0000000000..bb06df9082 --- /dev/null +++ b/source/tests/consistent/test_activation.py @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np + +from deepmd.dpmodel.utils.network import get_activation_fn as get_activation_fn_dp + +from .common import ( + INSTALLED_PT, + INSTALLED_TF, + parameterized, +) + +if INSTALLED_PT: + from deepmd.pt.utils.utils import get_activation_fn as get_activation_fn_pt + from deepmd.pt.utils.utils import ( + to_numpy_array, + to_torch_tensor, + ) +if INSTALLED_TF: + from deepmd.tf.common import get_activation_func as get_activation_fn_tf + from deepmd.tf.env import ( + tf, + ) + + +@parameterized( + ( + "Relu", + "Relu6", + "Softplus", + "Sigmoid", + "Tanh", + "Gelu", + "Gelu_tf", + "Linear", + "None", + ), +) +class TestActivationFunctionConsistent(unittest.TestCase): + def setUp(self): + (self.activation,) = self.param + self.random_input = np.random.default_rng().normal(scale=10, size=(10, 10)) + self.ref = get_activation_fn_dp(self.activation)(self.random_input) + + @unittest.skipUnless(INSTALLED_TF, "TensorFlow is not installed") + def test_tf_consistent_with_ref(self): + if INSTALLED_TF: + place_holder = tf.placeholder(tf.float64, self.random_input.shape) + t_test = get_activation_fn_tf(self.activation)(place_holder) + with tf.Session() as sess: + test = sess.run(t_test, feed_dict={place_holder: self.random_input}) + np.testing.assert_allclose(self.ref, test, atol=1e-10) + + @unittest.skipUnless(INSTALLED_PT, "PyTorch is not installed") + def test_pt_consistent_with_ref(self): + if INSTALLED_PT: + test = to_numpy_array( + get_activation_fn_pt(self.activation)( + to_torch_tensor(self.random_input) + ) + ) + np.testing.assert_allclose(self.ref, test, atol=1e-10) From 66edd1f991254bd0c5dbbd3ff04774ac7c34c76d Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 8 Mar 2024 03:26:51 -0500 Subject: [PATCH 201/686] fix errors when `dp` is executed without any subcommands (#3437) Signed-off-by: Jinzhe Zeng --- deepmd/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deepmd/main.py b/deepmd/main.py index 870a04a088..09457419e8 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -798,7 +798,8 @@ def main(): ): deepmd_main = BACKENDS[args.backend]().entry_point_hook elif args.command is None: - pass + # help message has been printed in parse_args + return else: raise RuntimeError(f"unknown command {args.command}") From d3dd6044fed98a23053634e0ae4d1a21e2a0a3c6 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 8 Mar 2024 04:32:11 -0500 Subject: [PATCH 202/686] pt: avoid torch.tensor(constant) during forward (#3421) `torch.tensor(constant)` copies memory from the CPU to the GPU, so it is host blocking and should be avoided in the `forward` method. Before, the CPU waited for the GPU using `cudaStreamSynchronize`, blocking the CPU from doing the following things, where the CPU memory needs to be copied to the GPU, a.k.a. host-to-device (H2D). ![1709693858444](https://github.com/deepmodeling/deepmd-kit/assets/9496702/e6fb6281-245f-4620-82bd-dbcd02121e32) After this PR, all ops in the energy loss are asynchronous, as no H2D happens. ![1709694622120](https://github.com/deepmodeling/deepmd-kit/assets/9496702/172e1601-1a9c-4236-a1e2-a749edc25c50) --------- Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/pt/loss/denoise.py | 14 +++++++------- deepmd/pt/loss/ener.py | 2 +- deepmd/pt/loss/tensor.py | 2 +- .../pt/model/atomic_model/linear_atomic_model.py | 14 ++++++++------ 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/deepmd/pt/loss/denoise.py b/deepmd/pt/loss/denoise.py index cd12e70bb1..57691558cb 100644 --- a/deepmd/pt/loss/denoise.py +++ b/deepmd/pt/loss/denoise.py @@ -52,7 +52,7 @@ def forward(self, model_pred, label, natoms, learning_rate, mae=False): coord_mask = label["coord_mask"] type_mask = label["type_mask"] - loss = torch.tensor(0.0, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE) + loss = torch.zeros(1, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE)[0] more_loss = {} if self.has_coord: if self.mask_loss_coord: @@ -66,9 +66,9 @@ def forward(self, model_pred, label, natoms, learning_rate, mae=False): beta=self.beta, ) else: - coord_loss = torch.tensor( - 0.0, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE - ) + coord_loss = torch.zeros( + 1, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + )[0] else: coord_loss = F.smooth_l1_loss( updated_coord.view(-1, 3), @@ -89,9 +89,9 @@ def forward(self, model_pred, label, natoms, learning_rate, mae=False): reduction="mean", ) else: - token_loss = torch.tensor( - 0.0, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE - ) + token_loss = torch.zeros( + 1, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + )[0] else: token_loss = F.nll_loss( F.log_softmax(logits.view(-1, self.ntypes - 1), dim=-1), diff --git a/deepmd/pt/loss/ener.py b/deepmd/pt/loss/ener.py index 2834733112..1d70528e88 100644 --- a/deepmd/pt/loss/ener.py +++ b/deepmd/pt/loss/ener.py @@ -108,7 +108,7 @@ def forward(self, model_pred, label, natoms, learning_rate, mae=False): pref_e = self.limit_pref_e + (self.start_pref_e - self.limit_pref_e) * coef pref_f = self.limit_pref_f + (self.start_pref_f - self.limit_pref_f) * coef pref_v = self.limit_pref_v + (self.start_pref_v - self.limit_pref_v) * coef - loss = torch.tensor(0.0, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE) + loss = torch.zeros(1, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE)[0] more_loss = {} # more_loss['log_keys'] = [] # showed when validation on the fly # more_loss['test_keys'] = [] # showed when doing dp test diff --git a/deepmd/pt/loss/tensor.py b/deepmd/pt/loss/tensor.py index ee42536557..5ac0a6e37b 100644 --- a/deepmd/pt/loss/tensor.py +++ b/deepmd/pt/loss/tensor.py @@ -83,7 +83,7 @@ def forward(self, model_pred, label, natoms, learning_rate=0.0, mae=False): Other losses for display. """ del learning_rate, mae - loss = torch.tensor(0.0, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE) + loss = torch.zeros(1, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE)[0] more_loss = {} if ( self.has_local_weight diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 5e1a80087e..68705049ae 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -78,6 +78,10 @@ def __init__( self.atomic_bias = None self.mixed_types_list = [model.mixed_types() for model in self.models] + self.rcuts = torch.tensor( + self.get_model_rcuts(), dtype=torch.float64, device=env.DEVICE + ) + self.nsels = torch.tensor(self.get_model_nsels(), device=env.DEVICE) BaseAtomicModel.__init__(self, **kwargs) def mixed_types(self) -> bool: @@ -117,14 +121,12 @@ def get_model_sels(self) -> List[List[int]]: """Get the sels for each individual models.""" return [model.get_sel() for model in self.models] - def _sort_rcuts_sels(self, device: torch.device) -> Tuple[List[float], List[int]]: + def _sort_rcuts_sels(self) -> Tuple[List[float], List[int]]: # sort the pair of rcut and sels in ascending order, first based on sel, then on rcut. - rcuts = torch.tensor(self.get_model_rcuts(), dtype=torch.float64, device=device) - nsels = torch.tensor(self.get_model_nsels(), device=device) zipped = torch.stack( [ - rcuts, - nsels, + self.rcuts, + self.nsels, ], dim=0, ).T @@ -171,7 +173,7 @@ def forward_atomic( if self.do_grad_r() or self.do_grad_c(): extended_coord.requires_grad_(True) extended_coord = extended_coord.view(nframes, -1, 3) - sorted_rcuts, sorted_sels = self._sort_rcuts_sels(device=extended_coord.device) + sorted_rcuts, sorted_sels = self._sort_rcuts_sels() nlists = build_multiple_neighbor_list( extended_coord, nlist, From a9bcf4153847ddb3773f46b0cf7e011eaecacf43 Mon Sep 17 00:00:00 2001 From: Lysithea <52808607+CaRoLZhangxy@users.noreply.github.com> Date: Sat, 9 Mar 2024 01:51:52 +0800 Subject: [PATCH 203/686] clean up the init interface of pt.dataloader (#3434) https://github.com/deepmodeling/deepmd-kit/issues/3427 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/pt/entrypoints/main.py | 6 +++--- deepmd/pt/utils/dataloader.py | 21 +++++++++++++++---- deepmd/pt/utils/dataset.py | 13 +++--------- source/tests/pt/model/test_model.py | 13 +----------- source/tests/pt/model/test_saveload_dpa1.py | 13 +----------- .../tests/pt/model/test_saveload_se_e2_a.py | 13 +----------- source/tests/pt/test_sampler.py | 9 +------- source/tests/pt/test_stat.py | 9 +------- 8 files changed, 28 insertions(+), 69 deletions(-) diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 0e5767cb4e..76796f6197 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -134,7 +134,7 @@ def prepare_trainer_input_single( DpLoaderSet( validation_systems, validation_dataset_params["batch_size"], - model_params_single, + model_params_single["type_map"], ) if validation_systems else None @@ -143,13 +143,13 @@ def prepare_trainer_input_single( train_data_single = DpLoaderSet( training_systems, training_dataset_params["batch_size"], - model_params_single, + model_params_single["type_map"], ) else: train_data_single = DpLoaderSet( training_systems, training_dataset_params["batch_size"], - model_params_single, + model_params_single["type_map"], ) return ( train_data_single, diff --git a/deepmd/pt/utils/dataloader.py b/deepmd/pt/utils/dataloader.py index 2715bced52..0359071d71 100644 --- a/deepmd/pt/utils/dataloader.py +++ b/deepmd/pt/utils/dataloader.py @@ -55,13 +55,27 @@ def setup_seed(seed): class DpLoaderSet(Dataset): - """A dataset for storing DataLoaders to multiple Systems.""" + """A dataset for storing DataLoaders to multiple Systems. + + Parameters + ---------- + sys_path + Path to the data system + batch_size + Max frame count in a batch. + type_map + Gives the name of different atom types + seed + Random seed for dataloader + shuffle + If the data are shuffled (Only effective in serial mode. Always shuffle in distributed data parallelism) + """ def __init__( self, systems, batch_size, - model_params, + type_map, seed=10, shuffle=True, ): @@ -77,8 +91,7 @@ def __init__( def construct_dataset(system): return DeepmdDataSetForLoader( system=system, - type_map=model_params["type_map"], - shuffle=shuffle, + type_map=type_map, ) with Pool( diff --git a/deepmd/pt/utils/dataset.py b/deepmd/pt/utils/dataset.py index 77297d980c..dbe4d92a0f 100644 --- a/deepmd/pt/utils/dataset.py +++ b/deepmd/pt/utils/dataset.py @@ -3,6 +3,7 @@ from typing import ( List, + Optional, ) from torch.utils.data import ( @@ -16,24 +17,16 @@ class DeepmdDataSetForLoader(Dataset): - def __init__( - self, - system: str, - type_map: str, - shuffle=True, - ): + def __init__(self, system: str, type_map: Optional[List[str]] = None): """Construct DeePMD-style dataset containing frames cross different systems. Args: - systems: Paths to systems. - - batch_size: Max frame count in a batch. - type_map: Atom types. """ self.system = system self._type_map = type_map - self._data_system = DeepmdData( - sys_path=system, shuffle_test=shuffle, type_map=self._type_map - ) + self._data_system = DeepmdData(sys_path=system, type_map=self._type_map) self.mixed_type = self._data_system.mixed_type self._ntypes = self._data_system.get_ntypes() self._natoms = self._data_system.get_natoms() diff --git a/source/tests/pt/model/test_model.py b/source/tests/pt/model/test_model.py index 69ec88f5d7..f42c11aa4c 100644 --- a/source/tests/pt/model/test_model.py +++ b/source/tests/pt/model/test_model.py @@ -273,18 +273,7 @@ def test_consistency(self): self.wanted_step ) # Build DeePMD graph - my_ds = DpLoaderSet( - self.systems, - self.batch_size, - model_params={ - "descriptor": { - "type": "se_e2_a", - "sel": self.sel, - "rcut": self.rcut, - }, - "type_map": self.type_map, - }, - ) + my_ds = DpLoaderSet(self.systems, self.batch_size, self.type_map) my_ds.add_data_requirement(energy_data_requirement) my_model = get_model( model_params={ diff --git a/source/tests/pt/model/test_saveload_dpa1.py b/source/tests/pt/model/test_saveload_dpa1.py index 408afbef43..712b44485e 100644 --- a/source/tests/pt/model/test_saveload_dpa1.py +++ b/source/tests/pt/model/test_saveload_dpa1.py @@ -46,18 +46,7 @@ def get_dataset(config): batch_size = config["training"]["training_data"]["batch_size"] type_map = model_config["type_map"] - dataset = DpLoaderSet( - systems, - batch_size, - model_params={ - "descriptor": { - "type": "dpa1", - "sel": sel, - "rcut": rcut, - }, - "type_map": type_map, - }, - ) + dataset = DpLoaderSet(systems, batch_size, type_map) data_stat_nbatch = model_config.get("data_stat_nbatch", 10) sampled = make_stat_input(dataset.systems, dataset.dataloaders, data_stat_nbatch) return dataset, sampled diff --git a/source/tests/pt/model/test_saveload_se_e2_a.py b/source/tests/pt/model/test_saveload_se_e2_a.py index 382f119c30..56ea3283d9 100644 --- a/source/tests/pt/model/test_saveload_se_e2_a.py +++ b/source/tests/pt/model/test_saveload_se_e2_a.py @@ -46,18 +46,7 @@ def get_dataset(config): batch_size = config["training"]["training_data"]["batch_size"] type_map = model_config["type_map"] - dataset = DpLoaderSet( - systems, - batch_size, - model_params={ - "descriptor": { - "type": "se_e2_a", - "sel": sel, - "rcut": rcut, - }, - "type_map": type_map, - }, - ) + dataset = DpLoaderSet(systems, batch_size, type_map) data_stat_nbatch = model_config.get("data_stat_nbatch", 10) sampled = make_stat_input(dataset.systems, dataset.dataloaders, data_stat_nbatch) return dataset, sampled diff --git a/source/tests/pt/test_sampler.py b/source/tests/pt/test_sampler.py index 25980cc144..4f1091c936 100644 --- a/source/tests/pt/test_sampler.py +++ b/source/tests/pt/test_sampler.py @@ -46,14 +46,7 @@ def setUp(self): self.my_dataset = DpLoaderSet( self.systems, self.batch_size, - model_params={ - "descriptor": { - "type": "se_e2_a", - "sel": self.sel, - "rcut": self.rcut, - }, - "type_map": model_config["type_map"], - }, + model_config["type_map"], seed=10, shuffle=False, ) diff --git a/source/tests/pt/test_stat.py b/source/tests/pt/test_stat.py index e69caad502..51ca903bc2 100644 --- a/source/tests/pt/test_stat.py +++ b/source/tests/pt/test_stat.py @@ -137,14 +137,7 @@ def setUp(self): self.my_dataset = DpLoaderSet( self.systems, self.batch_size, - model_params={ - "descriptor": { - "type": "se_e2_a", - "sel": self.sel, - "rcut": self.rcut, - }, - "type_map": model_config["type_map"], - }, + model_config["type_map"], seed=10, ) self.filter_neuron = model_config["descriptor"]["neuron"] From fd82f0484e9d2d6625937f889b3b379b9cc5af23 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Sun, 10 Mar 2024 02:02:57 +0800 Subject: [PATCH 204/686] Add `max_ckpt_keep` for trainer (#3441) Signed-off-by: Duo <50307526+iProzd@users.noreply.github.com> Co-authored-by: Jinzhe Zeng --- deepmd/pt/train/training.py | 10 ++++++++++ deepmd/tf/train/trainer.py | 5 ++++- deepmd/utils/argcheck.py | 6 ++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 62bc5a4c97..fb28f0c4f2 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -132,6 +132,7 @@ def __init__( self.disp_freq = training_params.get("disp_freq", 1000) self.save_ckpt = training_params.get("save_ckpt", "model.ckpt") self.save_freq = training_params.get("save_freq", 1000) + self.max_ckpt_keep = training_params.get("max_ckpt_keep", 5) self.lcurve_should_print_header = True def get_opt_param(params): @@ -924,6 +925,15 @@ def save_model(self, save_path, lr=0.0, step=0): {"model": module.state_dict(), "optimizer": self.optimizer.state_dict()}, save_path, ) + checkpoint_dir = save_path.parent + checkpoint_files = [ + f + for f in checkpoint_dir.glob("*.pt") + if not f.is_symlink() and f.name.startswith(self.save_ckpt) + ] + if len(checkpoint_files) > self.max_ckpt_keep: + checkpoint_files.sort(key=lambda x: x.stat().st_mtime) + checkpoint_files[0].unlink() def get_data(self, is_train=True, task_key="Default"): if not self.multi_task: diff --git a/deepmd/tf/train/trainer.py b/deepmd/tf/train/trainer.py index 1dd31fd0bb..27478abaa1 100644 --- a/deepmd/tf/train/trainer.py +++ b/deepmd/tf/train/trainer.py @@ -164,6 +164,7 @@ def get_lr_and_coef(lr_param): self.disp_freq = tr_data.get("disp_freq", 1000) self.save_freq = tr_data.get("save_freq", 1000) self.save_ckpt = tr_data.get("save_ckpt", "model.ckpt") + self.max_ckpt_keep = tr_data.get("max_ckpt_keep", 5) self.display_in_training = tr_data.get("disp_training", True) self.timing_in_training = tr_data.get("time_training", True) self.profiling = self.run_opt.is_chief and tr_data.get("profiling", False) @@ -498,7 +499,9 @@ def _init_session(self): # Initializes or restore global variables init_op = tf.global_variables_initializer() if self.run_opt.is_chief: - self.saver = tf.train.Saver(save_relative_paths=True) + self.saver = tf.train.Saver( + save_relative_paths=True, max_to_keep=self.max_ckpt_keep + ) if self.run_opt.init_mode == "init_from_scratch": log.info("initialize model from scratch") run_sess(self.sess, init_op) diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 5e8db431f8..e822e18d50 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -2134,6 +2134,11 @@ def training_args(): # ! modified by Ziyao: data configuration isolated. doc_disp_freq = "The frequency of printing learning curve." doc_save_freq = "The frequency of saving check point." doc_save_ckpt = "The path prefix of saving check point files." + doc_max_ckpt_keep = ( + "The maximum number of checkpoints to keep. " + "The oldest checkpoints will be deleted once the number of checkpoints exceeds max_ckpt_keep. " + "Defaults to 5." + ) doc_disp_training = "Displaying verbose information during training." doc_time_training = "Timing durining training." doc_profiling = "Profiling during training." @@ -2192,6 +2197,7 @@ def training_args(): # ! modified by Ziyao: data configuration isolated. Argument( "save_ckpt", str, optional=True, default="model.ckpt", doc=doc_save_ckpt ), + Argument("max_ckpt_keep", int, optional=True, default=5, doc=doc_max_ckpt_keep), Argument( "disp_training", bool, optional=True, default=True, doc=doc_disp_training ), From a286bd498ca4ea30c952b6b19587db4757a3fa55 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Sun, 10 Mar 2024 15:49:51 +0800 Subject: [PATCH 205/686] pt: cleanup tester (#3442) --- deepmd/pt/entrypoints/main.py | 4 +- deepmd/pt/infer/inference.py | 350 ------------------------------ source/tests/pt/model/test_jit.py | 2 +- 3 files changed, 2 insertions(+), 354 deletions(-) diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 76796f6197..46d284a395 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -281,9 +281,7 @@ def train(FLAGS): def freeze(FLAGS): - model = torch.jit.script( - inference.Tester(FLAGS.model, numb_test=1, head=FLAGS.head).model - ) + model = torch.jit.script(inference.Tester(FLAGS.model, head=FLAGS.head).model) torch.jit.save( model, FLAGS.output, diff --git a/deepmd/pt/infer/inference.py b/deepmd/pt/infer/inference.py index e97623dd24..6c13b363bc 100644 --- a/deepmd/pt/infer/inference.py +++ b/deepmd/pt/infer/inference.py @@ -1,41 +1,20 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import json import logging -import math from copy import ( deepcopy, ) -from pathlib import ( - Path, -) -import numpy as np import torch -from torch.utils.data import ( - DataLoader, - RandomSampler, -) -from deepmd.common import ( - expand_sys_str, -) -from deepmd.pt.loss import ( - DenoiseLoss, - EnergyStdLoss, -) from deepmd.pt.model.model import ( get_model, ) from deepmd.pt.train.wrapper import ( ModelWrapper, ) -from deepmd.pt.utils.dataloader import ( - DpLoaderSet, -) from deepmd.pt.utils.env import ( DEVICE, JIT, - NUM_WORKERS, ) if torch.__version__.startswith("2"): @@ -47,12 +26,6 @@ class Tester: def __init__( self, model_ckpt, - input_script=None, - system=None, - datafile=None, - numb_test=100, - detail_file=None, - shuffle_test=False, head=None, ): """Construct a DeePMD tester. @@ -60,9 +33,6 @@ def __init__( Args: - config: The Dict-like configuration with training options. """ - self.numb_test = numb_test - self.detail_file = detail_file - self.shuffle_test = shuffle_test # Model state_dict = torch.load(model_ckpt, map_location=DEVICE) if "model" in state_dict: @@ -85,54 +55,6 @@ def __init__( ] = state_dict[item].clone() state_dict = state_dict_head - # Data - if input_script is not None: - with open(input_script) as fin: - self.input_script = json.load(fin) - training_params = self.input_script["training"] - if not self.multi_task: - assert ( - "validation_data" in training_params - ), f"Validation systems not found in {input_script}!" - self.systems = training_params["validation_data"]["systems"] - self.batchsize = training_params["validation_data"]["batch_size"] - log.info(f"Testing validation systems in input script: {input_script}") - else: - assert ( - "data_dict" in training_params - ), f"Input script {input_script} is not in multi-task mode!" - assert head in training_params["data_dict"], ( - f"Specified head {head} not found in input script {input_script}! " - f"Available ones are {list(training_params['data_dict'].keys())}." - ) - assert ( - "validation_data" in training_params["data_dict"][head] - ), f"Validation systems not found in head {head} of {input_script}!" - self.systems = training_params["data_dict"][head]["validation_data"][ - "systems" - ] - self.batchsize = training_params["data_dict"][head]["validation_data"][ - "batch_size" - ] - log.info( - f"Testing validation systems in head {head} of input script: {input_script}" - ) - elif system is not None: - self.systems = expand_sys_str(system) - self.batchsize = "auto" - log.info("Testing systems in path: %s", system) - elif datafile is not None: - with open(datafile) as fin: - self.systems = fin.read().splitlines() - self.batchsize = "auto" - log.info("Testing systems in file: %s", datafile) - else: - self.systems = None - self.batchsize = None - - self.type_split = False - if model_params["descriptor"]["type"] in ["se_e2_a"]: - self.type_split = True self.model_params = deepcopy(model_params) model_params["resuming"] = True self.model = get_model(model_params).to(DEVICE) @@ -142,275 +64,3 @@ def __init__( if JIT: self.wrapper = torch.jit.script(self.wrapper) self.wrapper.load_state_dict(state_dict) - - # Loss - if "fitting_net" not in model_params: - assert ( - input_script is not None - ), "Denoise model must use --input-script mode!" - loss_params = self.input_script["loss"] - loss_type = loss_params.pop("type", "ener") - assert ( - loss_type == "denoise" - ), "Models without fitting_net only support denoise test!" - self.noise_settings = { - "noise_type": loss_params.pop("noise_type", "uniform"), - "noise": loss_params.pop("noise", 1.0), - "noise_mode": loss_params.pop("noise_mode", "fix_num"), - "mask_num": loss_params.pop("mask_num", 8), - "same_mask": loss_params.pop("same_mask", False), - "mask_coord": loss_params.pop("mask_coord", False), - "mask_type": loss_params.pop("mask_type", False), - "mask_type_idx": len(model_params["type_map"]) - 1, - } - loss_params["ntypes"] = len(model_params["type_map"]) - self.loss = DenoiseLoss(**loss_params) - else: - self.noise_settings = None - self.loss = EnergyStdLoss(inference=True) - - @staticmethod - def get_data(data): - with torch.device("cpu"): - batch_data = next(iter(data)) - for key in batch_data.keys(): - if key == "sid" or key == "fid": - continue - elif not isinstance(batch_data[key], list): - if batch_data[key] is not None: - batch_data[key] = batch_data[key].to(DEVICE) - else: - batch_data[key] = [item.to(DEVICE) for item in batch_data[key]] - input_dict = {} - for item in [ - "coord", - "atype", - "box", - ]: - if item in batch_data: - input_dict[item] = batch_data[item] - else: - input_dict[item] = None - label_dict = {} - for item in [ - "energy", - "force", - "virial", - "clean_coord", - "clean_type", - "coord_mask", - "type_mask", - ]: - if item in batch_data: - label_dict[item] = batch_data[item] - return input_dict, label_dict - - def run(self): - systems = self.systems - system_results = {} - global_sum_natoms = 0 - for cc, system in enumerate(systems): - log.info("# ---------------output of dp test--------------- ") - log.info(f"# testing system : {system}") - system_pred = [] - system_label = [] - dataset = DpLoaderSet( - [system], - self.batchsize, - self.model_params, - shuffle=self.shuffle_test, - ) - sampler = RandomSampler( - dataset, replacement=True, num_samples=dataset.total_batch - ) - if sampler is None: - log.warning( - "Sampler not specified!" - ) # None sampler will lead to a premature stop iteration. Replacement should be True in attribute of the sampler to produce expected number of items in one iteration. - dataloader = DataLoader( - dataset, - sampler=sampler, - batch_size=None, - num_workers=min( - NUM_WORKERS, 1 - ), # setting to 0 diverges the behavior of its iterator; should be >=1 - drop_last=False, - ) - with torch.device("cpu"): - data = iter(dataloader) - - single_results = {} - sum_natoms = 0 - sys_natoms = None - for ii in range(self.numb_test): - try: - input_dict, label_dict = self.get_data(data) - except StopIteration: - if ( - ii < dataset.total_batch - ): # Unexpected stop iteration.(test step < total batch) - raise StopIteration - else: - break - model_pred, _, _ = self.wrapper(**input_dict) - system_pred.append( - { - item: model_pred[item].detach().cpu().numpy() - for item in model_pred - } - ) - system_label.append( - { - item: label_dict[item].detach().cpu().numpy() - for item in label_dict - } - ) - natoms = int(input_dict["atype"].shape[-1]) - _, more_loss = self.loss( - model_pred, label_dict, natoms, 1.0, mae=True - ) # TODO: lr here is useless - if sys_natoms is None: - sys_natoms = natoms - else: - assert ( - sys_natoms == natoms - ), "Frames in one system must be the same!" - sum_natoms += natoms - for k, v in more_loss.items(): - if "mae" in k: - single_results[k] = single_results.get(k, 0.0) + v * natoms - else: - single_results[k] = single_results.get(k, 0.0) + v**2 * natoms - if self.detail_file is not None: - save_detail_file( - Path(self.detail_file), - system_pred, - system_label, - sys_natoms, - system_name=system, - append=(cc != 0), - ) - results = { - k: v / sum_natoms if "mae" in k else math.sqrt(v / sum_natoms) - for k, v in single_results.items() - } - for item in sorted(results.keys()): - log.info(f"{item}: {results[item]:.4f}") - log.info("# ----------------------------------------------- ") - for k, v in single_results.items(): - system_results[k] = system_results.get(k, 0.0) + v - global_sum_natoms += sum_natoms - - global_results = { - k: v / global_sum_natoms if "mae" in k else math.sqrt(v / global_sum_natoms) - for k, v in system_results.items() - } - log.info("# ----------weighted average of errors----------- ") - if not self.multi_task: - log.info(f"# number of systems : {len(systems)}") - else: - log.info(f"# number of systems for {self.head}: {len(systems)}") - for item in sorted(global_results.keys()): - log.info(f"{item}: {global_results[item]:.4f}") - log.info("# ----------------------------------------------- ") - return global_results - - -def save_txt_file( - fname: Path, data: np.ndarray, header: str = "", append: bool = False -): - """Save numpy array to test file. - - Parameters - ---------- - fname : str - filename - data : np.ndarray - data to save to disk - header : str, optional - header string to use in file, by default "" - append : bool, optional - if true file will be appended insted of overwriting, by default False - """ - flags = "ab" if append else "w" - with fname.open(flags) as fp: - np.savetxt(fp, data, header=header) - - -def save_detail_file( - detail_path, system_pred, system_label, natoms, system_name, append=False -): - ntest = len(system_pred) - data_e = np.concatenate([item["energy"] for item in system_label]).reshape([-1, 1]) - pred_e = np.concatenate([item["energy"] for item in system_pred]).reshape([-1, 1]) - pe = np.concatenate( - ( - data_e, - pred_e, - ), - axis=1, - ) - save_txt_file( - detail_path.with_suffix(".e.out"), - pe, - header="%s: data_e pred_e" % system_name, - append=append, - ) - pe_atom = pe / natoms - save_txt_file( - detail_path.with_suffix(".e_peratom.out"), - pe_atom, - header="%s: data_e pred_e" % system_name, - append=append, - ) - if "force" in system_pred[0]: - data_f = np.concatenate([item["force"] for item in system_label]).reshape( - [-1, 3] - ) - pred_f = np.concatenate([item["force"] for item in system_pred]).reshape( - [-1, 3] - ) - pf = np.concatenate( - ( - data_f, - pred_f, - ), - axis=1, - ) - save_txt_file( - detail_path.with_suffix(".f.out"), - pf, - header="%s: data_fx data_fy data_fz pred_fx pred_fy pred_fz" % system_name, - append=append, - ) - if "virial" in system_pred[0]: - data_v = np.concatenate([item["virial"] for item in system_label]).reshape( - [-1, 9] - ) - pred_v = np.concatenate([item["virial"] for item in system_pred]).reshape( - [-1, 9] - ) - pv = np.concatenate( - ( - data_v, - pred_v, - ), - axis=1, - ) - save_txt_file( - detail_path.with_suffix(".v.out"), - pv, - header=f"{system_name}: data_vxx data_vxy data_vxz data_vyx data_vyy " - "data_vyz data_vzx data_vzy data_vzz pred_vxx pred_vxy pred_vxz pred_vyx " - "pred_vyy pred_vyz pred_vzx pred_vzy pred_vzz", - append=append, - ) - pv_atom = pv / natoms - save_txt_file( - detail_path.with_suffix(".v_peratom.out"), - pv_atom, - header=f"{system_name}: data_vxx data_vxy data_vxz data_vyx data_vyy " - "data_vyz data_vzx data_vzy data_vzz pred_vxx pred_vxy pred_vxz pred_vyx " - "pred_vyy pred_vyz pred_vzx pred_vzy pred_vzz", - append=append, - ) diff --git a/source/tests/pt/model/test_jit.py b/source/tests/pt/model/test_jit.py index fc07267b88..81ea49a68e 100644 --- a/source/tests/pt/model/test_jit.py +++ b/source/tests/pt/model/test_jit.py @@ -31,7 +31,7 @@ class JITTest: def test_jit(self): trainer = get_trainer(deepcopy(self.config)) trainer.run() - model = torch.jit.script(inference.Tester("./model.pt", numb_test=1).model) + model = torch.jit.script(inference.Tester("./model.pt").model) torch.jit.save(model, "./frozen_model.pth", {}) def tearDown(self): From 2ee8a3b1e7bce6a4fcfbccaf3cd16c6e490e7ba0 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:00:31 +0800 Subject: [PATCH 206/686] Feat: Add polar stat constant matrix calculation to PT (#3426) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/dpmodel/fitting/dipole_fitting.py | 10 ++ deepmd/dpmodel/fitting/ener_fitting.py | 4 + deepmd/dpmodel/fitting/general_fitting.py | 4 - deepmd/dpmodel/fitting/invar_fitting.py | 10 ++ .../dpmodel/fitting/polarizability_fitting.py | 35 ++++++ deepmd/pt/model/task/dipole.py | 10 ++ deepmd/pt/model/task/ener.py | 10 ++ deepmd/pt/model/task/fitting.py | 4 - deepmd/pt/model/task/polarizability.py | 102 +++++++++++++++++- deepmd/tf/fit/polar.py | 18 ++-- source/tests/consistent/common.py | 5 + source/tests/pt/model/test_polar_stat.py | 75 +++++++++++++ source/tests/pt/test_training.py | 3 + 13 files changed, 272 insertions(+), 18 deletions(-) create mode 100644 source/tests/pt/model/test_polar_stat.py diff --git a/deepmd/dpmodel/fitting/dipole_fitting.py b/deepmd/dpmodel/fitting/dipole_fitting.py index e00f031549..6d6324770c 100644 --- a/deepmd/dpmodel/fitting/dipole_fitting.py +++ b/deepmd/dpmodel/fitting/dipole_fitting.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy from typing import ( Any, Dict, @@ -19,6 +20,9 @@ OutputVariableDef, fitting_check_output, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .general_fitting import ( GeneralFitting, @@ -153,6 +157,12 @@ def serialize(self) -> dict: data["c_differentiable"] = self.c_differentiable return data + @classmethod + def deserialize(cls, data: dict) -> "GeneralFitting": + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) + return super().deserialize(data) + def output_def(self): return FittingOutputDef( [ diff --git a/deepmd/dpmodel/fitting/ener_fitting.py b/deepmd/dpmodel/fitting/ener_fitting.py index 3a0e9909b9..7f83f1e886 100644 --- a/deepmd/dpmodel/fitting/ener_fitting.py +++ b/deepmd/dpmodel/fitting/ener_fitting.py @@ -18,6 +18,9 @@ from deepmd.dpmodel.fitting.general_fitting import ( GeneralFitting, ) +from deepmd.utils.version import ( + check_version_compatibility, +) @InvarFitting.register("ener") @@ -69,6 +72,7 @@ def __init__( @classmethod def deserialize(cls, data: dict) -> "GeneralFitting": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("var_name") data.pop("dim_out") return super().deserialize(data) diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index 01bf107c63..e9dddae2de 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -21,9 +21,6 @@ FittingNet, NetworkCollection, ) -from deepmd.utils.version import ( - check_version_compatibility, -) from .base_fitting import ( BaseFitting, @@ -256,7 +253,6 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "GeneralFitting": data = copy.deepcopy(data) - check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") variables = data.pop("@variables") diff --git a/deepmd/dpmodel/fitting/invar_fitting.py b/deepmd/dpmodel/fitting/invar_fitting.py index fd556ff074..e795953a75 100644 --- a/deepmd/dpmodel/fitting/invar_fitting.py +++ b/deepmd/dpmodel/fitting/invar_fitting.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy from typing import ( Any, Dict, @@ -16,6 +17,9 @@ OutputVariableDef, fitting_check_output, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .general_fitting import ( GeneralFitting, @@ -169,6 +173,12 @@ def serialize(self) -> dict: data["atom_ener"] = self.atom_ener return data + @classmethod + def deserialize(cls, data: dict) -> "GeneralFitting": + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) + return super().deserialize(data) + def _net_out_dim(self): """Set the FittingNet output dim.""" return self.dim_out diff --git a/deepmd/dpmodel/fitting/polarizability_fitting.py b/deepmd/dpmodel/fitting/polarizability_fitting.py index 4f7c33b9a8..5d75037137 100644 --- a/deepmd/dpmodel/fitting/polarizability_fitting.py +++ b/deepmd/dpmodel/fitting/polarizability_fitting.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy from typing import ( Any, Dict, @@ -22,6 +23,9 @@ OutputVariableDef, fitting_check_output, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .general_fitting import ( GeneralFitting, @@ -139,6 +143,7 @@ def __init__( ntypes, 1 ) self.shift_diag = shift_diag + self.constant_matrix = np.zeros(ntypes, dtype=GLOBAL_NP_FLOAT_PRECISION) super().__init__( var_name=var_name, ntypes=ntypes, @@ -168,15 +173,36 @@ def _net_out_dim(self): else self.embedding_width * self.embedding_width ) + def __setitem__(self, key, value): + if key in ["constant_matrix"]: + self.constant_matrix = value + else: + super().__setitem__(key, value) + + def __getitem__(self, key): + if key in ["constant_matrix"]: + return self.constant_matrix + else: + return super().__getitem__(key) + def serialize(self) -> dict: data = super().serialize() data["type"] = "polar" + data["@version"] = 2 data["embedding_width"] = self.embedding_width data["old_impl"] = self.old_impl data["fit_diag"] = self.fit_diag + data["shift_diag"] = self.shift_diag data["@variables"]["scale"] = self.scale + data["@variables"]["constant_matrix"] = self.constant_matrix return data + @classmethod + def deserialize(cls, data: dict) -> "GeneralFitting": + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 2, 1) + return super().deserialize(data) + def output_def(self): return FittingOutputDef( [ @@ -246,4 +272,13 @@ def call( "bim,bmj->bij", np.transpose(gr, axes=(0, 2, 1)), out ) # (nframes * nloc, 3, 3) out = out.reshape(nframes, nloc, 3, 3) + if self.shift_diag: + bias = self.constant_matrix[atype] + # (nframes, nloc, 1) + bias = np.expand_dims(bias, axis=-1) * self.scale[atype] + eye = np.eye(3) + eye = np.tile(eye, (nframes, nloc, 1, 1)) + # (nframes, nloc, 3, 3) + bias = np.expand_dims(bias, axis=-1) * eye + out = out + bias return {self.var_name: out} diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py index 21372888d6..b8892c2d95 100644 --- a/deepmd/pt/model/task/dipole.py +++ b/deepmd/pt/model/task/dipole.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy import logging from typing import ( Callable, @@ -25,6 +26,9 @@ from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) log = logging.getLogger(__name__) @@ -123,6 +127,12 @@ def serialize(self) -> dict: data["c_differentiable"] = self.c_differentiable return data + @classmethod + def deserialize(cls, data: dict) -> "GeneralFitting": + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) + return super().deserialize(data) + def output_def(self) -> FittingOutputDef: return FittingOutputDef( [ diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index b593ddc3cc..55ffd8c650 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -36,6 +36,9 @@ from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) dtype = env.GLOBAL_PT_FLOAT_PRECISION device = env.DEVICE @@ -140,6 +143,12 @@ def serialize(self) -> dict: data["atom_ener"] = self.atom_ener return data + @classmethod + def deserialize(cls, data: dict) -> "GeneralFitting": + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) + return super().deserialize(data) + def compute_output_stats( self, merged: Union[Callable[[], List[dict]], List[dict]], @@ -241,6 +250,7 @@ def __init__( @classmethod def deserialize(cls, data: dict) -> "GeneralFitting": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("var_name") data.pop("dim_out") return super().deserialize(data) diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 09f8563bfb..48ffe34084 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -49,9 +49,6 @@ from deepmd.utils.finetune import ( change_energy_bias_lower, ) -from deepmd.utils.version import ( - check_version_compatibility, -) dtype = env.GLOBAL_PT_FLOAT_PRECISION device = env.DEVICE @@ -371,7 +368,6 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "GeneralFitting": data = copy.deepcopy(data) - check_version_compatibility(data.pop("@version", 1), 1, 1) variables = data.pop("@variables") nets = data.pop("nets") obj = cls(**data) diff --git a/deepmd/pt/model/task/polarizability.py b/deepmd/pt/model/task/polarizability.py index fa4f6d7f37..eb6ccc2b7d 100644 --- a/deepmd/pt/model/task/polarizability.py +++ b/deepmd/pt/model/task/polarizability.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy import logging from typing import ( Callable, @@ -7,6 +8,7 @@ Union, ) +import numpy as np import torch from deepmd.dpmodel import ( @@ -25,9 +27,16 @@ from deepmd.pt.utils.utils import ( to_numpy_array, ) +from deepmd.utils.out_stat import ( + compute_stats_from_atomic, + compute_stats_from_redu, +) from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) log = logging.getLogger(__name__) @@ -114,6 +123,9 @@ def __init__( self.scale, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE ).view(ntypes, 1) self.shift_diag = shift_diag + self.constant_matrix = torch.zeros( + ntypes, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE + ) super().__init__( var_name=kwargs.pop("var_name", "polar"), ntypes=ntypes, @@ -140,16 +152,36 @@ def _net_out_dim(self): else self.embedding_width * self.embedding_width ) + def __setitem__(self, key, value): + if key in ["constant_matrix"]: + self.constant_matrix = value + else: + super().__setitem__(key, value) + + def __getitem__(self, key): + if key in ["constant_matrix"]: + return self.constant_matrix + else: + return super().__getitem__(key) + def serialize(self) -> dict: data = super().serialize() data["type"] = "polar" + data["@version"] = 2 data["embedding_width"] = self.embedding_width data["old_impl"] = self.old_impl data["fit_diag"] = self.fit_diag - data["fit_diag"] = self.fit_diag + data["shift_diag"] = self.shift_diag data["@variables"]["scale"] = to_numpy_array(self.scale) + data["@variables"]["constant_matrix"] = to_numpy_array(self.constant_matrix) return data + @classmethod + def deserialize(cls, data: dict) -> "GeneralFitting": + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 2, 1) + return super().deserialize(data) + def output_def(self) -> FittingOutputDef: return FittingOutputDef( [ @@ -167,7 +199,7 @@ def compute_output_stats( self, merged: Union[Callable[[], List[dict]], List[dict]], stat_file_path: Optional[DPPath] = None, - ): + ) -> None: """ Compute the output statistics (e.g. energy bias) for the fitting net from packed data. @@ -184,7 +216,60 @@ def compute_output_stats( The path to the stat file. """ - pass + if self.shift_diag: + if stat_file_path is not None: + stat_file_path = stat_file_path / "constant_matrix" + if stat_file_path is not None and stat_file_path.is_file(): + constant_matrix = stat_file_path.load_numpy() + else: + if callable(merged): + # only get data for once + sampled = merged() + else: + sampled = merged + + sys_constant_matrix = [] + for sys in range(len(sampled)): + nframs = sampled[sys]["type"].shape[0] + + if sampled[sys]["find_atomic_polarizability"] > 0.0: + sys_atom_polar = compute_stats_from_atomic( + sampled[sys]["atomic_polarizability"].numpy(force=True), + sampled[sys]["type"].numpy(force=True), + )[0] + else: + if not sampled[sys]["find_polarizability"] > 0.0: + continue + sys_type_count = np.zeros( + (nframs, self.ntypes), dtype=env.GLOBAL_NP_FLOAT_PRECISION + ) + for itype in range(self.ntypes): + type_mask = sampled[sys]["type"] == itype + sys_type_count[:, itype] = type_mask.sum(dim=1).numpy( + force=True + ) + + sys_bias_redu = sampled[sys]["polarizability"].numpy(force=True) + + sys_atom_polar = compute_stats_from_redu( + sys_bias_redu, sys_type_count, rcond=self.rcond + )[0] + cur_constant_matrix = np.zeros( + self.ntypes, dtype=env.GLOBAL_NP_FLOAT_PRECISION + ) + + for itype in range(self.ntypes): + cur_constant_matrix[itype] = np.mean( + np.diagonal(sys_atom_polar[itype].reshape(3, 3)) + ) + sys_constant_matrix.append(cur_constant_matrix) + constant_matrix = np.stack(sys_constant_matrix).mean(axis=0) + + # handle nan values. + constant_matrix = np.nan_to_num(constant_matrix) + if stat_file_path is not None: + stat_file_path.save_numpy(constant_matrix) + self.constant_matrix = torch.tensor(constant_matrix, device=env.DEVICE) def forward( self, @@ -218,5 +303,16 @@ def forward( "bim,bmj->bij", gr.transpose(1, 2), out ) # (nframes * nloc, 3, 3) out = out.view(nframes, nloc, 3, 3) + if self.shift_diag: + bias = self.constant_matrix[atype] + + # (nframes, nloc, 1) + bias = bias.unsqueeze(-1) * self.scale[atype] + + eye = torch.eye(3, device=env.DEVICE) + eye = eye.repeat(nframes, nloc, 1, 1) + # (nframes, nloc, 3, 3) + bias = bias.unsqueeze(-1) * eye + out = out + bias return {self.var_name: out.to(env.GLOBAL_PT_FLOAT_PRECISION)} diff --git a/deepmd/tf/fit/polar.py b/deepmd/tf/fit/polar.py index 7ac31809f3..41ea989521 100644 --- a/deepmd/tf/fit/polar.py +++ b/deepmd/tf/fit/polar.py @@ -183,6 +183,7 @@ def compute_output_stats(self, all_stat): mean_polar = np.zeros([len(self.sel_type), 9]) sys_matrix, polar_bias = [], [] for ss in range(len(all_stat["type"])): + nframes = all_stat["type"][ss].shape[0] atom_has_polar = [ w for w in all_stat["type"][ss][0] if (w in self.sel_type) ] # select atom with polar @@ -193,7 +194,7 @@ def compute_output_stats(self, all_stat): index_lis = [ index for index, w in enumerate(atom_has_polar) - if atom_has_polar[index] == self.sel_type[itype] + if w == self.sel_type[itype] ] # select index in this type sys_matrix.append(np.zeros((1, len(self.sel_type)))) @@ -201,10 +202,9 @@ def compute_output_stats(self, all_stat): polar_bias.append( np.sum( - all_stat["atomic_polarizability"][ss].reshape((-1, 9))[ - index_lis - ], - axis=0, + all_stat["atomic_polarizability"][ss][:, index_lis, :] + / nframes, + axis=(0, 1), ).reshape((1, 9)) ) else: # No atomic polar in this system, so it should have global polar @@ -228,7 +228,9 @@ def compute_output_stats(self, all_stat): sys_matrix[-1][0, itype] = len(index_lis) # add polar_bias - polar_bias.append(all_stat["polarizability"][ss].reshape((1, 9))) + polar_bias.append( + np.mean(all_stat["polarizability"][ss], axis=0).reshape((1, 9)) + ) matrix, bias = ( np.concatenate(sys_matrix, axis=0), @@ -584,7 +586,9 @@ def deserialize(cls, data: dict, suffix: str): The deserialized model """ data = data.copy() - check_version_compatibility(data.pop("@version", 1), 1, 1) + check_version_compatibility( + data.pop("@version", 1), 2, 1 + ) # to allow PT version. fitting = cls(**data) fitting.fitting_net_variables = cls.deserialize_network( data["nets"], diff --git a/source/tests/consistent/common.py b/source/tests/consistent/common.py index 622e2ed3cf..5a35ced0a1 100644 --- a/source/tests/consistent/common.py +++ b/source/tests/consistent/common.py @@ -257,6 +257,11 @@ def test_tf_consistent_with_ref(self): common_keys = set(data1.keys()) & set(data2.keys()) data1 = {k: data1[k] for k in common_keys} data2 = {k: data2[k] for k in common_keys} + + # not comparing version + data1.pop("@version") + data2.pop("@version") + np.testing.assert_equal(data1, data2) for rr1, rr2 in zip(ret1, ret2): np.testing.assert_allclose( diff --git a/source/tests/pt/model/test_polar_stat.py b/source/tests/pt/model/test_polar_stat.py new file mode 100644 index 0000000000..ca3b037011 --- /dev/null +++ b/source/tests/pt/model/test_polar_stat.py @@ -0,0 +1,75 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np +import torch + +from deepmd.pt.model.task.polarizability import ( + PolarFittingNet, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, +) +from deepmd.tf.fit.polar import ( + PolarFittingSeA, +) + + +class TestConsistency(unittest.TestCase): + def setUp(self) -> None: + types = torch.randint(0, 4, (1, 5), device=env.DEVICE) + types = torch.cat((types, types, types), dim=0) + types[:, -1] = 3 + ntypes = 4 + atomic_polarizability = torch.rand((3, 5, 9), device=env.DEVICE) + polarizability = torch.rand((3, 9), device=env.DEVICE) + find_polarizability = torch.rand(1, device=env.DEVICE) + find_atomic_polarizability = torch.rand(1, device=env.DEVICE) + self.sampled = [ + { + "type": types, + "find_atomic_polarizability": find_atomic_polarizability, + "atomic_polarizability": atomic_polarizability, + "polarizability": polarizability, + "find_polarizability": find_polarizability, + } + ] + self.all_stat = { + k: [v.numpy(force=True)] for d in self.sampled for k, v in d.items() + } + self.tfpolar = PolarFittingSeA( + ntypes=ntypes, + dim_descrpt=1, + embedding_width=1, + sel_type=list(range(ntypes)), + ) + self.ptpolar = PolarFittingNet( + ntypes=ntypes, + dim_descrpt=1, + embedding_width=1, + ) + + def test_atomic_consistency(self): + self.tfpolar.compute_output_stats(self.all_stat) + tfbias = self.tfpolar.constant_matrix + self.ptpolar.compute_output_stats(self.sampled) + ptbias = self.ptpolar.constant_matrix + np.testing.assert_allclose(tfbias, to_numpy_array(ptbias)) + + def test_global_consistency(self): + self.sampled[0]["find_atomic_polarizability"] = -1 + self.sampled[0]["polarizability"] = self.sampled[0][ + "atomic_polarizability" + ].sum(dim=1) + self.all_stat["find_atomic_polarizability"] = [-1] + self.all_stat["polarizability"] = [ + self.all_stat["atomic_polarizability"][0].sum(axis=1) + ] + self.tfpolar.compute_output_stats(self.all_stat) + tfbias = self.tfpolar.constant_matrix + self.ptpolar.compute_output_stats(self.sampled) + ptbias = self.ptpolar.constant_matrix + np.testing.assert_allclose(tfbias, to_numpy_array(ptbias), rtol=1e-5, atol=1e-5) diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index db69a1bcea..d3b6bd67b5 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -293,6 +293,7 @@ def setUp(self): self.config["model"]["atom_exclude_types"] = [1] self.config["model"]["fitting_net"]["type"] = "polar" self.config["model"]["fitting_net"]["fit_diag"] = False + self.config["model"]["fitting_net"]["shift_diag"] = False self.config["training"]["numb_steps"] = 1 self.config["training"]["save_freq"] = 1 # can not set requires_grad false for all parameters, @@ -326,6 +327,7 @@ def setUp(self): self.config["model"]["atom_exclude_types"] = [1] self.config["model"]["fitting_net"]["type"] = "polar" self.config["model"]["fitting_net"]["fit_diag"] = False + self.config["model"]["fitting_net"]["shift_diag"] = False self.config["training"]["numb_steps"] = 1 self.config["training"]["save_freq"] = 1 # can not set requires_grad false for all parameters, @@ -359,6 +361,7 @@ def setUp(self): self.config["model"]["atom_exclude_types"] = [1] self.config["model"]["fitting_net"]["type"] = "polar" self.config["model"]["fitting_net"]["fit_diag"] = False + self.config["model"]["fitting_net"]["shift_diag"] = False self.config["training"]["numb_steps"] = 1 self.config["training"]["save_freq"] = 1 # can not set requires_grad false for all parameters, From 804848a5b33ca116d9db5bbad5104b9dd19eee0f Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 11 Mar 2024 07:18:17 -0400 Subject: [PATCH 207/686] fix: do not install tf-keras for cu11 (#3444) Signed-off-by: Jinzhe Zeng --- backend/find_tensorflow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/find_tensorflow.py b/backend/find_tensorflow.py index fb9e719600..4d63f3118d 100644 --- a/backend/find_tensorflow.py +++ b/backend/find_tensorflow.py @@ -83,6 +83,7 @@ def find_tensorflow() -> Tuple[Optional[str], List[str]]: # TypeError if submodule_search_locations are None # IndexError if submodule_search_locations is an empty list except (AttributeError, TypeError, IndexError): + tf_version = "" if os.environ.get("CIBUILDWHEEL", "0") == "1": cuda_version = os.environ.get("CUDA_VERSION", "12.2") if cuda_version == "" or cuda_version in SpecifierSet(">=12,<13"): @@ -99,9 +100,10 @@ def find_tensorflow() -> Tuple[Optional[str], List[str]]: "tensorflow-cpu>=2.5.0rc0,<2.15; platform_machine=='x86_64' and platform_system == 'Linux'", ] ) + tf_version = "2.14.1" else: raise RuntimeError("Unsupported CUDA version") - requires.extend(get_tf_requirement()["cpu"]) + requires.extend(get_tf_requirement(tf_version)["cpu"]) # setuptools will re-find tensorflow after installing setup_requires tf_install_dir = None return tf_install_dir, requires From b54488513447f8767293444c0fa7bc884607fb74 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 11 Mar 2024 11:05:09 -0400 Subject: [PATCH 208/686] pt: make jit happy with torch 2.0.0 (#3443) Signed-off-by: Jinzhe Zeng Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- deepmd/pt/model/task/dipole.py | 3 +++ deepmd/pt/model/task/ener.py | 6 ++++++ deepmd/pt/model/task/polarizability.py | 3 +++ 3 files changed, 12 insertions(+) diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py index b8892c2d95..ca445c8588 100644 --- a/deepmd/pt/model/task/dipole.py +++ b/deepmd/pt/model/task/dipole.py @@ -192,3 +192,6 @@ def forward( # (nframes, nloc, 3) out = torch.bmm(out, gr).squeeze(-2).view(nframes, nloc, 3) return {self.var_name: out.to(env.GLOBAL_PT_FLOAT_PRECISION)} + + # make jit happy with torch 2.0.0 + exclude_types: List[int] diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 55ffd8c650..b58b0c9b19 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -214,6 +214,9 @@ def forward( """ return self._forward_common(descriptor, atype, gr, g2, h2, fparam, aparam) + # make jit happy with torch 2.0.0 + exclude_types: List[int] + @Fitting.register("ener") class EnergyFittingNet(InvarFitting): @@ -262,6 +265,9 @@ def serialize(self) -> dict: "type": "ener", } + # make jit happy with torch 2.0.0 + exclude_types: List[int] + @Fitting.register("direct_force") @Fitting.register("direct_force_ener") diff --git a/deepmd/pt/model/task/polarizability.py b/deepmd/pt/model/task/polarizability.py index eb6ccc2b7d..d7428c4d53 100644 --- a/deepmd/pt/model/task/polarizability.py +++ b/deepmd/pt/model/task/polarizability.py @@ -316,3 +316,6 @@ def forward( out = out + bias return {self.var_name: out.to(env.GLOBAL_PT_FLOAT_PRECISION)} + + # make jit happy with torch 2.0.0 + exclude_types: List[int] From 619fd1cb0d4c155591a8ca362f22389caf885853 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Mon, 11 Mar 2024 23:19:39 +0800 Subject: [PATCH 209/686] Fix: ZBL null test (#3447) This PR should address #3392 . --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- deepmd/pt/model/atomic_model/linear_atomic_model.py | 2 +- source/tests/pt/model/test_null_input.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 68705049ae..66d19c0a02 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -486,7 +486,7 @@ def _compute_weight( dim=-1, ) # handle masked nnei. - sigma = numerator / denominator # nfrmes, nloc + sigma = numerator / torch.clamp(denominator, 1e-20) # nfrmes, nloc u = (sigma - self.sw_rmin) / (self.sw_rmax - self.sw_rmin) coef = torch.zeros_like(u) left_mask = sigma < self.sw_rmin diff --git a/source/tests/pt/model/test_null_input.py b/source/tests/pt/model/test_null_input.py index c8f4307d52..d5cf2475fb 100644 --- a/source/tests/pt/model/test_null_input.py +++ b/source/tests/pt/model/test_null_input.py @@ -125,7 +125,6 @@ def setUp(self): self.model = get_model(model_params).to(env.DEVICE) -@unittest.skip("FAILED at the moment") class TestEnergyModelZBL(unittest.TestCase, NullTest): def setUp(self): model_params = copy.deepcopy(model_zbl) From 24d02b7bccf711a9d52982a56df64b57ecb5ca5e Mon Sep 17 00:00:00 2001 From: Lysithea <52808607+CaRoLZhangxy@users.noreply.github.com> Date: Mon, 11 Mar 2024 23:25:04 +0800 Subject: [PATCH 210/686] pt: Add parallel implementation for LKF (#3436) Add parallel implementation for LKF, enabling distributed storage of the P matrix across multiple GPUs, reducing memory overhead. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- deepmd/pt/optimizer/LKF.py | 167 +++++++++++++++++++++++++++++-------- 1 file changed, 132 insertions(+), 35 deletions(-) diff --git a/deepmd/pt/optimizer/LKF.py b/deepmd/pt/optimizer/LKF.py index ebc9242d49..06b341d987 100644 --- a/deepmd/pt/optimizer/LKF.py +++ b/deepmd/pt/optimizer/LKF.py @@ -3,11 +3,25 @@ import math import torch +import torch.distributed as dist from torch.optim.optimizer import ( Optimizer, ) -log = logging.getLogger(__name__) + +def distribute_indices(total_length, num_workers): + indices_per_worker = total_length // num_workers + remainder = total_length % num_workers + + indices = [] + start = 0 + + for i in range(num_workers): + end = start + indices_per_worker + (1 if i < remainder else 0) + indices.append((start, end)) + start = end + + return indices, remainder class LKFOptimizer(Optimizer): @@ -18,11 +32,8 @@ def __init__( kalman_nue=0.9987, block_size=5120, ): - defaults = { - "lr": 0.1, - "kalman_nue": kalman_nue, - "block_size": block_size, - } + defaults = {"lr": 0.1, "kalman_nue": kalman_nue, "block_size": block_size} + super().__init__(params, defaults) self._params = self.param_groups[0]["params"] @@ -36,7 +47,10 @@ def __init__( # the first param, because this helps with casting in load_state_dict self._state = self.state[self._params[0]] self._state.setdefault("kalman_lambda", kalman_lambda) - + self.dist_init = dist.is_initialized() + self.rank = dist.get_rank() if self.dist_init else 0 + self.dindex = [] + self.remainder = 0 self.__init_P() def __init_P(self): @@ -61,32 +75,84 @@ def __init_P(self): P = [] params_packed_index = [] - log.info("LKF parameter nums: %s" % param_nums) - for param_num in param_nums: - if param_num >= block_size: - block_num = math.ceil(param_num / block_size) - for i in range(block_num): - if i != block_num - 1: + logging.info("LKF parameter nums: %s" % param_nums) + if self.dist_init: + block_num = 0 + for param_num in param_nums: + if param_num >= block_size: + block_num += math.ceil(param_num / block_size) + else: + block_num += 1 + num_workers = dist.get_world_size() + self.dindex, self.remainder = distribute_indices(block_num, num_workers) + index = 0 + for param_num in param_nums: + if param_num >= block_size: + block_num = math.ceil(param_num / block_size) + for i in range(block_num): + device_id = self.get_device_id(index) + index += 1 + dist_device = torch.device("cuda:" + str(device_id)) + if i != block_num - 1: + params_packed_index.append(block_size) + if self.rank == device_id: + P.append( + torch.eye( + block_size, + dtype=data_type, + device=dist_device, + ) + ) + else: + continue + else: + params_packed_index.append(param_num - block_size * i) + if self.rank == device_id: + P.append( + torch.eye( + param_num - block_size * i, + dtype=data_type, + device=dist_device, + ) + ) + else: + continue + + else: + device_id = self.get_device_id(index) + index += 1 + params_packed_index.append(param_num) + if self.rank == device_id: + dist_device = torch.device("cuda:" + str(device_id)) P.append( - torch.eye( - block_size, - dtype=data_type, - device=device, - ) + torch.eye(param_num, dtype=data_type, device=dist_device) ) - params_packed_index.append(block_size) - else: - P.append( - torch.eye( - param_num - block_size * i, - dtype=data_type, - device=device, + else: + for param_num in param_nums: + if param_num >= block_size: + block_num = math.ceil(param_num / block_size) + for i in range(block_num): + if i != block_num - 1: + P.append( + torch.eye( + block_size, + dtype=data_type, + device=device, + ) ) - ) - params_packed_index.append(param_num - block_size * i) - else: - P.append(torch.eye(param_num, dtype=data_type, device=device)) - params_packed_index.append(param_num) + params_packed_index.append(block_size) + else: + P.append( + torch.eye( + param_num - block_size * i, + dtype=data_type, + device=device, + ) + ) + params_packed_index.append(param_num - block_size * i) + else: + P.append(torch.eye(param_num, dtype=data_type, device=device)) + params_packed_index.append(param_num) self._state.setdefault("P", P) self._state.setdefault("weights_num", len(P)) @@ -125,16 +191,35 @@ def __update(self, H, error, weights): tmp = 0 for i in range(weights_num): tmp = tmp + (kalman_lambda + torch.matmul(torch.matmul(H[i].T, P[i]), H[i])) - + if self.dist_init: + dist.all_reduce(tmp, op=dist.ReduceOp.SUM) A = 1 / tmp - for i in range(weights_num): K = torch.matmul(P[i], H[i]) weights[i] = weights[i] + A * error * K P[i] = (1 / kalman_lambda) * (P[i] - A * torch.matmul(K, K.T)) - + if self.dist_init: + device = torch.device("cuda:" + str(self.rank)) + local_shape = [tensor.shape[0] for tensor in weights] + shape_list = [ + torch.zeros_like(torch.empty(1), dtype=torch.float64, device=device) + for _ in range(dist.get_world_size()) + ] + dist.all_gather_object(shape_list, local_shape) + weight_tensor = torch.cat(weights) + world_shape = [sum(inner_list) for inner_list in shape_list] + weight_list = [None] * len(world_shape) + for i in range(len(world_shape)): + weight_list[i] = torch.zeros( + world_shape[i], dtype=torch.float64, device=device + ) + dist.all_gather(weight_list, weight_tensor) + result = [] + for i in range(dist.get_world_size()): + result = result + list(torch.split(weight_list[i], shape_list[i])) + weights = result kalman_lambda = kalman_nue * kalman_lambda + 1 - kalman_nue self._state.update({"kalman_lambda": kalman_lambda}) @@ -215,9 +300,21 @@ def step(self, error): param_sum += nelement if param_sum == params_packed_index[param_index]: - H.append(res_grad) - weights.append(res) param_sum = 0 + if self.dist_init: + device_id = self.get_device_id(param_index) + if self.rank == device_id: + weights.append(res) + H.append(res_grad) + else: + weights.append(res) + H.append(res_grad) param_index += 1 self.__update(H, error, weights) + + def get_device_id(self, index): + for i, (start, end) in enumerate(self.dindex): + if start <= index < end: + return i + return None From 4c845142ff26d739dc562f80f4bd26695c86edbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:36:27 -0400 Subject: [PATCH 211/686] build(deps): bump softprops/action-gh-release from 1 to 2 (#3446) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.

Release notes

Sourced from softprops/action-gh-release's releases.

v2.0.0

  • update actions.yml declaration to node20 to address warnings
Changelog

Sourced from softprops/action-gh-release's changelog.

0.1.12

  • fix bug leading to empty strings subsituted for inputs users don't provide breaking api calls #144
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=softprops/action-gh-release&package-manager=github_actions&previous-version=1&new-version=2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- .github/workflows/package_c.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/package_c.yml b/.github/workflows/package_c.yml index 72977bd339..e11f773b3a 100644 --- a/.github/workflows/package_c.yml +++ b/.github/workflows/package_c.yml @@ -42,7 +42,7 @@ jobs: - name: Test C library run: ./source/install/docker_test_package_c.sh - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: files: ${{ matrix.filename }} From a88a213fe36a8307b1742da951ad9353b2eb3247 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Mon, 11 Mar 2024 16:21:26 -0400 Subject: [PATCH 212/686] refactor: split Model and AtomicModel (#3438) Model is not inherited from AtomicModel anymore. --------- Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../atomic_model/make_base_atomic_model.py | 12 +- deepmd/dpmodel/model/__init__.py | 2 + deepmd/dpmodel/model/dp_model.py | 3 +- deepmd/dpmodel/model/make_model.py | 113 ++++++++++++++- .../model/atomic_model/base_atomic_model.py | 31 +++- .../pt/model/atomic_model/dp_atomic_model.py | 4 - .../model/atomic_model/linear_atomic_model.py | 6 - .../atomic_model/pairtab_atomic_model.py | 6 - deepmd/pt/model/model/__init__.py | 6 +- deepmd/pt/model/model/dipole_model.py | 4 +- deepmd/pt/model/model/dp_model.py | 51 ++++++- deepmd/pt/model/model/dp_zbl_model.py | 2 +- deepmd/pt/model/model/ener_model.py | 4 +- deepmd/pt/model/model/make_model.py | 134 +++++++++++++++++- deepmd/pt/model/model/model.py | 57 +------- deepmd/pt/model/model/polar_model.py | 4 +- deepmd/pt/train/training.py | 9 +- deepmd/pt/train/wrapper.py | 10 +- .../pt/model/test_linear_atomic_model.py | 10 +- source/tests/pt/model/test_model.py | 10 +- .../pt/model/test_pairtab_atomic_model.py | 10 +- source/tests/pt/test_finetune.py | 18 +-- source/tests/pt/test_training.py | 4 +- 23 files changed, 375 insertions(+), 135 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py index ce1a6708e6..e3d6d8bcd1 100644 --- a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later from abc import ( ABC, - abstractclassmethod, abstractmethod, ) from typing import ( @@ -13,6 +12,10 @@ from deepmd.dpmodel.output_def import ( FittingOutputDef, ) +from deepmd.utils.plugin import ( + PluginVariant, + make_plugin_registry, +) def make_base_atomic_model( @@ -31,7 +34,7 @@ def make_base_atomic_model( """ - class BAM(ABC): + class BAM(ABC, PluginVariant, make_plugin_registry("atomic model")): """Base Atomic Model provides the interfaces of an atomic model.""" @abstractmethod @@ -128,8 +131,9 @@ def fwd( def serialize(self) -> dict: pass - @abstractclassmethod - def deserialize(cls): + @classmethod + @abstractmethod + def deserialize(cls, data: dict): pass def do_grad_r( diff --git a/deepmd/dpmodel/model/__init__.py b/deepmd/dpmodel/model/__init__.py index cb796e6d35..c1ff15ab0d 100644 --- a/deepmd/dpmodel/model/__init__.py +++ b/deepmd/dpmodel/model/__init__.py @@ -8,6 +8,8 @@ according to output variable definition `deepmd.dpmodel.OutputVariableDef`. +All models should be inherited from :class:`deepmd.dpmodel.model.base_model.BaseModel`. +Models generated by `make_model` have already done it. """ from .dp_model import ( diff --git a/deepmd/dpmodel/model/dp_model.py b/deepmd/dpmodel/model/dp_model.py index 15f9027d4c..8d84c435b4 100644 --- a/deepmd/dpmodel/model/dp_model.py +++ b/deepmd/dpmodel/model/dp_model.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later + from deepmd.dpmodel.atomic_model import ( DPAtomicModel, ) @@ -17,7 +18,7 @@ # use "class" to resolve "Variable not allowed in type expression" @BaseModel.register("standard") -class DPModel(make_model(DPAtomicModel), BaseModel): +class DPModel(make_model(DPAtomicModel)): @classmethod def update_sel(cls, global_jdata: dict, local_jdata: dict): """Update the selection and perform neighbor statistics. diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py index d1f671c8de..6022fd3e73 100644 --- a/deepmd/dpmodel/model/make_model.py +++ b/deepmd/dpmodel/model/make_model.py @@ -4,10 +4,14 @@ List, Optional, Tuple, + Type, ) import numpy as np +from deepmd.dpmodel.atomic_model.base_atomic_model import ( + BaseAtomicModel, +) from deepmd.dpmodel.common import ( GLOBAL_ENER_FLOAT_PRECISION, GLOBAL_NP_FLOAT_PRECISION, @@ -15,7 +19,11 @@ RESERVED_PRECISON_DICT, NativeOP, ) +from deepmd.dpmodel.model.base_model import ( + BaseModel, +) from deepmd.dpmodel.output_def import ( + FittingOutputDef, ModelOutputDef, OutputVariableCategory, OutputVariableOperation, @@ -34,7 +42,7 @@ ) -def make_model(T_AtomicModel): +def make_model(T_AtomicModel: Type[BaseAtomicModel]): """Make a model as a derived class of an atomic model. The model provide two interfaces. @@ -57,16 +65,18 @@ def make_model(T_AtomicModel): """ - class CM(T_AtomicModel, NativeOP): + class CM(NativeOP, BaseModel): def __init__( self, *args, + # underscore to prevent conflict with normal inputs + atomic_model_: Optional[T_AtomicModel] = None, **kwargs, ): - super().__init__( - *args, - **kwargs, - ) + if atomic_model_ is not None: + self.atomic_model: T_AtomicModel = atomic_model_ + else: + self.atomic_model: T_AtomicModel = T_AtomicModel(*args, **kwargs) self.precision_dict = PRECISION_DICT self.reverse_precision_dict = RESERVED_PRECISON_DICT self.global_np_float_precision = GLOBAL_NP_FLOAT_PRECISION @@ -208,7 +218,7 @@ def call_lower( extended_coord, fparam=fparam, aparam=aparam ) del extended_coord, fparam, aparam - atomic_ret = self.forward_common_atomic( + atomic_ret = self.atomic_model.forward_common_atomic( cc_ext, extended_atype, nlist, @@ -377,4 +387,93 @@ def _format_nlist( assert ret.shape[-1] == nnei return ret + def do_grad_r( + self, + var_name: Optional[str] = None, + ) -> bool: + """Tell if the output variable `var_name` is r_differentiable. + if var_name is None, returns if any of the variable is r_differentiable. + """ + return self.atomic_model.do_grad_r(var_name) + + def do_grad_c( + self, + var_name: Optional[str] = None, + ) -> bool: + """Tell if the output variable `var_name` is c_differentiable. + if var_name is None, returns if any of the variable is c_differentiable. + """ + return self.atomic_model.do_grad_c(var_name) + + def serialize(self) -> dict: + return self.atomic_model.serialize() + + @classmethod + def deserialize(cls, data) -> "CM": + return cls(atomic_model_=T_AtomicModel.deserialize(data)) + + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this atomic model.""" + return self.atomic_model.get_dim_fparam() + + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this atomic model.""" + return self.atomic_model.get_dim_aparam() + + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return self.atomic_model.get_sel_type() + + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + + If False, the shape is (nframes, nloc, ndim). + """ + return self.atomic_model.is_aparam_nall() + + def get_rcut(self) -> float: + """Get the cut-off radius.""" + return self.atomic_model.get_rcut() + + def get_type_map(self) -> List[str]: + """Get the type map.""" + return self.atomic_model.get_type_map() + + def get_nsel(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + return self.atomic_model.get_nsel() + + def get_nnei(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + return self.atomic_model.get_nnei() + + def get_model_def_script(self) -> str: + """Get the model definition script.""" + return self.atomic_model.get_model_def_script() + + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + return self.atomic_model.get_sel() + + def mixed_types(self) -> bool: + """If true, the model + 1. assumes total number of atoms aligned across frames; + 2. uses a neighbor list that does not distinguish different atomic types. + + If false, the model + 1. assumes total number of atoms of each atom type aligned across frames; + 2. uses a neighbor list that distinguishes different atomic types. + + """ + return self.atomic_model.mixed_types() + + def atomic_output_def(self) -> FittingOutputDef: + """Get the output def of the atomic model.""" + return self.atomic_model.atomic_output_def() + return CM diff --git a/deepmd/pt/model/atomic_model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py index 8180c48c81..d045220b6e 100644 --- a/deepmd/pt/model/atomic_model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -21,6 +21,9 @@ AtomExcludeMask, PairExcludeMask, ) +from deepmd.utils.path import ( + DPPath, +) BaseAtomicModel_ = make_base_atomic_model(torch.Tensor) @@ -55,12 +58,6 @@ def reinit_pair_exclude( else: self.pair_excl = PairExcludeMask(self.get_ntypes(), self.pair_exclude_types) - # export public methods that are not abstract - get_nsel = torch.jit.export(BaseAtomicModel_.get_nsel) - get_nnei = torch.jit.export(BaseAtomicModel_.get_nnei) - get_ntypes = torch.jit.export(BaseAtomicModel_.get_ntypes) - - @torch.jit.export def get_model_def_script(self) -> str: return self.model_def_script @@ -126,3 +123,25 @@ def serialize(self) -> dict: "atom_exclude_types": self.atom_exclude_types, "pair_exclude_types": self.pair_exclude_types, } + + def compute_or_load_stat( + self, + sampled_func, + stat_file_path: Optional[DPPath] = None, + ): + """ + Compute or load the statistics parameters of the model, + such as mean and standard deviation of descriptors or the energy bias of the fitting net. + When `sampled` is provided, all the statistics parameters will be calculated (or re-calculated for update), + and saved in the `stat_file_path`(s). + When `sampled` is not provided, it will check the existence of `stat_file_path`(s) + and load the calculated statistics parameters. + + Parameters + ---------- + sampled_func + The sampled data frames from different data systems. + stat_file_path + The path to the statistics files. + """ + raise NotImplementedError diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index cad1e1cc88..ec08850524 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -223,17 +223,14 @@ def wrapped_sampler(): if self.fitting_net is not None: self.fitting_net.compute_output_stats(wrapped_sampler, stat_file_path) - @torch.jit.export def get_dim_fparam(self) -> int: """Get the number (dimension) of frame parameters of this atomic model.""" return self.fitting_net.get_dim_fparam() - @torch.jit.export def get_dim_aparam(self) -> int: """Get the number (dimension) of atomic parameters of this atomic model.""" return self.fitting_net.get_dim_aparam() - @torch.jit.export def get_sel_type(self) -> List[int]: """Get the selected atom types of this model. @@ -243,7 +240,6 @@ def get_sel_type(self) -> List[int]: """ return self.fitting_net.get_sel_type() - @torch.jit.export def is_aparam_nall(self) -> bool: """Check whether the shape of atomic parameters is (nframes, nall, ndim). diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 66d19c0a02..3fb3ee90dd 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -96,12 +96,10 @@ def mixed_types(self) -> bool: """ return True - @torch.jit.export def get_rcut(self) -> float: """Get the cut-off radius.""" return max(self.get_model_rcuts()) - @torch.jit.export def get_type_map(self) -> List[str]: """Get the type map.""" return self.type_map @@ -292,18 +290,15 @@ def _compute_weight( """This should be a list of user defined weights that matches the number of models to be combined.""" raise NotImplementedError - @torch.jit.export def get_dim_fparam(self) -> int: """Get the number (dimension) of frame parameters of this atomic model.""" # tricky... return max([model.get_dim_fparam() for model in self.models]) - @torch.jit.export def get_dim_aparam(self) -> int: """Get the number (dimension) of atomic parameters of this atomic model.""" return max([model.get_dim_aparam() for model in self.models]) - @torch.jit.export def get_sel_type(self) -> List[int]: """Get the selected atom types of this model. @@ -324,7 +319,6 @@ def get_sel_type(self) -> List[int]: ) ).tolist() - @torch.jit.export def is_aparam_nall(self) -> bool: """Check whether the shape of atomic parameters is (nframes, nall, ndim). diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index 19a67fc8ff..db0a2efa4a 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -139,11 +139,9 @@ def fitting_output_def(self) -> FittingOutputDef: ] ) - @torch.jit.export def get_rcut(self) -> float: return self.rcut - @torch.jit.export def get_type_map(self) -> List[str]: return self.type_map @@ -454,17 +452,14 @@ def _calculate_ener(coef: torch.Tensor, uu: torch.Tensor) -> torch.Tensor: ener = etmp * uu + a0 # this energy has the extrapolated value when rcut > rmax return ener - @torch.jit.export def get_dim_fparam(self) -> int: """Get the number (dimension) of frame parameters of this atomic model.""" return 0 - @torch.jit.export def get_dim_aparam(self) -> int: """Get the number (dimension) of atomic parameters of this atomic model.""" return 0 - @torch.jit.export def get_sel_type(self) -> List[int]: """Get the selected atom types of this model. @@ -474,7 +469,6 @@ def get_sel_type(self) -> List[int]: """ return [] - @torch.jit.export def is_aparam_nall(self) -> bool: """Check whether the shape of atomic parameters is (nframes, nall, ndim). diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index 8e4352e60c..3098dc7677 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -7,6 +7,8 @@ communication of the atomic properties according to output variable definition `deepmd.dpmodel.OutputVariableDef`. +All models should be inherited from :class:`deepmd.pt.model.model.model.BaseModel`. +Models generated by `make_model` have already done it. """ import copy @@ -147,8 +149,8 @@ def get_standard_model(model_params): pair_exclude_types = model_params.get("pair_exclude_types", []) model = DPModel( - descriptor, - fitting, + descriptor=descriptor, + fitting=fitting, type_map=model_params["type_map"], atom_exclude_types=atom_exclude_types, pair_exclude_types=pair_exclude_types, diff --git a/deepmd/pt/model/model/dipole_model.py b/deepmd/pt/model/model/dipole_model.py index 8b6f2c47c1..45b120771b 100644 --- a/deepmd/pt/model/model/dipole_model.py +++ b/deepmd/pt/model/model/dipole_model.py @@ -38,7 +38,7 @@ def forward( aparam=aparam, do_atomic_virial=do_atomic_virial, ) - if self.fitting_net is not None: + if self.get_fitting_net() is not None: model_predict = {} model_predict["dipole"] = model_ret["dipole"] model_predict["global_dipole"] = model_ret["dipole_redu"] @@ -77,7 +77,7 @@ def forward_lower( aparam=aparam, do_atomic_virial=do_atomic_virial, ) - if self.fitting_net is not None: + if self.get_fitting_net() is not None: model_predict = {} model_predict["dipole"] = model_ret["dipole"] model_predict["global_dipole"] = model_ret["dipole_redu"] diff --git a/deepmd/pt/model/model/dp_model.py b/deepmd/pt/model/model/dp_model.py index 0df45d4f84..138398539a 100644 --- a/deepmd/pt/model/model/dp_model.py +++ b/deepmd/pt/model/model/dp_model.py @@ -1,4 +1,11 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + Optional, +) + +import torch + from deepmd.pt.model.atomic_model import ( DPAtomicModel, ) @@ -25,8 +32,16 @@ @BaseModel.register("standard") -class DPModel(make_model(DPAtomicModel), BaseModel): - def __new__(cls, descriptor, fitting, *args, **kwargs): +class DPModel(make_model(DPAtomicModel)): + def __new__( + cls, + descriptor=None, + fitting=None, + *args, + # disallow positional atomic_model_ + atomic_model_: Optional[DPAtomicModel] = None, + **kwargs, + ): from deepmd.pt.model.model.dipole_model import ( DipoleModel, ) @@ -37,6 +52,11 @@ def __new__(cls, descriptor, fitting, *args, **kwargs): PolarModel, ) + if atomic_model_ is not None: + fitting = atomic_model_.fitting_net + else: + assert fitting is not None, "fitting network is not provided" + # according to the fitting network to decide the type of the model if cls is DPModel: # map fitting to model @@ -67,3 +87,30 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): global_jdata, local_jdata["descriptor"] ) return local_jdata_cpy + + def get_fitting_net(self): + """Get the fitting network.""" + return self.atomic_model.fitting_net + + def get_descriptor(self): + """Get the descriptor.""" + return self.atomic_model.descriptor + + def forward( + self, + coord, + atype, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + # directly call the forward_common method when no specific transform rule + return self.forward_common( + coord, + atype, + box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) diff --git a/deepmd/pt/model/model/dp_zbl_model.py b/deepmd/pt/model/model/dp_zbl_model.py index fdf9334119..bbc82b8d77 100644 --- a/deepmd/pt/model/model/dp_zbl_model.py +++ b/deepmd/pt/model/model/dp_zbl_model.py @@ -24,7 +24,7 @@ @BaseModel.register("zbl") -class DPZBLModel(DPZBLModel_, BaseModel): +class DPZBLModel(DPZBLModel_): model_type = "ener" def __init__( diff --git a/deepmd/pt/model/model/ener_model.py b/deepmd/pt/model/model/ener_model.py index cd4f78a2e2..5217293623 100644 --- a/deepmd/pt/model/model/ener_model.py +++ b/deepmd/pt/model/model/ener_model.py @@ -38,7 +38,7 @@ def forward( aparam=aparam, do_atomic_virial=do_atomic_virial, ) - if self.fitting_net is not None: + if self.get_fitting_net() is not None: model_predict = {} model_predict["atom_energy"] = model_ret["energy"] model_predict["energy"] = model_ret["energy_redu"] @@ -79,7 +79,7 @@ def forward_lower( aparam=aparam, do_atomic_virial=do_atomic_virial, ) - if self.fitting_net is not None: + if self.get_fitting_net() is not None: model_predict = {} model_predict["atom_energy"] = model_ret["energy"] model_predict["energy"] = model_ret["energy_redu"] diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index f9daa916a8..0a5f286040 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -4,6 +4,7 @@ List, Optional, Tuple, + Type, ) import torch @@ -12,10 +13,17 @@ ModelOutputDef, ) from deepmd.dpmodel.output_def import ( + FittingOutputDef, OutputVariableCategory, OutputVariableOperation, check_operation_applied, ) +from deepmd.pt.model.atomic_model.base_atomic_model import ( + BaseAtomicModel, +) +from deepmd.pt.model.model.model import ( + BaseModel, +) from deepmd.pt.model.model.transform_output import ( communicate_extended_output, fit_output_to_model_output, @@ -30,9 +38,12 @@ extend_input_and_build_neighbor_list, nlist_distinguish_types, ) +from deepmd.utils.path import ( + DPPath, +) -def make_model(T_AtomicModel): +def make_model(T_AtomicModel: Type[BaseAtomicModel]): """Make a model as a derived class of an atomic model. The model provide two interfaces. @@ -55,16 +66,19 @@ def make_model(T_AtomicModel): """ - class CM(T_AtomicModel): + class CM(BaseModel): def __init__( self, *args, + # underscore to prevent conflict with normal inputs + atomic_model_: Optional[T_AtomicModel] = None, **kwargs, ): - super().__init__( - *args, - **kwargs, - ) + super().__init__(*args, **kwargs) + if atomic_model_ is not None: + self.atomic_model: T_AtomicModel = atomic_model_ + else: + self.atomic_model: T_AtomicModel = T_AtomicModel(*args, **kwargs) self.precision_dict = PRECISION_DICT self.reverse_precision_dict = RESERVED_PRECISON_DICT self.global_pt_float_precision = GLOBAL_PT_FLOAT_PRECISION @@ -203,7 +217,7 @@ def forward_common_lower( extended_coord, fparam=fparam, aparam=aparam ) del extended_coord, fparam, aparam - atomic_ret = self.forward_common_atomic( + atomic_ret = self.atomic_model.forward_common_atomic( cc_ext, extended_atype, nlist, @@ -382,4 +396,110 @@ def _format_nlist( assert nlist.shape[-1] == nnei return nlist + def do_grad_r( + self, + var_name: Optional[str] = None, + ) -> bool: + """Tell if the output variable `var_name` is r_differentiable. + if var_name is None, returns if any of the variable is r_differentiable. + """ + return self.atomic_model.do_grad_r(var_name) + + def do_grad_c( + self, + var_name: Optional[str] = None, + ) -> bool: + """Tell if the output variable `var_name` is c_differentiable. + if var_name is None, returns if any of the variable is c_differentiable. + """ + return self.atomic_model.do_grad_c(var_name) + + def serialize(self) -> dict: + return self.atomic_model.serialize() + + @classmethod + def deserialize(cls, data) -> "CM": + return cls(atomic_model_=T_AtomicModel.deserialize(data)) + + @torch.jit.export + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this atomic model.""" + return self.atomic_model.get_dim_fparam() + + @torch.jit.export + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this atomic model.""" + return self.atomic_model.get_dim_aparam() + + @torch.jit.export + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return self.atomic_model.get_sel_type() + + @torch.jit.export + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + + If False, the shape is (nframes, nloc, ndim). + """ + return self.atomic_model.is_aparam_nall() + + @torch.jit.export + def get_rcut(self) -> float: + """Get the cut-off radius.""" + return self.atomic_model.get_rcut() + + @torch.jit.export + def get_type_map(self) -> List[str]: + """Get the type map.""" + return self.atomic_model.get_type_map() + + @torch.jit.export + def get_nsel(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + return self.atomic_model.get_nsel() + + @torch.jit.export + def get_nnei(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + return self.atomic_model.get_nnei() + + @torch.jit.export + def get_model_def_script(self) -> str: + """Get the model definition script.""" + return self.atomic_model.get_model_def_script() + + def atomic_output_def(self) -> FittingOutputDef: + """Get the output def of the atomic model.""" + return self.atomic_model.atomic_output_def() + + def compute_or_load_stat( + self, + sampled_func, + stat_file_path: Optional[DPPath] = None, + ): + """Compute or load the statistics.""" + return self.atomic_model.compute_or_load_stat(sampled_func, stat_file_path) + + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + return self.atomic_model.get_sel() + + def mixed_types(self) -> bool: + """If true, the model + 1. assumes total number of atoms aligned across frames; + 2. uses a neighbor list that does not distinguish different atomic types. + + If false, the model + 1. assumes total number of atoms of each atom type aligned across frames; + 2. uses a neighbor list that distinguishes different atomic types. + + """ + return self.atomic_model.mixed_types() + return CM diff --git a/deepmd/pt/model/model/model.py b/deepmd/pt/model/model/model.py index e32d2f307d..3d4618449a 100644 --- a/deepmd/pt/model/model/model.py +++ b/deepmd/pt/model/model/model.py @@ -3,6 +3,8 @@ Optional, ) +import torch + from deepmd.dpmodel.model.base_model import ( make_base_model, ) @@ -11,61 +13,14 @@ ) -# trick: torch.nn.Module should not be inherbited here, otherwise, -# the abstract method will override the method from the atomic model -# as Python resolves method lookups using the C3 linearisation. -# See https://stackoverflow.com/a/47117600/9567349 -# Take an example, this is the situation for only inheriting make_model(): -# torch.nn.Module BaseAtomicModel make_model() -# | | | -# ------------------------- | -# | | -# DPAtomicModel BaseModel -# | | -# make_model(DPAtomicModel) | -# | | -# ---------------------------------- -# | -# DPModel -# -# The order is: DPModel -> make_model(DPAtomicModel) -> DPAtomicModel -> -# torch.nn.Module -> BaseAtomicModel -> BaseModel -> make_model() -# -# However, if BaseModel also inherbits from torch.nn.Module: -# torch.nn.Module make_model() -# | | -# |--------------------------- | -# | | | -# | BaseAtomicModel | | -# | | | | -# |------------- ---------- -# | | -# DPAtomicModel BaseModel -# | | -# | | -# make_model(DPAtomicModel) | -# | | -# | | -# -------------------------------- -# | -# | -# DPModel -# -# The order is DPModel -> make_model(DPAtomicModel) -> DPAtomicModel -> -# BaseModel -> torch.nn.Module -> BaseAtomicModel -> make_model() -# BaseModel has higher proirity than BaseAtomicModel, which is not what -# we want. -# Alternatively, we can also make BaseAtomicModel in front of torch.nn.Module -# in DPAtomicModel (and other classes), but this requires the developer aware -# of it when developing it... -class BaseModel(make_base_model()): +class BaseModel(torch.nn.Module, make_base_model()): def __init__(self, *args, **kwargs): """Construct a basic model for different tasks.""" - super().__init__(*args, **kwargs) + torch.nn.Module.__init__(self) def compute_or_load_stat( self, - sampled, + sampled_func, stat_file_path: Optional[DPPath] = None, ): """ @@ -78,7 +33,7 @@ def compute_or_load_stat( Parameters ---------- - sampled + sampled_func The sampled data frames from different data systems. stat_file_path The path to the statistics files. diff --git a/deepmd/pt/model/model/polar_model.py b/deepmd/pt/model/model/polar_model.py index bf430c6706..403058aa47 100644 --- a/deepmd/pt/model/model/polar_model.py +++ b/deepmd/pt/model/model/polar_model.py @@ -38,7 +38,7 @@ def forward( aparam=aparam, do_atomic_virial=do_atomic_virial, ) - if self.fitting_net is not None: + if self.get_fitting_net() is not None: model_predict = {} model_predict["polar"] = model_ret["polar"] model_predict["global_polar"] = model_ret["polar_redu"] @@ -69,7 +69,7 @@ def forward_lower( aparam=aparam, do_atomic_virial=do_atomic_virial, ) - if self.fitting_net is not None: + if self.get_fitting_net() is not None: model_predict = {} model_predict["polar"] = model_ret["polar"] model_predict["global_polar"] = model_ret["polar_redu"] diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index fb28f0c4f2..b20d80c629 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -520,8 +520,11 @@ def get_loss(loss_params, start_lr, _ntypes, _model): model_params["type_map"], model_params["new_type_map"], ) - if hasattr(self.model, "fitting_net"): - self.model.fitting_net.change_energy_bias( + # TODO: need an interface instead of fetching fitting_net!!!!!!!!! + if hasattr(self.model, "atomic_model") and hasattr( + self.model.atomic_model, "fitting_net" + ): + self.model.atomic_model.fitting_net.change_energy_bias( config, self.model, old_type_map, @@ -531,7 +534,7 @@ def get_loss(loss_params, start_lr, _ntypes, _model): ) elif isinstance(self.model, DPZBLModel): # need to updated - self.model.change_energy_bias() + self.model.atomic_model.change_energy_bias() else: raise NotImplementedError if init_frz_model is not None: diff --git a/deepmd/pt/train/wrapper.py b/deepmd/pt/train/wrapper.py index c1040fb9e3..061cd777db 100644 --- a/deepmd/pt/train/wrapper.py +++ b/deepmd/pt/train/wrapper.py @@ -75,12 +75,12 @@ def share_params(self, shared_links, resume=False): shared_level_base = shared_base["shared_level"] if "descriptor" in class_type_base: if class_type_base == "descriptor": - base_class = self.model[model_key_base].__getattr__("descriptor") + base_class = self.model[model_key_base].get_descriptor() elif "hybrid" in class_type_base: hybrid_index = int(class_type_base.split("_")[-1]) base_class = ( self.model[model_key_base] - .__getattr__("descriptor") + .get_descriptor() .descriptor_list[hybrid_index] ) else: @@ -96,14 +96,12 @@ def share_params(self, shared_links, resume=False): "descriptor" in class_type_link ), f"Class type mismatched: {class_type_base} vs {class_type_link}!" if class_type_link == "descriptor": - link_class = self.model[model_key_link].__getattr__( - "descriptor" - ) + link_class = self.model[model_key_link].get_descriptor() elif "hybrid" in class_type_link: hybrid_index = int(class_type_link.split("_")[-1]) link_class = ( self.model[model_key_link] - .__getattr__("descriptor") + .get_descriptor() .descriptor_list[hybrid_index] ) else: diff --git a/source/tests/pt/model/test_linear_atomic_model.py b/source/tests/pt/model/test_linear_atomic_model.py index adc682a41f..7f24ffdc53 100644 --- a/source/tests/pt/model/test_linear_atomic_model.py +++ b/source/tests/pt/model/test_linear_atomic_model.py @@ -178,11 +178,13 @@ def test_self_consistency(self): def test_jit(self): md1 = torch.jit.script(self.md1) - self.assertEqual(md1.get_rcut(), self.rcut) - self.assertEqual(md1.get_type_map(), ["foo", "bar"]) + # atomic model no more export methods + # self.assertEqual(md1.get_rcut(), self.rcut) + # self.assertEqual(md1.get_type_map(), ["foo", "bar"]) md3 = torch.jit.script(self.md3) - self.assertEqual(md3.get_rcut(), self.rcut) - self.assertEqual(md3.get_type_map(), ["foo", "bar"]) + # atomic model no more export methods + # self.assertEqual(md3.get_rcut(), self.rcut) + # self.assertEqual(md3.get_type_map(), ["foo", "bar"]) class TestRemmapMethod(unittest.TestCase): diff --git a/source/tests/pt/model/test_model.py b/source/tests/pt/model/test_model.py index f42c11aa4c..5a30de7ac8 100644 --- a/source/tests/pt/model/test_model.py +++ b/source/tests/pt/model/test_model.py @@ -60,13 +60,13 @@ def torch2tf(torch_name, last_layer_id=None): fields = torch_name.split(".") - offset = int(fields[2] == "networks") + offset = int(fields[3] == "networks") + 1 element_id = int(fields[2 + offset]) - if fields[0] == "descriptor": + if fields[1] == "descriptor": layer_id = int(fields[4 + offset]) + 1 weight_type = fields[5 + offset] ret = "filter_type_all/%s_%d_%d:0" % (weight_type, layer_id, element_id) - elif fields[0] == "fitting_net": + elif fields[1] == "fitting_net": layer_id = int(fields[4 + offset]) weight_type = fields[5 + offset] if layer_id != last_layer_id: @@ -301,7 +301,7 @@ def test_consistency(self): ) # Keep statistics consistency between 2 implentations - my_em = my_model.descriptor + my_em = my_model.get_descriptor() mean = stat_dict["descriptor.mean"].reshape([self.ntypes, my_em.get_nsel(), 4]) stddev = stat_dict["descriptor.stddev"].reshape( [self.ntypes, my_em.get_nsel(), 4] @@ -310,7 +310,7 @@ def test_consistency(self): torch.tensor(mean, device=DEVICE), torch.tensor(stddev, device=DEVICE), ) - my_model.fitting_net.bias_atom_e = torch.tensor( + my_model.get_fitting_net().bias_atom_e = torch.tensor( stat_dict["fitting_net.bias_atom_e"], device=DEVICE ) diff --git a/source/tests/pt/model/test_pairtab_atomic_model.py b/source/tests/pt/model/test_pairtab_atomic_model.py index 322de51a2c..165e3dead7 100644 --- a/source/tests/pt/model/test_pairtab_atomic_model.py +++ b/source/tests/pt/model/test_pairtab_atomic_model.py @@ -98,8 +98,9 @@ def test_with_mask(self): def test_jit(self): model = torch.jit.script(self.model) - self.assertEqual(model.get_rcut(), 0.02) - self.assertEqual(model.get_type_map(), ["H", "O"]) + # atomic model no more export methods + # self.assertEqual(model.get_rcut(), 0.02) + # self.assertEqual(model.get_type_map(), ["H", "O"]) def test_deserialize(self): model1 = PairTabAtomicModel.deserialize(self.model.serialize()) @@ -121,8 +122,9 @@ def test_deserialize(self): ) model1 = torch.jit.script(model1) - self.assertEqual(model1.get_rcut(), 0.02) - self.assertEqual(model1.get_type_map(), ["H", "O"]) + # atomic model no more export methods + # self.assertEqual(model1.get_rcut(), 0.02) + # self.assertEqual(model1.get_type_map(), ["H", "O"]) def test_cross_deserialize(self): model_dict = self.model.serialize() # pytorch model to dict diff --git a/source/tests/pt/test_finetune.py b/source/tests/pt/test_finetune.py index d21a44acc7..dd72eb4718 100644 --- a/source/tests/pt/test_finetune.py +++ b/source/tests/pt/test_finetune.py @@ -44,27 +44,29 @@ def test_finetune_change_energy_bias(self): else: model = get_model(self.model_config) if isinstance(model, EnergyModel): - model.fitting_net.bias_atom_e = torch.rand_like( - model.fitting_net.bias_atom_e + model.get_fitting_net().bias_atom_e = torch.rand_like( + model.get_fitting_net().bias_atom_e ) energy_bias_before = deepcopy( - model.fitting_net.bias_atom_e.detach().cpu().numpy().reshape(-1) + model.get_fitting_net().bias_atom_e.detach().cpu().numpy().reshape(-1) ) bias_atom_e_input = deepcopy( - model.fitting_net.bias_atom_e.detach().cpu().numpy().reshape(-1) + model.get_fitting_net().bias_atom_e.detach().cpu().numpy().reshape(-1) ) elif isinstance(model, DPZBLModel): - model.dp_model.fitting_net.bias_atom_e = torch.rand_like( - model.dp_model.fitting_net.bias_atom_e + model.dp_model.get_fitting_net().bias_atom_e = torch.rand_like( + model.dp_model.get_fitting_net().bias_atom_e ) energy_bias_before = deepcopy( - model.dp_model.fitting_net.bias_atom_e.detach() + model.dp_model.get_fitting_net() + .bias_atom_e.detach() .cpu() .numpy() .reshape(-1) ) bias_atom_e_input = deepcopy( - model.dp_model.fitting_net.bias_atom_e.detach() + model.dp_model.get_fitting_net() + .bias_atom_e.detach() .cpu() .numpy() .reshape(-1) diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index d3b6bd67b5..76055c6f4a 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -52,11 +52,11 @@ def test_trainable(self): fix_params["model"]["descriptor"]["trainable"] = True trainer_fix = get_trainer(fix_params) model_dict_before_training = deepcopy( - trainer_fix.model.fitting_net.state_dict() + trainer_fix.model.get_fitting_net().state_dict() ) trainer_fix.run() model_dict_after_training = deepcopy( - trainer_fix.model.fitting_net.state_dict() + trainer_fix.model.get_fitting_net().state_dict() ) else: trainer_fix = get_trainer(fix_params) From bc35ac99800fe4a6bd4609c33a12cd024e1469c2 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Tue, 12 Mar 2024 09:30:16 +0800 Subject: [PATCH 213/686] Fix: ZBL state_dict duplicated keys (#3432) --- deepmd/pt/model/atomic_model/linear_atomic_model.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 3fb3ee90dd..1649921270 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -361,8 +361,6 @@ def __init__( models = [dp_model, zbl_model] super().__init__(models, type_map, **kwargs) self.model_def_script = "" - self.dp_model = dp_model - self.zbl_model = zbl_model self.sw_rmin = sw_rmin self.sw_rmax = sw_rmax @@ -391,8 +389,8 @@ def compute_or_load_stat( stat_file_path The dictionary of paths to the statistics files. """ - self.dp_model.compute_or_load_stat(sampled_func, stat_file_path) - self.zbl_model.compute_or_load_stat(sampled_func, stat_file_path) + self.models[0].compute_or_load_stat(sampled_func, stat_file_path) + self.models[1].compute_or_load_stat(sampled_func, stat_file_path) def change_energy_bias(self): # need to implement @@ -406,7 +404,7 @@ def serialize(self) -> dict: "@version": 1, "type": "zbl", "models": LinearEnergyAtomicModel.serialize( - [self.dp_model, self.zbl_model], self.type_map + [self.models[0], self.models[1]], self.type_map ), "sw_rmin": self.sw_rmin, "sw_rmax": self.sw_rmax, From 9bcae14a617800b3d7714efa5504af1fca40da6a Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 12 Mar 2024 01:41:22 -0400 Subject: [PATCH 214/686] fix: remove model_def_script from AtomicModel (#3449) After #3438, `model_def_script` is no more saved in AtomicModel. --------- Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/atomic_model/make_base_atomic_model.py | 4 ---- deepmd/dpmodel/model/base_model.py | 7 +++++++ deepmd/dpmodel/model/make_model.py | 5 +---- deepmd/pt/model/atomic_model/base_atomic_model.py | 3 --- deepmd/pt/model/atomic_model/dp_atomic_model.py | 1 - deepmd/pt/model/atomic_model/linear_atomic_model.py | 1 - deepmd/pt/model/atomic_model/pairtab_atomic_model.py | 1 - deepmd/pt/model/model/make_model.py | 5 ----- deepmd/pt/model/model/model.py | 6 ++++++ 9 files changed, 14 insertions(+), 19 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py index e3d6d8bcd1..dfbb4e435a 100644 --- a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py @@ -178,10 +178,6 @@ def do_grad_(self, var_name: str, base: str) -> bool: return self.fitting_output_def()[var_name].c_differentiable return self.fitting_output_def()[var_name].r_differentiable - def get_model_def_script(self) -> str: - # TODO: implement this method; saved to model - raise NotImplementedError - setattr(BAM, fwd_method_name, BAM.fwd) delattr(BAM, "fwd") diff --git a/deepmd/dpmodel/model/base_model.py b/deepmd/dpmodel/model/base_model.py index 95c448442e..5169d1b5fe 100644 --- a/deepmd/dpmodel/model/base_model.py +++ b/deepmd/dpmodel/model/base_model.py @@ -172,3 +172,10 @@ class BaseModel(make_base_model()): deepmd.dpmodel.model.base_model.BaseBaseModel Backend-independent BaseModel class. """ + + def __init__(self) -> None: + self.model_def_script = "" + + def get_model_def_script(self) -> str: + """Get the model definition script.""" + return self.model_def_script diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py index 6022fd3e73..68889ad331 100644 --- a/deepmd/dpmodel/model/make_model.py +++ b/deepmd/dpmodel/model/make_model.py @@ -73,6 +73,7 @@ def __init__( atomic_model_: Optional[T_AtomicModel] = None, **kwargs, ): + BaseModel.__init__(self) if atomic_model_ is not None: self.atomic_model: T_AtomicModel = atomic_model_ else: @@ -452,10 +453,6 @@ def get_nnei(self) -> int: """Returns the total number of selected neighboring atoms in the cut-off radius.""" return self.atomic_model.get_nnei() - def get_model_def_script(self) -> str: - """Get the model definition script.""" - return self.atomic_model.get_model_def_script() - def get_sel(self) -> List[int]: """Returns the number of selected atoms for each type.""" return self.atomic_model.get_sel() diff --git a/deepmd/pt/model/atomic_model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py index d045220b6e..d3a1cfb459 100644 --- a/deepmd/pt/model/atomic_model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -58,9 +58,6 @@ def reinit_pair_exclude( else: self.pair_excl = PairExcludeMask(self.get_ntypes(), self.pair_exclude_types) - def get_model_def_script(self) -> str: - return self.model_def_script - def atomic_output_def(self) -> FittingOutputDef: old_def = self.fitting_output_def() if self.atom_excl is None: diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index ec08850524..e764be6998 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -55,7 +55,6 @@ def __init__( **kwargs, ): torch.nn.Module.__init__(self) - self.model_def_script = "" ntypes = len(type_map) self.type_map = type_map self.ntypes = ntypes diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 1649921270..8185fd1591 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -360,7 +360,6 @@ def __init__( ): models = [dp_model, zbl_model] super().__init__(models, type_map, **kwargs) - self.model_def_script = "" self.sw_rmin = sw_rmin self.sw_rmax = sw_rmax diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index db0a2efa4a..897a7cd9d4 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -78,7 +78,6 @@ def __init__( **kwargs, ): torch.nn.Module.__init__(self) - self.model_def_script = "" self.tab_file = tab_file self.rcut = rcut self.tab = self._set_pairtab(tab_file, rcut) diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 0a5f286040..167ad81923 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -469,11 +469,6 @@ def get_nnei(self) -> int: """Returns the total number of selected neighboring atoms in the cut-off radius.""" return self.atomic_model.get_nnei() - @torch.jit.export - def get_model_def_script(self) -> str: - """Get the model definition script.""" - return self.atomic_model.get_model_def_script() - def atomic_output_def(self) -> FittingOutputDef: """Get the output def of the atomic model.""" return self.atomic_model.atomic_output_def() diff --git a/deepmd/pt/model/model/model.py b/deepmd/pt/model/model/model.py index 3d4618449a..a62050b2d1 100644 --- a/deepmd/pt/model/model/model.py +++ b/deepmd/pt/model/model/model.py @@ -17,6 +17,7 @@ class BaseModel(torch.nn.Module, make_base_model()): def __init__(self, *args, **kwargs): """Construct a basic model for different tasks.""" torch.nn.Module.__init__(self) + self.model_def_script = "" def compute_or_load_stat( self, @@ -39,3 +40,8 @@ def compute_or_load_stat( The path to the statistics files. """ raise NotImplementedError + + @torch.jit.export + def get_model_def_script(self) -> str: + """Get the model definition script.""" + return self.model_def_script From da9b526f34f3280bfd8b58dd2e246b45d82f9355 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 12 Mar 2024 05:26:49 -0400 Subject: [PATCH 215/686] feat(pt): consistent "frozen" model (#3450) This PR is based on #3449, as the test needs #3449 to pass. Add a consistent `frozen` model in pt. Both TF and PT now support using models in any format. --------- Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/utils/network.py | 4 + deepmd/pt/model/model/__init__.py | 4 + deepmd/pt/model/model/frozen.py | 174 +++++++++++++++++++ deepmd/tf/fit/ener.py | 4 +- deepmd/tf/model/frozen.py | 35 +++- deepmd/tf/model/model.py | 3 +- deepmd/utils/argcheck.py | 1 - source/tests/consistent/model/test_frozen.py | 167 ++++++++++++++++++ source/tests/infer/deeppot.dp | Bin 0 -> 43424 bytes 9 files changed, 387 insertions(+), 5 deletions(-) create mode 100644 deepmd/pt/model/model/frozen.py create mode 100644 source/tests/consistent/model/test_frozen.py create mode 100644 source/tests/infer/deeppot.dp diff --git a/deepmd/dpmodel/utils/network.py b/deepmd/dpmodel/utils/network.py index 6206367b1b..1747e25527 100644 --- a/deepmd/dpmodel/utils/network.py +++ b/deepmd/dpmodel/utils/network.py @@ -230,6 +230,10 @@ def deserialize(cls, data: dict) -> "NativeLayer": variables.get("b", None), variables.get("idt", None), ) + if obj.b is not None: + obj.b = obj.b.ravel() + if obj.idt is not None: + obj.idt = obj.idt.ravel() obj.check_shape_consistency() return obj diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index 3098dc7677..f93ec88bde 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -37,6 +37,9 @@ from .ener_model import ( EnergyModel, ) +from .frozen import ( + FrozenModel, +) from .make_hessian_model import ( make_hessian_model, ) @@ -173,6 +176,7 @@ def get_model(model_params): "get_model", "DPModel", "EnergyModel", + "FrozenModel", "SpinModel", "SpinEnergyModel", "DPZBLModel", diff --git a/deepmd/pt/model/model/frozen.py b/deepmd/pt/model/model/frozen.py new file mode 100644 index 0000000000..e3dcd389bb --- /dev/null +++ b/deepmd/pt/model/model/frozen.py @@ -0,0 +1,174 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import tempfile +from typing import ( + Dict, + List, + Optional, +) + +import torch + +from deepmd.dpmodel.output_def import ( + FittingOutputDef, +) +from deepmd.entrypoints.convert_backend import ( + convert_backend, +) +from deepmd.pt.model.model.model import ( + BaseModel, +) + + +@BaseModel.register("frozen") +class FrozenModel(BaseModel): + """Load model from a frozen model, which cannot be trained. + + Parameters + ---------- + model_file : str + The path to the frozen model + """ + + def __init__(self, model_file: str, **kwargs): + super().__init__(**kwargs) + self.model_file = model_file + if model_file.endswith(".pth"): + self.model = torch.jit.load(model_file) + else: + # try to convert from other formats + with tempfile.NamedTemporaryFile(suffix=".pth") as f: + convert_backend(INPUT=model_file, OUTPUT=f.name) + self.model = torch.jit.load(f.name) + + @torch.jit.export + def fitting_output_def(self) -> FittingOutputDef: + """Get the output def of developer implemented atomic models.""" + return self.model.fitting_output_def() + + @torch.jit.export + def get_rcut(self) -> float: + """Get the cut-off radius.""" + return self.model.get_rcut() + + @torch.jit.export + def get_type_map(self) -> List[str]: + """Get the type map.""" + return self.model.get_type_map() + + @torch.jit.export + def get_sel(self) -> List[int]: + """Returns the number of selected atoms for each type.""" + return self.model.get_sel() + + @torch.jit.export + def get_dim_fparam(self) -> int: + """Get the number (dimension) of frame parameters of this atomic model.""" + return self.model.get_dim_fparam() + + @torch.jit.export + def get_dim_aparam(self) -> int: + """Get the number (dimension) of atomic parameters of this atomic model.""" + return self.model.get_dim_aparam() + + @torch.jit.export + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return self.model.get_sel_type() + + @torch.jit.export + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + + If False, the shape is (nframes, nloc, ndim). + """ + return self.model.is_aparam_nall() + + @torch.jit.export + def mixed_types(self) -> bool: + """If true, the model + 1. assumes total number of atoms aligned across frames; + 2. uses a neighbor list that does not distinguish different atomic types. + + If false, the model + 1. assumes total number of atoms of each atom type aligned across frames; + 2. uses a neighbor list that distinguishes different atomic types. + + """ + return self.model.mixed_types() + + @torch.jit.export + def forward( + self, + coord, + atype, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + return self.model.forward( + coord, + atype, + box=box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + + @torch.jit.export + def get_model_def_script(self) -> str: + """Get the model definition script.""" + # try to use the original script instead of "frozen model" + # Note: this cannot change the script of the parent model + # it may still try to load hard-coded filename, which might + # be a problem + return self.model.get_model_def_script() + + def serialize(self) -> dict: + from deepmd.pt.model.model import ( + get_model, + ) + + # try to recover the original model + model_def_script = json.loads(self.get_model_def_script()) + model = get_model(model_def_script) + model.load_state_dict(self.model.state_dict()) + return model.serialize() + + @classmethod + def deserialize(cls, data: dict): + raise RuntimeError("Should not touch here.") + + @torch.jit.export + def get_nnei(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + return self.model.get_nnei() + + @torch.jit.export + def get_nsel(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + return self.model.get_nsel() + + @classmethod + def update_sel(cls, global_jdata: dict, local_jdata: dict): + """Update the selection and perform neighbor statistics. + + Parameters + ---------- + global_jdata : dict + The global data, containing the training section + local_jdata : dict + The local data refer to the current class + """ + return local_jdata + + @torch.jit.export + def model_output_type(self) -> str: + """Get the output type for the model.""" + return self.model.model_output_type() diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index 780ae76c96..f8f5c3b346 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -868,7 +868,7 @@ def deserialize(cls, data: dict, suffix: str = ""): data["nets"], suffix=suffix, ) - fitting.bias_atom_e = data["@variables"]["bias_atom_e"] + fitting.bias_atom_e = data["@variables"]["bias_atom_e"].ravel() if fitting.numb_fparam > 0: fitting.fparam_avg = data["@variables"]["fparam_avg"] fitting.fparam_inv_std = data["@variables"]["fparam_inv_std"] @@ -922,7 +922,7 @@ def serialize(self, suffix: str = "") -> dict: suffix=suffix, ), "@variables": { - "bias_atom_e": self.bias_atom_e, + "bias_atom_e": self.bias_atom_e.reshape(-1, 1), "fparam_avg": self.fparam_avg, "fparam_inv_std": self.fparam_inv_std, "aparam_avg": self.aparam_avg, diff --git a/deepmd/tf/model/frozen.py b/deepmd/tf/model/frozen.py index 1933690ca7..86676bfe0b 100644 --- a/deepmd/tf/model/frozen.py +++ b/deepmd/tf/model/frozen.py @@ -1,4 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import json +import os +import tempfile from enum import ( Enum, ) @@ -7,6 +10,9 @@ Union, ) +from deepmd.entrypoints.convert_backend import ( + convert_backend, +) from deepmd.infer.deep_pot import ( DeepPot, ) @@ -24,6 +30,10 @@ from deepmd.tf.loss.loss import ( Loss, ) +from deepmd.tf.utils.graph import ( + get_tensor_by_name_from_graph, + load_graph_def, +) from .model import ( Model, @@ -43,7 +53,14 @@ class FrozenModel(Model): def __init__(self, model_file: str, **kwargs): super().__init__(**kwargs) self.model_file = model_file - self.model = DeepPotential(model_file) + if not model_file.endswith(".pb"): + # try to convert from other formats + with tempfile.NamedTemporaryFile( + suffix=".pb", dir=os.curdir, delete=False + ) as f: + convert_backend(INPUT=model_file, OUTPUT=f.name) + self.model_file = f.name + self.model = DeepPotential(self.model_file) if isinstance(self.model, DeepPot): self.model_type = "ener" else: @@ -228,3 +245,19 @@ def update_sel(cls, global_jdata: dict, local_jdata: dict): """ # we don't know how to compress it, so no neighbor statistics here return local_jdata + + def serialize(self, suffix: str = "") -> dict: + # try to recover the original model + # the current graph contains a prefix "load", + # so it cannot used to recover the original model + graph, graph_def = load_graph_def(self.model_file) + t_jdata = get_tensor_by_name_from_graph(graph, "train_attr/training_script") + jdata = json.loads(t_jdata) + model = Model(**jdata["model"]) + # important! must be called before serialize + model.init_variables(graph=graph, graph_def=graph_def) + return model.serialize() + + @classmethod + def deserialize(cls, data: dict, suffix: str = ""): + raise RuntimeError("Should not touch here.") diff --git a/deepmd/tf/model/model.py b/deepmd/tf/model/model.py index ca660f8e95..a0e234a547 100644 --- a/deepmd/tf/model/model.py +++ b/deepmd/tf/model/model.py @@ -566,7 +566,8 @@ def deserialize(cls, data: dict, suffix: str = "") -> "Model": """ if cls is Model: return Model.get_class_by_type(data.get("type", "standard")).deserialize( - data + data, + suffix=suffix, ) raise NotImplementedError("Not implemented in class %s" % cls.__name__) diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index e822e18d50..57f1145d55 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -1461,7 +1461,6 @@ def frozen_model_args() -> Argument: [ Argument("model_file", str, optional=False, doc=doc_model_file), ], - doc=doc_only_tf_supported, ) return ca diff --git a/source/tests/consistent/model/test_frozen.py b/source/tests/consistent/model/test_frozen.py new file mode 100644 index 0000000000..a60a6abb3f --- /dev/null +++ b/source/tests/consistent/model/test_frozen.py @@ -0,0 +1,167 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import os +import unittest +from typing import ( + Any, + Tuple, +) + +import numpy as np + +from deepmd.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) + +from ..common import ( + INSTALLED_PT, + INSTALLED_TF, + CommonTest, + parameterized, +) +from .common import ( + ModelTest, +) + +if INSTALLED_PT: + from deepmd.pt.model.model import BaseModel as FrozenModelPT + +else: + FrozenModelPT = None +if INSTALLED_TF: + from deepmd.tf.model.model import Model as FrozenModelTF +else: + FrozenModelTF = None +from pathlib import ( + Path, +) + +from deepmd.entrypoints.convert_backend import ( + convert_backend, +) +from deepmd.utils.argcheck import ( + model_args, +) + +original_model = str(Path(__file__).parent.parent.parent / "infer" / "deeppot.dp") +pt_model = "deeppot_for_consistent_frozen.pth" +tf_model = "deeppot_for_consistent_frozen.pb" +dp_model = original_model + + +def setUpModule(): + convert_backend( + INPUT=dp_model, + OUTPUT=tf_model, + ) + convert_backend( + INPUT=dp_model, + OUTPUT=pt_model, + ) + + +def tearDownModule(): + for model_file in (pt_model, tf_model): + try: + os.remove(model_file) + except FileNotFoundError: + pass + + +@parameterized((pt_model, tf_model, dp_model)) +class TestFrozen(CommonTest, ModelTest, unittest.TestCase): + @property + def data(self) -> dict: + (model_file,) = self.param + if not INSTALLED_PT and model_file.endswith(".pth"): + raise unittest.SkipTest("PyTorch is not installed") + if not INSTALLED_TF and model_file.endswith(".pb"): + raise unittest.SkipTest("TensorFlow is not installed") + return { + "type": "frozen", + "model_file": model_file, + } + + tf_class = FrozenModelTF + dp_class = None + pt_class = FrozenModelPT + args = model_args() + + def skip_dp(self): + return True + + def setUp(self): + CommonTest.setUp(self) + + self.ntypes = 2 + self.coords = np.array( + [ + 12.83, + 2.56, + 2.18, + 12.09, + 2.87, + 2.74, + 00.25, + 3.32, + 1.68, + 3.36, + 3.00, + 1.81, + 3.51, + 2.51, + 2.60, + 4.27, + 3.22, + 1.56, + ], + dtype=GLOBAL_NP_FLOAT_PRECISION, + ).reshape(1, -1, 3) + self.atype = np.array([0, 1, 1, 0, 1, 1], dtype=np.int32).reshape(1, -1) + self.box = np.array( + [13.0, 0.0, 0.0, 0.0, 13.0, 0.0, 0.0, 0.0, 13.0], + dtype=GLOBAL_NP_FLOAT_PRECISION, + ).reshape(1, 9) + self.natoms = np.array([6, 6, 2, 4], dtype=np.int32) + + # TF requires the atype to be sort + idx_map = np.argsort(self.atype.ravel()) + self.atype = self.atype[:, idx_map] + self.coords = self.coords[:, idx_map] + + def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: + return self.build_tf_model( + obj, + self.natoms, + self.coords, + self.atype, + self.box, + suffix, + ) + + def eval_dp(self, dp_obj: Any) -> Any: + return self.eval_dp_model( + dp_obj, + self.natoms, + self.coords, + self.atype, + self.box, + ) + + def eval_pt(self, pt_obj: Any) -> Any: + return self.eval_pt_model( + pt_obj, + self.natoms, + self.coords, + self.atype, + self.box, + ) + + def extract_ret(self, ret: Any, backend) -> Tuple[np.ndarray, ...]: + # shape not matched. ravel... + if backend is self.RefBackend.DP: + return (ret["energy_redu"].ravel(), ret["energy"].ravel()) + elif backend is self.RefBackend.PT: + return (ret["energy"].ravel(), ret["atom_energy"].ravel()) + elif backend is self.RefBackend.TF: + return (ret[0].ravel(), ret[1].ravel()) + raise ValueError(f"Unknown backend: {backend}") diff --git a/source/tests/infer/deeppot.dp b/source/tests/infer/deeppot.dp new file mode 100644 index 0000000000000000000000000000000000000000..2f7d9e3f6f8175bcf013137b2989e4407c197e68 GIT binary patch literal 43424 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 literal 0 HcmV?d00001 From 36fdf53f872bfabeb54340f3f8f98911ebdb3154 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 12 Mar 2024 21:29:05 -0400 Subject: [PATCH 216/686] chore: remove unused init_fitting_stat (#3453) See also #3278. Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/fitting/invar_fitting.py | 4 ---- deepmd/dpmodel/fitting/make_base_fitting.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/deepmd/dpmodel/fitting/invar_fitting.py b/deepmd/dpmodel/fitting/invar_fitting.py index e795953a75..f7c091843b 100644 --- a/deepmd/dpmodel/fitting/invar_fitting.py +++ b/deepmd/dpmodel/fitting/invar_fitting.py @@ -187,10 +187,6 @@ def compute_output_stats(self, merged): """Update the output bias for fitting net.""" raise NotImplementedError - def init_fitting_stat(self, result_dict): - """Initialize the model bias by the statistics.""" - raise NotImplementedError - def output_def(self): return FittingOutputDef( [ diff --git a/deepmd/dpmodel/fitting/make_base_fitting.py b/deepmd/dpmodel/fitting/make_base_fitting.py index 041076ba89..c7341798c3 100644 --- a/deepmd/dpmodel/fitting/make_base_fitting.py +++ b/deepmd/dpmodel/fitting/make_base_fitting.py @@ -67,10 +67,6 @@ def compute_output_stats(self, merged): """Update the output bias for fitting net.""" raise NotImplementedError - def init_fitting_stat(self, **kwargs): - """Initialize the model bias by the statistics.""" - raise NotImplementedError - @abstractmethod def serialize(self) -> dict: """Serialize the obj to dict.""" From dda4bc6ca2eab45e237ec300ddd6a368f03f7657 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:44:33 +0800 Subject: [PATCH 217/686] Chore: refactor LinearAtomicModel serialize/deserialize (#3451) This PR refactors the serialization/ deserialization in LinearEnergyAtomicModel using the plugin mechanism introduced in #3438 . --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../dpmodel/atomic_model/dp_atomic_model.py | 1 + .../atomic_model/linear_atomic_model.py | 49 ++++++++--------- .../atomic_model/pairtab_atomic_model.py | 1 + .../pt/model/atomic_model/dp_atomic_model.py | 1 + .../model/atomic_model/linear_atomic_model.py | 52 +++++++++---------- .../atomic_model/pairtab_atomic_model.py | 1 + 6 files changed, 50 insertions(+), 55 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/dp_atomic_model.py b/deepmd/dpmodel/atomic_model/dp_atomic_model.py index 110aa26162..4907483d1d 100644 --- a/deepmd/dpmodel/atomic_model/dp_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/dp_atomic_model.py @@ -26,6 +26,7 @@ ) +@BaseAtomicModel.register("standard") class DPAtomicModel(BaseAtomicModel): """Model give atomic prediction of some physical property. diff --git a/deepmd/dpmodel/atomic_model/linear_atomic_model.py b/deepmd/dpmodel/atomic_model/linear_atomic_model.py index ac2a73a381..088cf34900 100644 --- a/deepmd/dpmodel/atomic_model/linear_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/linear_atomic_model.py @@ -1,9 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import copy -import sys -from abc import ( - abstractmethod, -) from typing import ( Dict, List, @@ -225,40 +221,38 @@ def fitting_output_def(self) -> FittingOutputDef: ] ) - @staticmethod - def serialize(models, type_map) -> dict: + def serialize(self) -> dict: return { "@class": "Model", "type": "linear", "@version": 1, - "models": [model.serialize() for model in models], - "model_name": [model.__class__.__name__ for model in models], - "type_map": type_map, + "models": [model.serialize() for model in self.models], + "type_map": self.type_map, } - @staticmethod - def deserialize(data) -> Tuple[List[BaseAtomicModel], List[str]]: + @classmethod + def deserialize(cls, data: dict) -> "LinearEnergyAtomicModel": data = copy.deepcopy(data) check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") - model_names = data["model_name"] - type_map = data["type_map"] + type_map = data.pop("type_map") models = [ - getattr(sys.modules[__name__], name).deserialize(model) - for name, model in zip(model_names, data["models"]) + BaseAtomicModel.get_class_by_type(model["type"]).deserialize(model) + for model in data["models"] ] - return models, type_map + data.pop("models") + return cls(models, type_map, **data) - @abstractmethod def _compute_weight( self, extended_coord: np.ndarray, extended_atype: np.ndarray, nlists_: List[np.ndarray], - ) -> np.ndarray: + ) -> List[np.ndarray]: """This should be a list of user defined weights that matches the number of models to be combined.""" - raise NotImplementedError + nmodels = len(self.models) + return [np.ones(1) / nmodels for _ in range(nmodels)] def get_dim_fparam(self) -> int: """Get the number (dimension) of frame parameters of this atomic model.""" @@ -335,10 +329,10 @@ def serialize(self) -> dict: { "@class": "Model", "type": "zbl", - "@version": 1, - "models": LinearEnergyAtomicModel.serialize( - [self.dp_model, self.zbl_model], self.type_map - ), + "@version": 2, + "models": LinearEnergyAtomicModel( + models=[self.models[0], self.models[1]], type_map=self.type_map + ).serialize(), "sw_rmin": self.sw_rmin, "sw_rmax": self.sw_rmax, "smin_alpha": self.smin_alpha, @@ -349,16 +343,15 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "DPZBLLinearEnergyAtomicModel": data = copy.deepcopy(data) - check_version_compatibility(data.pop("@version", 1), 1, 1) + check_version_compatibility(data.pop("@version", 1), 2, 1) data.pop("@class") data.pop("type") sw_rmin = data.pop("sw_rmin") sw_rmax = data.pop("sw_rmax") smin_alpha = data.pop("smin_alpha") - - ([dp_model, zbl_model], type_map) = LinearEnergyAtomicModel.deserialize( - data.pop("models") - ) + linear_model = LinearEnergyAtomicModel.deserialize(data.pop("models")) + dp_model, zbl_model = linear_model.models + type_map = linear_model.type_map return cls( dp_model=dp_model, diff --git a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py index 46ec808ad4..99b8ec1eff 100644 --- a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py @@ -25,6 +25,7 @@ ) +@BaseAtomicModel.register("pairtab") class PairTabAtomicModel(BaseAtomicModel): """Pairwise tabulation energy model. diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index e764be6998..6aa8df7aee 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -33,6 +33,7 @@ log = logging.getLogger(__name__) +@BaseAtomicModel.register("standard") class DPAtomicModel(torch.nn.Module, BaseAtomicModel): """Model give atomic prediction of some physical property. diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 8185fd1591..f7216f46ef 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -1,9 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import copy -import sys -from abc import ( - abstractmethod, -) from typing import ( Dict, List, @@ -260,35 +256,38 @@ def fitting_output_def(self) -> FittingOutputDef: ] ) - @staticmethod - def serialize(models, type_map) -> dict: + def serialize(self) -> dict: return { "@class": "Model", "@version": 1, "type": "linear", - "models": [model.serialize() for model in models], - "model_name": [model.__class__.__name__ for model in models], - "type_map": type_map, + "models": [model.serialize() for model in self.models], + "type_map": self.type_map, } - @staticmethod - def deserialize(data) -> Tuple[List[BaseAtomicModel], List[str]]: + @classmethod + def deserialize(cls, data: dict) -> "LinearEnergyAtomicModel": data = copy.deepcopy(data) check_version_compatibility(data.pop("@version", 1), 1, 1) - model_names = data["model_name"] - type_map = data["type_map"] + data.pop("@class") + data.pop("type") + type_map = data.pop("type_map") models = [ - getattr(sys.modules[__name__], name).deserialize(model) - for name, model in zip(model_names, data["models"]) + BaseAtomicModel.get_class_by_type(model["type"]).deserialize(model) + for model in data["models"] ] - return models, type_map + data.pop("models") + return cls(models, type_map, **data) - @abstractmethod def _compute_weight( self, extended_coord, extended_atype, nlists_ ) -> List[torch.Tensor]: """This should be a list of user defined weights that matches the number of models to be combined.""" - raise NotImplementedError + nmodels = len(self.models) + return [ + torch.ones(1, dtype=torch.float64, device=env.DEVICE) / nmodels + for _ in range(nmodels) + ] def get_dim_fparam(self) -> int: """Get the number (dimension) of frame parameters of this atomic model.""" @@ -400,11 +399,11 @@ def serialize(self) -> dict: dd.update( { "@class": "Model", - "@version": 1, + "@version": 2, "type": "zbl", - "models": LinearEnergyAtomicModel.serialize( - [self.models[0], self.models[1]], self.type_map - ), + "models": LinearEnergyAtomicModel( + models=[self.models[0], self.models[1]], type_map=self.type_map + ).serialize(), "sw_rmin": self.sw_rmin, "sw_rmax": self.sw_rmax, "smin_alpha": self.smin_alpha, @@ -415,14 +414,13 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "DPZBLLinearEnergyAtomicModel": data = copy.deepcopy(data) - check_version_compatibility(data.pop("@version", 1), 1, 1) + check_version_compatibility(data.pop("@version", 1), 2, 1) sw_rmin = data.pop("sw_rmin") sw_rmax = data.pop("sw_rmax") smin_alpha = data.pop("smin_alpha") - - [dp_model, zbl_model], type_map = LinearEnergyAtomicModel.deserialize( - data.pop("models") - ) + linear_model = LinearEnergyAtomicModel.deserialize(data.pop("models")) + dp_model, zbl_model = linear_model.models + type_map = linear_model.type_map data.pop("@class", None) data.pop("type", None) diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index 897a7cd9d4..a4aa43ede1 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -35,6 +35,7 @@ ) +@BaseAtomicModel.register("pairtab") class PairTabAtomicModel(torch.nn.Module, BaseAtomicModel): """Pairwise tabulation energy model. From da68686dfe021c92a4125840d6076eab8a1d8a6a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 04:26:13 +0000 Subject: [PATCH 218/686] [pre-commit.ci] pre-commit autoupdate (#3454) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.2 → v0.3.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.2...v0.3.2) - [github.com/pre-commit/mirrors-clang-format: v17.0.6 → v18.1.1](https://github.com/pre-commit/mirrors-clang-format/compare/v17.0.6...v18.1.1) --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 +- backend/dp_backend.py | 1 + deepmd/__init__.py | 1 + deepmd/backend/__init__.py | 1 + deepmd/dpmodel/atomic_model/__init__.py | 1 - deepmd/dpmodel/utils/network.py | 1 + deepmd/dpmodel/utils/nlist.py | 2 +- deepmd/driver.py | 1 + deepmd/entrypoints/test.py | 1 + deepmd/infer/model_devi.py | 6 +- deepmd/main.py | 1 + deepmd/pt/loss/tensor.py | 6 +- deepmd/pt/model/task/fitting.py | 4 +- deepmd/pt/utils/nlist.py | 2 +- deepmd/pt/utils/plugin.py | 1 + deepmd/pt/utils/utils.py | 12 ++-- deepmd/tf/entrypoints/freeze.py | 6 +- deepmd/tf/entrypoints/ipi.py | 1 + deepmd/tf/env.py | 18 ++--- deepmd/tf/fit/fitting.py | 6 +- deepmd/tf/lmp.py | 1 + deepmd/tf/loggers/loggers.py | 1 + deepmd/tf/model/model_stat.py | 1 + deepmd/tf/model/multi.py | 6 +- deepmd/tf/nvnmd/utils/argcheck.py | 1 + deepmd/tf/op/_gelu.py | 1 + deepmd/tf/train/trainer.py | 19 ++---- deepmd/tf/utils/argcheck.py | 1 + deepmd/tf/utils/compat.py | 1 + deepmd/tf/utils/data.py | 1 + deepmd/tf/utils/data_system.py | 1 + deepmd/tf/utils/finetune.py | 4 +- deepmd/tf/utils/graph.py | 12 ++-- deepmd/tf/utils/multi_init.py | 4 +- deepmd/tf/utils/pair_tab.py | 1 + deepmd/tf/utils/path.py | 1 + deepmd/tf/utils/plugin.py | 1 + deepmd/tf/utils/random.py | 1 + deepmd/tf/utils/weight_avg.py | 1 + deepmd/utils/argcheck.py | 32 +++------ deepmd/utils/data.py | 8 +-- deepmd/utils/finetune.py | 4 +- deepmd/utils/out_stat.py | 1 + source/api_cc/src/common.cc | 13 ++-- source/api_cc/tests/test_ewald.cc | 4 +- source/ipi/driver.cc | 9 +-- source/lib/tests/test_ewald.cc | 2 +- source/lmp/tests/test_deeptensor.py | 12 +--- source/lmp/tests/test_dplr.py | 6 +- source/lmp/tests/test_lammps.py | 50 +++----------- source/lmp/tests/test_lammps_3types.py | 28 ++------ source/lmp/tests/test_lammps_faparam.py | 7 +- source/lmp/tests/test_lammps_pt.py | 44 +++--------- source/md/src/GroFileManager.cc | 9 +-- source/md/src/Poly.cpp | 12 ++-- source/nodejs/prepublish.py | 1 + source/op/dotmul_flt_nvnmd.cc | 2 +- source/op/map_flt_nvnmd.cc | 8 +-- source/op/matmul_fitnet_nvnmd.cc | 2 +- source/op/matmul_flt2fix_nvnmd.cc | 6 +- source/op/matmul_flt_nvnmd.cc | 6 +- source/op/tanh4_flt_nvnmd.cc | 4 +- source/tests/common/test_examples.py | 1 + source/tests/pt/test_multitask.py | 72 ++++++++++---------- source/tests/tf/test_init_frz_model_multi.py | 36 +++++----- source/tests/tf/test_pairwise_dprc.py | 1 + source/tests/tf/test_virtual_type.py | 1 + 67 files changed, 199 insertions(+), 317 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 041d47f0da..892962b2fe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^source/3rdparty - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.2.2 + rev: v0.3.2 hooks: - id: ruff args: ["--fix"] @@ -53,7 +53,7 @@ repos: - id: blacken-docs # C++ - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v17.0.6 + rev: v18.1.1 hooks: - id: clang-format exclude: ^source/3rdparty|source/lib/src/gpu/cudart/.+\.inc diff --git a/backend/dp_backend.py b/backend/dp_backend.py index d28afdb239..2ca0ff2f93 100644 --- a/backend/dp_backend.py +++ b/backend/dp_backend.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """A PEP-517 backend to find TensorFlow.""" + from typing import ( List, ) diff --git a/deepmd/__init__.py b/deepmd/__init__.py index 5664c3edc6..1ce4beb723 100644 --- a/deepmd/__init__.py +++ b/deepmd/__init__.py @@ -7,6 +7,7 @@ The top module (deepmd.__init__) should not import any third-party modules for performance. """ + try: from deepmd._version import version as __version__ except ImportError: diff --git a/deepmd/backend/__init__.py b/deepmd/backend/__init__.py index 8969edd480..2b3f24c5ed 100644 --- a/deepmd/backend/__init__.py +++ b/deepmd/backend/__init__.py @@ -3,6 +3,7 @@ Avoid directly importing third-party libraries in this module for performance. """ + # copy from dpdata from importlib import ( import_module, diff --git a/deepmd/dpmodel/atomic_model/__init__.py b/deepmd/dpmodel/atomic_model/__init__.py index e51ca0a65e..37f6b8bf28 100644 --- a/deepmd/dpmodel/atomic_model/__init__.py +++ b/deepmd/dpmodel/atomic_model/__init__.py @@ -14,7 +14,6 @@ """ - from .base_atomic_model import ( BaseAtomicModel, ) diff --git a/deepmd/dpmodel/utils/network.py b/deepmd/dpmodel/utils/network.py index 1747e25527..a0dcc6b706 100644 --- a/deepmd/dpmodel/utils/network.py +++ b/deepmd/dpmodel/utils/network.py @@ -3,6 +3,7 @@ See issue #2982 for more information. """ + import copy import itertools import json diff --git a/deepmd/dpmodel/utils/nlist.py b/deepmd/dpmodel/utils/nlist.py index 1aa1820495..e5631bf2e3 100644 --- a/deepmd/dpmodel/utils/nlist.py +++ b/deepmd/dpmodel/utils/nlist.py @@ -182,7 +182,7 @@ def build_multiple_neighbor_list( ret = {} for rc, ns in zip(rcuts[::-1], nsels[::-1]): tnlist_1 = np.copy(nlist0[:, :, :ns]) - tnlist_1[rr[:, :, :ns] > rc] = int(-1) + tnlist_1[rr[:, :, :ns] > rc] = -1 ret[get_multiple_nlist_key(rc, ns)] = tnlist_1 return ret diff --git a/deepmd/driver.py b/deepmd/driver.py index 1e5e36c652..0b48f2ac84 100644 --- a/deepmd/driver.py +++ b/deepmd/driver.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """dpdata driver.""" + # Derived from https://github.com/deepmodeling/dpdata/blob/18a0ed5ebced8b1f6887038883d46f31ae9990a4/dpdata/plugins/deepmd.py#L361-L443 # under LGPL-3.0-or-later license. # The original deepmd driver maintained in the dpdata package will be overriden. diff --git a/deepmd/entrypoints/test.py b/deepmd/entrypoints/test.py index ccf8b1da1e..cad6e12d2b 100644 --- a/deepmd/entrypoints/test.py +++ b/deepmd/entrypoints/test.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Test trained DeePMD model.""" + import logging from pathlib import ( Path, diff --git a/deepmd/infer/model_devi.py b/deepmd/infer/model_devi.py index cb5d79797b..c214e39e92 100644 --- a/deepmd/infer/model_devi.py +++ b/deepmd/infer/model_devi.py @@ -29,8 +29,7 @@ def calc_model_devi_f( real_f: Optional[np.ndarray] = None, relative: Optional[float] = None, atomic: Literal[False] = False, -) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: - ... +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: ... @overload @@ -40,8 +39,7 @@ def calc_model_devi_f( relative: Optional[float] = None, *, atomic: Literal[True], -) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - ... +) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: ... def calc_model_devi_f( diff --git a/deepmd/main.py b/deepmd/main.py index 09457419e8..b503107c73 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -4,6 +4,7 @@ If only printing the help message, this module does not call the main DeePMD-kit module to avoid the slow import of TensorFlow. """ + import argparse import logging import os diff --git a/deepmd/pt/loss/tensor.py b/deepmd/pt/loss/tensor.py index 5ac0a6e37b..ec7ef0b323 100644 --- a/deepmd/pt/loss/tensor.py +++ b/deepmd/pt/loss/tensor.py @@ -127,9 +127,9 @@ def forward(self, model_pred, label, natoms, learning_rate=0.0, mae=False): atom_num = natoms l2_global_loss = torch.mean(torch.square(diff)) if not self.inference: - more_loss[ - f"l2_global_{self.tensor_name}_loss" - ] = l2_global_loss.detach() + more_loss[f"l2_global_{self.tensor_name}_loss"] = ( + l2_global_loss.detach() + ) loss += self.global_weight * l2_global_loss rmse_global = l2_global_loss.sqrt() / atom_num more_loss[f"rmse_global_{self.tensor_name}"] = rmse_global.detach() diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 48ffe34084..4637178318 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -118,8 +118,8 @@ def change_energy_bias( The number of test samples in a system to change the energy bias. """ log.info( - "Changing energy bias in pretrained model for types {}... " - "(this step may take long time)".format(str(new_type_map)) + f"Changing energy bias in pretrained model for types {new_type_map!s}... " + "(this step may take long time)" ) # data systems = config["training"]["training_data"]["systems"] diff --git a/deepmd/pt/utils/nlist.py b/deepmd/pt/utils/nlist.py index d37931b65a..7e92f44e8d 100644 --- a/deepmd/pt/utils/nlist.py +++ b/deepmd/pt/utils/nlist.py @@ -256,7 +256,7 @@ def build_multiple_neighbor_list( nlist0 = nlist ret = {} for rc, ns in zip(rcuts[::-1], nsels[::-1]): - nlist0 = nlist0[:, :, :ns].masked_fill(rr[:, :, :ns] > rc, int(-1)) + nlist0 = nlist0[:, :, :ns].masked_fill(rr[:, :, :ns] > rc, -1) ret[get_multiple_nlist_key(rc, ns)] = nlist0 return ret diff --git a/deepmd/pt/utils/plugin.py b/deepmd/pt/utils/plugin.py index c24f36f574..aa901c06e8 100644 --- a/deepmd/pt/utils/plugin.py +++ b/deepmd/pt/utils/plugin.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Base of plugin systems.""" + from deepmd.utils.plugin import ( Plugin, PluginVariant, diff --git a/deepmd/pt/utils/utils.py b/deepmd/pt/utils/utils.py index 10dcadadac..3337036ca9 100644 --- a/deepmd/pt/utils/utils.py +++ b/deepmd/pt/utils/utils.py @@ -65,13 +65,11 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: @overload -def to_numpy_array(xx: torch.Tensor) -> np.ndarray: - ... +def to_numpy_array(xx: torch.Tensor) -> np.ndarray: ... @overload -def to_numpy_array(xx: None) -> None: - ... +def to_numpy_array(xx: None) -> None: ... def to_numpy_array( @@ -91,13 +89,11 @@ def to_numpy_array( @overload -def to_torch_tensor(xx: np.ndarray) -> torch.Tensor: - ... +def to_torch_tensor(xx: np.ndarray) -> torch.Tensor: ... @overload -def to_torch_tensor(xx: None) -> None: - ... +def to_torch_tensor(xx: None) -> None: ... def to_torch_tensor( diff --git a/deepmd/tf/entrypoints/freeze.py b/deepmd/tf/entrypoints/freeze.py index c7ab1023fa..3d2a609797 100755 --- a/deepmd/tf/entrypoints/freeze.py +++ b/deepmd/tf/entrypoints/freeze.py @@ -152,10 +152,8 @@ def _modify_model_suffix(output_graph_def, out_suffix, freeze_type): else: jdata["training"]["training_data"] = {} log.warning( - "The fitting net {} has no training data in input script, resulting in " - "untrained frozen model, and cannot be compressed directly! ".format( - out_suffix - ) + f"The fitting net {out_suffix} has no training data in input script, resulting in " + "untrained frozen model, and cannot be compressed directly! " ) # loss if "loss_dict" in jdata: diff --git a/deepmd/tf/entrypoints/ipi.py b/deepmd/tf/entrypoints/ipi.py index 49f72434f3..1631a35c2e 100644 --- a/deepmd/tf/entrypoints/ipi.py +++ b/deepmd/tf/entrypoints/ipi.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Use dp_ipi inside the Python package.""" + import os import subprocess import sys diff --git a/deepmd/tf/env.py b/deepmd/tf/env.py index 3127e01e97..0b16c8758a 100644 --- a/deepmd/tf/env.py +++ b/deepmd/tf/env.py @@ -376,20 +376,14 @@ def get_module(module_name: str) -> "ModuleType": if TF_VERSION != tf_py_version: raise RuntimeError( "The version of TensorFlow used to compile this " - "deepmd-kit package is {}, but the version of TensorFlow " - "runtime you are using is {}. These two versions are " - "incompatible and thus an error is raised when loading {}. " - "You need to install TensorFlow {}, or rebuild deepmd-kit " - "against TensorFlow {}.\nIf you are using a wheel from " + f"deepmd-kit package is {TF_VERSION}, but the version of TensorFlow " + f"runtime you are using is {tf_py_version}. These two versions are " + f"incompatible and thus an error is raised when loading {module_name}. " + f"You need to install TensorFlow {TF_VERSION}, or rebuild deepmd-kit " + f"against TensorFlow {tf_py_version}.\nIf you are using a wheel from " "pypi, you may consider to install deepmd-kit execuating " "`pip install deepmd-kit --no-binary deepmd-kit` " - "instead.".format( - TF_VERSION, - tf_py_version, - module_name, - TF_VERSION, - tf_py_version, - ) + "instead." ) from e error_message = ( "This deepmd-kit package is inconsitent with TensorFlow " diff --git a/deepmd/tf/fit/fitting.py b/deepmd/tf/fit/fitting.py index 6a7398daac..0f73230bc8 100644 --- a/deepmd/tf/fit/fitting.py +++ b/deepmd/tf/fit/fitting.py @@ -246,9 +246,9 @@ def deserialize_network(cls, data: dict, suffix: str = "") -> dict: fitting_net_variables[f"{layer_name}{key}{suffix}/matrix"] = layer.w fitting_net_variables[f"{layer_name}{key}{suffix}/bias"] = layer.b if layer.idt is not None: - fitting_net_variables[ - f"{layer_name}{key}{suffix}/idt" - ] = layer.idt.reshape(1, -1) + fitting_net_variables[f"{layer_name}{key}{suffix}/idt"] = ( + layer.idt.reshape(1, -1) + ) else: # prevent keyError fitting_net_variables[f"{layer_name}{key}{suffix}/idt"] = 0.0 diff --git a/deepmd/tf/lmp.py b/deepmd/tf/lmp.py index f8497bef59..b2e47308ed 100644 --- a/deepmd/tf/lmp.py +++ b/deepmd/tf/lmp.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Register entry points for lammps-wheel.""" + import os import platform from importlib import ( diff --git a/deepmd/tf/loggers/loggers.py b/deepmd/tf/loggers/loggers.py index eae99f5367..be948c9858 100644 --- a/deepmd/tf/loggers/loggers.py +++ b/deepmd/tf/loggers/loggers.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias of deepmd.loggers.loggers for backward compatibility.""" + from deepmd.loggers.loggers import ( set_log_handles, ) diff --git a/deepmd/tf/model/model_stat.py b/deepmd/tf/model/model_stat.py index 9149c0b666..db70262d50 100644 --- a/deepmd/tf/model/model_stat.py +++ b/deepmd/tf/model/model_stat.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" + from deepmd.utils.model_stat import ( _make_all_stat_ref, make_stat_input, diff --git a/deepmd/tf/model/multi.py b/deepmd/tf/model/multi.py index 6280fcd2f6..8fd4b539f1 100644 --- a/deepmd/tf/model/multi.py +++ b/deepmd/tf/model/multi.py @@ -135,9 +135,9 @@ def __init__( fitting_dict[item] = item_fitting_param else: if item_fitting_param["type"] in ["dipole", "polar"]: - item_fitting_param[ - "embedding_width" - ] = self.descrpt.get_dim_rot_mat_1() + item_fitting_param["embedding_width"] = ( + self.descrpt.get_dim_rot_mat_1() + ) fitting_dict[item] = Fitting( **item_fitting_param, descrpt=self.descrpt, diff --git a/deepmd/tf/nvnmd/utils/argcheck.py b/deepmd/tf/nvnmd/utils/argcheck.py index c22d9e0cd4..1f10a1c03e 100644 --- a/deepmd/tf/nvnmd/utils/argcheck.py +++ b/deepmd/tf/nvnmd/utils/argcheck.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" + from deepmd.utils.argcheck_nvnmd import ( nvnmd_args, ) diff --git a/deepmd/tf/op/_gelu.py b/deepmd/tf/op/_gelu.py index fcfd2d49fa..04ae124f70 100644 --- a/deepmd/tf/op/_gelu.py +++ b/deepmd/tf/op/_gelu.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: LGPL-3.0-or-later """First-order derivatives and second-order derivatives for gelu function.""" + import tensorflow from tensorflow.python.framework import ( ops, diff --git a/deepmd/tf/train/trainer.py b/deepmd/tf/train/trainer.py index 27478abaa1..c8668174fd 100644 --- a/deepmd/tf/train/trainer.py +++ b/deepmd/tf/train/trainer.py @@ -236,9 +236,7 @@ def build(self, data=None, stop_batch=0, origin_type_map=None, suffix=""): if data[fitting_key].mixed_type: assert isinstance( self.fitting[fitting_key], EnerFitting - ), "Data for fitting net {} in mixed_type format must use ener fitting!".format( - fitting_key - ) + ), f"Data for fitting net {fitting_key} in mixed_type format must use ener fitting!" if self.numb_fparam_dict[fitting_key] > 0: log.info( "fitting net %s training with %d frame parameter(s)" @@ -1086,10 +1084,7 @@ def _init_from_frz_model(self): except FileNotFoundError as e: # throw runtime error if there's no frozen model raise RuntimeError( - "The input frozen model {} ({}) does not exist! Please check the path of the frozen model. ".format( - self.run_opt.init_frz_model, - os.path.abspath(self.run_opt.init_frz_model), - ) + f"The input frozen model {self.run_opt.init_frz_model} ({os.path.abspath(self.run_opt.init_frz_model)}) does not exist! Please check the path of the frozen model. " ) from e # get the model type from the frozen model(self.run_opt.init_frz_model) try: @@ -1142,10 +1137,8 @@ def _init_from_pretrained_model( except FileNotFoundError as e: # throw runtime error if there's no frozen model raise RuntimeError( - "The input frozen pretrained model {} ({}) does not exist! " - "Please check the path of the frozen pretrained model. ".format( - self.run_opt.finetune, os.path.abspath(self.run_opt.finetune) - ) + f"The input frozen pretrained model {self.run_opt.finetune} ({os.path.abspath(self.run_opt.finetune)}) does not exist! " + "Please check the path of the frozen pretrained model. " ) from e # get the model type from the frozen model(self.run_opt.finetune) try: @@ -1164,8 +1157,8 @@ def _init_from_pretrained_model( ), "Compressed models are not supported for finetuning!" self.model.init_variables(graph, graph_def, model_type=self.model_type) log.info( - "Changing energy bias in pretrained model for types {}... " - "(this step may take long time)".format(str(origin_type_map)) + f"Changing energy bias in pretrained model for types {origin_type_map!s}... " + "(this step may take long time)" ) self._change_energy_bias( data, self.run_opt.finetune, origin_type_map, bias_shift diff --git a/deepmd/tf/utils/argcheck.py b/deepmd/tf/utils/argcheck.py index c3c0ed4f22..caec33c319 100644 --- a/deepmd/tf/utils/argcheck.py +++ b/deepmd/tf/utils/argcheck.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" + from deepmd.utils.argcheck import ( gen_args, gen_doc, diff --git a/deepmd/tf/utils/compat.py b/deepmd/tf/utils/compat.py index 6c95476ac8..e80a366b83 100644 --- a/deepmd/tf/utils/compat.py +++ b/deepmd/tf/utils/compat.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" + from deepmd.utils.compat import ( convert_input_v0_v1, convert_input_v1_v2, diff --git a/deepmd/tf/utils/data.py b/deepmd/tf/utils/data.py index 3c2eb4298d..54130c18f4 100644 --- a/deepmd/tf/utils/data.py +++ b/deepmd/tf/utils/data.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" + from deepmd.utils.data import ( DeepmdData, ) diff --git a/deepmd/tf/utils/data_system.py b/deepmd/tf/utils/data_system.py index 88c38d3dd4..da0cce28e8 100644 --- a/deepmd/tf/utils/data_system.py +++ b/deepmd/tf/utils/data_system.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" + from deepmd.utils.data_system import ( DeepmdDataSystem, prob_sys_size_ext, diff --git a/deepmd/tf/utils/finetune.py b/deepmd/tf/utils/finetune.py index 01b5eaaafe..38824b6954 100644 --- a/deepmd/tf/utils/finetune.py +++ b/deepmd/tf/utils/finetune.py @@ -63,9 +63,7 @@ def replace_model_params_with_pretrained_model( ) if cur_type_map != pretrained_type_map: log.info( - "Change the type_map from {} to {}.".format( - str(cur_type_map), str(pretrained_type_map) - ) + f"Change the type_map from {cur_type_map!s} to {pretrained_type_map!s}." ) jdata["model"]["type_map"] = pretrained_type_map diff --git a/deepmd/tf/utils/graph.py b/deepmd/tf/utils/graph.py index 8c4b0fcc84..f09250aa9f 100644 --- a/deepmd/tf/utils/graph.py +++ b/deepmd/tf/utils/graph.py @@ -308,13 +308,13 @@ def get_extra_embedding_net_variables_from_graph_def( extra_embedding_net_variables = {} for i in range(1, layer_size + 1): matrix_pattern = f"filter_type_all{suffix}/matrix_{i}{extra_suffix}" - extra_embedding_net_variables[ - matrix_pattern - ] = get_variables_from_graph_def_as_numpy_array(graph_def, matrix_pattern) + extra_embedding_net_variables[matrix_pattern] = ( + get_variables_from_graph_def_as_numpy_array(graph_def, matrix_pattern) + ) bias_pattern = f"filter_type_all{suffix}/bias_{i}{extra_suffix}" - extra_embedding_net_variables[ - bias_pattern - ] = get_variables_from_graph_def_as_numpy_array(graph_def, bias_pattern) + extra_embedding_net_variables[bias_pattern] = ( + get_variables_from_graph_def_as_numpy_array(graph_def, bias_pattern) + ) return extra_embedding_net_variables diff --git a/deepmd/tf/utils/multi_init.py b/deepmd/tf/utils/multi_init.py index 056a6694e8..2e3c43c069 100644 --- a/deepmd/tf/utils/multi_init.py +++ b/deepmd/tf/utils/multi_init.py @@ -59,9 +59,7 @@ def replace_model_params_with_frz_multi_model( ) if cur_type_map != pretrained_type_map: log.info( - "Change the type_map from {} to {}.".format( - str(cur_type_map), str(pretrained_type_map) - ) + f"Change the type_map from {cur_type_map!s} to {pretrained_type_map!s}." ) jdata["model"]["type_map"] = pretrained_type_map diff --git a/deepmd/tf/utils/pair_tab.py b/deepmd/tf/utils/pair_tab.py index a5f5e64aae..a9747c4367 100644 --- a/deepmd/tf/utils/pair_tab.py +++ b/deepmd/tf/utils/pair_tab.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" + from deepmd.utils.pair_tab import ( PairTab, ) diff --git a/deepmd/tf/utils/path.py b/deepmd/tf/utils/path.py index 63c82b9da0..67990543ae 100644 --- a/deepmd/tf/utils/path.py +++ b/deepmd/tf/utils/path.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" + from deepmd.utils.path import ( DPH5Path, DPOSPath, diff --git a/deepmd/tf/utils/plugin.py b/deepmd/tf/utils/plugin.py index 436a80a819..f2f0336691 100644 --- a/deepmd/tf/utils/plugin.py +++ b/deepmd/tf/utils/plugin.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" + from deepmd.utils.plugin import ( Plugin, PluginVariant, diff --git a/deepmd/tf/utils/random.py b/deepmd/tf/utils/random.py index 6d875df224..55b8eba91e 100644 --- a/deepmd/tf/utils/random.py +++ b/deepmd/tf/utils/random.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" + from deepmd.utils.random import ( choice, random, diff --git a/deepmd/tf/utils/weight_avg.py b/deepmd/tf/utils/weight_avg.py index fe162aa1ea..fb3ae27934 100644 --- a/deepmd/tf/utils/weight_avg.py +++ b/deepmd/tf/utils/weight_avg.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Alias for backward compatibility.""" + from deepmd.utils.weight_avg import ( weighted_average, ) diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 57f1145d55..1c2b7935b5 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -2385,10 +2385,10 @@ def normalize_multi_task(data): data["model"]["fitting_net_dict"].keys(), data["learning_rate_dict"] ) elif single_learning_rate: - data[ - "learning_rate_dict" - ] = normalize_learning_rate_dict_with_single_learning_rate( - data["model"]["fitting_net_dict"].keys(), data["learning_rate"] + data["learning_rate_dict"] = ( + normalize_learning_rate_dict_with_single_learning_rate( + data["model"]["fitting_net_dict"].keys(), data["learning_rate"] + ) ) fitting_weight = ( data["training"]["fitting_weight"] if multi_fitting_weight else None @@ -2431,11 +2431,7 @@ def normalize_data_dict(data_dict): def normalize_loss_dict(fitting_keys, loss_dict): # check the loss dict failed_loss_keys = [item for item in loss_dict if item not in fitting_keys] - assert ( - not failed_loss_keys - ), "Loss dict key(s) {} not have corresponding fitting keys in {}! ".format( - str(failed_loss_keys), str(list(fitting_keys)) - ) + assert not failed_loss_keys, f"Loss dict key(s) {failed_loss_keys!s} not have corresponding fitting keys in {list(fitting_keys)!s}! " new_dict = {} base = Argument("base", dict, [], [loss_variant_type_args()], doc="") for item in loss_dict: @@ -2450,9 +2446,7 @@ def normalize_learning_rate_dict(fitting_keys, learning_rate_dict): failed_learning_rate_keys = [ item for item in learning_rate_dict if item not in fitting_keys ] - assert not failed_learning_rate_keys, "Learning rate dict key(s) {} not have corresponding fitting keys in {}! ".format( - str(failed_learning_rate_keys), str(list(fitting_keys)) - ) + assert not failed_learning_rate_keys, f"Learning rate dict key(s) {failed_learning_rate_keys!s} not have corresponding fitting keys in {list(fitting_keys)!s}! " new_dict = {} base = Argument("base", dict, [], [learning_rate_variant_type_args()], doc="") for item in learning_rate_dict: @@ -2475,11 +2469,7 @@ def normalize_learning_rate_dict_with_single_learning_rate(fitting_keys, learnin def normalize_fitting_weight(fitting_keys, data_keys, fitting_weight=None): # check the mapping failed_data_keys = [item for item in data_keys if item not in fitting_keys] - assert ( - not failed_data_keys - ), "Data dict key(s) {} not have corresponding fitting keys in {}! ".format( - str(failed_data_keys), str(list(fitting_keys)) - ) + assert not failed_data_keys, f"Data dict key(s) {failed_data_keys!s} not have corresponding fitting keys in {list(fitting_keys)!s}! " empty_fitting_keys = [] valid_fitting_keys = [] for item in fitting_keys: @@ -2489,9 +2479,7 @@ def normalize_fitting_weight(fitting_keys, data_keys, fitting_weight=None): valid_fitting_keys.append(item) if empty_fitting_keys: log.warning( - "Fitting net(s) {} have no data and will not be used in training.".format( - str(empty_fitting_keys) - ) + f"Fitting net(s) {empty_fitting_keys!s} have no data and will not be used in training." ) num_pair = len(valid_fitting_keys) assert num_pair > 0, "No valid training data systems for fitting nets!" @@ -2506,9 +2494,7 @@ def normalize_fitting_weight(fitting_keys, data_keys, fitting_weight=None): failed_weight_keys = [ item for item in fitting_weight if item not in fitting_keys ] - assert not failed_weight_keys, "Fitting weight key(s) {} not have corresponding fitting keys in {}! ".format( - str(failed_weight_keys), str(list(fitting_keys)) - ) + assert not failed_weight_keys, f"Fitting weight key(s) {failed_weight_keys!s} not have corresponding fitting keys in {list(fitting_keys)!s}! " sum_prob = 0.0 for item in fitting_keys: if item in valid_fitting_keys: diff --git a/deepmd/utils/data.py b/deepmd/utils/data.py index 1e1d7c2251..6ad76be3c7 100644 --- a/deepmd/utils/data.py +++ b/deepmd/utils/data.py @@ -549,9 +549,7 @@ def _load_set(self, set_name: DPPath): atom_type_mix_ = self.type_idx_map[atom_type_mix].astype(np.int32) except IndexError as e: raise IndexError( - "some types in 'real_atom_types.npy' of set {} are not contained in {} types!".format( - set_name, self.get_ntypes() - ) + f"some types in 'real_atom_types.npy' of set {set_name} are not contained in {self.get_ntypes()} types!" ) from e atom_type_mix = atom_type_mix_ real_type = atom_type_mix.reshape([nframes, self.natoms]) @@ -568,9 +566,7 @@ def _load_set(self, set_name: DPPath): ).T assert ( atom_type_nums.sum(axis=-1) + ghost_nums.sum(axis=-1) == natoms - ).all(), "some types in 'real_atom_types.npy' of set {} are not contained in {} types!".format( - set_name, self.get_ntypes() - ) + ).all(), f"some types in 'real_atom_types.npy' of set {set_name} are not contained in {self.get_ntypes()} types!" data["real_natoms_vec"] = np.concatenate( ( np.tile(np.array([natoms, natoms], dtype=np.int32), (nframes, 1)), diff --git a/deepmd/utils/finetune.py b/deepmd/utils/finetune.py index b6d04b9bc5..a454ad72ea 100644 --- a/deepmd/utils/finetune.py +++ b/deepmd/utils/finetune.py @@ -135,8 +135,6 @@ def change_energy_bias_lower( else: raise RuntimeError("Unknown bias_shift mode: " + bias_shift) log.info( - "Change energy bias of {} from {} to {}.".format( - str(origin_type_map), str(old_bias), str(bias_atom_e[idx_type_map]) - ) + f"Change energy bias of {origin_type_map!s} from {old_bias!s} to {bias_atom_e[idx_type_map]!s}." ) return bias_atom_e diff --git a/deepmd/utils/out_stat.py b/deepmd/utils/out_stat.py index 8f68e32417..3659e57305 100644 --- a/deepmd/utils/out_stat.py +++ b/deepmd/utils/out_stat.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Output statistics.""" + from typing import ( Optional, Tuple, diff --git a/source/api_cc/src/common.cc b/source/api_cc/src/common.cc index f104433468..aa1e27ace1 100644 --- a/source/api_cc/src/common.cc +++ b/source/api_cc/src/common.cc @@ -329,8 +329,8 @@ void deepmd::check_status(const tensorflow::Status& status) { void throw_env_not_set_warning(std::string env_name) { std::cerr << "DeePMD-kit WARNING: Environmental variable " << env_name - << " is not set. " - << "Tune " << env_name << " for the best performance. " + << " is not set. " << "Tune " << env_name + << " for the best performance. " << "See https://deepmd.rtfd.io/parallelism/ for more information." << std::endl; } @@ -1341,14 +1341,11 @@ void deepmd::print_summary(const std::string& pre) { std::cout << pre << "source commit at: " + global_git_date << "\n"; std::cout << pre << "support model ver.: " + global_model_version << "\n"; #if defined(GOOGLE_CUDA) - std::cout << pre << "build variant: cuda" - << "\n"; + std::cout << pre << "build variant: cuda" << "\n"; #elif defined(TENSORFLOW_USE_ROCM) - std::cout << pre << "build variant: rocm" - << "\n"; + std::cout << pre << "build variant: rocm" << "\n"; #else - std::cout << pre << "build variant: cpu" - << "\n"; + std::cout << pre << "build variant: cpu" << "\n"; #endif #ifdef BUILD_TENSORFLOW std::cout << pre << "build with tf inc: " + global_tf_include_dir << "\n"; diff --git a/source/api_cc/tests/test_ewald.cc b/source/api_cc/tests/test_ewald.cc index 7eb433816d..d5aa6993a9 100644 --- a/source/api_cc/tests/test_ewald.cc +++ b/source/api_cc/tests/test_ewald.cc @@ -18,8 +18,8 @@ class TestInferEwald : public ::testing::Test { 3.51, 2.51, 2.60, 4.27, 3.22, 1.56}; std::vector charge = {-2, 1, 1, -2, 1, 1}; std::vector box = {13., 0., 0., 0., 13., 0., 0., 0., 13.}; - void SetUp() override{}; - void TearDown() override{}; + void SetUp() override {}; + void TearDown() override {}; }; TYPED_TEST_SUITE(TestInferEwald, ValueTypes); diff --git a/source/ipi/driver.cc b/source/ipi/driver.cc index 9a91a27ad3..977d76011a 100644 --- a/source/ipi/driver.cc +++ b/source/ipi/driver.cc @@ -126,20 +126,17 @@ int main(int argc, char *argv[]) { if (!isinit) { writebuffer_(&socket, msg_needinit, MSGLEN); if (b_verb) { - std::cout << "# send back " - << "NEEDINIT" << std::endl; + std::cout << "# send back " << "NEEDINIT" << std::endl; } } else if (hasdata) { writebuffer_(&socket, msg_havedata, MSGLEN); if (b_verb) { - std::cout << "# send back " - << "HAVEDATA" << std::endl; + std::cout << "# send back " << "HAVEDATA" << std::endl; } } else { writebuffer_(&socket, msg_ready, MSGLEN); if (b_verb) { - std::cout << "# send back " - << "READY" << std::endl; + std::cout << "# send back " << "READY" << std::endl; } } } else if (header_str == "INIT") { diff --git a/source/lib/tests/test_ewald.cc b/source/lib/tests/test_ewald.cc index 45c8ea7bf1..ca6f3a845e 100644 --- a/source/lib/tests/test_ewald.cc +++ b/source/lib/tests/test_ewald.cc @@ -30,7 +30,7 @@ class TestEwald : public ::testing::Test { 1.9076542856278367e+00, 1.3101841366497322e+00, 1.9794445391572657e-01, -9.8010077026955389e-01, 1.9794445391572657e-01, 1.9232614011636004e+00}; - void SetUp() override{}; + void SetUp() override {}; }; TEST_F(TestEwald, cpu) { diff --git a/source/lmp/tests/test_deeptensor.py b/source/lmp/tests/test_deeptensor.py index 3e684b386e..6df0a8617a 100644 --- a/source/lmp/tests/test_deeptensor.py +++ b/source/lmp/tests/test_deeptensor.py @@ -57,19 +57,11 @@ sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( - sys.executable, - pbtxt_file.resolve(), - pb_file.resolve(), - ).split() + f"{sys.executable} -m deepmd convert-from pbtxt -i {pbtxt_file.resolve()} -o {pb_file.resolve()}".split() ) sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( - sys.executable, - pbtxt_file2.resolve(), - pb_file2.resolve(), - ).split() + f"{sys.executable} -m deepmd convert-from pbtxt -i {pbtxt_file2.resolve()} -o {pb_file2.resolve()}".split() ) diff --git a/source/lmp/tests/test_dplr.py b/source/lmp/tests/test_dplr.py index 9c8f1c0d4f..2dd3531894 100644 --- a/source/lmp/tests/test_dplr.py +++ b/source/lmp/tests/test_dplr.py @@ -264,11 +264,7 @@ sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( - sys.executable, - pbtxt_file.resolve(), - pb_file.resolve(), - ).split() + f"{sys.executable} -m deepmd convert-from pbtxt -i {pbtxt_file.resolve()} -o {pb_file.resolve()}".split() ) diff --git a/source/lmp/tests/test_lammps.py b/source/lmp/tests/test_lammps.py index 028b403abf..c495f16ffd 100644 --- a/source/lmp/tests/test_lammps.py +++ b/source/lmp/tests/test_lammps.py @@ -219,18 +219,10 @@ sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( - sys.executable, - pbtxt_file.resolve(), - pb_file.resolve(), - ).split() + f"{sys.executable} -m deepmd convert-from pbtxt -i {pbtxt_file.resolve()} -o {pb_file.resolve()}".split() ) sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( - sys.executable, - pbtxt_file2.resolve(), - pb_file2.resolve(), - ).split() + f"{sys.executable} -m deepmd convert-from pbtxt -i {pbtxt_file2.resolve()} -o {pb_file2.resolve()}".split() ) @@ -348,9 +340,7 @@ def test_pair_deepmd_virial(lammps): def test_pair_deepmd_model_devi(lammps): lammps.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve() - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic" ) lammps.pair_coeff("* *") lammps.run(0) @@ -376,9 +366,7 @@ def test_pair_deepmd_model_devi(lammps): def test_pair_deepmd_model_devi_virial(lammps): lammps.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve() - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic" ) lammps.pair_coeff("* *") lammps.compute("virial all centroid/stress/atom NULL pair") @@ -417,9 +405,7 @@ def test_pair_deepmd_model_devi_virial(lammps): def test_pair_deepmd_model_devi_atomic_relative(lammps): relative = 1.0 lammps.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic relative {}".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve(), relative - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic relative {relative}" ) lammps.pair_coeff("* *") lammps.run(0) @@ -448,9 +434,7 @@ def test_pair_deepmd_model_devi_atomic_relative(lammps): def test_pair_deepmd_model_devi_atomic_relative_v(lammps): relative = 1.0 lammps.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic relative_v {}".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve(), relative - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic relative_v {relative}" ) lammps.pair_coeff("* *") lammps.run(0) @@ -535,9 +519,7 @@ def test_pair_deepmd_virial_real(lammps_real): def test_pair_deepmd_model_devi_real(lammps_real): lammps_real.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve() - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic" ) lammps_real.pair_coeff("* *") lammps_real.run(0) @@ -567,9 +549,7 @@ def test_pair_deepmd_model_devi_real(lammps_real): def test_pair_deepmd_model_devi_virial_real(lammps_real): lammps_real.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve() - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic" ) lammps_real.pair_coeff("* *") lammps_real.compute("virial all centroid/stress/atom NULL pair") @@ -614,12 +594,7 @@ def test_pair_deepmd_model_devi_virial_real(lammps_real): def test_pair_deepmd_model_devi_atomic_relative_real(lammps_real): relative = 1.0 lammps_real.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic relative {}".format( - pb_file.resolve(), - pb_file2.resolve(), - md_file.resolve(), - relative * constants.force_metal2real, - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic relative {relative * constants.force_metal2real}" ) lammps_real.pair_coeff("* *") lammps_real.run(0) @@ -652,12 +627,7 @@ def test_pair_deepmd_model_devi_atomic_relative_real(lammps_real): def test_pair_deepmd_model_devi_atomic_relative_v_real(lammps_real): relative = 1.0 lammps_real.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic relative_v {}".format( - pb_file.resolve(), - pb_file2.resolve(), - md_file.resolve(), - relative * constants.ener_metal2real, - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic relative_v {relative * constants.ener_metal2real}" ) lammps_real.pair_coeff("* *") lammps_real.run(0) diff --git a/source/lmp/tests/test_lammps_3types.py b/source/lmp/tests/test_lammps_3types.py index 46e1a00c8f..e4e64d9ecf 100644 --- a/source/lmp/tests/test_lammps_3types.py +++ b/source/lmp/tests/test_lammps_3types.py @@ -245,18 +245,10 @@ nktv2p = 1.6021765e6 sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( - sys.executable, - pbtxt_file.resolve(), - pb_file.resolve(), - ).split() + f"{sys.executable} -m deepmd convert-from pbtxt -i {pbtxt_file.resolve()} -o {pb_file.resolve()}".split() ) sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( - sys.executable, - pbtxt_file2.resolve(), - pb_file2.resolve(), - ).split() + f"{sys.executable} -m deepmd convert-from pbtxt -i {pbtxt_file2.resolve()} -o {pb_file2.resolve()}".split() ) @@ -337,9 +329,7 @@ def test_pair_deepmd_virial(lammps): def test_pair_deepmd_model_devi(lammps): lammps.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve() - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic" ) lammps.pair_coeff("* *") lammps.run(0) @@ -365,9 +355,7 @@ def test_pair_deepmd_model_devi(lammps): def test_pair_deepmd_model_devi_virial(lammps): lammps.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve() - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic" ) lammps.pair_coeff("* *") lammps.compute("virial all centroid/stress/atom NULL pair") @@ -406,9 +394,7 @@ def test_pair_deepmd_model_devi_virial(lammps): def test_pair_deepmd_model_devi_atomic_relative(lammps): relative = 1.0 lammps.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic relative {}".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve(), relative - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic relative {relative}" ) lammps.pair_coeff("* *") lammps.run(0) @@ -437,9 +423,7 @@ def test_pair_deepmd_model_devi_atomic_relative(lammps): def test_pair_deepmd_model_devi_atomic_relative_v(lammps): relative = 1.0 lammps.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic relative_v {}".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve(), relative - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic relative_v {relative}" ) lammps.pair_coeff("* *") lammps.run(0) diff --git a/source/lmp/tests/test_lammps_faparam.py b/source/lmp/tests/test_lammps_faparam.py index 064928eeb1..f78639a96b 100644 --- a/source/lmp/tests/test_lammps_faparam.py +++ b/source/lmp/tests/test_lammps_faparam.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Test LAMMPS fparam and aparam input.""" + import os import subprocess as sp import sys @@ -134,11 +135,7 @@ sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( - sys.executable, - pbtxt_file.resolve(), - pb_file.resolve(), - ).split() + f"{sys.executable} -m deepmd convert-from pbtxt -i {pbtxt_file.resolve()} -o {pb_file.resolve()}".split() ) diff --git a/source/lmp/tests/test_lammps_pt.py b/source/lmp/tests/test_lammps_pt.py index bf1ef97e2b..55eaf4fde7 100644 --- a/source/lmp/tests/test_lammps_pt.py +++ b/source/lmp/tests/test_lammps_pt.py @@ -218,11 +218,7 @@ sp.check_output( - "{} -m deepmd convert-from pbtxt -i {} -o {}".format( - sys.executable, - pbtxt_file2.resolve(), - pb_file2.resolve(), - ).split() + f"{sys.executable} -m deepmd convert-from pbtxt -i {pbtxt_file2.resolve()} -o {pb_file2.resolve()}".split() ) @@ -340,9 +336,7 @@ def test_pair_deepmd_virial(lammps): def test_pair_deepmd_model_devi(lammps): lammps.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve() - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic" ) lammps.pair_coeff("* *") lammps.run(0) @@ -368,9 +362,7 @@ def test_pair_deepmd_model_devi(lammps): def test_pair_deepmd_model_devi_virial(lammps): lammps.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve() - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic" ) lammps.pair_coeff("* *") lammps.compute("virial all centroid/stress/atom NULL pair") @@ -409,9 +401,7 @@ def test_pair_deepmd_model_devi_virial(lammps): def test_pair_deepmd_model_devi_atomic_relative(lammps): relative = 1.0 lammps.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic relative {}".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve(), relative - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic relative {relative}" ) lammps.pair_coeff("* *") lammps.run(0) @@ -440,9 +430,7 @@ def test_pair_deepmd_model_devi_atomic_relative(lammps): def test_pair_deepmd_model_devi_atomic_relative_v(lammps): relative = 1.0 lammps.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic relative_v {}".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve(), relative - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic relative_v {relative}" ) lammps.pair_coeff("* *") lammps.run(0) @@ -527,9 +515,7 @@ def test_pair_deepmd_virial_real(lammps_real): def test_pair_deepmd_model_devi_real(lammps_real): lammps_real.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve() - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic" ) lammps_real.pair_coeff("* *") lammps_real.run(0) @@ -559,9 +545,7 @@ def test_pair_deepmd_model_devi_real(lammps_real): def test_pair_deepmd_model_devi_virial_real(lammps_real): lammps_real.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic".format( - pb_file.resolve(), pb_file2.resolve(), md_file.resolve() - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic" ) lammps_real.pair_coeff("* *") lammps_real.compute("virial all centroid/stress/atom NULL pair") @@ -606,12 +590,7 @@ def test_pair_deepmd_model_devi_virial_real(lammps_real): def test_pair_deepmd_model_devi_atomic_relative_real(lammps_real): relative = 1.0 lammps_real.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic relative {}".format( - pb_file.resolve(), - pb_file2.resolve(), - md_file.resolve(), - relative * constants.force_metal2real, - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic relative {relative * constants.force_metal2real}" ) lammps_real.pair_coeff("* *") lammps_real.run(0) @@ -644,12 +623,7 @@ def test_pair_deepmd_model_devi_atomic_relative_real(lammps_real): def test_pair_deepmd_model_devi_atomic_relative_v_real(lammps_real): relative = 1.0 lammps_real.pair_style( - "deepmd {} {} out_file {} out_freq 1 atomic relative_v {}".format( - pb_file.resolve(), - pb_file2.resolve(), - md_file.resolve(), - relative * constants.ener_metal2real, - ) + f"deepmd {pb_file.resolve()} {pb_file2.resolve()} out_file {md_file.resolve()} out_freq 1 atomic relative_v {relative * constants.ener_metal2real}" ) lammps_real.pair_coeff("* *") lammps_real.run(0) diff --git a/source/md/src/GroFileManager.cc b/source/md/src/GroFileManager.cc index 5969168a72..d61fbb7b97 100644 --- a/source/md/src/GroFileManager.cc +++ b/source/md/src/GroFileManager.cc @@ -125,8 +125,7 @@ void GroFileManager::read(const std::string &name, std::cerr << "cannot open file " << name << std::endl; return; } - while (fgetc(fp) != '\n') - ; + while (fgetc(fp) != '\n'); int npart; fscanf(fp, "%d\n", &npart); fclose(fp); @@ -141,10 +140,8 @@ void GroFileManager::read(const std::string &name, boxsize.resize(3); fp = fopen(name.c_str(), "r"); - while (fgetc(fp) != '\n') - ; - while (fgetc(fp) != '\n') - ; + while (fgetc(fp) != '\n'); + while (fgetc(fp) != '\n'); char line[1024]; for (int i = 0; i < npart; ++i) { fgets(line, 1024, fp); diff --git a/source/md/src/Poly.cpp b/source/md/src/Poly.cpp index 49d2897f14..80db3a139f 100644 --- a/source/md/src/Poly.cpp +++ b/source/md/src/Poly.cpp @@ -38,11 +38,9 @@ double PiecewisePoly::value_periodic(const double& xx_) const { double xx(xx_); double T = x.back() - x.front(); if (xx < x.front()) { - while ((xx += T) < x.front()) - ; + while ((xx += T) < x.front()); } else if (xx >= x.back()) { - while ((xx -= T) >= x.back()) - ; + while ((xx -= T) >= x.back()); } unsigned begin = 0; unsigned end = x.size() - 1; @@ -126,11 +124,9 @@ void PiecewisePoly::value_periodic(const std::vector& r, presentStart = presentEnd; double shift = 0; if (r[presentStart] < x.front()) { - while (r[presentStart] + (shift += T) < x.front()) - ; + while (r[presentStart] + (shift += T) < x.front()); } else if (r[presentStart] >= x.back()) { - while (r[presentStart] + (shift -= T) >= x.back()) - ; + while (r[presentStart] + (shift -= T) >= x.back()); } while (presentEnd < r.size() && r[presentEnd] + shift >= x.front() && r[presentEnd] + shift < x.back()) { diff --git a/source/nodejs/prepublish.py b/source/nodejs/prepublish.py index 2f607a7d07..cb60659f02 100644 --- a/source/nodejs/prepublish.py +++ b/source/nodejs/prepublish.py @@ -4,6 +4,7 @@ The NPM package downloads the C library binary from GitHub releases. This script changes the package.json to make it work. """ + import json import shutil diff --git a/source/op/dotmul_flt_nvnmd.cc b/source/op/dotmul_flt_nvnmd.cc index fd7c831ef1..d7c2c8d3c3 100644 --- a/source/op/dotmul_flt_nvnmd.cc +++ b/source/op/dotmul_flt_nvnmd.cc @@ -159,7 +159,7 @@ class DotmulFltNvnmdOp : public OpKernel { ufi3.nint &= FLT_MASK; y[ii] = ufi3.nflt; } // loop ii - } // Compute + } // Compute }; // DotmulFltNvnmdOp diff --git a/source/op/map_flt_nvnmd.cc b/source/op/map_flt_nvnmd.cc index b23deac9c8..77b788e537 100644 --- a/source/op/map_flt_nvnmd.cc +++ b/source/op/map_flt_nvnmd.cc @@ -141,10 +141,10 @@ class MapFltNvnmdOp : public OpKernel { add_flt_nvnmd(ytmp, d, ytmp); y[ii * M + jj] = ytmp; } // jj - } // ii - } // ss - } // Compute -}; // MapFltNvnmdOp + } // ii + } // ss + } // Compute +}; // MapFltNvnmdOp #define REGISTER_CPU(T) \ REGISTER_KERNEL_BUILDER( \ diff --git a/source/op/matmul_fitnet_nvnmd.cc b/source/op/matmul_fitnet_nvnmd.cc index b5dc32a642..acc8e4b591 100644 --- a/source/op/matmul_fitnet_nvnmd.cc +++ b/source/op/matmul_fitnet_nvnmd.cc @@ -160,7 +160,7 @@ class MatmulFitnetNvnmdOp : public OpKernel { s = floor(s * prec * precx) * div_precx; y[ii * K + kk] = s; } // loop xx - } // loop kk + } // loop kk } // Compute diff --git a/source/op/matmul_flt2fix_nvnmd.cc b/source/op/matmul_flt2fix_nvnmd.cc index ab823a829d..10cfb3d3ba 100644 --- a/source/op/matmul_flt2fix_nvnmd.cc +++ b/source/op/matmul_flt2fix_nvnmd.cc @@ -138,9 +138,9 @@ class MatmulFlt2fixNvnmdOp : public OpKernel { ufi.nint &= FLT_MASK; y[hh * N * K + ii * K + kk] = ufi.nflt; } // loop jj - } // loop ii - } // loop hh - } // Compute + } // loop ii + } // loop hh + } // Compute private: int nbit; diff --git a/source/op/matmul_flt_nvnmd.cc b/source/op/matmul_flt_nvnmd.cc index 92b6375100..22ed23c0a3 100644 --- a/source/op/matmul_flt_nvnmd.cc +++ b/source/op/matmul_flt_nvnmd.cc @@ -188,9 +188,9 @@ class MatmulFltNvnmdOp : public OpKernel { ufi3.nint &= FLT_MASK; y[hh * N * K + ii * K + kk] = ufi3.nflt; } // loop kk - } // loop ii - } // loop hh - } // Compute + } // loop ii + } // loop hh + } // Compute private: int normx; diff --git a/source/op/tanh4_flt_nvnmd.cc b/source/op/tanh4_flt_nvnmd.cc index 987013a5e6..3351a366e4 100644 --- a/source/op/tanh4_flt_nvnmd.cc +++ b/source/op/tanh4_flt_nvnmd.cc @@ -117,8 +117,8 @@ class Tanh4FltNvnmdOp : public OpKernel { y = floor(y * prechi) / prechi; ys(ii, jj) = (x < 0) ? (-y) : y; } // loop jj - } // loop ii - } // Compute + } // loop ii + } // Compute //- define the private variable for calculation }; // Tanh4FltNvnmd diff --git a/source/tests/common/test_examples.py b/source/tests/common/test_examples.py index 1ec4cef3a5..91bb9c0174 100644 --- a/source/tests/common/test_examples.py +++ b/source/tests/common/test_examples.py @@ -2,6 +2,7 @@ """This module ensures input in the examples directory could pass the argument checking. """ + import unittest from pathlib import ( Path, diff --git a/source/tests/pt/test_multitask.py b/source/tests/pt/test_multitask.py index d06733b016..e959e9a128 100644 --- a/source/tests/pt/test_multitask.py +++ b/source/tests/pt/test_multitask.py @@ -73,24 +73,24 @@ def setUp(self): self.stat_files = "se_e2_a" os.makedirs(self.stat_files, exist_ok=True) self.config = multitask_se_e2_a - self.config["training"]["data_dict"]["model_1"]["training_data"][ - "systems" - ] = data_file + self.config["training"]["data_dict"]["model_1"]["training_data"]["systems"] = ( + data_file + ) self.config["training"]["data_dict"]["model_1"]["validation_data"][ "systems" ] = data_file - self.config["training"]["data_dict"]["model_1"][ - "stat_file" - ] = f"{self.stat_files}/model_1" - self.config["training"]["data_dict"]["model_2"]["training_data"][ - "systems" - ] = data_file + self.config["training"]["data_dict"]["model_1"]["stat_file"] = ( + f"{self.stat_files}/model_1" + ) + self.config["training"]["data_dict"]["model_2"]["training_data"]["systems"] = ( + data_file + ) self.config["training"]["data_dict"]["model_2"]["validation_data"][ "systems" ] = data_file - self.config["training"]["data_dict"]["model_2"][ - "stat_file" - ] = f"{self.stat_files}/model_2" + self.config["training"]["data_dict"]["model_2"]["stat_file"] = ( + f"{self.stat_files}/model_2" + ) self.config["training"]["numb_steps"] = 1 self.config["training"]["save_freq"] = 1 self.config["model"], self.shared_links = preprocess_shared_params( @@ -111,24 +111,24 @@ def setUp(self): self.stat_files = "DPA1" os.makedirs(self.stat_files, exist_ok=True) self.config = multitask_DPA1 - self.config["training"]["data_dict"]["model_1"]["training_data"][ - "systems" - ] = data_file + self.config["training"]["data_dict"]["model_1"]["training_data"]["systems"] = ( + data_file + ) self.config["training"]["data_dict"]["model_1"]["validation_data"][ "systems" ] = data_file - self.config["training"]["data_dict"]["model_1"][ - "stat_file" - ] = f"{self.stat_files}/model_1" - self.config["training"]["data_dict"]["model_2"]["training_data"][ - "systems" - ] = data_file + self.config["training"]["data_dict"]["model_1"]["stat_file"] = ( + f"{self.stat_files}/model_1" + ) + self.config["training"]["data_dict"]["model_2"]["training_data"]["systems"] = ( + data_file + ) self.config["training"]["data_dict"]["model_2"]["validation_data"][ "systems" ] = data_file - self.config["training"]["data_dict"]["model_2"][ - "stat_file" - ] = f"{self.stat_files}/model_2" + self.config["training"]["data_dict"]["model_2"]["stat_file"] = ( + f"{self.stat_files}/model_2" + ) self.config["training"]["numb_steps"] = 1 self.config["training"]["save_freq"] = 1 self.config["model"], self.shared_links = preprocess_shared_params( @@ -149,24 +149,24 @@ def setUp(self): self.stat_files = "DPA2" os.makedirs(self.stat_files, exist_ok=True) self.config = multitask_DPA2 - self.config["training"]["data_dict"]["model_1"]["training_data"][ - "systems" - ] = data_file + self.config["training"]["data_dict"]["model_1"]["training_data"]["systems"] = ( + data_file + ) self.config["training"]["data_dict"]["model_1"]["validation_data"][ "systems" ] = data_file - self.config["training"]["data_dict"]["model_1"][ - "stat_file" - ] = f"{self.stat_files}/model_1" - self.config["training"]["data_dict"]["model_2"]["training_data"][ - "systems" - ] = data_file + self.config["training"]["data_dict"]["model_1"]["stat_file"] = ( + f"{self.stat_files}/model_1" + ) + self.config["training"]["data_dict"]["model_2"]["training_data"]["systems"] = ( + data_file + ) self.config["training"]["data_dict"]["model_2"]["validation_data"][ "systems" ] = data_file - self.config["training"]["data_dict"]["model_2"][ - "stat_file" - ] = f"{self.stat_files}/model_2" + self.config["training"]["data_dict"]["model_2"]["stat_file"] = ( + f"{self.stat_files}/model_2" + ) self.config["training"]["numb_steps"] = 1 self.config["training"]["save_freq"] = 1 self.config["model"], self.shared_links = preprocess_shared_params( diff --git a/source/tests/tf/test_init_frz_model_multi.py b/source/tests/tf/test_init_frz_model_multi.py index b723134ca1..b6209a7e69 100644 --- a/source/tests/tf/test_init_frz_model_multi.py +++ b/source/tests/tf/test_init_frz_model_multi.py @@ -64,12 +64,12 @@ def _init_models(): jdata["training"]["data_dict"]["water_ener"] = {} jdata["training"]["data_dict"]["water_ener"]["training_data"] = training_data_config jdata["training"]["data_dict"]["water_ener"]["training_data"]["systems"] = data_file - jdata["training"]["data_dict"]["water_ener"][ - "validation_data" - ] = validation_data_config - jdata["training"]["data_dict"]["water_ener"]["validation_data"][ - "systems" - ] = data_file + jdata["training"]["data_dict"]["water_ener"]["validation_data"] = ( + validation_data_config + ) + jdata["training"]["data_dict"]["water_ener"]["validation_data"]["systems"] = ( + data_file + ) jdata["training"]["save_ckpt"] = ckpt jdata["model"]["fitting_net_dict"] = {} jdata["model"]["fitting_net_dict"]["water_ener"] = fitting_config @@ -98,18 +98,18 @@ def _init_models(): jdata["learning_rate_dict"]["water_ener_new"] = learning_rate_config jdata["training"]["data_dict"] = {} jdata["training"]["data_dict"]["water_ener_new"] = {} - jdata["training"]["data_dict"]["water_ener_new"][ - "training_data" - ] = training_data_config - jdata["training"]["data_dict"]["water_ener_new"]["training_data"][ - "systems" - ] = data_file - jdata["training"]["data_dict"]["water_ener_new"][ - "validation_data" - ] = validation_data_config - jdata["training"]["data_dict"]["water_ener_new"]["validation_data"][ - "systems" - ] = data_file + jdata["training"]["data_dict"]["water_ener_new"]["training_data"] = ( + training_data_config + ) + jdata["training"]["data_dict"]["water_ener_new"]["training_data"]["systems"] = ( + data_file + ) + jdata["training"]["data_dict"]["water_ener_new"]["validation_data"] = ( + validation_data_config + ) + jdata["training"]["data_dict"]["water_ener_new"]["validation_data"]["systems"] = ( + data_file + ) jdata["training"].pop("fitting_weight") jdata = replace_model_params_with_frz_multi_model(jdata, frozen_model) diff --git a/source/tests/tf/test_pairwise_dprc.py b/source/tests/tf/test_pairwise_dprc.py index afe6885542..38b8d8b775 100644 --- a/source/tests/tf/test_pairwise_dprc.py +++ b/source/tests/tf/test_pairwise_dprc.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Test pairwise DPRc features.""" + import json import unittest diff --git a/source/tests/tf/test_virtual_type.py b/source/tests/tf/test_virtual_type.py index e9c675fe3a..a3e87a35ed 100644 --- a/source/tests/tf/test_virtual_type.py +++ b/source/tests/tf/test_virtual_type.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later """Test virtual atomic type.""" + import os import unittest From 571ddece778cdd65c87f1e4a4841d7b3e9776654 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Thu, 14 Mar 2024 00:46:41 +0800 Subject: [PATCH 219/686] fix the bug of empty fitting net neuron. (#3458) fixes #3448 Co-authored-by: Han Wang --- deepmd/dpmodel/utils/network.py | 3 ++- source/tests/pt/model/test_ener_fitting.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/deepmd/dpmodel/utils/network.py b/deepmd/dpmodel/utils/network.py index a0dcc6b706..817448ac50 100644 --- a/deepmd/dpmodel/utils/network.py +++ b/deepmd/dpmodel/utils/network.py @@ -609,7 +609,8 @@ def __init__( resnet_dt=resnet_dt, precision=precision, ) - i_in, i_ot = neuron[-1], out_dim + i_in = neuron[-1] if len(neuron) > 0 else in_dim + i_ot = out_dim self.layers.append( T_NetworkLayer( i_in, diff --git a/source/tests/pt/model/test_ener_fitting.py b/source/tests/pt/model/test_ener_fitting.py index 69bd4b42a3..f63e17c2fa 100644 --- a/source/tests/pt/model/test_ener_fitting.py +++ b/source/tests/pt/model/test_ener_fitting.py @@ -44,12 +44,13 @@ def test_consistency( ) atype = torch.tensor(self.atype_ext[:, :nloc], dtype=int, device=env.DEVICE) - for od, mixed_types, nfp, nap, et in itertools.product( + for od, mixed_types, nfp, nap, et, nn in itertools.product( [1, 3], [True, False], [0, 3], [0, 4], [[], [0], [1]], + [[4, 4, 4], []], ): ft0 = InvarFitting( "foo", @@ -60,6 +61,7 @@ def test_consistency( numb_aparam=nap, mixed_types=mixed_types, exclude_types=et, + neuron=nn, ).to(env.DEVICE) ft1 = DPInvarFitting.deserialize(ft0.serialize()) ft2 = InvarFitting.deserialize(ft0.serialize()) From 487f85c60b18206ad86b5874687083ec3c44c872 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 14 Mar 2024 04:29:33 -0400 Subject: [PATCH 220/686] ci: reduce ASLR entropy (#3461) This week, a random segfault occurred in GHA when using `-fsanitize=leak`. It seems related to https://github.com/actions/runner-images/issues/9491. Workaround: https://github.com/actions/runner-images/issues/9491#issuecomment-1989718917 See also: https://stackoverflow.com/questions/77894856/possible-bug-in-gcc-sanitizers Signed-off-by: Jinzhe Zeng --- .github/workflows/test_cc.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test_cc.yml b/.github/workflows/test_cc.yml index d98f8ca58d..0e2243b75e 100644 --- a/.github/workflows/test_cc.yml +++ b/.github/workflows/test_cc.yml @@ -31,6 +31,10 @@ jobs: run: | wget https://download.pytorch.org/libtorch/cpu/libtorch-cxx11-abi-shared-with-deps-2.1.2%2Bcpu.zip -O libtorch.zip unzip libtorch.zip + # https://github.com/actions/runner-images/issues/9491 + - name: Fix kernel mmap rnd bits + run: sudo sysctl vm.mmap_rnd_bits=28 + if: ${{ matrix.check_memleak }} - run: | export CMAKE_PREFIX_PATH=$GITHUB_WORKSPACE/libtorch source/install/test_cc_local.sh From da866a2c60d1e6e24a9c9c7397b81fab534b9939 Mon Sep 17 00:00:00 2001 From: Chun Cai Date: Fri, 15 Mar 2024 10:30:54 +0800 Subject: [PATCH 221/686] Fix: remove debug output in infer (#3465) Signed-off-by: Chun Cai --- source/api_cc/src/DeepPotPT.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/source/api_cc/src/DeepPotPT.cc b/source/api_cc/src/DeepPotPT.cc index 919d690bed..2c3fd1d865 100644 --- a/source/api_cc/src/DeepPotPT.cc +++ b/source/api_cc/src/DeepPotPT.cc @@ -116,7 +116,6 @@ void DeepPotPT::compute(ENERGYVTYPE& ener, select_real_atoms_coord(dcoord, datype, aparam_, nghost_real, fwd_map, bkw_map, nall_real, nloc_real, coord, atype, aparam, nghost, ntypes, 1, daparam, nall, aparam_nall); - std::cout << datype.size() << std::endl; std::vector coord_wrapped = dcoord; at::Tensor coord_wrapped_Tensor = torch::from_blob(coord_wrapped.data(), {1, nall_real, 3}, options) From d61b152af72143cac3be114786eaaf2789ee6f10 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 14 Mar 2024 23:30:55 -0400 Subject: [PATCH 222/686] fix(tf): fix DeepEval degradation for virtual types (#3464) The energy fitting network computes `nloc` by summing over `natoms[2:]` instead of reading `natoms[0]`. https://github.com/deepmodeling/deepmd-kit/blob/8dab33bbe8248d9f337933f778be3e119948357e/deepmd/tf/fit/ener.py#L717-L720 This causes an issue for the virtual types after refactoring `DeepEval`. Before, `natoms_vec` is `[nloc, nall, nloc, ...]` for mixed types. After refactoring, we use the same `natoms_vec` for mixed_types and the normal case, so the virtual type support is broken. This was not detected by the test, as the test model for the virtual types was added 12 months ago, but the energy fitting was changed 10 months ago. Signed-off-by: Jinzhe Zeng --- deepmd/infer/deep_eval.py | 4 ++++ deepmd/tf/infer/deep_eval.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/deepmd/infer/deep_eval.py b/deepmd/infer/deep_eval.py index 065982a870..aae2082e13 100644 --- a/deepmd/infer/deep_eval.py +++ b/deepmd/infer/deep_eval.py @@ -242,6 +242,10 @@ def _check_mixed_types(self, atom_types: np.ndarray) -> bool: atom_types : np.ndarray The atom types of all frames, in shape nframes * natoms. """ + if np.count_nonzero(atom_types[0] == -1) > 0: + # assume mixed_types if there are virtual types, even when + # the atom types of all frames are the same + return False return np.all(np.equal(atom_types, atom_types[0])) @property diff --git a/deepmd/tf/infer/deep_eval.py b/deepmd/tf/infer/deep_eval.py index b9db0863b5..ccbd44cf97 100644 --- a/deepmd/tf/infer/deep_eval.py +++ b/deepmd/tf/infer/deep_eval.py @@ -489,6 +489,11 @@ def make_natoms_vec( natoms_vec[1] = natoms for ii in range(self.ntypes): natoms_vec[ii + 2] = np.count_nonzero(atom_types[0] == ii) + if np.count_nonzero(atom_types[0] == -1) > 0: + # contains virtual atoms + # energy fitting sums over natoms_vec[2:] instead of reading from natoms_vec[0] + # causing errors for shape mismatch + natoms_vec[2] += np.count_nonzero(atom_types[0] == -1) return natoms_vec def eval_typeebd(self) -> np.ndarray: From 2caf92c6990359dd1d6c599c85f3a894a5a77fc0 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Fri, 15 Mar 2024 15:21:17 +0800 Subject: [PATCH 223/686] Feat: add DOS net (#3452) This PR provides DOS fitting net in Pytorch. Future TODO: - [ ] Loss implementation - [ ] Training/Fine-tuning test - [ ] Jit test - [ ] Doc --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: anyangml --- deepmd/dpmodel/fitting/__init__.py | 4 + deepmd/dpmodel/fitting/dos_fitting.py | 93 +++++ deepmd/dpmodel/fitting/general_fitting.py | 9 +- deepmd/dpmodel/fitting/invar_fitting.py | 6 +- deepmd/infer/deep_dos.py | 5 + deepmd/pt/model/model/__init__.py | 3 +- deepmd/pt/model/model/dos_model.py | 80 +++++ deepmd/pt/model/model/dp_model.py | 8 + deepmd/pt/model/task/dos.py | 128 +++++++ deepmd/pt/train/training.py | 3 + deepmd/tf/fit/dos.py | 121 ++++++- source/tests/consistent/common.py | 4 +- source/tests/consistent/fitting/test_dos.py | 211 +++++++++++ source/tests/pt/dos/data/set.000/atom_dos.npy | Bin 0 -> 352128 bytes source/tests/pt/dos/data/set.000/box.npy | Bin 0 -> 524 bytes source/tests/pt/dos/data/set.000/coord.npy | Bin 0 -> 4352 bytes source/tests/pt/dos/data/set.000/dos.npy | Bin 0 -> 11128 bytes source/tests/pt/dos/data/type.raw | 32 ++ source/tests/pt/dos/data/type_map.raw | 1 + source/tests/pt/dos/input.json | 80 +++++ source/tests/pt/model/test_permutation.py | 29 ++ source/tests/pt/model/test_rot.py | 8 + source/tests/pt/model/test_smooth.py | 9 + source/tests/pt/model/test_trans.py | 8 + source/tests/pt/test_training.py | 18 + source/tests/tf/test_fitting_dos.py | 20 +- source/tests/tf/test_model_dos.py | 335 +++++++++--------- 27 files changed, 1019 insertions(+), 196 deletions(-) create mode 100644 deepmd/dpmodel/fitting/dos_fitting.py create mode 100644 deepmd/pt/model/model/dos_model.py create mode 100644 deepmd/pt/model/task/dos.py create mode 100644 source/tests/consistent/fitting/test_dos.py create mode 100644 source/tests/pt/dos/data/set.000/atom_dos.npy create mode 100644 source/tests/pt/dos/data/set.000/box.npy create mode 100644 source/tests/pt/dos/data/set.000/coord.npy create mode 100644 source/tests/pt/dos/data/set.000/dos.npy create mode 100644 source/tests/pt/dos/data/type.raw create mode 100644 source/tests/pt/dos/data/type_map.raw create mode 100644 source/tests/pt/dos/input.json diff --git a/deepmd/dpmodel/fitting/__init__.py b/deepmd/dpmodel/fitting/__init__.py index 929a63fda7..866a710a3b 100644 --- a/deepmd/dpmodel/fitting/__init__.py +++ b/deepmd/dpmodel/fitting/__init__.py @@ -2,6 +2,9 @@ from .dipole_fitting import ( DipoleFitting, ) +from .dos_fitting import ( + DOSFittingNet, +) from .ener_fitting import ( EnergyFittingNet, ) @@ -21,4 +24,5 @@ "DipoleFitting", "EnergyFittingNet", "PolarFitting", + "DOSFittingNet", ] diff --git a/deepmd/dpmodel/fitting/dos_fitting.py b/deepmd/dpmodel/fitting/dos_fitting.py new file mode 100644 index 0000000000..7c86d392b0 --- /dev/null +++ b/deepmd/dpmodel/fitting/dos_fitting.py @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +from typing import ( + TYPE_CHECKING, + List, + Optional, + Union, +) + +import numpy as np + +from deepmd.dpmodel.common import ( + DEFAULT_PRECISION, +) +from deepmd.dpmodel.fitting.invar_fitting import ( + InvarFitting, +) + +if TYPE_CHECKING: + from deepmd.dpmodel.fitting.general_fitting import ( + GeneralFitting, + ) + +from deepmd.utils.version import ( + check_version_compatibility, +) + + +@InvarFitting.register("dos") +class DOSFittingNet(InvarFitting): + def __init__( + self, + ntypes: int, + dim_descrpt: int, + numb_dos: int = 300, + neuron: List[int] = [120, 120, 120], + resnet_dt: bool = True, + numb_fparam: int = 0, + numb_aparam: int = 0, + bias_dos: Optional[np.ndarray] = None, + rcond: Optional[float] = None, + trainable: Union[bool, List[bool]] = True, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + mixed_types: bool = False, + exclude_types: List[int] = [], + # not used + seed: Optional[int] = None, + ): + if bias_dos is not None: + self.bias_dos = bias_dos + else: + self.bias_dos = np.zeros((ntypes, numb_dos), dtype=DEFAULT_PRECISION) + super().__init__( + var_name="dos", + ntypes=ntypes, + dim_descrpt=dim_descrpt, + dim_out=numb_dos, + neuron=neuron, + resnet_dt=resnet_dt, + bias_atom=bias_dos, + numb_fparam=numb_fparam, + numb_aparam=numb_aparam, + rcond=rcond, + trainable=trainable, + activation_function=activation_function, + precision=precision, + mixed_types=mixed_types, + exclude_types=exclude_types, + ) + + @classmethod + def deserialize(cls, data: dict) -> "GeneralFitting": + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) + data["numb_dos"] = data.pop("dim_out") + data.pop("tot_ener_zero", None) + data.pop("var_name", None) + data.pop("layer_name", None) + data.pop("use_aparam_as_mask", None) + data.pop("spin", None) + data.pop("atom_ener", None) + return super().deserialize(data) + + def serialize(self) -> dict: + """Serialize the fitting to dict.""" + dd = { + **super().serialize(), + "type": "dos", + } + dd["@variables"]["bias_atom_e"] = self.bias_atom_e + + return dd diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index e9dddae2de..3b0d022562 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -40,6 +40,8 @@ class GeneralFitting(NativeOP, BaseFitting): The dimension of the input descriptor. neuron Number of neurons :math:`N` in each hidden layer of the fitting net + bias_atom_e + Average enery per atom for each element. resnet_dt Time-step `dt` in the resnet construction: :math:`y = x + dt * \phi (Wx + b)` @@ -85,6 +87,7 @@ def __init__( resnet_dt: bool = True, numb_fparam: int = 0, numb_aparam: int = 0, + bias_atom_e: Optional[np.ndarray] = None, rcond: Optional[float] = None, tot_ener_zero: bool = False, trainable: Optional[List[bool]] = None, @@ -125,7 +128,11 @@ def __init__( net_dim_out = self._net_out_dim() # init constants - self.bias_atom_e = np.zeros([self.ntypes, net_dim_out]) + if bias_atom_e is None: + self.bias_atom_e = np.zeros([self.ntypes, net_dim_out]) + else: + assert bias_atom_e.shape == (self.ntypes, net_dim_out) + self.bias_atom_e = bias_atom_e if self.numb_fparam > 0: self.fparam_avg = np.zeros(self.numb_fparam) self.fparam_inv_std = np.ones(self.numb_fparam) diff --git a/deepmd/dpmodel/fitting/invar_fitting.py b/deepmd/dpmodel/fitting/invar_fitting.py index f7c091843b..9bf1731830 100644 --- a/deepmd/dpmodel/fitting/invar_fitting.py +++ b/deepmd/dpmodel/fitting/invar_fitting.py @@ -82,6 +82,8 @@ class InvarFitting(GeneralFitting): Number of atomic parameter rcond The condition number for the regression of atomic energy. + bias_atom + Bias for each element. tot_ener_zero Force the total energy to zero. Useful for the charge fitting. trainable @@ -117,10 +119,11 @@ def __init__( resnet_dt: bool = True, numb_fparam: int = 0, numb_aparam: int = 0, + bias_atom: Optional[np.ndarray] = None, rcond: Optional[float] = None, tot_ener_zero: bool = False, trainable: Optional[List[bool]] = None, - atom_ener: Optional[List[float]] = [], + atom_ener: Optional[List[float]] = None, activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, layer_name: Optional[List[Optional[str]]] = None, @@ -152,6 +155,7 @@ def __init__( numb_fparam=numb_fparam, numb_aparam=numb_aparam, rcond=rcond, + bias_atom_e=bias_atom, tot_ener_zero=tot_ener_zero, trainable=trainable, activation_function=activation_function, diff --git a/deepmd/infer/deep_dos.py b/deepmd/infer/deep_dos.py index d95d2a119f..7823f02999 100644 --- a/deepmd/infer/deep_dos.py +++ b/deepmd/infer/deep_dos.py @@ -56,6 +56,11 @@ def output_def(self) -> ModelOutputDef: ) ) + @property + def numb_dos(self) -> int: + """Get the number of DOS.""" + return self.get_numb_dos() + def eval( self, coords: np.ndarray, diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index f93ec88bde..7a2070e476 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -140,7 +140,8 @@ def get_standard_model(model_params): fitting_net["type"] = fitting_net.get("type", "ener") fitting_net["ntypes"] = descriptor.get_ntypes() fitting_net["mixed_types"] = descriptor.mixed_types() - fitting_net["embedding_width"] = descriptor.get_dim_emb() + if fitting_net["type"] in ["dipole", "polar"]: + fitting_net["embedding_width"] = descriptor.get_dim_emb() fitting_net["dim_descrpt"] = descriptor.get_dim_out() grad_force = "direct" not in fitting_net["type"] if not grad_force: diff --git a/deepmd/pt/model/model/dos_model.py b/deepmd/pt/model/model/dos_model.py new file mode 100644 index 0000000000..680eac41f5 --- /dev/null +++ b/deepmd/pt/model/model/dos_model.py @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + Optional, +) + +import torch + +from .dp_model import ( + DPModel, +) + + +class DOSModel(DPModel): + model_type = "dos" + + def __init__( + self, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) + + def forward( + self, + coord, + atype, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + model_ret = self.forward_common( + coord, + atype, + box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + if self.get_fitting_net() is not None: + model_predict = {} + model_predict["atom_dos"] = model_ret["dos"] + model_predict["dos"] = model_ret["dos_redu"] + + if "mask" in model_ret: + model_predict["mask"] = model_ret["mask"] + else: + model_predict = model_ret + model_predict["updated_coord"] += coord + return model_predict + + @torch.jit.export + def forward_lower( + self, + extended_coord, + extended_atype, + nlist, + mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ): + model_ret = self.forward_common_lower( + extended_coord, + extended_atype, + nlist, + mapping, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + if self.get_fitting_net() is not None: + model_predict = {} + model_predict["atom_dos"] = model_ret["dos"] + model_predict["dos"] = model_ret["dos_redu"] + + else: + model_predict = model_ret + return model_predict diff --git a/deepmd/pt/model/model/dp_model.py b/deepmd/pt/model/model/dp_model.py index 138398539a..d7b3c4f4e2 100644 --- a/deepmd/pt/model/model/dp_model.py +++ b/deepmd/pt/model/model/dp_model.py @@ -18,6 +18,9 @@ from deepmd.pt.model.task.dipole import ( DipoleFittingNet, ) +from deepmd.pt.model.task.dos import ( + DOSFittingNet, +) from deepmd.pt.model.task.ener import ( EnergyFittingNet, EnergyFittingNetDirect, @@ -45,6 +48,9 @@ def __new__( from deepmd.pt.model.model.dipole_model import ( DipoleModel, ) + from deepmd.pt.model.model.dos_model import ( + DOSModel, + ) from deepmd.pt.model.model.ener_model import ( EnergyModel, ) @@ -68,6 +74,8 @@ def __new__( cls = DipoleModel elif isinstance(fitting, PolarFittingNet): cls = PolarModel + elif isinstance(fitting, DOSFittingNet): + cls = DOSModel # else: unknown fitting type, fall back to DPModel return super().__new__(cls) diff --git a/deepmd/pt/model/task/dos.py b/deepmd/pt/model/task/dos.py new file mode 100644 index 0000000000..c37b05277a --- /dev/null +++ b/deepmd/pt/model/task/dos.py @@ -0,0 +1,128 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import logging +from typing import ( + List, + Optional, + Union, +) + +import torch + +from deepmd.dpmodel import ( + FittingOutputDef, + OutputVariableDef, +) +from deepmd.pt.model.task.ener import ( + InvarFitting, +) +from deepmd.pt.model.task.fitting import ( + Fitting, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.env import ( + DEFAULT_PRECISION, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, +) +from deepmd.utils.version import ( + check_version_compatibility, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION +device = env.DEVICE + +log = logging.getLogger(__name__) + + +@Fitting.register("dos") +class DOSFittingNet(InvarFitting): + def __init__( + self, + ntypes: int, + dim_descrpt: int, + numb_dos: int = 300, + neuron: List[int] = [128, 128, 128], + resnet_dt: bool = True, + numb_fparam: int = 0, + numb_aparam: int = 0, + rcond: Optional[float] = None, + bias_dos: Optional[torch.Tensor] = None, + trainable: Union[bool, List[bool]] = True, + seed: Optional[int] = None, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + exclude_types: List[int] = [], + mixed_types: bool = True, + ): + if bias_dos is not None: + self.bias_dos = bias_dos + else: + self.bias_dos = torch.zeros( + (ntypes, numb_dos), dtype=dtype, device=env.DEVICE + ) + super().__init__( + var_name="dos", + ntypes=ntypes, + dim_descrpt=dim_descrpt, + dim_out=numb_dos, + neuron=neuron, + bias_atom_e=bias_dos, + resnet_dt=resnet_dt, + numb_fparam=numb_fparam, + numb_aparam=numb_aparam, + activation_function=activation_function, + precision=precision, + mixed_types=mixed_types, + rcond=rcond, + seed=seed, + exclude_types=exclude_types, + trainable=trainable, + ) + + def output_def(self) -> FittingOutputDef: + return FittingOutputDef( + [ + OutputVariableDef( + self.var_name, + [self.dim_out], + reduciable=True, + r_differentiable=False, + c_differentiable=False, + ), + ] + ) + + @classmethod + def deserialize(cls, data: dict) -> "DOSFittingNet": + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) + data.pop("@class", None) + data.pop("var_name", None) + data.pop("tot_ener_zero", None) + data.pop("layer_name", None) + data.pop("use_aparam_as_mask", None) + data.pop("spin", None) + data.pop("atom_ener", None) + data["numb_dos"] = data.pop("dim_out") + obj = super().deserialize(data) + + return obj + + def serialize(self) -> dict: + """Serialize the fitting to dict.""" + # dd = super(InvarFitting, self).serialize() + dd = { + **InvarFitting.serialize(self), + "type": "dos", + "dim_out": self.dim_out, + } + dd["@variables"]["bias_atom_e"] = to_numpy_array(self.bias_atom_e) + + return dd + + # make jit happy with torch 2.0.0 + exclude_types: List[int] diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index b20d80c629..fc293f70ec 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -274,6 +274,9 @@ def get_loss(loss_params, start_lr, _ntypes, _model): if loss_type == "ener": loss_params["starter_learning_rate"] = start_lr return EnergyStdLoss(**loss_params) + elif loss_type == "dos": + loss_params["starter_learning_rate"] = start_lr + raise NotImplementedError() elif loss_type == "ener_spin": loss_params["starter_learning_rate"] = start_lr return EnergySpinLoss(**loss_params) diff --git a/deepmd/tf/fit/dos.py b/deepmd/tf/fit/dos.py index 0cc5a7df62..aef134da92 100644 --- a/deepmd/tf/fit/dos.py +++ b/deepmd/tf/fit/dos.py @@ -46,6 +46,9 @@ from deepmd.utils.out_stat import ( compute_stats_from_redu, ) +from deepmd.utils.version import ( + check_version_compatibility, +) log = logging.getLogger(__name__) @@ -57,8 +60,10 @@ class DOSFitting(Fitting): Parameters ---------- - descrpt - The descrptor :math:`\mathcal{D}` + ntypes + The ntypes of the descrptor :math:`\mathcal{D}` + dim_descrpt + The dimension of the descrptor :math:`\mathcal{D}` neuron Number of neurons :math:`N` in each hidden layer of the fitting net resnet_dt @@ -94,7 +99,8 @@ class DOSFitting(Fitting): def __init__( self, - descrpt: tf.Tensor, + ntypes: int, + dim_descrpt: int, neuron: List[int] = [120, 120, 120], resnet_dt: bool = True, numb_fparam: int = 0, @@ -112,8 +118,8 @@ def __init__( ) -> None: """Constructor.""" # model param - self.ntypes = descrpt.get_ntypes() - self.dim_descrpt = descrpt.get_dim_out() + self.ntypes = ntypes + self.dim_descrpt = dim_descrpt self.use_aparam_as_mask = use_aparam_as_mask self.numb_fparam = numb_fparam @@ -127,6 +133,7 @@ def __init__( self.seed = seed self.uniform_seed = uniform_seed self.seed_shift = one_layer_rand_seed_shift() + self.activation_function = activation_function self.fitting_activation_fn = get_activation_func(activation_function) self.fitting_precision = get_precision(precision) self.trainable = trainable @@ -145,16 +152,16 @@ def __init__( add_data_requirement( "fparam", self.numb_fparam, atomic=False, must=True, high_prec=False ) - self.fparam_avg = None - self.fparam_std = None - self.fparam_inv_std = None + self.fparam_avg = None + self.fparam_std = None + self.fparam_inv_std = None if self.numb_aparam > 0: add_data_requirement( "aparam", self.numb_aparam, atomic=True, must=True, high_prec=False ) - self.aparam_avg = None - self.aparam_std = None - self.aparam_inv_std = None + self.aparam_avg = None + self.aparam_std = None + self.aparam_inv_std = None self.fitting_net_variables = None self.mixed_prec = None @@ -521,7 +528,11 @@ def build( final_layer = tf.reshape( final_layer, - [tf.shape(inputs)[0] * self.numb_dos, natoms[2 + type_i]], + [ + tf.shape(inputs)[0], + natoms[2 + type_i], + self.numb_dos, + ], ) outs_list.append(final_layer) start_index += natoms[2 + type_i] @@ -550,7 +561,8 @@ def build( ) outs = tf.reshape( - final_layer, [tf.shape(inputs)[0] * self.numb_dos, natoms[0]] + final_layer, + [tf.shape(inputs)[0], natoms[0], self.numb_dos], ) # add bias # self.atom_ener_before = outs @@ -562,7 +574,7 @@ def build( # self.atom_ener_after = outs tf.summary.histogram("fitting_net_output", outs) - return tf.reshape(outs, [-1]) + return outs def init_variables( self, @@ -641,3 +653,84 @@ def get_loss(self, loss: dict, lr) -> Loss: return DOSLoss( **loss, starter_learning_rate=lr.start_lr(), numb_dos=self.get_numb_dos() ) + + @classmethod + def deserialize(cls, data: dict, suffix: str = ""): + """Deserialize the model. + + Parameters + ---------- + data : dict + The serialized data + + Returns + ------- + Model + The deserialized model + """ + data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) + data["numb_dos"] = data.pop("dim_out") + fitting = cls(**data) + fitting.fitting_net_variables = cls.deserialize_network( + data["nets"], + suffix=suffix, + ) + fitting.bias_dos = data["@variables"]["bias_atom_e"] + if fitting.numb_fparam > 0: + fitting.fparam_avg = data["@variables"]["fparam_avg"] + fitting.fparam_inv_std = data["@variables"]["fparam_inv_std"] + if fitting.numb_aparam > 0: + fitting.aparam_avg = data["@variables"]["aparam_avg"] + fitting.aparam_inv_std = data["@variables"]["aparam_inv_std"] + return fitting + + def serialize(self, suffix: str = "") -> dict: + """Serialize the model. + + Returns + ------- + dict + The serialized data + """ + data = { + "@class": "Fitting", + "type": "dos", + "@version": 1, + "var_name": "dos", + "ntypes": self.ntypes, + "dim_descrpt": self.dim_descrpt, + # very bad design: type embedding is not passed to the class + # TODO: refactor the class + "mixed_types": False, + "dim_out": self.numb_dos, + "neuron": self.n_neuron, + "resnet_dt": self.resnet_dt, + "numb_fparam": self.numb_fparam, + "numb_aparam": self.numb_aparam, + "rcond": self.rcond, + "trainable": self.trainable, + "activation_function": self.activation_function, + "precision": self.fitting_precision.name, + "exclude_types": [], + "nets": self.serialize_network( + ntypes=self.ntypes, + # TODO: consider type embeddings + ndim=1, + in_dim=self.dim_descrpt + self.numb_fparam + self.numb_aparam, + out_dim=self.numb_dos, + neuron=self.n_neuron, + activation_function=self.activation_function, + resnet_dt=self.resnet_dt, + variables=self.fitting_net_variables, + suffix=suffix, + ), + "@variables": { + "bias_atom_e": self.bias_dos, + "fparam_avg": self.fparam_avg, + "fparam_inv_std": self.fparam_inv_std, + "aparam_avg": self.aparam_avg, + "aparam_inv_std": self.aparam_inv_std, + }, + } + return data diff --git a/source/tests/consistent/common.py b/source/tests/consistent/common.py index 5a35ced0a1..cbcb987c89 100644 --- a/source/tests/consistent/common.py +++ b/source/tests/consistent/common.py @@ -252,7 +252,7 @@ def test_tf_consistent_with_ref(self): tf_obj = self.tf_class.deserialize(data1, suffix=self.unique_id) ret2, data2 = self.get_tf_ret_serialization_from_cls(tf_obj) ret2 = self.extract_ret(ret2, self.RefBackend.TF) - if tf_obj.__class__.__name__.startswith(("Polar", "Dipole")): + if tf_obj.__class__.__name__.startswith(("Polar", "Dipole", "DOS")): # tf, pt serialization mismatch common_keys = set(data1.keys()) & set(data2.keys()) data1 = {k: data1[k] for k in common_keys} @@ -331,7 +331,7 @@ def test_pt_consistent_with_ref(self): ret2 = self.eval_pt(obj) ret2 = self.extract_ret(ret2, self.RefBackend.PT) data2 = obj.serialize() - if obj.__class__.__name__.startswith(("Polar", "Dipole")): + if obj.__class__.__name__.startswith(("Polar", "Dipole", "DOS")): # tf, pt serialization mismatch common_keys = set(data1.keys()) & set(data2.keys()) data1 = {k: data1[k] for k in common_keys} diff --git a/source/tests/consistent/fitting/test_dos.py b/source/tests/consistent/fitting/test_dos.py new file mode 100644 index 0000000000..2832d67641 --- /dev/null +++ b/source/tests/consistent/fitting/test_dos.py @@ -0,0 +1,211 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest +from typing import ( + Any, + Tuple, +) + +import numpy as np + +from deepmd.dpmodel.fitting.dos_fitting import DOSFittingNet as DOSFittingDP +from deepmd.env import ( + GLOBAL_NP_FLOAT_PRECISION, +) + +from ..common import ( + INSTALLED_PT, + INSTALLED_TF, + CommonTest, + parameterized, +) +from .common import ( + FittingTest, +) + +if INSTALLED_PT: + import torch + + from deepmd.pt.model.task.dos import DOSFittingNet as DOSFittingPT + from deepmd.pt.utils.env import DEVICE as PT_DEVICE +else: + DOSFittingPT = object +if INSTALLED_TF: + from deepmd.tf.fit.dos import DOSFitting as DOSFittingTF +else: + DOSFittingTF = object +from deepmd.utils.argcheck import ( + fitting_dos, +) + + +@parameterized( + (True, False), # resnet_dt + ("float64", "float32"), # precision + (True, False), # mixed_types + (0, 1), # numb_fparam + (10, 20), # numb_dos +) +class TestDOS(CommonTest, FittingTest, unittest.TestCase): + @property + def data(self) -> dict: + ( + resnet_dt, + precision, + mixed_types, + numb_fparam, + numb_dos, + ) = self.param + return { + "neuron": [5, 5, 5], + "resnet_dt": resnet_dt, + "precision": precision, + "numb_fparam": numb_fparam, + "seed": 20240217, + "numb_dos": numb_dos, + } + + @property + def skip_tf(self) -> bool: + ( + resnet_dt, + precision, + mixed_types, + numb_fparam, + numb_dos, + ) = self.param + # TODO: mixed_types + return mixed_types or CommonTest.skip_pt + + @property + def skip_pt(self) -> bool: + ( + resnet_dt, + precision, + mixed_types, + numb_fparam, + numb_dos, + ) = self.param + return CommonTest.skip_pt + + tf_class = DOSFittingTF + dp_class = DOSFittingDP + pt_class = DOSFittingPT + args = fitting_dos() + + def setUp(self): + CommonTest.setUp(self) + + self.ntypes = 2 + self.natoms = np.array([6, 6, 2, 4], dtype=np.int32) + self.inputs = np.ones((1, 6, 20), dtype=GLOBAL_NP_FLOAT_PRECISION) + self.atype = np.array([0, 1, 1, 0, 1, 1], dtype=np.int32) + # inconsistent if not sorted + self.atype.sort() + self.fparam = -np.ones((1,), dtype=GLOBAL_NP_FLOAT_PRECISION) + + @property + def addtional_data(self) -> dict: + ( + resnet_dt, + precision, + mixed_types, + numb_fparam, + numb_dos, + ) = self.param + return { + "ntypes": self.ntypes, + "dim_descrpt": self.inputs.shape[-1], + "mixed_types": mixed_types, + } + + def build_tf(self, obj: Any, suffix: str) -> Tuple[list, dict]: + ( + resnet_dt, + precision, + mixed_types, + numb_fparam, + numb_dos, + ) = self.param + return self.build_tf_fitting( + obj, + self.inputs.ravel(), + self.natoms, + self.atype, + self.fparam if numb_fparam else None, + suffix, + ) + + def eval_pt(self, pt_obj: Any) -> Any: + ( + resnet_dt, + precision, + mixed_types, + numb_fparam, + numb_dos, + ) = self.param + return ( + pt_obj( + torch.from_numpy(self.inputs).to(device=PT_DEVICE), + torch.from_numpy(self.atype.reshape(1, -1)).to(device=PT_DEVICE), + fparam=torch.from_numpy(self.fparam).to(device=PT_DEVICE) + if numb_fparam + else None, + )["dos"] + .detach() + .cpu() + .numpy() + ) + + def eval_dp(self, dp_obj: Any) -> Any: + ( + resnet_dt, + precision, + mixed_types, + numb_fparam, + numb_dos, + ) = self.param + return dp_obj( + self.inputs, + self.atype.reshape(1, -1), + fparam=self.fparam if numb_fparam else None, + )["dos"] + + def extract_ret(self, ret: Any, backend) -> Tuple[np.ndarray, ...]: + if backend == self.RefBackend.TF: + # shape is not same + ret = ret[0].reshape(-1, self.natoms[0], 1) + return (ret,) + + @property + def rtol(self) -> float: + """Relative tolerance for comparing the return value.""" + ( + resnet_dt, + precision, + mixed_types, + numb_fparam, + numb_dos, + ) = self.param + if precision == "float64": + return 1e-10 + elif precision == "float32": + return 1e-4 + else: + raise ValueError(f"Unknown precision: {precision}") + + @property + def atol(self) -> float: + """Absolute tolerance for comparing the return value.""" + ( + resnet_dt, + precision, + mixed_types, + numb_fparam, + numb_dos, + ) = self.param + if precision == "float64": + return 1e-10 + elif precision == "float32": + return 1e-4 + else: + raise ValueError(f"Unknown precision: {precision}") diff --git a/source/tests/pt/dos/data/set.000/atom_dos.npy b/source/tests/pt/dos/data/set.000/atom_dos.npy new file mode 100644 index 0000000000000000000000000000000000000000..22809c1068d553bace9c93cfa37e8d8979c56f9e GIT binary patch literal 352128 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 XysFVK{a|pMySEm5Vu{E9TIc@&5cI8x literal 0 HcmV?d00001 diff --git a/source/tests/pt/dos/data/set.000/box.npy b/source/tests/pt/dos/data/set.000/box.npy new file mode 100644 index 0000000000000000000000000000000000000000..6265bf150e5b7b332a05fd8fa91c8d82a7d8376f GIT binary patch literal 524 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 literal 0 HcmV?d00001 diff --git a/source/tests/pt/dos/data/set.000/dos.npy b/source/tests/pt/dos/data/set.000/dos.npy new file mode 100644 index 0000000000000000000000000000000000000000..904b23e70910c3afc43983eb7c6d7899592877ff GIT binary patch literal 11128 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 caMyx9f# None: DPTrainTest.tearDown(self) +@unittest.skip("loss not implemented") +class TestDOSModelSeA(unittest.TestCase, DPTrainTest): + def setUp(self): + input_json = str(Path(__file__).parent / "dos/input.json") + with open(input_json) as f: + self.config = json.load(f) + data_file = [str(Path(__file__).parent / "dos/data/")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_dos) + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + + def tearDown(self) -> None: + DPTrainTest.tearDown(self) + + class TestEnergyZBLModelSeA(unittest.TestCase, DPTrainTest): def setUp(self): input_json = str(Path(__file__).parent / "water/zbl.json") diff --git a/source/tests/tf/test_fitting_dos.py b/source/tests/tf/test_fitting_dos.py index a2a54d6287..f9df5fc126 100644 --- a/source/tests/tf/test_fitting_dos.py +++ b/source/tests/tf/test_fitting_dos.py @@ -59,7 +59,8 @@ def test_fitting(self): descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"].pop("type", None) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = DOSFitting(**jdata["model"]["fitting_net"], uniform_seed=True) # model._compute_dstats([test_data['coord']], [test_data['box']], [test_data['type']], [test_data['natoms_vec']], [test_data['default_mesh']]) @@ -189,21 +190,20 @@ def test_fitting(self): ref_atom_dos_1 = [ -0.32495014, - -0.87979356, - -0.26630668, -0.32495882, - -0.87979767, - -0.2663072, + -0.32496842, + -0.32495892, + -0.32495469, + -0.32496075, ] ref_atom_dos_2 = [ - -0.26630917, 0.21549911, - -0.87979638, - -0.26630564, 0.21550413, - -0.87979585, + 0.21551077, + 0.21550547, + 0.21550303, + 0.21550645, ] places = 4 - np.testing.assert_almost_equal(pred_atom_dos[:, 0], ref_atom_dos_1, places) np.testing.assert_almost_equal(pred_atom_dos[:, 50], ref_atom_dos_2, places) diff --git a/source/tests/tf/test_model_dos.py b/source/tests/tf/test_model_dos.py index d88c81c332..9c01b14e32 100644 --- a/source/tests/tf/test_model_dos.py +++ b/source/tests/tf/test_model_dos.py @@ -66,7 +66,8 @@ def test_model(self): descrpt = DescrptSeA(**jdata["model"]["descriptor"], uniform_seed=True) jdata["model"]["fitting_net"].pop("type", None) - jdata["model"]["fitting_net"]["descrpt"] = descrpt + jdata["model"]["fitting_net"]["ntypes"] = descrpt.get_ntypes() + jdata["model"]["fitting_net"]["dim_descrpt"] = descrpt.get_dim_out() fitting = DOSFitting(**jdata["model"]["fitting_net"], uniform_seed=True) model = DOSModel(descrpt, fitting) @@ -123,106 +124,106 @@ def test_model(self): ref_dos = np.array( [ - -2.98834333, - -0.63166985, - -3.37199568, - -1.88397887, - 0.87560992, - 4.85426159, - -1.22677731, - -0.60918118, - 8.80472675, - -1.12006829, - -3.72653765, - -3.03698828, - 3.50906891, - 5.55140795, - -3.34920924, - -4.43507641, - -6.1729281, - -8.34865917, - 0.14371788, - -4.38078479, - -6.43141133, - 4.07791938, - 7.14102837, - -0.52347718, - 0.82663796, - -1.64225631, - -4.63088421, - 3.3910594, - -9.09682274, - 1.61104204, - 4.45900773, - -2.44688559, - -2.83298183, - -2.00733658, - 7.33444256, - 7.09187373, - -1.97065392, - 0.01623084, - -7.48861264, - -1.17790161, - 2.77126775, - -2.55552037, - 3.3518257, - -0.09316856, - -1.94521413, - 0.50089251, - -2.75763233, - -1.94382637, - 1.30562041, - 5.08351043, - -1.90604837, - -0.80030045, - -4.87093267, - 4.18009666, - -2.9011435, - 2.58497143, - 4.47495176, - -0.9639419, - 8.15692179, - 0.48758731, - -0.62264663, - -1.70677258, - -5.51641378, - 3.98621565, - 0.57749944, - 2.9658081, - -4.10467591, - -7.14827888, - 0.02838605, - -2.48630333, - -4.82178216, - -0.7444178, - 2.48224802, - -1.54683936, - 0.46969412, - -0.0960347, - -2.08290541, - 6.357031, - -3.49716615, - 3.28959028, - 7.83932727, - 1.51457023, - -4.14575033, - 0.02007839, - 4.20953773, - 3.66456664, - -4.67441496, - -0.13296372, - -3.77145766, - 1.49368976, - -2.53627817, - -3.14188618, - 0.24991722, - 0.8770123, - 0.16635733, - -3.15391098, - -3.7733242, - -2.25134676, - 1.00975552, - 1.38717682, + -1.98049388, + -4.58033899, + -6.95508968, + -0.79619016, + 15.58478599, + 2.7636959, + -2.99147438, + -6.94430794, + -1.77877141, + -4.5000298, + -3.12026893, + -8.42191319, + 3.8991195, + 4.85271854, + 8.30541908, + -1.0435944, + -4.42713079, + 19.70011955, + -6.53945284, + 0.85064846, + 4.36868488, + 4.77303801, + 3.00829128, + 0.70043584, + -7.69047143, + -0.0647043, + 4.56830405, + -8.67154404, + -4.64015279, + -7.62202078, + -8.97078455, + -5.19685985, + -1.66080276, + -6.03225716, + -4.06780949, + -0.53046979, + 8.3543131, + -1.84893576, + 2.42669245, + -4.26357086, + -11.33995527, + 10.98529887, + -10.70000829, + -4.50179402, + -1.34978505, + -8.83091676, + -11.85324773, + -3.6305035, + 2.89933807, + 4.65750153, + 1.25464578, + -5.06196944, + 10.05305042, + -1.83868447, + -11.57017913, + -2.03900316, + -3.37235187, + -1.37010554, + -2.93769471, + 0.11905709, + 6.99367431, + 3.48640865, + -4.16242817, + 4.44778342, + -0.98405367, + 1.81581506, + -5.31481686, + 8.72426364, + 4.78954098, + 7.67879332, + -5.00417706, + 0.79717914, + -3.20581567, + -2.96034568, + 6.31165294, + 2.9891188, + -12.2013139, + -13.67496037, + 4.77102881, + 2.71353286, + 6.83849229, + -3.50400312, + 1.3839428, + -5.07550528, + -8.5623218, + 17.64081151, + 6.46051807, + 2.89067584, + 14.23057359, + 17.85941763, + -6.46129295, + -3.43602528, + -3.13520203, + 4.45313732, + -5.23012576, + -2.65929557, + -0.66191939, + 4.47530191, + 9.33992973, + -6.29808733, ] ) @@ -230,104 +231,104 @@ def test_model(self): [ -0.33019322, -0.76332506, - -0.32665648, - -0.76601747, - -1.16441856, - -0.13627609, -1.15916671, -0.13280604, - 2.60139518, - 0.44470952, - -0.48316771, - -1.15926141, 2.59680457, 0.46049936, - -0.29459777, - -0.76433726, - -0.52091744, - -1.39903065, -0.49890317, -1.15747878, - 0.66585524, - 0.81804842, - 1.38592217, - -0.18025826, -0.2964021, -0.74953328, - -0.7427461, - 3.27935087, - -1.09340192, - 0.1462458, -0.51982728, -1.40236941, - 0.73902497, - 0.79969456, - 0.50726592, - 0.11403234, 0.64964525, 0.8084967, - -1.27543102, - -0.00571457, - 0.7748912, - -1.42492251, 1.38371838, -0.17366078, - -0.76119888, - -1.26083707, - -1.48263244, - -0.85698727, -0.7374573, 3.28274006, - -0.27029769, - -1.00478711, - -0.67481511, - -0.07978058, -1.09001574, 0.14173437, - 1.4092343, - -0.31785424, - 0.40551362, - -0.71900495, 0.7269307, 0.79545851, - -1.88407155, - 1.83983772, - -1.78413438, - -0.74852344, 0.50059876, 0.1165872, - -0.2139368, - -1.44989426, - -1.96651281, - -0.6031689, -1.28106632, -0.01107711, - 0.48796663, - 0.76500912, - 0.21308153, - -0.85297893, 0.76139868, -1.44547292, - 1.68105021, - -0.30655702, - -1.93123, - -0.34294737, -0.77352498, -1.26982082, - -0.5562998, - -0.22048683, - -0.48641512, - 0.01124872, -1.49597963, -0.86647985, - 1.17310075, - 0.59402879, - -0.705076, - 0.72991794, -0.27728806, -1.00542829, - -0.16289102, - 0.29464248, + -0.67794229, + -0.08898442, + 1.39205396, + -0.30789099, + 0.40393006, + -0.70982912, + -1.88961087, + 1.830906, + -1.78326071, + -0.75013615, + -0.22537904, + -1.47257916, + -1.9756803, + -0.60493323, + 0.48350014, + 0.77676571, + 0.20885468, + -0.84351691, + 1.67501205, + -0.30662021, + -1.92884376, + -0.34021625, + -0.56212664, + -0.22884438, + -0.4891038, + 0.0199886, + 1.16506594, + 0.58068956, + -0.69376438, + 0.74156043, + -0.16360848, + 0.30303168, + -0.88639571, + 1.453683, + 0.79818052, + 1.2796414, + -0.8335433, + 0.13359098, + -0.53425462, + -0.4939294, + 1.05247266, + 0.49770575, + -2.03320073, + -2.27918678, + 0.79462598, + 0.45187804, + 1.13925239, + -0.58410808, + 0.23092918, + -0.84611213, + -1.42726499, + 2.93985879, + 1.07635712, + 0.48092082, + 2.37197063, + 2.97647126, + -1.07670667, + -0.57300341, + -0.52316403, + 0.74274268, + -0.87188274, + -0.44279998, + -0.11060956, + 0.74619435, + 1.55646754, + -1.05043903, ] ) From 80b690056eec4cd3587a17f4d9ada891ee968318 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Fri, 15 Mar 2024 09:23:40 -0400 Subject: [PATCH 224/686] docs: add deprecation notice for the official conda channel and more conda docs (#3462) Signed-off-by: Jinzhe Zeng Co-authored-by: Chun Cai --- doc/install/build-conda.md | 7 ++++++ doc/install/easy-install-dev.md | 5 ++++ doc/install/easy-install.md | 44 ++++++++++++++++++++------------- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/doc/install/build-conda.md b/doc/install/build-conda.md index 41c9f90a6e..fee9f77acc 100644 --- a/doc/install/build-conda.md +++ b/doc/install/build-conda.md @@ -1,5 +1,12 @@ # Building conda packages +::::{danger} +:::{deprecated} 3.0.0 +The official channel has been deprecated since 3.0.0. +Refer to [conda-forge documentation](https://conda-forge.org/docs/maintainer/adding_pkgs/) for how to contribute and build packages locally. +::: +:::: + One may want to keep both convenience and personalization of the DeePMD-kit. To achieve this goal, one can consider building conda packages. We provide building scripts in [deepmd-kit-recipes organization](https://github.com/deepmd-kit-recipes/). These building tools are driven by [conda-build](https://github.com/conda/conda-build) and [conda-smithy](https://github.com/conda-forge/conda-smithy). For example, if one wants to turn on `MPIIO` package in LAMMPS, go to [`lammps-feedstock`](https://github.com/deepmd-kit-recipes/lammps-feedstock/) repository and modify `recipe/build.sh`. `-D PKG_MPIIO=OFF` should be changed to `-D PKG_MPIIO=ON`. Then go to the main directory and execute diff --git a/doc/install/easy-install-dev.md b/doc/install/easy-install-dev.md index 43ff1c80a5..bb68272ace 100644 --- a/doc/install/easy-install-dev.md +++ b/doc/install/easy-install-dev.md @@ -35,3 +35,8 @@ The [pre-comiled C library](./install-from-c-library.md) can be downloaded from ```sh wget https://nightly.link/deepmodeling/deepmd-kit/workflows/package_c/devel/libdeepmd_c-0-libdeepmd_c.tar.gz.zip && unzip libdeepmd_c-0-libdeepmd_c.tar.gz.zip ``` + +## Pre-release conda-forge packages + +Pre-release conda-forge packages are in `conda-forge/label/deepmd-kit_dev` or `conda-forge/label/deepmd-kit_rc` channels, other than the `conda-forge` channel. +See [conda-forge documentation](https://conda-forge.org/docs/maintainer/knowledge_base/#pre-release-builds) for more information. diff --git a/doc/install/easy-install.md b/doc/install/easy-install.md index e1861a6096..6acfd98cb0 100644 --- a/doc/install/easy-install.md +++ b/doc/install/easy-install.md @@ -6,6 +6,7 @@ After your easy installation, DeePMD-kit (`dp`) and LAMMPS (`lmp`) will be avail :::{note} Note: The off-line packages and conda packages require the [GNU C Library](https://www.gnu.org/software/libc/) 2.17 or above. The GPU version requires [compatible NVIDIA driver](https://docs.nvidia.com/deploy/cuda-compatibility/index.html#minor-version-compatibility) to be installed in advance. It is possible to force conda to [override detection](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-virtual.html#overriding-detected-packages) when installation, but these requirements are still necessary during runtime. +You can refer to [DeepModeling conda FAQ](https://docs.deepmodeling.com/faq/conda.html) for more information. ::: :::{note} @@ -23,7 +24,7 @@ Both CPU and GPU version offline packages are available on [the Releases page](h Some packages are split into two files due to the size limit of GitHub. One may merge them into one after downloading: ```bash -cat deepmd-kit-2.1.1-cuda11.6_gpu-Linux-x86_64.sh.0 deepmd-kit-2.1.1-cuda11.6_gpu-Linux-x86_64.sh.1 > deepmd-kit-2.1.1-cuda11.6_gpu-Linux-x86_64.sh +cat deepmd-kit-2.2.9-cuda118-Linux-x86_64.sh.0 deepmd-kit-2.2.9-cuda118-Linux-x86_64.sh.1 > deepmd-kit-2.2.9-cuda118-Linux-x86_64.sh ``` One may enable the environment using @@ -32,9 +33,29 @@ conda activate /path/to/deepmd-kit ``` ## Install with conda -DeePMD-kit is available with [conda](https://github.com/conda/conda). Install [Anaconda](https://www.anaconda.com/distribution/#download-section) or [Miniconda](https://docs.conda.io/en/latest/miniconda.html) first. +DeePMD-kit is available with [conda](https://github.com/conda/conda). Install [Anaconda](https://www.anaconda.com/distribution/#download-section), [Miniconda](https://docs.conda.io/en/latest/miniconda.html), or [miniforge](https://conda-forge.org/download/) first. +You can refer to [DeepModeling conda FAQ](https://docs.deepmodeling.com/faq/conda.html) for how to setup a conda environment. -### Official channel +### conda-forge channel + +DeePMD-kit is available on the [conda-forge](https://conda-forge.org/) channel: + +```bash +conda create -n deepmd deepmd-kit lammps horovod -c conda-forge +``` + +The supported platforms include Linux x86-64, macOS x86-64, and macOS arm64. +Read [conda-forge FAQ](https://conda-forge.org/docs/user/tipsandtricks.html#installing-cuda-enabled-packages-like-tensorflow-and-pytorch) to learn how to install CUDA-enabled packages. + +### Official channel (deprecated) + +::::{danger} +:::{deprecated} 3.0.0 +The official channel has been deprecated since 3.0.0, due to the challenging work of building dependencies for [multiple backends](../backend.md). +Old packages will still be available at https://conda.deepmodeling.com. +Maintainers will build packages in the conda-forge organization together with other conda-forge members. +::: +:::: One may create an environment that contains the CPU version of DeePMD-kit and LAMMPS: ```bash @@ -47,9 +68,9 @@ conda create -n deepmd deepmd-kit=*=*gpu libdeepmd=*=*gpu lammps cudatoolkit=11. ``` One could change the CUDA Toolkit version from `10.2` or `11.6`. -One may specify the DeePMD-kit version such as `2.1.1` using +One may specify the DeePMD-kit version such as `2.2.9` using ```bash -conda create -n deepmd deepmd-kit=2.1.1=*cpu libdeepmd=2.1.1=*cpu lammps horovod -c https://conda.deepmodeling.com -c defaults +conda create -n deepmd deepmd-kit=2.2.9=*cpu libdeepmd=2.2.9=*cpu lammps horovod -c https://conda.deepmodeling.com -c defaults ``` One may enable the environment using @@ -57,19 +78,8 @@ One may enable the environment using conda activate deepmd ``` -### conda-forge channel - -DeePMD-kit is also available on the [conda-forge](https://conda-forge.org/) channel: - -```bash -conda create -n deepmd deepmd-kit lammps horovod -c conda-forge -``` - -The supported platform includes Linux x86-64, macOS x86-64, and macOS arm64. -Read [conda-forge FAQ](https://conda-forge.org/docs/user/tipsandtricks.html#installing-cuda-enabled-packages-like-tensorflow-and-pytorch) to learn how to install CUDA-enabled packages. - ## Install with docker -A docker for installing the DeePMD-kit is available [here](https://github.com/orgs/deepmodeling/packages/container/package/deepmd-kit). +A docker for installing the DeePMD-kit is available [here](https://github.com/deepmodeling/deepmd-kit/pkgs/container/deepmd-kit). To pull the CPU version: ```bash From 39cb4d17f3fe7d37606b988fd0dee7ed3ec7da2a Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Fri, 15 Mar 2024 23:47:56 +0800 Subject: [PATCH 225/686] Chore: Move InvarFitting (#3468) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/pt/model/task/ener.py | 183 +--------------------- deepmd/pt/model/task/invar_fitting.py | 212 ++++++++++++++++++++++++++ 2 files changed, 215 insertions(+), 180 deletions(-) create mode 100644 deepmd/pt/model/task/invar_fitting.py diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index b58b0c9b19..12c0917dd2 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -2,11 +2,9 @@ import copy import logging from typing import ( - Callable, List, Optional, Tuple, - Union, ) import numpy as np @@ -24,18 +22,15 @@ Fitting, GeneralFitting, ) +from deepmd.pt.model.task.invar_fitting import ( + InvarFitting, +) from deepmd.pt.utils import ( env, ) from deepmd.pt.utils.env import ( DEFAULT_PRECISION, ) -from deepmd.pt.utils.stat import ( - compute_output_stats, -) -from deepmd.utils.path import ( - DPPath, -) from deepmd.utils.version import ( check_version_compatibility, ) @@ -46,178 +41,6 @@ log = logging.getLogger(__name__) -@GeneralFitting.register("invar") -@fitting_check_output -class InvarFitting(GeneralFitting): - """Construct a fitting net for energy. - - Parameters - ---------- - var_name : str - The atomic property to fit, 'energy', 'dipole', and 'polar'. - ntypes : int - Element count. - dim_descrpt : int - Embedding width per atom. - dim_out : int - The output dimension of the fitting net. - neuron : List[int] - Number of neurons in each hidden layers of the fitting net. - bias_atom_e : torch.Tensor, optional - Average enery per atom for each element. - resnet_dt : bool - Using time-step in the ResNet construction. - numb_fparam : int - Number of frame parameters. - numb_aparam : int - Number of atomic parameters. - activation_function : str - Activation function. - precision : str - Numerical precision. - mixed_types : bool - If true, use a uniform fitting net for all atom types, otherwise use - different fitting nets for different atom types. - rcond : float, optional - The condition number for the regression of atomic energy. - seed : int, optional - Random seed. - exclude_types: List[int] - Atomic contributions of the excluded atom types are set zero. - atom_ener: List[float], optional - Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. - - """ - - def __init__( - self, - var_name: str, - ntypes: int, - dim_descrpt: int, - dim_out: int, - neuron: List[int] = [128, 128, 128], - bias_atom_e: Optional[torch.Tensor] = None, - resnet_dt: bool = True, - numb_fparam: int = 0, - numb_aparam: int = 0, - activation_function: str = "tanh", - precision: str = DEFAULT_PRECISION, - mixed_types: bool = True, - rcond: Optional[float] = None, - seed: Optional[int] = None, - exclude_types: List[int] = [], - atom_ener: Optional[List[float]] = None, - **kwargs, - ): - self.dim_out = dim_out - self.atom_ener = atom_ener - super().__init__( - var_name=var_name, - ntypes=ntypes, - dim_descrpt=dim_descrpt, - neuron=neuron, - bias_atom_e=bias_atom_e, - resnet_dt=resnet_dt, - numb_fparam=numb_fparam, - numb_aparam=numb_aparam, - activation_function=activation_function, - precision=precision, - mixed_types=mixed_types, - rcond=rcond, - seed=seed, - exclude_types=exclude_types, - remove_vaccum_contribution=None - if atom_ener is None or len([x for x in atom_ener if x is not None]) == 0 - else [x is not None for x in atom_ener], - **kwargs, - ) - - def _net_out_dim(self): - """Set the FittingNet output dim.""" - return self.dim_out - - def serialize(self) -> dict: - data = super().serialize() - data["type"] = "invar" - data["dim_out"] = self.dim_out - data["atom_ener"] = self.atom_ener - return data - - @classmethod - def deserialize(cls, data: dict) -> "GeneralFitting": - data = copy.deepcopy(data) - check_version_compatibility(data.pop("@version", 1), 1, 1) - return super().deserialize(data) - - def compute_output_stats( - self, - merged: Union[Callable[[], List[dict]], List[dict]], - stat_file_path: Optional[DPPath] = None, - ): - """ - Compute the output statistics (e.g. energy bias) for the fitting net from packed data. - - Parameters - ---------- - merged : Union[Callable[[], List[dict]], List[dict]] - - List[dict]: A list of data samples from various data systems. - Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` - originating from the `i`-th data system. - - Callable[[], List[dict]]: A lazy function that returns data samples in the above format - only when needed. Since the sampling process can be slow and memory-intensive, - the lazy function helps by only sampling once. - stat_file_path : Optional[DPPath] - The path to the stat file. - - """ - bias_atom_e = compute_output_stats( - merged, self.ntypes, stat_file_path, self.rcond, self.atom_ener - ) - self.bias_atom_e.copy_( - torch.tensor(bias_atom_e, device=env.DEVICE).view( - [self.ntypes, self.dim_out] - ) - ) - - def output_def(self) -> FittingOutputDef: - return FittingOutputDef( - [ - OutputVariableDef( - self.var_name, - [self.dim_out], - reduciable=True, - r_differentiable=True, - c_differentiable=True, - ), - ] - ) - - def forward( - self, - descriptor: torch.Tensor, - atype: torch.Tensor, - gr: Optional[torch.Tensor] = None, - g2: Optional[torch.Tensor] = None, - h2: Optional[torch.Tensor] = None, - fparam: Optional[torch.Tensor] = None, - aparam: Optional[torch.Tensor] = None, - ): - """Based on embedding net output, alculate total energy. - - Args: - - inputs: Embedding matrix. Its shape is [nframes, natoms[0], self.dim_descrpt]. - - natoms: Tell atom count and element count. Its shape is [2+self.ntypes]. - - Returns - ------- - - `torch.Tensor`: Total energy with shape [nframes, natoms[0]]. - """ - return self._forward_common(descriptor, atype, gr, g2, h2, fparam, aparam) - - # make jit happy with torch 2.0.0 - exclude_types: List[int] - - @Fitting.register("ener") class EnergyFittingNet(InvarFitting): def __init__( diff --git a/deepmd/pt/model/task/invar_fitting.py b/deepmd/pt/model/task/invar_fitting.py new file mode 100644 index 0000000000..1699b440ac --- /dev/null +++ b/deepmd/pt/model/task/invar_fitting.py @@ -0,0 +1,212 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import logging +from typing import ( + Callable, + List, + Optional, + Union, +) + +import torch + +from deepmd.dpmodel import ( + FittingOutputDef, + OutputVariableDef, + fitting_check_output, +) +from deepmd.pt.model.task.fitting import ( + GeneralFitting, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.env import ( + DEFAULT_PRECISION, +) +from deepmd.pt.utils.stat import ( + compute_output_stats, +) +from deepmd.utils.path import ( + DPPath, +) +from deepmd.utils.version import ( + check_version_compatibility, +) + +dtype = env.GLOBAL_PT_FLOAT_PRECISION +device = env.DEVICE + +log = logging.getLogger(__name__) + + +@GeneralFitting.register("invar") +@fitting_check_output +class InvarFitting(GeneralFitting): + """Construct a fitting net for energy. + + Parameters + ---------- + var_name : str + The atomic property to fit, 'energy', 'dipole', and 'polar'. + ntypes : int + Element count. + dim_descrpt : int + Embedding width per atom. + dim_out : int + The output dimension of the fitting net. + neuron : List[int] + Number of neurons in each hidden layers of the fitting net. + bias_atom_e : torch.Tensor, optional + Average enery per atom for each element. + resnet_dt : bool + Using time-step in the ResNet construction. + numb_fparam : int + Number of frame parameters. + numb_aparam : int + Number of atomic parameters. + activation_function : str + Activation function. + precision : str + Numerical precision. + mixed_types : bool + If true, use a uniform fitting net for all atom types, otherwise use + different fitting nets for different atom types. + rcond : float, optional + The condition number for the regression of atomic energy. + seed : int, optional + Random seed. + exclude_types: List[int] + Atomic contributions of the excluded atom types are set zero. + atom_ener: List[float], optional + Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. + + """ + + def __init__( + self, + var_name: str, + ntypes: int, + dim_descrpt: int, + dim_out: int, + neuron: List[int] = [128, 128, 128], + bias_atom_e: Optional[torch.Tensor] = None, + resnet_dt: bool = True, + numb_fparam: int = 0, + numb_aparam: int = 0, + activation_function: str = "tanh", + precision: str = DEFAULT_PRECISION, + mixed_types: bool = True, + rcond: Optional[float] = None, + seed: Optional[int] = None, + exclude_types: List[int] = [], + atom_ener: Optional[List[float]] = None, + **kwargs, + ): + self.dim_out = dim_out + self.atom_ener = atom_ener + super().__init__( + var_name=var_name, + ntypes=ntypes, + dim_descrpt=dim_descrpt, + neuron=neuron, + bias_atom_e=bias_atom_e, + resnet_dt=resnet_dt, + numb_fparam=numb_fparam, + numb_aparam=numb_aparam, + activation_function=activation_function, + precision=precision, + mixed_types=mixed_types, + rcond=rcond, + seed=seed, + exclude_types=exclude_types, + remove_vaccum_contribution=None + if atom_ener is None or len([x for x in atom_ener if x is not None]) == 0 + else [x is not None for x in atom_ener], + **kwargs, + ) + + def _net_out_dim(self): + """Set the FittingNet output dim.""" + return self.dim_out + + def serialize(self) -> dict: + data = super().serialize() + data["type"] = "invar" + data["dim_out"] = self.dim_out + data["atom_ener"] = self.atom_ener + return data + + @classmethod + def deserialize(cls, data: dict) -> "GeneralFitting": + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) + return super().deserialize(data) + + def compute_output_stats( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + stat_file_path: Optional[DPPath] = None, + ): + """ + Compute the output statistics (e.g. energy bias) for the fitting net from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + stat_file_path : Optional[DPPath] + The path to the stat file. + + """ + bias_atom_e = compute_output_stats( + merged, self.ntypes, stat_file_path, self.rcond, self.atom_ener + ) + self.bias_atom_e.copy_( + torch.tensor(bias_atom_e, device=env.DEVICE).view( + [self.ntypes, self.dim_out] + ) + ) + + def output_def(self) -> FittingOutputDef: + return FittingOutputDef( + [ + OutputVariableDef( + self.var_name, + [self.dim_out], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + ), + ] + ) + + def forward( + self, + descriptor: torch.Tensor, + atype: torch.Tensor, + gr: Optional[torch.Tensor] = None, + g2: Optional[torch.Tensor] = None, + h2: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + ): + """Based on embedding net output, alculate total energy. + + Args: + - inputs: Embedding matrix. Its shape is [nframes, natoms[0], self.dim_descrpt]. + - natoms: Tell atom count and element count. Its shape is [2+self.ntypes]. + + Returns + ------- + - `torch.Tensor`: Total energy with shape [nframes, natoms[0]]. + """ + return self._forward_common(descriptor, atype, gr, g2, h2, fparam, aparam) + + # make jit happy with torch 2.0.0 + exclude_types: List[int] From 4b3a77b77e7e97182ea515f87dd4311010e31149 Mon Sep 17 00:00:00 2001 From: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> Date: Sat, 16 Mar 2024 16:38:41 +0800 Subject: [PATCH 226/686] Feat: support virtual atom (#3469) - support virtual atoms. the atoms with type -1 will be treated as virtual. - the atomic contribution of virtual atoms is zero - provide ret_dict["mask"] to indicate which atomic contribution is real (==1) or virtual (==0) --------- Co-authored-by: Han Wang --- .../dpmodel/atomic_model/base_atomic_model.py | 82 +++++++++----- .../atomic_model/make_base_atomic_model.py | 22 ++++ deepmd/dpmodel/utils/nlist.py | 22 +++- .../model/atomic_model/base_atomic_model.py | 105 +++++++++++++----- deepmd/pt/utils/nlist.py | 24 +++- .../dpmodel/case_single_frame_with_nlist.py | 50 +++++++++ .../common/dpmodel/test_dp_atomic_model.py | 53 ++++++++- source/tests/common/dpmodel/test_dp_model.py | 11 +- source/tests/common/dpmodel/test_nlist.py | 17 +-- source/tests/pt/model/test_dp_atomic_model.py | 53 ++++++++- source/tests/pt/model/test_dp_model.py | 10 +- source/tests/pt/model/test_env_mat.py | 50 +++++++++ source/tests/pt/model/test_nlist.py | 23 ++-- 13 files changed, 431 insertions(+), 91 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/base_atomic_model.py b/deepmd/dpmodel/atomic_model/base_atomic_model.py index 990847c1de..42d1e67138 100644 --- a/deepmd/dpmodel/atomic_model/base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/base_atomic_model.py @@ -56,22 +56,19 @@ def reinit_pair_exclude( def atomic_output_def(self) -> FittingOutputDef: old_def = self.fitting_output_def() - if self.atom_excl is None: - return old_def - else: - old_list = list(old_def.get_data().values()) - return FittingOutputDef( - old_list # noqa:RUF005 - + [ - OutputVariableDef( - name="mask", - shape=[1], - reduciable=False, - r_differentiable=False, - c_differentiable=False, - ) - ] - ) + old_list = list(old_def.get_data().values()) + return FittingOutputDef( + old_list # noqa:RUF005 + + [ + OutputVariableDef( + name="mask", + shape=[1], + reduciable=False, + r_differentiable=False, + c_differentiable=False, + ) + ] + ) def forward_common_atomic( self, @@ -82,6 +79,37 @@ def forward_common_atomic( fparam: Optional[np.ndarray] = None, aparam: Optional[np.ndarray] = None, ) -> Dict[str, np.ndarray]: + """Common interface for atomic inference. + + This method accept extended coordinates, extended atom typs, neighbor list, + and predict the atomic contribution of the fit property. + + Parameters + ---------- + extended_coord + extended coodinates, shape: nf x (nall x 3) + extended_atype + extended atom typs, shape: nf x nall + for a type < 0 indicating the atomic is virtual. + nlist + neighbor list, shape: nf x nloc x nsel + mapping + extended to local index mapping, shape: nf x nall + fparam + frame parameters, shape: nf x dim_fparam + aparam + atomic parameter, shape: nf x nloc x dim_aparam + + Returns + ------- + ret_dict + dict of output atomic properties. + should implement the definition of `fitting_output_def`. + ret_dict["mask"] of shape nf x nloc will be provided. + ret_dict["mask"][ff,ii] == 1 indicating the ii-th atom of the ff-th frame is real. + ret_dict["mask"][ff,ii] == 0 indicating the ii-th atom of the ff-th frame is virtual. + + """ _, nloc, _ = nlist.shape atype = extended_atype[:, :nloc] if self.pair_excl is not None: @@ -89,24 +117,28 @@ def forward_common_atomic( # exclude neighbors in the nlist nlist = np.where(pair_mask == 1, nlist, -1) + ext_atom_mask = self.make_atom_mask(extended_atype) ret_dict = self.forward_atomic( extended_coord, - extended_atype, + np.where(ext_atom_mask, extended_atype, 0), nlist, mapping=mapping, fparam=fparam, aparam=aparam, ) + # nf x nloc + atom_mask = ext_atom_mask[:, :nloc].astype(np.int32) if self.atom_excl is not None: - atom_mask = self.atom_excl.build_type_exclude_mask(atype) - for kk in ret_dict.keys(): - out_shape = ret_dict[kk].shape - ret_dict[kk] = ( - ret_dict[kk].reshape([out_shape[0], out_shape[1], -1]) - * atom_mask[:, :, None] - ).reshape(out_shape) - ret_dict["mask"] = atom_mask + atom_mask *= self.atom_excl.build_type_exclude_mask(atype) + + for kk in ret_dict.keys(): + out_shape = ret_dict[kk].shape + ret_dict[kk] = ( + ret_dict[kk].reshape([out_shape[0], out_shape[1], -1]) + * atom_mask[:, :, None] + ).reshape(out_shape) + ret_dict["mask"] = atom_mask return ret_dict diff --git a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py index dfbb4e435a..936c2b0943 100644 --- a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py @@ -136,6 +136,28 @@ def serialize(self) -> dict: def deserialize(cls, data: dict): pass + def make_atom_mask( + self, + atype: t_tensor, + ) -> t_tensor: + """The atoms with type < 0 are treated as virutal atoms, + which serves as place-holders for multi-frame calculations + with different number of atoms in different frames. + + Parameters + ---------- + atype + Atom types. >= 0 for real atoms <0 for virtual atoms. + + Returns + ------- + mask + True for real atoms and False for virutal atoms. + + """ + # supposed to be supported by all backends + return atype >= 0 + def do_grad_r( self, var_name: Optional[str] = None, diff --git a/deepmd/dpmodel/utils/nlist.py b/deepmd/dpmodel/utils/nlist.py index e5631bf2e3..ca8b18023b 100644 --- a/deepmd/dpmodel/utils/nlist.py +++ b/deepmd/dpmodel/utils/nlist.py @@ -15,7 +15,7 @@ ## translated from torch implemantation by chatgpt def build_neighbor_list( - coord1: np.ndarray, + coord: np.ndarray, atype: np.ndarray, nloc: int, rcut: float, @@ -26,10 +26,11 @@ def build_neighbor_list( Parameters ---------- - coord1 : np.ndarray + coord : np.ndarray exptended coordinates of shape [batch_size, nall x 3] atype : np.ndarray extended atomic types of shape [batch_size, nall] + type < 0 the atom is treat as virtual atoms. nloc : int number of local atoms. rcut : float @@ -54,11 +55,20 @@ def build_neighbor_list( if distinguish_types==True and we have two types |---- nsel[0] -----| |---- nsel[1] -----| xx xx xx xx -1 -1 -1 xx xx xx -1 -1 -1 -1 + For virtual atoms all neighboring positions are filled with -1. """ - batch_size = coord1.shape[0] - coord1 = coord1.reshape(batch_size, -1) - nall = coord1.shape[1] // 3 + batch_size = coord.shape[0] + coord = coord.reshape(batch_size, -1) + nall = coord.shape[1] // 3 + # fill virtual atoms with large coords so they are not neighbors of any + # real atom. + xmax = np.max(coord) + 2.0 * rcut + # nf x nall + is_vir = atype < 0 + coord1 = np.where(is_vir[:, :, None], xmax, coord.reshape(-1, nall, 3)).reshape( + -1, nall * 3 + ) if isinstance(sel, int): sel = [sel] nsel = sum(sel) @@ -88,7 +98,7 @@ def build_neighbor_list( axis=-1, ) assert list(nlist.shape) == [batch_size, nloc, nsel] - nlist = np.where((rr > rcut), -1, nlist) + nlist = np.where(np.logical_or((rr > rcut), is_vir[:, :nloc, None]), -1, nlist) if distinguish_types: return nlist_distinguish_types(nlist, atype, sel) diff --git a/deepmd/pt/model/atomic_model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py index d3a1cfb459..c921538203 100644 --- a/deepmd/pt/model/atomic_model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -58,24 +58,44 @@ def reinit_pair_exclude( else: self.pair_excl = PairExcludeMask(self.get_ntypes(), self.pair_exclude_types) + # to make jit happy... + def make_atom_mask( + self, + atype: torch.Tensor, + ) -> torch.Tensor: + """The atoms with type < 0 are treated as virutal atoms, + which serves as place-holders for multi-frame calculations + with different number of atoms in different frames. + + Parameters + ---------- + atype + Atom types. >= 0 for real atoms <0 for virtual atoms. + + Returns + ------- + mask + True for real atoms and False for virutal atoms. + + """ + # supposed to be supported by all backends + return atype >= 0 + def atomic_output_def(self) -> FittingOutputDef: old_def = self.fitting_output_def() - if self.atom_excl is None: - return old_def - else: - old_list = list(old_def.get_data().values()) - return FittingOutputDef( - old_list # noqa:RUF005 - + [ - OutputVariableDef( - name="mask", - shape=[1], - reduciable=False, - r_differentiable=False, - c_differentiable=False, - ) - ] - ) + old_list = list(old_def.get_data().values()) + return FittingOutputDef( + old_list # noqa:RUF005 + + [ + OutputVariableDef( + name="mask", + shape=[1], + reduciable=False, + r_differentiable=False, + c_differentiable=False, + ) + ] + ) def forward_common_atomic( self, @@ -86,6 +106,37 @@ def forward_common_atomic( fparam: Optional[torch.Tensor] = None, aparam: Optional[torch.Tensor] = None, ) -> Dict[str, torch.Tensor]: + """Common interface for atomic inference. + + This method accept extended coordinates, extended atom typs, neighbor list, + and predict the atomic contribution of the fit property. + + Parameters + ---------- + extended_coord + extended coodinates, shape: nf x (nall x 3) + extended_atype + extended atom typs, shape: nf x nall + for a type < 0 indicating the atomic is virtual. + nlist + neighbor list, shape: nf x nloc x nsel + mapping + extended to local index mapping, shape: nf x nall + fparam + frame parameters, shape: nf x dim_fparam + aparam + atomic parameter, shape: nf x nloc x dim_aparam + + Returns + ------- + ret_dict + dict of output atomic properties. + should implement the definition of `fitting_output_def`. + ret_dict["mask"] of shape nf x nloc will be provided. + ret_dict["mask"][ff,ii] == 1 indicating the ii-th atom of the ff-th frame is real. + ret_dict["mask"][ff,ii] == 0 indicating the ii-th atom of the ff-th frame is virtual. + + """ _, nloc, _ = nlist.shape atype = extended_atype[:, :nloc] @@ -94,24 +145,28 @@ def forward_common_atomic( # exclude neighbors in the nlist nlist = torch.where(pair_mask == 1, nlist, -1) + ext_atom_mask = self.make_atom_mask(extended_atype) ret_dict = self.forward_atomic( extended_coord, - extended_atype, + torch.where(ext_atom_mask, extended_atype, 0), nlist, mapping=mapping, fparam=fparam, aparam=aparam, ) + # nf x nloc + atom_mask = ext_atom_mask[:, :nloc].to(torch.int32) if self.atom_excl is not None: - atom_mask = self.atom_excl(atype) - for kk in ret_dict.keys(): - out_shape = ret_dict[kk].shape - ret_dict[kk] = ( - ret_dict[kk].reshape([out_shape[0], out_shape[1], -1]) - * atom_mask[:, :, None] - ).reshape(out_shape) - ret_dict["mask"] = atom_mask + atom_mask *= self.atom_excl(atype) + + for kk in ret_dict.keys(): + out_shape = ret_dict[kk].shape + ret_dict[kk] = ( + ret_dict[kk].reshape([out_shape[0], out_shape[1], -1]) + * atom_mask[:, :, None] + ).view(out_shape) + ret_dict["mask"] = atom_mask return ret_dict diff --git a/deepmd/pt/utils/nlist.py b/deepmd/pt/utils/nlist.py index 7e92f44e8d..cdee6e3722 100644 --- a/deepmd/pt/utils/nlist.py +++ b/deepmd/pt/utils/nlist.py @@ -51,7 +51,7 @@ def extend_input_and_build_neighbor_list( def build_neighbor_list( - coord1: torch.Tensor, + coord: torch.Tensor, atype: torch.Tensor, nloc: int, rcut: float, @@ -62,10 +62,11 @@ def build_neighbor_list( Parameters ---------- - coord1 : torch.Tensor + coord : torch.Tensor exptended coordinates of shape [batch_size, nall x 3] atype : torch.Tensor extended atomic types of shape [batch_size, nall] + if type < 0 the atom is treat as virtual atoms. nloc : int number of local atoms. rcut : float @@ -90,11 +91,20 @@ def build_neighbor_list( if distinguish_types==True and we have two types |---- nsel[0] -----| |---- nsel[1] -----| xx xx xx xx -1 -1 -1 xx xx xx -1 -1 -1 -1 + For virtual atoms all neighboring positions are filled with -1. """ - batch_size = coord1.shape[0] - coord1 = coord1.view(batch_size, -1) - nall = coord1.shape[1] // 3 + batch_size = coord.shape[0] + coord = coord.view(batch_size, -1) + nall = coord.shape[1] // 3 + # fill virtual atoms with large coords so they are not neighbors of any + # real atom. + xmax = torch.max(coord) + 2.0 * rcut + # nf x nall + is_vir = atype < 0 + coord1 = torch.where(is_vir[:, :, None], xmax, coord.view(-1, nall, 3)).view( + -1, nall * 3 + ) if isinstance(sel, int): sel = [sel] nsel = sum(sel) @@ -133,7 +143,9 @@ def build_neighbor_list( dim=-1, ) assert list(nlist.shape) == [batch_size, nloc, nsel] - nlist = nlist.masked_fill((rr > rcut), -1) + nlist = torch.where( + torch.logical_or((rr > rcut), is_vir[:, :nloc, None]), -1, nlist + ) if distinguish_types: return nlist_distinguish_types(nlist, atype, sel) diff --git a/source/tests/common/dpmodel/case_single_frame_with_nlist.py b/source/tests/common/dpmodel/case_single_frame_with_nlist.py index c260a18527..828e090cad 100644 --- a/source/tests/common/dpmodel/case_single_frame_with_nlist.py +++ b/source/tests/common/dpmodel/case_single_frame_with_nlist.py @@ -72,3 +72,53 @@ def setUp(self): nlist1 = inv_perm[nlist1] nlist1 = np.where(mask, -1, nlist1) self.nlist = np.concatenate([self.nlist, nlist1], axis=0) + + +class TestCaseSingleFrameWithNlistWithVirtual: + def setUp(self): + # nloc == 3, nall == 4 + self.nloc = 4 + self.nall = 5 + self.nf, self.nt = 2, 2 + self.coord_ext = np.array( + [ + [0, 0, 0], + [0, 0, 0], + [0, 1, 0], + [0, 0, 1], + [0, -2, 0], + ], + dtype=np.float64, + ).reshape([1, self.nall, 3]) + self.atype_ext = np.array([0, -1, 0, 1, 0], dtype=int).reshape([1, self.nall]) + # sel = [5, 2] + self.sel = [5, 2] + self.nlist = np.array( + [ + [2, 4, -1, -1, -1, 3, -1], + [-1, -1, -1, -1, -1, -1, -1], + [0, -1, -1, -1, -1, 3, -1], + [0, 2, -1, -1, -1, -1, -1], + ], + dtype=int, + ).reshape([1, self.nloc, sum(self.sel)]) + self.rcut = 2.2 + self.rcut_smth = 0.4 + # permutations + self.perm = np.array([3, 0, 1, 2, 4], dtype=np.int32) + inv_perm = np.argsort(self.perm) + # permute the coord and atype + self.coord_ext = np.concatenate( + [self.coord_ext, self.coord_ext[:, self.perm, :]], axis=0 + ).reshape(self.nf, self.nall * 3) + self.atype_ext = np.concatenate( + [self.atype_ext, self.atype_ext[:, self.perm]], axis=0 + ) + # permute the nlist + nlist1 = self.nlist[:, self.perm[: self.nloc], :] + mask = nlist1 == -1 + nlist1 = inv_perm[nlist1] + nlist1 = np.where(mask, -1, nlist1) + self.nlist = np.concatenate([self.nlist, nlist1], axis=0) + self.get_real_mapping = np.array([[0, 2, 3], [0, 1, 3]], dtype=np.int32) + self.atol = 1e-12 diff --git a/source/tests/common/dpmodel/test_dp_atomic_model.py b/source/tests/common/dpmodel/test_dp_atomic_model.py index ac49280b82..c69de6161d 100644 --- a/source/tests/common/dpmodel/test_dp_atomic_model.py +++ b/source/tests/common/dpmodel/test_dp_atomic_model.py @@ -16,6 +16,7 @@ from .case_single_frame_with_nlist import ( TestCaseSingleFrameWithNlist, + TestCaseSingleFrameWithNlistWithVirtual, ) @@ -92,10 +93,8 @@ def test_excl_consistency(self): # check output def out_names = [vv.name for vv in md0.atomic_output_def().get_data().values()] - if atom_excl == []: - self.assertEqual(out_names, ["energy"]) - else: - self.assertEqual(out_names, ["energy", "mask"]) + self.assertEqual(out_names, ["energy", "mask"]) + if atom_excl != []: for ii in md0.atomic_output_def().get_data().values(): if ii.name == "mask": self.assertEqual(ii.shape, [1]) @@ -115,3 +114,49 @@ def test_excl_consistency(self): np.testing.assert_array_equal(ret0["mask"], expected) else: raise ValueError(f"not expected atom_excl {atom_excl}") + + +class TestDPAtomicModelVirtualConsistency(unittest.TestCase): + def setUp(self): + self.case0 = TestCaseSingleFrameWithNlist() + self.case1 = TestCaseSingleFrameWithNlistWithVirtual() + self.case0.setUp() + self.case1.setUp() + + def test_virtual_consistency(self): + nf, _, _ = self.case0.nlist.shape + ds = DescrptSeA( + self.case0.rcut, + self.case0.rcut_smth, + self.case0.sel, + ) + ft = InvarFitting( + "energy", + self.case0.nt, + ds.get_dim_out(), + 1, + mixed_types=ds.mixed_types(), + ) + type_map = ["foo", "bar"] + md1 = DPAtomicModel(ds, ft, type_map=type_map) + + args0 = [self.case0.coord_ext, self.case0.atype_ext, self.case0.nlist] + # args0 = [np.array(ii) for ii in args0] + args1 = [self.case1.coord_ext, self.case1.atype_ext, self.case1.nlist] + # args1 = [np.array(ii) for ii in args1] + + ret0 = md1.forward_common_atomic(*args0) + ret1 = md1.forward_common_atomic(*args1) + + for dd in range(self.case0.nf): + np.testing.assert_allclose( + ret0["energy"][dd], + ret1["energy"][dd, self.case1.get_real_mapping[dd], :], + ) + expected_mask = np.array( + [ + [1, 0, 1, 1], + [1, 1, 0, 1], + ] + ) + np.testing.assert_equal(ret1["mask"], expected_mask) diff --git a/source/tests/common/dpmodel/test_dp_model.py b/source/tests/common/dpmodel/test_dp_model.py index c3de1f4cdf..9121c7cd07 100644 --- a/source/tests/common/dpmodel/test_dp_model.py +++ b/source/tests/common/dpmodel/test_dp_model.py @@ -87,7 +87,10 @@ def test_prec_consistency(self): self.assertEqual(model_l_ret_32[ii].dtype, np.float64) else: self.assertEqual(model_l_ret_32[ii].dtype, np.float32) - self.assertEqual(model_l_ret_64[ii].dtype, np.float64) + if ii != "mask": + self.assertEqual(model_l_ret_64[ii].dtype, np.float64) + else: + self.assertEqual(model_l_ret_64[ii].dtype, np.int32) np.testing.assert_allclose( model_l_ret_32[ii], model_l_ret_64[ii], @@ -138,8 +141,10 @@ def test_prec_consistency(self): self.assertEqual(model_l_ret_32[ii].dtype, np.float64) else: self.assertEqual(model_l_ret_32[ii].dtype, np.float32) - self.assertEqual(model_l_ret_64[ii].dtype, np.float64) - self.assertEqual(model_l_ret_64[ii].dtype, np.float64) + if ii != "mask": + self.assertEqual(model_l_ret_64[ii].dtype, np.float64) + else: + self.assertEqual(model_l_ret_64[ii].dtype, np.int32) np.testing.assert_allclose( model_l_ret_32[ii], model_l_ret_64[ii], diff --git a/source/tests/common/dpmodel/test_nlist.py b/source/tests/common/dpmodel/test_nlist.py index 35145cde39..ee8a7139e7 100644 --- a/source/tests/common/dpmodel/test_nlist.py +++ b/source/tests/common/dpmodel/test_nlist.py @@ -125,12 +125,12 @@ def test_nlist_lt(self): class TestNeighList(unittest.TestCase): def setUp(self): self.nf = 3 - self.nloc = 2 + self.nloc = 3 self.ns = 5 * 5 * 3 self.nall = self.ns * self.nloc self.cell = np.array([[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype) - self.icoord = np.array([[0, 0, 0], [0.5, 0.5, 0.1]], dtype=dtype) - self.atype = np.array([0, 1], dtype=np.int32) + self.icoord = np.array([[0, 0, 0], [0, 0, 0], [0.5, 0.5, 0.1]], dtype=dtype) + self.atype = np.array([-1, 0, 1], dtype=np.int32) [self.cell, self.icoord, self.atype] = [ np.expand_dims(ii, 0) for ii in [self.cell, self.icoord, self.atype] ] @@ -144,8 +144,9 @@ def setUp(self): self.nsel = [10, 10] self.ref_nlist = np.array( [ - [0, 0, 0, 0, 0, 0, -1, -1, -1, -1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1], - [0, 0, 0, 0, -1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1], + [-1] * sum(self.nsel), + [1, 1, 1, 1, 1, 1, -1, -1, -1, -1, 2, 2, 2, 2, -1, -1, -1, -1, -1, -1], + [1, 1, 1, 1, -1, -1, -1, -1, -1, -1, 2, 2, 2, 2, 2, 2, -1, -1, -1, -1], ] ) @@ -269,7 +270,7 @@ def test_extend_coord(self): ) np.testing.assert_allclose( cc, - np.array([30, 30, 30, 30, 30], dtype=np.int32), + np.array([self.ns * self.nloc // 5] * 5, dtype=np.int32), rtol=self.prec, atol=self.prec, ) @@ -282,7 +283,7 @@ def test_extend_coord(self): ) np.testing.assert_allclose( cc, - np.array([30, 30, 30, 30, 30], dtype=np.int32), + np.array([self.ns * self.nloc // 5] * 5, dtype=np.int32), rtol=self.prec, atol=self.prec, ) @@ -295,7 +296,7 @@ def test_extend_coord(self): ) np.testing.assert_allclose( cc, - np.array([50, 50, 50], dtype=np.int32), + np.array([self.ns * self.nloc // 3] * 3, dtype=np.int32), rtol=self.prec, atol=self.prec, ) diff --git a/source/tests/pt/model/test_dp_atomic_model.py b/source/tests/pt/model/test_dp_atomic_model.py index 6daaeef2ef..4a35b4676a 100644 --- a/source/tests/pt/model/test_dp_atomic_model.py +++ b/source/tests/pt/model/test_dp_atomic_model.py @@ -27,6 +27,7 @@ from .test_env_mat import ( TestCaseSingleFrameWithNlist, + TestCaseSingleFrameWithNlistWithVirtual, ) dtype = env.GLOBAL_PT_FLOAT_PRECISION @@ -166,10 +167,8 @@ def test_excl_consistency(self): # check output def out_names = [vv.name for vv in md0.atomic_output_def().get_data().values()] - if atom_excl == []: - self.assertEqual(out_names, ["energy"]) - else: - self.assertEqual(out_names, ["energy", "mask"]) + self.assertEqual(out_names, ["energy", "mask"]) + if atom_excl != []: for ii in md0.atomic_output_def().get_data().values(): if ii.name == "mask": self.assertEqual(ii.shape, [1]) @@ -189,3 +188,49 @@ def test_excl_consistency(self): np.testing.assert_array_equal(to_numpy_array(ret0["mask"]), expected) else: raise ValueError(f"not expected atom_excl {atom_excl}") + + +class TestDPAtomicModelVirtualConsistency(unittest.TestCase): + def setUp(self): + self.case0 = TestCaseSingleFrameWithNlist() + self.case1 = TestCaseSingleFrameWithNlistWithVirtual() + self.case0.setUp() + self.case1.setUp() + + def test_virtual_consistency(self): + nf, _, _ = self.case0.nlist.shape + ds = DescrptSeA( + self.case0.rcut, + self.case0.rcut_smth, + self.case0.sel, + ) + ft = InvarFitting( + "energy", + self.case0.nt, + ds.get_dim_out(), + 1, + mixed_types=ds.mixed_types(), + ) + type_map = ["foo", "bar"] + md1 = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) + + args0 = [self.case0.coord_ext, self.case0.atype_ext, self.case0.nlist] + args0 = [to_torch_tensor(ii) for ii in args0] + args1 = [self.case1.coord_ext, self.case1.atype_ext, self.case1.nlist] + args1 = [to_torch_tensor(ii) for ii in args1] + + ret0 = md1.forward_common_atomic(*args0) + ret1 = md1.forward_common_atomic(*args1) + + for dd in range(self.case0.nf): + np.testing.assert_allclose( + to_numpy_array(ret0["energy"])[dd], + to_numpy_array(ret1["energy"])[dd, self.case1.get_real_mapping[dd], :], + ) + expected_mask = np.array( + [ + [1, 0, 1, 1], + [1, 1, 0, 1], + ] + ) + np.testing.assert_equal(to_numpy_array(ret1["mask"]), expected_mask) diff --git a/source/tests/pt/model/test_dp_model.py b/source/tests/pt/model/test_dp_model.py index c0b152b3d3..7470cf96d0 100644 --- a/source/tests/pt/model/test_dp_model.py +++ b/source/tests/pt/model/test_dp_model.py @@ -237,7 +237,10 @@ def test_prec_consistency(self): self.assertEqual(model_l_ret_32[ii].dtype, torch.float64) else: self.assertEqual(model_l_ret_32[ii].dtype, torch.float32) - self.assertEqual(model_l_ret_64[ii].dtype, torch.float64) + if ii != "mask": + self.assertEqual(model_l_ret_64[ii].dtype, torch.float64) + else: + self.assertEqual(model_l_ret_64[ii].dtype, torch.int32) np.testing.assert_allclose( to_numpy_array(model_l_ret_32[ii]), to_numpy_array(model_l_ret_64[ii]), @@ -377,7 +380,10 @@ def test_prec_consistency(self): self.assertEqual(model_l_ret_32[ii].dtype, torch.float64) else: self.assertEqual(model_l_ret_32[ii].dtype, torch.float32) - self.assertEqual(model_l_ret_64[ii].dtype, torch.float64) + if ii != "mask": + self.assertEqual(model_l_ret_64[ii].dtype, torch.float64) + else: + self.assertEqual(model_l_ret_64[ii].dtype, torch.int32) np.testing.assert_allclose( to_numpy_array(model_l_ret_32[ii]), to_numpy_array(model_l_ret_64[ii]), diff --git a/source/tests/pt/model/test_env_mat.py b/source/tests/pt/model/test_env_mat.py index 615e7c6230..e18093b2f1 100644 --- a/source/tests/pt/model/test_env_mat.py +++ b/source/tests/pt/model/test_env_mat.py @@ -64,6 +64,56 @@ def setUp(self): self.atol = 1e-12 +class TestCaseSingleFrameWithNlistWithVirtual: + def setUp(self): + # nloc == 3, nall == 4 + self.nloc = 4 + self.nall = 5 + self.nf, self.nt = 2, 2 + self.coord_ext = np.array( + [ + [0, 0, 0], + [0, 0, 0], + [0, 1, 0], + [0, 0, 1], + [0, -2, 0], + ], + dtype=np.float64, + ).reshape([1, self.nall, 3]) + self.atype_ext = np.array([0, -1, 0, 1, 0], dtype=int).reshape([1, self.nall]) + # sel = [5, 2] + self.sel = [5, 2] + self.nlist = np.array( + [ + [2, 4, -1, -1, -1, 3, -1], + [-1, -1, -1, -1, -1, -1, -1], + [0, -1, -1, -1, -1, 3, -1], + [0, 2, -1, -1, -1, -1, -1], + ], + dtype=int, + ).reshape([1, self.nloc, sum(self.sel)]) + self.rcut = 2.2 + self.rcut_smth = 0.4 + # permutations + self.perm = np.array([3, 0, 1, 2, 4], dtype=np.int32) + inv_perm = np.argsort(self.perm) + # permute the coord and atype + self.coord_ext = np.concatenate( + [self.coord_ext, self.coord_ext[:, self.perm, :]], axis=0 + ).reshape(self.nf, self.nall * 3) + self.atype_ext = np.concatenate( + [self.atype_ext, self.atype_ext[:, self.perm]], axis=0 + ) + # permute the nlist + nlist1 = self.nlist[:, self.perm[: self.nloc], :] + mask = nlist1 == -1 + nlist1 = inv_perm[nlist1] + nlist1 = np.where(mask, -1, nlist1) + self.nlist = np.concatenate([self.nlist, nlist1], axis=0) + self.get_real_mapping = np.array([[0, 2, 3], [0, 1, 3]], dtype=np.int32) + self.atol = 1e-12 + + class TestCaseSingleFrameWithoutNlist: def setUp(self): # nloc == 3, nall == 4 diff --git a/source/tests/pt/model/test_nlist.py b/source/tests/pt/model/test_nlist.py index 616af93081..244b3804c8 100644 --- a/source/tests/pt/model/test_nlist.py +++ b/source/tests/pt/model/test_nlist.py @@ -22,16 +22,16 @@ class TestNeighList(unittest.TestCase): def setUp(self): self.nf = 3 - self.nloc = 2 + self.nloc = 3 self.ns = 5 * 5 * 3 self.nall = self.ns * self.nloc self.cell = torch.tensor( [[1, 0, 0], [0.4, 0.8, 0], [0.1, 0.3, 2.1]], dtype=dtype, device=env.DEVICE ) self.icoord = torch.tensor( - [[0, 0, 0], [0.5, 0.5, 0.1]], dtype=dtype, device=env.DEVICE + [[0, 0, 0], [0, 0, 0], [0.5, 0.5, 0.1]], dtype=dtype, device=env.DEVICE ) - self.atype = torch.tensor([0, 1], dtype=torch.int, device=env.DEVICE) + self.atype = torch.tensor([-1, 0, 1], dtype=torch.int, device=env.DEVICE) [self.cell, self.icoord, self.atype] = [ ii.unsqueeze(0) for ii in [self.cell, self.icoord, self.atype] ] @@ -51,8 +51,9 @@ def setUp(self): # mapping[0], type_split=True, ) self.ref_nlist = torch.tensor( [ - [0, 0, 0, 0, 0, 0, -1, -1, -1, -1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1], - [0, 0, 0, 0, -1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1], + [-1] * sum(self.nsel), + [1, 1, 1, 1, 1, 1, -1, -1, -1, -1, 2, 2, 2, 2, -1, -1, -1, -1, -1, -1], + [1, 1, 1, 1, -1, -1, -1, -1, -1, -1, 2, 2, 2, 2, 2, 2, -1, -1, -1, -1], ], device=env.DEVICE, ) @@ -181,7 +182,9 @@ def test_extend_coord(self): ) torch.testing.assert_close( cc, - torch.tensor([30, 30, 30, 30, 30], dtype=torch.long, device=env.DEVICE), + torch.tensor( + [self.ns * self.nloc // 5] * 5, dtype=torch.long, device=env.DEVICE + ), rtol=self.prec, atol=self.prec, ) @@ -194,7 +197,9 @@ def test_extend_coord(self): ) torch.testing.assert_close( cc, - torch.tensor([30, 30, 30, 30, 30], dtype=torch.long, device=env.DEVICE), + torch.tensor( + [self.ns * self.nloc // 5] * 5, dtype=torch.long, device=env.DEVICE + ), rtol=self.prec, atol=self.prec, ) @@ -207,7 +212,9 @@ def test_extend_coord(self): ) torch.testing.assert_close( cc, - torch.tensor([50, 50, 50], dtype=torch.long, device=env.DEVICE), + torch.tensor( + [self.ns * self.nloc // 3] * 3, dtype=torch.long, device=env.DEVICE + ), rtol=self.prec, atol=self.prec, ) From abf3477472a0aed66ba70313c8069fde90099ba6 Mon Sep 17 00:00:00 2001 From: Chenxing Luo Date: Sun, 17 Mar 2024 16:58:44 -0400 Subject: [PATCH 227/686] Fix LAMMPS plugin symlink path on macOS platform (#3473) This pull request fixes the broken symlink for `dpplugin.so` on macOS. It should point to `libdeepmd_lmp.so` but point to `libdeepmd_lmp.dylib` instead. ### Details The `libdeepmd_lmp` is a shared module. https://github.com/deepmodeling/deepmd-kit/blob/b875ea8f6661b6e1567537ead7e2b4a8b14ea113/source/lmp/plugin/CMakeLists.txt#L72 The build target name on macOS is `libdeepmd_lmp.so`. Because on macOS, the `CMAKE_SHARED_MODULE_SUFFIX` (`.so`) is different from `CMAKE_SHARED_LIBRARY_SUFFIX` (`.dynlib`). As a result, in previous versions, on macOS the symbolic link `dpplugin.so` was pointed to `libdeepmd_lmp.dylib`, which does not exist. One can check the conda-forge builds to confirm (e.g., [osx-arm64/deepmd-kit-2.2.9-cpu_py311hf5376d5_mpi_openmpi_0.conda](https://anaconda.org/conda-forge/deepmd-kit/2.2.9/download/osx-arm64/deepmd-kit-2.2.9-cpu_py311hf5376d5_mpi_openmpi_0.conda)). --- source/lmp/plugin/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/lmp/plugin/CMakeLists.txt b/source/lmp/plugin/CMakeLists.txt index bfc2253412..4fdae7ac5b 100644 --- a/source/lmp/plugin/CMakeLists.txt +++ b/source/lmp/plugin/CMakeLists.txt @@ -126,7 +126,7 @@ if(DEFINED LAMMPS_SOURCE_ROOT OR DEFINED LAMMPS_VERSION) install( CODE "execute_process( \ COMMAND ${CMAKE_COMMAND} -E create_symlink \ - ../${CMAKE_SHARED_LIBRARY_PREFIX}${libname}${CMAKE_SHARED_LIBRARY_SUFFIX} \ + ../${CMAKE_SHARED_MODULE_PREFIX}${libname}${CMAKE_SHARED_MODULE_SUFFIX} \ ${CMAKE_INSTALL_PREFIX}/lib/${libname}/${PLUGINNAME} \ )") endif() From eca5b309a9602df6b3fdaad00c4f697d11989a30 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Sun, 17 Mar 2024 21:58:29 -0400 Subject: [PATCH 228/686] fix(pt): Fix PairTabAtomicModel OOM error (#3484) Reduce memory usage of `_extract_spline_coefficient`. Signed-off-by: Jinzhe Zeng --- .../atomic_model/pairtab_atomic_model.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index a4aa43ede1..7c7c8a2969 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -415,20 +415,28 @@ def _extract_spline_coefficient( # (nframes, nloc, nnei) expanded_i_type = i_type.unsqueeze(-1).expand(-1, -1, j_type.shape[-1]) - # (nframes, nloc, nnei, nspline, 4) - expanded_tab_data = tab_data[expanded_i_type, j_type] - - # (nframes, nloc, nnei, 1, 4) - expanded_idx = idx.unsqueeze(-1).unsqueeze(-1).expand(-1, -1, -1, -1, 4) - # handle the case where idx is beyond the number of splines - clipped_indices = torch.clamp(expanded_idx, 0, nspline - 1).to(torch.int64) - + clipped_indices = torch.clamp(idx, 0, nspline - 1).to(torch.int64) + + nframes = i_type.shape[0] + nloc = i_type.shape[1] + nnei = j_type.shape[2] + ntypes = tab_data.shape[0] + # tab_data_idx: (nframes, nloc, nnei) + tab_data_idx = ( + expanded_i_type * ntypes * nspline + j_type * nspline + clipped_indices + ) + # tab_data: (ntype, ntype, nspline, 4) + tab_data = tab_data.view(ntypes * ntypes * nspline, 4) + # tab_data_idx: (nframes * nloc * nnei, 4) + tab_data_idx = tab_data_idx.view(nframes * nloc * nnei, 1).expand(-1, 4) # (nframes, nloc, nnei, 4) - final_coef = torch.gather(expanded_tab_data, 3, clipped_indices).squeeze() + final_coef = torch.gather(tab_data, 0, tab_data_idx).view( + nframes, nloc, nnei, 4 + ) # when the spline idx is beyond the table, all spline coefficients are set to `0`, and the resulting ener corresponding to the idx is also `0`. - final_coef[expanded_idx.squeeze() > nspline] = 0 + final_coef[idx > nspline] = 0 return final_coef @staticmethod From 9b6042b2fba7ba5f9c310c49db5bdb4308499d6e Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Mon, 18 Mar 2024 11:09:43 +0800 Subject: [PATCH 229/686] Fix: Invar_fitting warning msg (#3485) This should fix #3476 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/pt/model/task/invar_fitting.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/deepmd/pt/model/task/invar_fitting.py b/deepmd/pt/model/task/invar_fitting.py index 1699b440ac..afb1d73658 100644 --- a/deepmd/pt/model/task/invar_fitting.py +++ b/deepmd/pt/model/task/invar_fitting.py @@ -167,11 +167,7 @@ def compute_output_stats( bias_atom_e = compute_output_stats( merged, self.ntypes, stat_file_path, self.rcond, self.atom_ener ) - self.bias_atom_e.copy_( - torch.tensor(bias_atom_e, device=env.DEVICE).view( - [self.ntypes, self.dim_out] - ) - ) + self.bias_atom_e.copy_(bias_atom_e.view([self.ntypes, self.dim_out])) def output_def(self) -> FittingOutputDef: return FittingOutputDef( From 76371ceed2fd0d8e259e482e046da350d4006c0a Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 19 Mar 2024 02:38:16 -0400 Subject: [PATCH 230/686] Clean TODOs and convert them into issues (#3519) About 50+ issues will be created after this PR is merged. --------- Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/todo.yml | 20 ++++++++++++++++++++ .pre-commit-config.yaml | 1 - deepmd/common.py | 14 ++++++++------ deepmd/dpmodel/fitting/general_fitting.py | 3 ++- deepmd/dpmodel/utils/network.py | 3 ++- deepmd/dpmodel/utils/update_sel.py | 2 +- deepmd/pt/entrypoints/main.py | 4 +--- deepmd/pt/loss/ener.py | 2 +- deepmd/pt/loss/ener_spin.py | 2 +- deepmd/pt/model/task/fitting.py | 3 ++- deepmd/pt/train/training.py | 6 ++++-- deepmd/pt/utils/update_sel.py | 2 +- deepmd/tf/descriptor/descriptor.py | 5 ----- deepmd/tf/descriptor/se_a.py | 3 ++- deepmd/tf/descriptor/se_a_mask.py | 7 +++---- deepmd/tf/descriptor/se_r.py | 3 ++- deepmd/tf/env.py | 3 ++- deepmd/tf/fit/dipole.py | 4 ++-- deepmd/tf/fit/dos.py | 4 ++-- deepmd/tf/fit/ener.py | 4 ++-- deepmd/tf/fit/polar.py | 4 ++-- deepmd/tf/infer/deep_tensor.py | 1 - deepmd/tf/loss/ener.py | 1 - deepmd/tf/model/linear.py | 1 - deepmd/tf/train/trainer.py | 2 -- deepmd/tf/utils/batch_size.py | 2 -- deepmd/tf/utils/finetune.py | 2 +- deepmd/tf/utils/graph.py | 1 - deepmd/tf/utils/multi_init.py | 2 +- deepmd/tf/utils/update_sel.py | 2 -- deepmd/utils/batch_size.py | 1 - deepmd/utils/data_system.py | 1 - deepmd/utils/path.py | 2 -- pyproject.toml | 4 ++-- source/api_cc/include/common.h | 1 - source/api_cc/src/DeepPot.cc | 3 +-- source/op/tabulate_multi_device.cc | 7 +++---- 37 files changed, 68 insertions(+), 64 deletions(-) create mode 100644 .github/workflows/todo.yml diff --git a/.github/workflows/todo.yml b/.github/workflows/todo.yml new file mode 100644 index 0000000000..2608bb1071 --- /dev/null +++ b/.github/workflows/todo.yml @@ -0,0 +1,20 @@ +name: TODO workflow +on: + push: + branches: + - devel +jobs: + build: + if: github.repository_owner == 'deepmodeling' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run tdg-github-action + uses: ribtoks/tdg-github-action@master + with: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + SHA: ${{ github.sha }} + REF: ${{ github.ref }} + EXCLUDE_PATTERN: "(source/3rdparty|.git)/.*" + COMMENT_ON_ISSUES: 1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 892962b2fe..a611e819f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,6 @@ repos: - id: check-json - id: check-added-large-files args: ['--maxkb=1024', '--enforce-all'] - # TODO: remove the following after resolved exclude: | (?x)^( source/tests/infer/dipolecharge_e.pbtxt| diff --git a/deepmd/common.py b/deepmd/common.py index c776975591..84f98c6318 100644 --- a/deepmd/common.py +++ b/deepmd/common.py @@ -71,8 +71,9 @@ ) -# TODO this is not a good way to do things. This is some global variable to which -# TODO anyone can write and there is no good way to keep track of the changes +# TODO: refactor data_requirement to make it not a global variable +# this is not a good way to do things. This is some global variable to which +# anyone can write and there is no good way to keep track of the changes data_requirement = {} @@ -180,9 +181,10 @@ def make_default_mesh(pbc: bool, mixed_type: bool) -> np.ndarray: return default_mesh -# TODO maybe rename this to j_deprecated and only warn about deprecated keys, -# TODO if the deprecated_key argument is left empty function puppose is only custom -# TODO error since dict[key] already raises KeyError when the key is missing +# TODO: rename j_must_have to j_deprecated and only warn about deprecated keys +# maybe rename this to j_deprecated and only warn about deprecated keys, +# if the deprecated_key argument is left empty function puppose is only custom +# error since dict[key] already raises KeyError when the key is missing def j_must_have( jdata: Dict[str, "_DICT_VAL"], key: str, deprecated_key: List[str] = [] ) -> "_DICT_VAL": @@ -238,7 +240,7 @@ def j_loader(filename: Union[str, Path]) -> Dict[str, Any]: raise TypeError("config file must be json, or yaml/yml") -# TODO port completely to pathlib when all callers are ported +# TODO port expand_sys_str completely to pathlib when all callers are ported def expand_sys_str(root_dir: Union[str, Path]) -> List[str]: """Recursively iterate over directories taking those that contain `type.raw` file. diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index 3b0d022562..5681f5bf0c 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -313,7 +313,8 @@ def _call_common( ) xx = descriptor if self.remove_vaccum_contribution is not None: - # TODO: Idealy, the input for vaccum should be computed; + # TODO: comput the input for vaccum when setting remove_vaccum_contribution + # Idealy, the input for vaccum should be computed; # we consider it as always zero for convenience. # Needs a compute_input_stats for vaccum passed from the # descriptor. diff --git a/deepmd/dpmodel/utils/network.py b/deepmd/dpmodel/utils/network.py index 817448ac50..661358ed70 100644 --- a/deepmd/dpmodel/utils/network.py +++ b/deepmd/dpmodel/utils/network.py @@ -90,7 +90,8 @@ def __call__(self): return self.count -# TODO: should be moved to otherwhere... +# TODO: move save_dp_model and load_dp_model to a seperated module +# should be moved to otherwhere... def save_dp_model(filename: str, model_dict: dict) -> None: """Save a DP model to a file in the native format. diff --git a/deepmd/dpmodel/utils/update_sel.py b/deepmd/dpmodel/utils/update_sel.py index f36e63651d..48463b5743 100644 --- a/deepmd/dpmodel/utils/update_sel.py +++ b/deepmd/dpmodel/utils/update_sel.py @@ -17,5 +17,5 @@ def neighbor_stat(self) -> Type[NeighborStat]: return NeighborStat def hook(self, min_nbor_dist, max_nbor_size): - # TODO: save to the model + # TODO: save to the model in UpdateSel.hook pass diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 46d284a395..b9c4971116 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -285,9 +285,7 @@ def freeze(FLAGS): torch.jit.save( model, FLAGS.output, - { - # TODO: _extra_files - }, + {}, ) diff --git a/deepmd/pt/loss/ener.py b/deepmd/pt/loss/ener.py index 1d70528e88..f29c3231f1 100644 --- a/deepmd/pt/loss/ener.py +++ b/deepmd/pt/loss/ener.py @@ -77,7 +77,7 @@ def __init__( self.has_f = (start_pref_f != 0.0 and limit_pref_f != 0.0) or inference self.has_v = (start_pref_v != 0.0 and limit_pref_v != 0.0) or inference - # TODO need support for atomic energy and atomic pref + # TODO EnergyStdLoss need support for atomic energy and atomic pref self.has_ae = (start_pref_ae != 0.0 and limit_pref_ae != 0.0) or inference self.has_pf = (start_pref_pf != 0.0 and limit_pref_pf != 0.0) or inference diff --git a/deepmd/pt/loss/ener_spin.py b/deepmd/pt/loss/ener_spin.py index b94acf26ea..1f55dcc5df 100644 --- a/deepmd/pt/loss/ener_spin.py +++ b/deepmd/pt/loss/ener_spin.py @@ -47,7 +47,7 @@ def __init__( self.has_fr = (start_pref_fr != 0.0 and limit_pref_fr != 0.0) or inference self.has_fm = (start_pref_fm != 0.0 and limit_pref_fm != 0.0) or inference - # TODO need support for virial, atomic energy and atomic pref + # TODO EnergySpinLoss needs support for virial, atomic energy and atomic pref self.has_v = (start_pref_v != 0.0 and limit_pref_v != 0.0) or inference self.has_ae = (start_pref_ae != 0.0 and limit_pref_ae != 0.0) or inference self.has_pf = (start_pref_pf != 0.0 and limit_pref_pf != 0.0) or inference diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 4637178318..c8edee5b94 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -457,7 +457,8 @@ def _forward_common( ): xx = descriptor if self.remove_vaccum_contribution is not None: - # TODO: Idealy, the input for vaccum should be computed; + # TODO: compute the input for vaccm when remove_vaccum_contribution is set + # Idealy, the input for vaccum should be computed; # we consider it as always zero for convenience. # Needs a compute_input_stats for vaccum passed from the # descriptor. diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index fc293f70ec..2056b9b305 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -558,14 +558,16 @@ def get_loss(loss_params, start_lr, _ntypes, _model): output_device=LOCAL_RANK, ) - # TODO ZD add lr warmups for multitask + # TODO add lr warmups for multitask + # author: iProzd def warm_up_linear(step, warmup_steps): if step < warmup_steps: return step / warmup_steps else: return self.lr_exp.value(step - warmup_steps) / self.lr_exp.start_lr - # TODO ZD add optimizers for multitask + # TODO add optimizers for multitask + # author: iProzd if self.opt_type == "Adam": self.optimizer = torch.optim.Adam( self.wrapper.parameters(), lr=self.lr_exp.start_lr diff --git a/deepmd/pt/utils/update_sel.py b/deepmd/pt/utils/update_sel.py index 2d077acac1..8c2d0699f2 100644 --- a/deepmd/pt/utils/update_sel.py +++ b/deepmd/pt/utils/update_sel.py @@ -17,5 +17,5 @@ def neighbor_stat(self) -> Type[NeighborStat]: return NeighborStat def hook(self, min_nbor_dist, max_nbor_size): - # TODO: save to the model + # TODO: save to the model in UpdateSel.hook pass diff --git a/deepmd/tf/descriptor/descriptor.py b/deepmd/tf/descriptor/descriptor.py index dbf260bfe8..82b09c95fb 100644 --- a/deepmd/tf/descriptor/descriptor.py +++ b/deepmd/tf/descriptor/descriptor.py @@ -102,9 +102,6 @@ def get_dim_rot_mat_1(self) -> int: int the first dimension of the rotation matrix """ - # TODO: I think this method should be implemented as it's called by dipole and - # polar fitting network. However, currently not all descriptors have this - # method. raise NotImplementedError def get_nlist(self) -> Tuple[tf.Tensor, tf.Tensor, List[int], List[int]]: @@ -121,8 +118,6 @@ def get_nlist(self) -> Tuple[tf.Tensor, tf.Tensor, List[int], List[int]]: sel_r : list[int] The number of neighbors with only radial information """ - # TODO: I think this method should be implemented as it's called by energy - # model. However, se_ar and hybrid doesn't have this method. raise NotImplementedError @abstractmethod diff --git a/deepmd/tf/descriptor/se_a.py b/deepmd/tf/descriptor/se_a.py index 4635554610..8b6ae3539b 100644 --- a/deepmd/tf/descriptor/se_a.py +++ b/deepmd/tf/descriptor/se_a.py @@ -1426,7 +1426,8 @@ def serialize(self, suffix: str = "") -> dict: raise NotImplementedError("spin is unsupported") assert self.davg is not None assert self.dstd is not None - # TODO: not sure how to handle type embedding - type embedding is not a model parameter, + # TODO: tf: handle type embedding in DescrptSeA.serialize + # not sure how to handle type embedding - type embedding is not a model parameter, # but instead a part of the input data. Maybe the interface should be refactored... return { diff --git a/deepmd/tf/descriptor/se_a_mask.py b/deepmd/tf/descriptor/se_a_mask.py index 55b34adf48..ace8a47bbc 100644 --- a/deepmd/tf/descriptor/se_a_mask.py +++ b/deepmd/tf/descriptor/se_a_mask.py @@ -249,10 +249,9 @@ def compute_input_stats( **kwargs Additional keyword arguments. """ - """ - TODO: Since not all input atoms are real in se_a_mask, - statistics should be reimplemented for se_a_mask descriptor. - """ + # TODO: implement compute_input_stats for DescrptSeAMask + # Since not all input atoms are real in se_a_mask, + # statistics should be reimplemented for se_a_mask descriptor. self.davg = None self.dstd = None diff --git a/deepmd/tf/descriptor/se_r.py b/deepmd/tf/descriptor/se_r.py index 9f88ebe37d..8ef48c0de2 100644 --- a/deepmd/tf/descriptor/se_r.py +++ b/deepmd/tf/descriptor/se_r.py @@ -766,7 +766,8 @@ def serialize(self, suffix: str = "") -> dict: raise NotImplementedError("spin is unsupported") assert self.davg is not None assert self.dstd is not None - # TODO: not sure how to handle type embedding - type embedding is not a model parameter, + # TODO: tf: handle type embedding in DescrptSeR.serialize + # not sure how to handle type embedding - type embedding is not a model parameter, # but instead a part of the input data. Maybe the interface should be refactored... return { "@class": "Descriptor", diff --git a/deepmd/tf/env.py b/deepmd/tf/env.py index 0b16c8758a..8cc1cacad1 100644 --- a/deepmd/tf/env.py +++ b/deepmd/tf/env.py @@ -157,7 +157,8 @@ def dlopen_library(module: str, filename: str): r"(final)_layer_type_(\d+)/(matrix)|" r"(final)_layer/(bias)|" r"(final)_layer_type_(\d+)/(bias)|" - # TODO: not sure how to parse for shared layers... + # TODO: supporting extracting parameters for shared layers + # not sure how to parse for shared layers... # layer_name r"share_.+_type_\d/matrix|" r"share_.+_type_\d/bias|" diff --git a/deepmd/tf/fit/dipole.py b/deepmd/tf/fit/dipole.py index f503789308..978fd958fb 100644 --- a/deepmd/tf/fit/dipole.py +++ b/deepmd/tf/fit/dipole.py @@ -355,7 +355,7 @@ def serialize(self, suffix: str) -> dict: "dim_descrpt": self.dim_descrpt, "embedding_width": self.dim_rot_mat_1, # very bad design: type embedding is not passed to the class - # TODO: refactor the class + # TODO: refactor the class for type embedding and dipole fitting "mixed_types": False, "dim_out": 3, "neuron": self.n_neuron, @@ -365,7 +365,7 @@ def serialize(self, suffix: str) -> dict: "exclude_types": [], "nets": self.serialize_network( ntypes=self.ntypes, - # TODO: consider type embeddings + # TODO: consider type embeddings in dipole fitting ndim=1, in_dim=self.dim_descrpt, out_dim=self.dim_rot_mat_1, diff --git a/deepmd/tf/fit/dos.py b/deepmd/tf/fit/dos.py index aef134da92..292db8d5b4 100644 --- a/deepmd/tf/fit/dos.py +++ b/deepmd/tf/fit/dos.py @@ -701,7 +701,7 @@ def serialize(self, suffix: str = "") -> dict: "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, # very bad design: type embedding is not passed to the class - # TODO: refactor the class + # TODO: refactor the class for DOSFitting and type embedding "mixed_types": False, "dim_out": self.numb_dos, "neuron": self.n_neuron, @@ -715,7 +715,7 @@ def serialize(self, suffix: str = "") -> dict: "exclude_types": [], "nets": self.serialize_network( ntypes=self.ntypes, - # TODO: consider type embeddings + # TODO: consider type embeddings for DOSFitting ndim=1, in_dim=self.dim_descrpt + self.numb_fparam + self.numb_aparam, out_dim=self.numb_dos, diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index f8f5c3b346..b391b00052 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -893,7 +893,7 @@ def serialize(self, suffix: str = "") -> dict: "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, # very bad design: type embedding is not passed to the class - # TODO: refactor the class + # TODO: refactor the class for energy fitting and type embedding "mixed_types": False, "dim_out": 1, "neuron": self.n_neuron, @@ -912,7 +912,7 @@ def serialize(self, suffix: str = "") -> dict: "exclude_types": [], "nets": self.serialize_network( ntypes=self.ntypes, - # TODO: consider type embeddings + # TODO: consider type embeddings for type embedding ndim=1, in_dim=self.dim_descrpt + self.numb_fparam + self.numb_aparam, neuron=self.n_neuron, diff --git a/deepmd/tf/fit/polar.py b/deepmd/tf/fit/polar.py index 41ea989521..21b9587b88 100644 --- a/deepmd/tf/fit/polar.py +++ b/deepmd/tf/fit/polar.py @@ -545,7 +545,7 @@ def serialize(self, suffix: str) -> dict: "dim_descrpt": self.dim_descrpt, "embedding_width": self.dim_rot_mat_1, # very bad design: type embedding is not passed to the class - # TODO: refactor the class + # TODO: refactor the class for polar fitting and type embedding "mixed_types": False, "dim_out": 3, "neuron": self.n_neuron, @@ -558,7 +558,7 @@ def serialize(self, suffix: str) -> dict: "shift_diag": self.shift_diag, "nets": self.serialize_network( ntypes=self.ntypes, - # TODO: consider type embeddings + # TODO: consider type embeddings for polar fitting ndim=1, in_dim=self.dim_descrpt, out_dim=self.dim_rot_mat_1, diff --git a/deepmd/tf/infer/deep_tensor.py b/deepmd/tf/infer/deep_tensor.py index 9e8acf8241..59fdab7cd1 100644 --- a/deepmd/tf/infer/deep_tensor.py +++ b/deepmd/tf/infer/deep_tensor.py @@ -412,7 +412,6 @@ def eval_full( if ghost_map is not None: # add the value of ghost atoms to real atoms force = np.reshape(force, [nframes * nout, -1, 3]) - # TODO: is there some way not to use for loop? for ii in range(nframes * nout): np.add.at(force[ii], ghost_map, force[ii, nloc:]) if atomic: diff --git a/deepmd/tf/loss/ener.py b/deepmd/tf/loss/ener.py index 48a13319e4..baa4aa3e02 100644 --- a/deepmd/tf/loss/ener.py +++ b/deepmd/tf/loss/ener.py @@ -120,7 +120,6 @@ def __init__( "atom_pref", 1, atomic=True, must=False, high_prec=False, repeat=3 ) # drdq: the partial derivative of atomic coordinates w.r.t. generalized coordinates - # TODO: could numb_generalized_coord decided from the training data? if self.has_gf > 0: add_data_requirement( "drdq", diff --git a/deepmd/tf/model/linear.py b/deepmd/tf/model/linear.py index da866ccc5f..ae1b0b5c78 100644 --- a/deepmd/tf/model/linear.py +++ b/deepmd/tf/model/linear.py @@ -54,7 +54,6 @@ def __init__(self, models: List[dict], weights: List[float], **kwargs): self.weights = [1 / len(models) for _ in range(len(models))] elif weights == "sum": self.weights = [1 for _ in range(len(models))] - # TODO: add more weights, for example, so-called committee models else: raise ValueError(f"Invalid weights {weights}") diff --git a/deepmd/tf/train/trainer.py b/deepmd/tf/train/trainer.py index c8668174fd..125b795d2e 100644 --- a/deepmd/tf/train/trainer.py +++ b/deepmd/tf/train/trainer.py @@ -296,8 +296,6 @@ def build(self, data=None, stop_batch=0, origin_type_map=None, suffix=""): ) # neighbor_stat is moved to train.py as duplicated - # TODO: this is a simple fix but we should have a clear - # architecture to call neighbor stat else: self.model.enable_compression() diff --git a/deepmd/tf/utils/batch_size.py b/deepmd/tf/utils/batch_size.py index 8436934cee..33f1ec0da0 100644 --- a/deepmd/tf/utils/batch_size.py +++ b/deepmd/tf/utils/batch_size.py @@ -35,6 +35,4 @@ def is_oom_error(self, e: Exception) -> bool: e : Exception Exception """ - # TODO: it's very slow to catch OOM error; I don't know what TF is doing here - # but luckily we only need to catch once return isinstance(e, (tf.errors.ResourceExhaustedError, OutOfMemoryError)) diff --git a/deepmd/tf/utils/finetune.py b/deepmd/tf/utils/finetune.py index 38824b6954..3d11130ba7 100644 --- a/deepmd/tf/utils/finetune.py +++ b/deepmd/tf/utils/finetune.py @@ -100,7 +100,7 @@ def replace_model_params_with_pretrained_model( ): target_para = pretrained_jdata["model"][config_key] cur_para = jdata["model"][config_key] - # keep some params that are irrelevant to model structures (need to discuss) TODO + # TODO: keep some params that are irrelevant to model structures (need to discuss) if "trainable" in cur_para.keys(): target_para["trainable"] = cur_para["trainable"] log.info(f"Change the '{config_key}' from {cur_para!s} to {target_para!s}.") diff --git a/deepmd/tf/utils/graph.py b/deepmd/tf/utils/graph.py index f09250aa9f..3ed43343fa 100644 --- a/deepmd/tf/utils/graph.py +++ b/deepmd/tf/utils/graph.py @@ -22,7 +22,6 @@ ) -# TODO (JZ): I think in this file we can merge some duplicated lines into one method... def load_graph_def(model_file: str) -> Tuple[tf.Graph, tf.GraphDef]: """Load graph as well as the graph_def from the frozen model(model_file). diff --git a/deepmd/tf/utils/multi_init.py b/deepmd/tf/utils/multi_init.py index 2e3c43c069..aafa9461b0 100644 --- a/deepmd/tf/utils/multi_init.py +++ b/deepmd/tf/utils/multi_init.py @@ -164,7 +164,7 @@ def replace_model_params_with_frz_multi_model( def _change_sub_config(jdata: Dict[str, Any], src_jdata: Dict[str, Any], sub_key: str): target_para = src_jdata[sub_key] cur_para = jdata[sub_key] - # keep some params that are irrelevant to model structures (need to discuss) TODO + # TODO: keep some params that are irrelevant to model structures (need to discuss) if "trainable" in cur_para.keys(): target_para["trainable"] = cur_para["trainable"] log.info(f"Change the '{sub_key}' from {cur_para!s} to {target_para!s}.") diff --git a/deepmd/tf/utils/update_sel.py b/deepmd/tf/utils/update_sel.py index bed6274f56..db0420dde8 100644 --- a/deepmd/tf/utils/update_sel.py +++ b/deepmd/tf/utils/update_sel.py @@ -24,8 +24,6 @@ def neighbor_stat(self) -> Type[NeighborStat]: def hook(self, min_nbor_dist, max_nbor_size): # moved from traier.py as duplicated - # TODO: this is a simple fix but we should have a clear - # architecture to call neighbor stat tf.constant( min_nbor_dist, name="train_attr/min_nbor_dist", diff --git a/deepmd/utils/batch_size.py b/deepmd/utils/batch_size.py index c85806458f..b35d9833d5 100644 --- a/deepmd/utils/batch_size.py +++ b/deepmd/utils/batch_size.py @@ -51,7 +51,6 @@ class AutoBatchSize(ABC): def __init__(self, initial_batch_size: int = 1024, factor: float = 2.0) -> None: # See also PyTorchLightning/pytorch-lightning#1638 - # TODO: discuss a proper initial batch size self.current_batch_size = initial_batch_size DP_INFER_BATCH_SIZE = int(os.environ.get("DP_INFER_BATCH_SIZE", 0)) if DP_INFER_BATCH_SIZE > 0: diff --git a/deepmd/utils/data_system.py b/deepmd/utils/data_system.py index 0c74abfed1..640083bc33 100644 --- a/deepmd/utils/data_system.py +++ b/deepmd/utils/data_system.py @@ -670,7 +670,6 @@ def print_summary( % ( _format_name_length(system_dirs[ii], sys_width), natoms[ii], - # TODO batch size * nbatches = number of structures batch_size[ii], nbatches[ii], sys_probs[ii], diff --git a/deepmd/utils/path.py b/deepmd/utils/path.py index 5887e91850..afe14703a0 100644 --- a/deepmd/utils/path.py +++ b/deepmd/utils/path.py @@ -39,7 +39,6 @@ def __new__(cls, path: str, mode: str = "r"): return super().__new__(DPOSPath) elif os.path.isfile(path.split("#")[0]): # assume h5 if it is not dir - # TODO: check if it is a real h5? or just check suffix? return super().__new__(DPH5Path) raise FileNotFoundError("%s not found" % path) return super().__new__(cls) @@ -217,7 +216,6 @@ def glob(self, pattern: str) -> List["DPPath"]: list of paths """ # currently DPOSPath will only derivative DPOSPath - # TODO: discuss if we want to mix DPOSPath and DPH5Path? return [type(self)(p, mode=self.mode) for p in self.path.glob(pattern)] def rglob(self, pattern: str) -> List["DPPath"]: diff --git a/pyproject.toml b/pyproject.toml index 84cc7237bc..128364249a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] requires = [ + # TODO: unpin the upper bound when scikit-build dynamic metadata API is stable # dynamic metadata API is still unstable - # TODO: unpin the upper bound when it is stable "scikit-build-core>=0.5,<0.9,!=0.6.0", "packaging", ] @@ -134,7 +134,7 @@ test-command = [ test-extras = ["cpu", "test", "lmp", "ipi"] build = ["cp310-*"] skip = ["*-win32", "*-manylinux_i686", "*-musllinux*"] -# TODO: uncomment when CUDA 11 is deprecated +# TODO: uncomment to use the latest image when CUDA 11 is deprecated # manylinux-x86_64-image = "manylinux_2_28" manylinux-x86_64-image = "quay.io/pypa/manylinux_2_28_x86_64:2022-11-19-1b19e81" manylinux-aarch64-image = "manylinux_2_28" diff --git a/source/api_cc/include/common.h b/source/api_cc/include/common.h index 4743336e0c..2010780a6c 100644 --- a/source/api_cc/include/common.h +++ b/source/api_cc/include/common.h @@ -13,7 +13,6 @@ namespace deepmd { typedef double ENERGYTYPE; -// TODO: currently we only implement TF; reserve for future use enum DPBackend { TensorFlow, PyTorch, Paddle, Unknown }; struct NeighborListData { diff --git a/source/api_cc/src/DeepPot.cc b/source/api_cc/src/DeepPot.cc index 442e2d90cc..498f35f46b 100644 --- a/source/api_cc/src/DeepPot.cc +++ b/source/api_cc/src/DeepPot.cc @@ -1,12 +1,11 @@ // SPDX-License-Identifier: LGPL-3.0-or-later #include "DeepPot.h" -#include "common.h" -// TODO: only include when TF backend is built #include #include #include "AtomMap.h" +#include "common.h" #ifdef BUILD_TENSORFLOW #include "DeepPotTF.h" #endif diff --git a/source/op/tabulate_multi_device.cc b/source/op/tabulate_multi_device.cc index 6a70f60a96..50267df556 100644 --- a/source/op/tabulate_multi_device.cc +++ b/source/op/tabulate_multi_device.cc @@ -191,7 +191,7 @@ class TabulateFusionSeAOp : public OpKernel { errors::InvalidArgument("Dim of input should be 3")); TensorShape descriptor_shape; descriptor_shape.AddDim(em_tensor.shape().dim_size(0)); - descriptor_shape.AddDim(4); // TODO: be careful here; + descriptor_shape.AddDim(4); // be careful here; descriptor_shape.AddDim(last_layer_size); int context_output_index = 0; Tensor* descriptor_tensor = NULL; @@ -390,7 +390,7 @@ class TabulateFusionSeAttenOp : public OpKernel { errors::InvalidArgument("Dim of input should be 2")); TensorShape descriptor_shape; descriptor_shape.AddDim(em_tensor.shape().dim_size(0)); - descriptor_shape.AddDim(4); // TODO: be careful here; + descriptor_shape.AddDim(4); // be careful here; descriptor_shape.AddDim(last_layer_size); int context_output_index = 0; Tensor* descriptor_tensor = NULL; @@ -786,8 +786,7 @@ class TabulateFusionSeROp : public OpKernel { errors::InvalidArgument("Dim of input should be 2")); TensorShape descriptor_shape; descriptor_shape.AddDim(em_tensor.shape().dim_size(0)); - descriptor_shape.AddDim( - em_tensor.shape().dim_size(1)); // TODO: be careful here; + descriptor_shape.AddDim(em_tensor.shape().dim_size(1)); // be careful here; descriptor_shape.AddDim(last_layer_size); int context_output_index = 0; Tensor* descriptor_tensor = NULL; From 2851fb94dc9f304e475030c0e4919ac4ee7a10be Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:38:35 +0800 Subject: [PATCH 231/686] [pre-commit.ci] pre-commit autoupdate (#3489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.3.2 → v0.3.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.2...v0.3.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a611e819f0..1860f87fea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: exclude: ^source/3rdparty - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.3.2 + rev: v0.3.3 hooks: - id: ruff args: ["--fix"] From be95d095318c2d4f050f5c63fbf5539b3ee064a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:10:30 -0400 Subject: [PATCH 232/686] build(deps): bump pypa/cibuildwheel from 2.16 to 2.17 (#3487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 2.16 to 2.17.

Changelog

Sourced from pypa/cibuildwheel's changelog.


title: Changelog

Changelog

v2.17.0

11 March 2024

  • 🌟 Adds the ability to inherit configuration in TOML overrides. This makes certain configurations much simpler. If you're overriding an option like before-build or environment, and you just want to add an extra command or environment variable, you can just append (or prepend) to the previous config. See the docs for more information. (#1730)
  • 🌟 Adds official support for native arm64 macOS GitHub runners. To use them, just specify macos-14 as an os of your job in your workflow file. You can also keep macos-13 in your build matrix to build x86_64. Check out the new GitHub Actions example config.
  • ✨ You no longer need to specify --platform to run cibuildwheel locally! Instead it will detect your platform automatically. This was a safety feature, no longer necessary. (#1727)
  • 🛠 Removed setuptools and wheel pinned versions. This only affects old-style projects without a pyproject.toml, projects with pyproject.toml are already getting fresh versions of their build-system.requires installed into an isolated environment. (#1725)
  • 🛠 Improve how the GitHub Action passes arguments (#1757)
  • 🛠 Remove a system-wide install of pipx in the GitHub Action (#1745)
  • 🐛 No longer will cibuildwheel override the PIP_CONSTRAINT environment variable when using the build frontend. Instead it will be extended. (#1675)
  • 🐛 Fix a bug where building and testing both x86_86 and arm64 wheels on the same runner caused the wrong architectures in the test environment (#1750)
  • 🐛 Fix a bug that prevented testing a CPython 3.8 wheel targeting macOS 11+ on x86_64 (#1768)
  • 📚 Moved the docs onto the official PyPA domain - they're now available at https://cibuildwheel.pypa.io . (#1775)
  • 📚 Docs and examples improvements (#1762, #1734)

v2.16.5

30 January 2024

  • 🐛 Fix an incompatibility with the GitHub Action and new GitHub Runner images for Windows that bundle Powershell 7.3+ (#1741)
  • 🛠 Preliminary support for new macos-14 arm64 runners (#1743)

v2.16.4

28 January 2024

  • 🛠 Update manylinux pins to upgrade from a problematic PyPy version. (#1737)

v2.16.3

26 January 2024

  • 🐛 Fix a bug when building from sdist, where relative paths to files in the package didn't work because the working directory was wrong (#1687)
  • 🛠 Adds the ability to disable mounting the host filesystem in containers to /host, through the disable_host_mount suboption on CIBW_CONTAINER_ENGINE.
  • 📚 A lot of docs improvements! (#1708, #1705, #1686, #1679, #1667, #1665)

v2.16.2

3 October 2023

  • 🛠 Updates CPython 3.12 version to 3.12.0, final release (#1635)
  • ✨ Adds a debug option CIBW_DEBUG_KEEP_CONTAINER to stop cibuildwheel deleting build containers after the build finishes. (#1620)

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pypa/cibuildwheel&package-manager=github_actions&previous-version=2.16&new-version=2.17)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_wheel.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 91bcae3702..18fd7a1ac1 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -90,7 +90,7 @@ jobs: rm -rf .git if: matrix.dp_pkg_name == 'deepmd-kit-cu11' - name: Build wheels - uses: pypa/cibuildwheel@v2.16 + uses: pypa/cibuildwheel@v2.17 env: CIBW_BUILD_VERBOSITY: 1 CIBW_ARCHS: all From 47366f6debe32d9cc112eeb4ecb22cce81065afd Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Wed, 20 Mar 2024 13:12:07 +0800 Subject: [PATCH 233/686] pt: add explicit decay_rate for lr (#3445) This is for multitask training, when explicitly setting decay_rate is much more convenient for long training. --- deepmd/pt/utils/learning_rate.py | 44 +++++++++++++++++++++--------- deepmd/utils/argcheck.py | 19 ++++++++++++- source/tests/pt/test_lr.py | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 14 deletions(-) diff --git a/deepmd/pt/utils/learning_rate.py b/deepmd/pt/utils/learning_rate.py index eca3c6ad87..94c657abd4 100644 --- a/deepmd/pt/utils/learning_rate.py +++ b/deepmd/pt/utils/learning_rate.py @@ -3,14 +3,35 @@ class LearningRateExp: - def __init__(self, start_lr, stop_lr, decay_steps, stop_steps, **kwargs): - """Construct an exponential-decayed learning rate. + def __init__( + self, + start_lr, + stop_lr, + decay_steps, + stop_steps, + decay_rate=None, + **kwargs, + ): + """ + Construct an exponential-decayed learning rate. - Args: - - start_lr: Initial learning rate. - - stop_lr: Learning rate at the last step. - - decay_steps: Decay learning rate every N steps. - - stop_steps: When is the last step. + Parameters + ---------- + start_lr + The learning rate at the start of the training. + stop_lr + The desired learning rate at the end of the training. + When decay_rate is explicitly set, this value will serve as + the minimum learning rate during training. In other words, + if the learning rate decays below stop_lr, stop_lr will be applied instead. + decay_steps + The learning rate is decaying every this number of training steps. + stop_steps + The total training steps for learning rate scheduler. + decay_rate + The decay rate for the learning rate. + If provided, the decay rate will be set instead of + calculating it through interpolation between start_lr and stop_lr. """ self.start_lr = start_lr default_ds = 100 if stop_steps // 10 > 100 else stop_steps // 100 + 1 @@ -20,12 +41,9 @@ def __init__(self, start_lr, stop_lr, decay_steps, stop_steps, **kwargs): self.decay_rate = np.exp( np.log(stop_lr / self.start_lr) / (stop_steps / self.decay_steps) ) - if "decay_rate" in kwargs: - self.decay_rate = kwargs["decay_rate"] - if "min_lr" in kwargs: - self.min_lr = kwargs["min_lr"] - else: - self.min_lr = 3e-10 + if decay_rate is not None: + self.decay_rate = decay_rate + self.min_lr = stop_lr def value(self, step): """Get the learning rate at the given step.""" diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 1c2b7935b5..7a8e0e98cb 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -1517,15 +1517,32 @@ def linear_ener_model_args() -> Argument: # --- Learning rate configurations: --- # def learning_rate_exp(): doc_start_lr = "The learning rate at the start of the training." - doc_stop_lr = "The desired learning rate at the end of the training." + doc_stop_lr = ( + "The desired learning rate at the end of the training. " + f"When decay_rate {doc_only_pt_supported}is explicitly set, " + "this value will serve as the minimum learning rate during training. " + "In other words, if the learning rate decays below stop_lr, stop_lr will be applied instead." + ) doc_decay_steps = ( "The learning rate is decaying every this number of training steps." ) + doc_decay_rate = ( + "The decay rate for the learning rate. " + "If this is provided, it will be used directly as the decay rate for learning rate " + "instead of calculating it through interpolation between start_lr and stop_lr." + ) args = [ Argument("start_lr", float, optional=True, default=1e-3, doc=doc_start_lr), Argument("stop_lr", float, optional=True, default=1e-8, doc=doc_stop_lr), Argument("decay_steps", int, optional=True, default=5000, doc=doc_decay_steps), + Argument( + "decay_rate", + float, + optional=True, + default=None, + doc=doc_only_pt_supported + doc_decay_rate, + ), ] return args diff --git a/source/tests/pt/test_lr.py b/source/tests/pt/test_lr.py index ca1ec7e490..9fbde599bb 100644 --- a/source/tests/pt/test_lr.py +++ b/source/tests/pt/test_lr.py @@ -27,6 +27,7 @@ def test_consistency(self): self.decay_step = decay_step self.stop_step = stop_step self.judge_it() + self.decay_rate_pt() def judge_it(self): base_lr = learning_rate.LearningRateExp( @@ -54,6 +55,52 @@ def judge_it(self): self.assertTrue(np.allclose(base_vals, my_vals)) tf.reset_default_graph() + def decay_rate_pt(self): + my_lr = LearningRateExp( + self.start_lr, self.stop_lr, self.decay_step, self.stop_step + ) + + default_ds = 100 if self.stop_step // 10 > 100 else self.stop_step // 100 + 1 + if self.decay_step >= self.stop_step: + self.decay_step = default_ds + decay_rate = np.exp( + np.log(self.stop_lr / self.start_lr) / (self.stop_step / self.decay_step) + ) + my_lr_decay = LearningRateExp( + self.start_lr, + 1e-10, + self.decay_step, + self.stop_step, + decay_rate=decay_rate, + ) + min_lr = 1e-5 + my_lr_decay_trunc = LearningRateExp( + self.start_lr, + min_lr, + self.decay_step, + self.stop_step, + decay_rate=decay_rate, + ) + my_vals = [ + my_lr.value(step_id) + for step_id in range(self.stop_step) + if step_id % self.decay_step != 0 + ] + my_vals_decay = [ + my_lr_decay.value(step_id) + for step_id in range(self.stop_step) + if step_id % self.decay_step != 0 + ] + my_vals_decay_trunc = [ + my_lr_decay_trunc.value(step_id) + for step_id in range(self.stop_step) + if step_id % self.decay_step != 0 + ] + self.assertTrue(np.allclose(my_vals_decay, my_vals)) + self.assertTrue( + np.allclose(my_vals_decay_trunc, np.clip(my_vals, a_min=min_lr, a_max=None)) + ) + if __name__ == "__main__": unittest.main() From 9c861c24f1be3874b08fe36274d8f4afea5b89f1 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Wed, 20 Mar 2024 13:12:58 +0800 Subject: [PATCH 234/686] pt: add index input for `use_spin` (#3456) It's convenient for spin multitask input file to handle different spin inputs. --- deepmd/pt/model/model/__init__.py | 8 ++++++++ deepmd/utils/argcheck.py | 11 ++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index 7a2070e476..1675215d7b 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -14,6 +14,8 @@ import copy import json +import numpy as np + from deepmd.pt.model.atomic_model import ( DPAtomicModel, PairTabAtomicModel, @@ -57,6 +59,12 @@ def get_spin_model(model_params): model_params = copy.deepcopy(model_params) + if not model_params["spin"]["use_spin"] or isinstance( + model_params["spin"]["use_spin"][0], int + ): + use_spin = np.full(len(model_params["type_map"]), False) + use_spin[model_params["spin"]["use_spin"]] = True + model_params["spin"]["use_spin"] = use_spin.tolist() # include virtual spin and placeholder types model_params["type_map"] += [item + "_spin" for item in model_params["type_map"]] spin = Spin( diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 7a8e0e98cb..564039ccd0 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -93,7 +93,12 @@ def type_embedding_args(): def spin_args(): - doc_use_spin = "Whether to use atomic spin model for each atom type" + doc_use_spin = ( + "Whether to use atomic spin model for each atom type. " + "List of boolean values with the shape of [ntypes] to specify which types use spin, " + f"or a list of integer values {doc_only_pt_supported} " + "to indicate the index of the type that uses spin." + ) doc_spin_norm = "The magnitude of atomic spin for each atom type with spin" doc_virtual_len = "The distance between virtual atom representing spin and its corresponding real atom for each atom type with spin" doc_virtual_scale = ( @@ -106,7 +111,7 @@ def spin_args(): ) return [ - Argument("use_spin", List[bool], doc=doc_use_spin), + Argument("use_spin", [List[bool], List[int]], doc=doc_use_spin), Argument( "spin_norm", List[float], @@ -121,7 +126,7 @@ def spin_args(): ), Argument( "virtual_scale", - List[float], + [List[float], float], optional=True, doc=doc_only_pt_supported + doc_virtual_scale, ), From 71ec631167875277515014d07c3b8ab27b5c4e38 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Wed, 20 Mar 2024 13:47:50 +0800 Subject: [PATCH 235/686] pt: refactor loss (#3569) This PR updates the loss interface to allow for a more flexible design. It enables processing input tensors before feeding them into the model, such as denoising operations (fyi @Chengqian-Zhang ). Previously, this was done in the data loader, which was less intuitive and more confusing. Now, users can easily handle these tasks within the loss function itself, as demonstrated in similar implementations in uni-mol: https://github.com/dptech-corp/Uni-Mol/blob/main/unimol/unimol/losses/unimol.py#L20. --- deepmd/pt/loss/ener.py | 30 ++++++++++++++------- deepmd/pt/loss/ener_spin.py | 13 ++++++--- deepmd/pt/loss/loss.py | 2 +- deepmd/pt/loss/tensor.py | 13 ++++++--- deepmd/pt/train/training.py | 9 +++++-- deepmd/pt/train/wrapper.py | 19 ++++++++----- source/tests/pt/model/test_model.py | 41 ++++++++++++++--------------- source/tests/pt/test_loss.py | 18 ++++++++++--- 8 files changed, 92 insertions(+), 53 deletions(-) diff --git a/deepmd/pt/loss/ener.py b/deepmd/pt/loss/ener.py index f29c3231f1..edae53a771 100644 --- a/deepmd/pt/loss/ener.py +++ b/deepmd/pt/loss/ener.py @@ -90,20 +90,30 @@ def __init__( self.use_l1_all = use_l1_all self.inference = inference - def forward(self, model_pred, label, natoms, learning_rate, mae=False): - """Return loss on loss and force. + def forward(self, input_dict, model, label, natoms, learning_rate, mae=False): + """Return loss on energy and force. - Args: - - natoms: Tell atom count. - - p_energy: Predicted energy of all atoms. - - p_force: Predicted force per atom. - - l_energy: Actual energy of all atoms. - - l_force: Actual force per atom. + Parameters + ---------- + input_dict : dict[str, torch.Tensor] + Model inputs. + model : torch.nn.Module + Model to be used to output the predictions. + label : dict[str, torch.Tensor] + Labels. + natoms : int + The local atom number. Returns ------- - - loss: Loss to minimize. + model_pred: dict[str, torch.Tensor] + Model predictions. + loss: torch.Tensor + Loss for model to minimize. + more_loss: dict[str, torch.Tensor] + Other losses for display. """ + model_pred = model(**input_dict) coef = learning_rate / self.starter_learning_rate pref_e = self.limit_pref_e + (self.start_pref_e - self.limit_pref_e) * coef pref_f = self.limit_pref_f + (self.start_pref_f - self.limit_pref_f) * coef @@ -200,7 +210,7 @@ def forward(self, model_pred, label, natoms, learning_rate, mae=False): more_loss["mae_v"] = mae_v.detach() if not self.inference: more_loss["rmse"] = torch.sqrt(loss.detach()) - return loss, more_loss + return model_pred, loss, more_loss @property def label_requirement(self) -> List[DataRequirementItem]: diff --git a/deepmd/pt/loss/ener_spin.py b/deepmd/pt/loss/ener_spin.py index 1f55dcc5df..1f10e3cf5f 100644 --- a/deepmd/pt/loss/ener_spin.py +++ b/deepmd/pt/loss/ener_spin.py @@ -63,13 +63,15 @@ def __init__( self.use_l1_all = use_l1_all self.inference = inference - def forward(self, model_pred, label, natoms, learning_rate, mae=False): + def forward(self, input_dict, model, label, natoms, learning_rate, mae=False): """Return energy loss with magnetic labels. Parameters ---------- - model_pred : dict[str, torch.Tensor] - Model predictions. + input_dict : dict[str, torch.Tensor] + Model inputs. + model : torch.nn.Module + Model to be used to output the predictions. label : dict[str, torch.Tensor] Labels. natoms : int @@ -77,11 +79,14 @@ def forward(self, model_pred, label, natoms, learning_rate, mae=False): Returns ------- + model_pred: dict[str, torch.Tensor] + Model predictions. loss: torch.Tensor Loss for model to minimize. more_loss: dict[str, torch.Tensor] Other losses for display. """ + model_pred = model(**input_dict) coef = learning_rate / self.starter_learning_rate pref_e = self.limit_pref_e + (self.start_pref_e - self.limit_pref_e) * coef pref_fr = self.limit_pref_fr + (self.start_pref_fr - self.limit_pref_fr) * coef @@ -175,7 +180,7 @@ def forward(self, model_pred, label, natoms, learning_rate, mae=False): if not self.inference: more_loss["rmse"] = torch.sqrt(loss.detach()) - return loss, more_loss + return model_pred, loss, more_loss @property def label_requirement(self) -> List[DataRequirementItem]: diff --git a/deepmd/pt/loss/loss.py b/deepmd/pt/loss/loss.py index 925ff8f4ef..cc253424ca 100644 --- a/deepmd/pt/loss/loss.py +++ b/deepmd/pt/loss/loss.py @@ -19,7 +19,7 @@ def __init__(self, **kwargs): """Construct loss.""" super().__init__() - def forward(self, model_pred, label, natoms, learning_rate): + def forward(self, input_dict, model, label, natoms, learning_rate): """Return loss .""" raise NotImplementedError diff --git a/deepmd/pt/loss/tensor.py b/deepmd/pt/loss/tensor.py index ec7ef0b323..238e6a7796 100644 --- a/deepmd/pt/loss/tensor.py +++ b/deepmd/pt/loss/tensor.py @@ -63,13 +63,15 @@ def __init__( "Can not assian zero weight both to `pref` and `pref_atomic`" ) - def forward(self, model_pred, label, natoms, learning_rate=0.0, mae=False): + def forward(self, input_dict, model, label, natoms, learning_rate=0.0, mae=False): """Return loss on local and global tensors. Parameters ---------- - model_pred : dict[str, torch.Tensor] - Model predictions. + input_dict : dict[str, torch.Tensor] + Model inputs. + model : torch.nn.Module + Model to be used to output the predictions. label : dict[str, torch.Tensor] Labels. natoms : int @@ -77,11 +79,14 @@ def forward(self, model_pred, label, natoms, learning_rate=0.0, mae=False): Returns ------- + model_pred: dict[str, torch.Tensor] + Model predictions. loss: torch.Tensor Loss for model to minimize. more_loss: dict[str, torch.Tensor] Other losses for display. """ + model_pred = model(**input_dict) del learning_rate, mae loss = torch.zeros(1, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE)[0] more_loss = {} @@ -133,7 +138,7 @@ def forward(self, model_pred, label, natoms, learning_rate=0.0, mae=False): loss += self.global_weight * l2_global_loss rmse_global = l2_global_loss.sqrt() / atom_num more_loss[f"rmse_global_{self.tensor_name}"] = rmse_global.detach() - return loss, more_loss + return model_pred, loss, more_loss @property def label_requirement(self) -> List[DataRequirementItem]: diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 2056b9b305..9fd675a8f2 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -696,8 +696,13 @@ def step(_step_id, task_key="Default"): module = ( self.wrapper.module if dist.is_initialized() else self.wrapper ) - loss, more_loss = module.loss[task_key]( - model_pred, + + def fake_model(): + return model_pred + + _, loss, more_loss = module.loss[task_key]( + {}, + fake_model, label_dict, int(input_dict["atype"].shape[-1]), learning_rate=pref_lr, diff --git a/deepmd/pt/train/wrapper.py b/deepmd/pt/train/wrapper.py index 061cd777db..6bc7cdc87a 100644 --- a/deepmd/pt/train/wrapper.py +++ b/deepmd/pt/train/wrapper.py @@ -168,15 +168,20 @@ def forward( has_spin = has_spin() if has_spin: input_dict["spin"] = spin - model_pred = self.model[task_key](**input_dict) - natoms = atype.shape[-1] - if not self.inference_only and not inference_only: - loss, more_loss = self.loss[task_key]( - model_pred, label, natoms=natoms, learning_rate=cur_lr + + if self.inference_only or inference_only: + model_pred = self.model[task_key](**input_dict) + return model_pred, None, None + else: + natoms = atype.shape[-1] + model_pred, loss, more_loss = self.loss[task_key]( + input_dict, + self.model[task_key], + label, + natoms=natoms, + learning_rate=cur_lr, ) return model_pred, loss, more_loss - else: - return model_pred, None, None def set_extra_state(self, state: Dict): self.model_params = state["model_params"] diff --git a/source/tests/pt/model/test_model.py b/source/tests/pt/model/test_model.py index 5a30de7ac8..aa1c0dd969 100644 --- a/source/tests/pt/model/test_model.py +++ b/source/tests/pt/model/test_model.py @@ -338,34 +338,33 @@ def test_consistency(self): batch["natoms"] = torch.tensor( batch["natoms_vec"], device=batch["coord"].device ).unsqueeze(0) - model_predict = my_model( - batch["coord"].to(env.DEVICE), - batch["atype"].to(env.DEVICE), - batch["box"].to(env.DEVICE), - do_atomic_virial=True, - ) - model_predict_1 = my_model( - batch["coord"].to(env.DEVICE), - batch["atype"].to(env.DEVICE), - batch["box"].to(env.DEVICE), - do_atomic_virial=False, + model_input = { + "coord": batch["coord"].to(env.DEVICE), + "atype": batch["atype"].to(env.DEVICE), + "box": batch["box"].to(env.DEVICE), + "do_atomic_virial": True, + } + model_input_1 = { + "coord": batch["coord"].to(env.DEVICE), + "atype": batch["atype"].to(env.DEVICE), + "box": batch["box"].to(env.DEVICE), + "do_atomic_virial": False, + } + label = { + "energy": batch["energy"].to(env.DEVICE), + "force": batch["force"].to(env.DEVICE), + } + cur_lr = my_lr.value(self.wanted_step) + model_predict, loss, _ = my_loss( + model_input, my_model, label, int(batch["natoms"][0, 0]), cur_lr ) + model_predict_1 = my_model(**model_input_1) p_energy, p_force, p_virial, p_atomic_virial = ( model_predict["energy"], model_predict["force"], model_predict["virial"], model_predict["atom_virial"], ) - cur_lr = my_lr.value(self.wanted_step) - model_pred = { - "energy": p_energy, - "force": p_force, - } - label = { - "energy": batch["energy"].to(env.DEVICE), - "force": batch["force"].to(env.DEVICE), - } - loss, _ = my_loss(model_pred, label, int(batch["natoms"][0, 0]), cur_lr) np.testing.assert_allclose( head_dict["energy"], p_energy.view(-1).cpu().detach().numpy() ) diff --git a/source/tests/pt/test_loss.py b/source/tests/pt/test_loss.py index dddc9af219..2abb22c2a9 100644 --- a/source/tests/pt/test_loss.py +++ b/source/tests/pt/test_loss.py @@ -171,8 +171,13 @@ def test_consistency(self): self.start_pref_v, self.limit_pref_v, ) - my_loss, my_more_loss = mine( - self.model_pred, + + def fake_model(): + return self.model_pred + + _, my_loss, my_more_loss = mine( + {}, + fake_model, self.label, self.nloc, self.cur_lr, @@ -345,8 +350,13 @@ def test_consistency(self): self.start_pref_fm, self.limit_pref_fm, ) - my_loss, my_more_loss = mine( - self.model_pred, + + def fake_model(): + return self.model_pred + + _, my_loss, my_more_loss = mine( + {}, + fake_model, self.label, self.nloc_tf, # use tf natoms pref self.cur_lr, From 5aa1b89901a31d8fd6fe55b33dcd9d3fee8f3d8d Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 20 Mar 2024 01:48:15 -0400 Subject: [PATCH 236/686] fix(pt): fix a typo in DeepEval to check do_atomic_virial (#3570) Signed-off-by: Jinzhe Zeng --- deepmd/pt/infer/deep_eval.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index b8031993c0..f46d5fce49 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -388,7 +388,7 @@ def _eval_model( else: aparam_input = None do_atomic_virial = any( - x.category == OutputVariableCategory.DERV_C_REDU for x in request_defs + x.category == OutputVariableCategory.DERV_C for x in request_defs ) batch_output = model( coord_input, From b2cc0e5d145b06d8dc9b75a5664ebc36c28f06b4 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Thu, 21 Mar 2024 14:56:47 +0800 Subject: [PATCH 237/686] pt: support multitask dp test (#3573) Fix #3471 --- deepmd/pt/infer/deep_eval.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index f46d5fce49..1262a56310 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -90,6 +90,7 @@ def __init__( *args: List[Any], auto_batch_size: Union[bool, int, AutoBatchSize] = True, neighbor_list: Optional["ase.neighborlist.NewPrimitiveNeighborList"] = None, + head: Optional[str] = None, **kwargs: Dict[str, Any], ): self.output_def = output_def @@ -99,9 +100,24 @@ def __init__( if "model" in state_dict: state_dict = state_dict["model"] self.input_param = state_dict["_extra_state"]["model_params"] - self.input_param["resuming"] = True self.multi_task = "model_dict" in self.input_param - assert not self.multi_task, "multitask mode currently not supported!" + if self.multi_task: + model_keys = list(self.input_param["model_dict"].keys()) + assert ( + head is not None + ), f"Head must be set for multitask model! Available heads are: {model_keys}" + assert ( + head in model_keys + ), f"No head named {head} in model! Available heads are: {model_keys}" + self.input_param = self.input_param["model_dict"][head] + state_dict_head = {"_extra_state": state_dict["_extra_state"]} + for item in state_dict: + if f"model.{head}." in item: + state_dict_head[ + item.replace(f"model.{head}.", "model.Default.") + ] = state_dict[item].clone() + state_dict = state_dict_head + self.input_param["resuming"] = True model = get_model(self.input_param).to(DEVICE) model = torch.jit.script(model) self.dp = ModelWrapper(model) From 145f50193c7ff79dccae365c2362ebb4e7c9f940 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 21 Mar 2024 03:02:16 -0400 Subject: [PATCH 238/686] ci: add linter for markdown, yaml, CSS (#3574) Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/labeler.yml | 76 +-- .pre-commit-config.yaml | 173 +++---- CONTRIBUTING.md | 100 ++-- README.md | 75 +-- doc/_static/css/custom.css | 24 +- doc/data/data-conv.md | 10 + doc/data/dpdata.md | 3 + doc/data/system.md | 64 +-- doc/development/cmake.md | 2 + doc/development/create-a-model-pt.md | 6 +- doc/development/create-a-model-tf.md | 2 + doc/development/type-embedding.md | 19 +- doc/environment.yml | 8 +- doc/freeze/compress.md | 29 +- doc/freeze/freeze.md | 9 +- doc/inference/cxx.md | 14 + doc/inference/nodejs.md | 16 +- doc/inference/python.md | 3 + doc/install/build-conda.md | 1 + doc/install/easy-install.md | 16 +- doc/install/install-from-source.md | 77 ++- doc/install/install-gromacs.md | 5 + doc/install/install-ipi.md | 3 + doc/install/install-lammps.md | 17 + doc/install/install-tf.1.12.md | 28 +- doc/install/install-tf.1.14-gpu.md | 19 +- doc/install/install-tf.1.14.md | 18 +- doc/install/install-tf.1.8.md | 26 +- doc/install/install-tf.2.12.md | 14 +- doc/install/install-tf.2.3.md | 14 +- doc/install/install-tf.2.8.md | 14 +- doc/logo.md | 10 +- doc/model/dplr.md | 45 +- doc/model/dprc.md | 139 ++--- doc/model/overall.md | 8 +- doc/model/pairtab.md | 11 +- doc/model/sel.md | 2 + doc/model/train-energy-spin.md | 11 +- doc/model/train-energy.md | 32 +- doc/model/train-fitting-dos.md | 13 +- doc/model/train-fitting-tensor.md | 479 +++++++++--------- doc/model/train-hybrid.md | 8 +- doc/model/train-se-a-mask.md | 29 +- doc/model/train-se-atten.md | 91 ++-- doc/model/train-se-e2-a-tebd.md | 18 +- doc/model/train-se-e2-a.md | 28 +- doc/model/train-se-e2-r.md | 9 +- doc/model/train-se-e3.md | 8 +- doc/nvnmd/nvnmd.md | 138 ++--- doc/test/model-deviation.md | 13 +- doc/test/test.md | 5 + doc/third-party/ase.md | 2 + doc/third-party/gromacs.md | 42 +- doc/third-party/ipi.md | 28 +- doc/third-party/lammps-command.md | 34 +- doc/third-party/out-of-deepmd-kit.md | 1 + doc/train/finetuning.md | 1 + doc/train/gpu-limitations.md | 1 + doc/train/multi-task-training-pt.md | 39 +- doc/train/multi-task-training-tf.md | 35 +- doc/train/parallel-training.md | 24 +- doc/train/tensorboard.md | 9 +- doc/train/training-advanced.md | 97 ++-- doc/train/training.md | 7 + doc/troubleshooting/howtoset_netsize.md | 204 ++++---- doc/troubleshooting/howtoset_num_nodes.md | 3 + doc/troubleshooting/installation.md | 6 + .../md-version-compatibility.md | 3 + doc/troubleshooting/model-compatability.md | 5 +- doc/troubleshooting/precision.md | 1 + source/nodejs/tests/test_deeppot.js | 18 +- .../tests/tf/yaml_inputs/water_se_a_v1.yaml | 20 +- source/tests/tf/yaml_inputs/water_v1.yaml | 22 +- 73 files changed, 1536 insertions(+), 1048 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index bca580cfea..b048574e77 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,38 +1,38 @@ -Python: -- changed-files: - - any-glob-to-any-file: - - deepmd/**/* - - source/tests/**/* -Docs: -- changed-files: - - any-glob-to-any-file: doc/**/* -Examples: -- changed-files: - - any-glob-to-any-file: examples/**/* -Core: -- changed-files: - - any-glob-to-any-file: source/lib/**/* -CUDA: -- changed-files: - - any-glob-to-any-file: source/lib/src/gpu/**/* -ROCM: -- changed-files: - - any-glob-to-any-file: source/lib/src/gpu/**/* -OP: -- changed-files: - - any-glob-to-any-file: source/op/**/* -C++: -- changed-files: - - any-glob-to-any-file: source/api_cc/**/* -C: -- changed-files: - - any-glob-to-any-file: source/api_c/**/* -LAMMPS: -- changed-files: - - any-glob-to-any-file: source/lmp/**/* -Gromacs: -- changed-files: - - any-glob-to-any-file: source/gmx/**/* -i-Pi: -- changed-files: - - any-glob-to-any-file: source/ipi/**/* +Python: + - changed-files: + - any-glob-to-any-file: + - deepmd/**/* + - source/tests/**/* +Docs: + - changed-files: + - any-glob-to-any-file: doc/**/* +Examples: + - changed-files: + - any-glob-to-any-file: examples/**/* +Core: + - changed-files: + - any-glob-to-any-file: source/lib/**/* +CUDA: + - changed-files: + - any-glob-to-any-file: source/lib/src/gpu/**/* +ROCM: + - changed-files: + - any-glob-to-any-file: source/lib/src/gpu/**/* +OP: + - changed-files: + - any-glob-to-any-file: source/op/**/* +C++: + - changed-files: + - any-glob-to-any-file: source/api_cc/**/* +C: + - changed-files: + - any-glob-to-any-file: source/api_c/**/* +LAMMPS: + - changed-files: + - any-glob-to-any-file: source/lmp/**/* +Gromacs: + - changed-files: + - any-glob-to-any-file: source/gmx/**/* +i-Pi: + - changed-files: + - any-glob-to-any-file: source/ipi/**/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1860f87fea..8e147fc2c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,118 +1,121 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - - id: trailing-whitespace + - id: trailing-whitespace exclude: "^.+\\.pbtxt$" - - id: end-of-file-fixer + - id: end-of-file-fixer exclude: "^.+\\.pbtxt$" - - id: check-yaml - - id: check-json - - id: check-added-large-files - args: ['--maxkb=1024', '--enforce-all'] + - id: check-yaml + - id: check-json + - id: check-added-large-files + args: ["--maxkb=1024", "--enforce-all"] exclude: | - (?x)^( - source/tests/infer/dipolecharge_e.pbtxt| - source/tests/infer/deeppolar_new.pbtxt - )$ - - id: check-merge-conflict - - id: check-symlinks - - id: check-toml -# Python -- repo: https://github.com/PyCQA/isort + (?x)^( + source/tests/infer/dipolecharge_e.pbtxt| + source/tests/infer/deeppolar_new.pbtxt + )$ + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + # Python + - repo: https://github.com/PyCQA/isort rev: 5.13.2 hooks: - - id: isort - files: \.py$ - exclude: ^source/3rdparty -- repo: https://github.com/astral-sh/ruff-pre-commit + - id: isort + files: \.py$ + exclude: ^source/3rdparty + - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.3.3 hooks: - - id: ruff - args: ["--fix"] - exclude: ^source/3rdparty - types_or: [python, pyi, jupyter] - - id: ruff-format - exclude: ^source/3rdparty - types_or: [python, pyi, jupyter] -# numpydoc -- repo: https://github.com/Carreau/velin + - id: ruff + args: ["--fix"] + exclude: ^source/3rdparty + types_or: [python, pyi, jupyter] + - id: ruff-format + exclude: ^source/3rdparty + types_or: [python, pyi, jupyter] + # numpydoc + - repo: https://github.com/Carreau/velin rev: 0.0.12 hooks: - - id: velin - args: ["--write"] - exclude: ^source/3rdparty -# Python inside docs -- repo: https://github.com/asottile/blacken-docs + - id: velin + args: ["--write"] + exclude: ^source/3rdparty + # Python inside docs + - repo: https://github.com/asottile/blacken-docs rev: 1.16.0 hooks: - - id: blacken-docs -# C++ -- repo: https://github.com/pre-commit/mirrors-clang-format + - id: blacken-docs + # C++ + - repo: https://github.com/pre-commit/mirrors-clang-format rev: v18.1.1 hooks: - - id: clang-format + - id: clang-format exclude: ^source/3rdparty|source/lib/src/gpu/cudart/.+\.inc -# CSS -- repo: https://github.com/pre-commit/mirrors-csslint - rev: v1.0.5 + # markdown, yaml, CSS, javascript + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + types_or: [markdown, yaml, css] + # workflow files cannot be modified by pre-commit.ci + exclude: ^(source/3rdparty|\.github/workflows|\.clang-format) + # Shell + - repo: https://github.com/scop/pre-commit-shfmt + rev: v3.8.0-1 + hooks: + - id: shfmt + # CMake + - repo: https://github.com/cheshirekow/cmake-format-precommit + rev: v0.6.13 + hooks: + - id: cmake-format + #- id: cmake-lint + # license header + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.5 hooks: - - id: csslint -# Shell -- repo: https://github.com/scop/pre-commit-shfmt - rev: v3.8.0-1 - hooks: - - id: shfmt -# CMake -- repo: https://github.com/cheshirekow/cmake-format-precommit - rev: v0.6.13 - hooks: - - id: cmake-format - #- id: cmake-lint -# license header -- repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.5 - hooks: - # C++, js - - id: insert-license + # C++, js + - id: insert-license files: \.(c|cc|cpp|js|ts|h|hpp)$ args: - - --license-filepath - - .license-header.txt - - --comment-style - - // - - --no-extra-eol + - --license-filepath + - .license-header.txt + - --comment-style + - // + - --no-extra-eol exclude: ^source/3rdparty|source/lib/src/gpu/cudart/.+\.inc - # CSS - - id: insert-license + # CSS + - id: insert-license files: \.(css|scss)$ args: - - --license-filepath - - .license-header.txt - - --comment-style - - /*| *| */ - - --no-extra-eol - # Python - - id: insert-license + - --license-filepath + - .license-header.txt + - --comment-style + - /*| *| */ + - --no-extra-eol + # Python + - id: insert-license files: \.(py|pyx)$ args: - - --license-filepath - - .license-header.txt - - --comment-style - - "#" - - --no-extra-eol + - --license-filepath + - .license-header.txt + - --comment-style + - "#" + - --no-extra-eol exclude: ^source/3rdparty - # HTML - - id: insert-license + # HTML + - id: insert-license files: \.(html|vue|xml)$ args: - - --license-filepath - - .license-header.txt - - --comment-style - - - - --no-extra-eol + - --license-filepath + - .license-header.txt + - --comment-style + - + - --no-extra-eol ci: autoupdate_branch: devel diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2c28ae59b..cb08609c2b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ Welcome to [DeePMD-kit](https://github.com/deepmodeling/deepmd-kit)! You can either make a code contribution, help improve our document or offer help to other users. Your help is always appreciated. Come and have fun! ### Code contribution + You can start from any one of the following items to help improve deepmd-kit - Smash a bug @@ -18,6 +19,7 @@ See [here](#before-you-contribute) for some before-hand heads-up. See [here](#how-to-contribute) to learn how to contribute. ### Document improvement + You can start from any one of the following items to help improve [DeePMD-kit Docs](https://deepmd.readthedocs.io/en/latest/?badge=latest): - Fix typos or format (punctuation, space, indentation, code block, etc.) @@ -26,21 +28,27 @@ You can start from any one of the following items to help improve [DeePMD-kit Do - Translate docs changes from English to Chinese ### Offer help + You can help other users of deepmd-kit in the following way - Submit, reply to, and resolve [issues](https://github.com/deepmodeling/deepmd-kit/issues) - (Advanced) Review Pull Requests created by others ## Before you contribute + ### Overview of DeePMD-kit + Currently, we maintain two main branch: + - master: stable branch with version tag -- devel : branch for developers +- devel : branch for developers ### Developer guide + See [documentation](https://deepmd.readthedocs.io/) for coding conventions, API and other needs-to-know of the code. ## How to contribute + Please perform the following steps to create your Pull Request to this repository. If don't like to use commands, you can also use [GitHub Desktop](https://desktop.github.com/), which is easier to get started. Go to [git documentation](https://git-scm.com/doc) if you want to really master git. ### Step 1: Fork the repository @@ -51,79 +59,82 @@ Please perform the following steps to create your Pull Request to this repositor ### Step 2: Clone the forked repository to local storage and set configurations 1. Clone your own repo, not the public repo (from deepmodeling) ! And change the branch to devel. - ```bash - git clone https://github.com/$username/deepmd-kit.git - # Replace `$username` with your GitHub ID - git checkout devel - ``` + ```bash + git clone https://github.com/$username/deepmd-kit.git + # Replace `$username` with your GitHub ID + + git checkout devel + ``` 2. Add deepmodeling's repo as your remote repo, we can name it "upstream". And fetch upstream's latest codes to your workstation. - ```bash - git remote add upstream https://github.com/deepmodeling/deepmd-kit.git - # After you add a remote repo, your local repo will be automatically named "origin". - git fetch upstream + ```bash + git remote add upstream https://github.com/deepmodeling/deepmd-kit.git + # After you add a remote repo, your local repo will be automatically named "origin". - # If your current codes are behind the latest codes, you should merge latest codes first. - # Notice you should merge from "devel"! - git merge upstream/devel - ``` + git fetch upstream + + # If your current codes are behind the latest codes, you should merge latest codes first. + # Notice you should merge from "devel"! + git merge upstream/devel + ``` 3. Modify your codes and design unit tests. 4. Commit your changes - ```bash - git status # Checks the local status - git add ... # Adds the file(s) you want to commit. If you want to commit all changes, you can directly use `git add.` - git commit -m "commit-message: update the xx" - ``` + + ```bash + git status # Checks the local status + git add ... # Adds the file(s) you want to commit. If you want to commit all changes, you can directly use `git add.` + git commit -m "commit-message: update the xx" + ``` 5. Push the changed codes to your original repo on github. - ```bash - git push origin devel - ``` + ```bash + git push origin devel + ``` ### Alternatively: Create a new branch 1. Get your local master up-to-date with upstream/master. - ```bash - cd $working_dir/deepmd-kit - git fetch upstream - git checkout master - git rebase upstream/master - ``` + ```bash + cd $working_dir/deepmd-kit + git fetch upstream + git checkout master + git rebase upstream/master + ``` 2. Create a new branch based on the master branch. - ```bash - git checkout -b new-branch-name - ``` + ```bash + git checkout -b new-branch-name + ``` 3. Modify your codes and design unit tests. 4. Commit your changes - ```bash - git status # Checks the local status - git add ... # Adds the file(s) you want to commit. If you want to commit all changes, you can directly use `git add.` - git commit -m "commit-message: update the xx" - ``` + ```bash + git status # Checks the local status + git add ... # Adds the file(s) you want to commit. If you want to commit all changes, you can directly use `git add.` + git commit -m "commit-message: update the xx" + ``` 5. Keep your branch in sync with upstream/master - ```bash - # While on your new branch - git fetch upstream - git rebase upstream/master - ``` + ```bash + # While on your new branch + git fetch upstream + git rebase upstream/master + ``` 6. Push your changes to the remote - ```bash - git push -u origin new-branch-name # "-u" is used to track the remote branch from origin - ``` + ```bash + git push -u origin new-branch-name # "-u" is used to track the remote branch from origin + ``` ### Step 3: Create a pull request @@ -133,4 +144,5 @@ Please perform the following steps to create your Pull Request to this repositor Now, your PR is successfully submitted! After this PR is merged, you will automatically become a contributor to DeePMD-kit. ## Contact us + E-mail: contact@deepmodeling.org diff --git a/README.md b/README.md index 2076e11f1b..3838f2596a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [DeePMD-kit logo](./doc/logo.md) --------------------------------------------------------------------------------- +--- # DeePMD-kit @@ -12,35 +12,40 @@ [![Documentation Status](https://readthedocs.org/projects/deepmd/badge/)](https://deepmd.readthedocs.io/) ## About DeePMD-kit + DeePMD-kit is a package written in Python/C++, designed to minimize the effort required to build deep learning-based model of interatomic potential energy and force field and to perform molecular dynamics (MD). This brings new hopes to addressing the accuracy-versus-efficiency dilemma in molecular simulations. Applications of DeePMD-kit span from finite molecules to extended systems and from metallic systems to chemically bonded systems. For more information, check the [documentation](https://deepmd.readthedocs.io/). ### Highlighted features -* **interfaced with multiple backends**, including TensorFlow and PyTorch, the most popular deep learning frameworks, making the training process highly automatic and efficient. -* **interfaced with high-performance classical MD and quantum (path-integral) MD packages**, including LAMMPS, i-PI, AMBER, CP2K, GROMACS, OpenMM, and ABUCUS. -* **implements the Deep Potential series models**, which have been successfully applied to finite and extended systems, including organic molecules, metals, semiconductors, insulators, etc. -* **implements MPI and GPU supports**, making it highly efficient for high-performance parallel and distributed computing. -* **highly modularized**, easy to adapt to different descriptors for deep learning-based potential energy models. + +- **interfaced with multiple backends**, including TensorFlow and PyTorch, the most popular deep learning frameworks, making the training process highly automatic and efficient. +- **interfaced with high-performance classical MD and quantum (path-integral) MD packages**, including LAMMPS, i-PI, AMBER, CP2K, GROMACS, OpenMM, and ABUCUS. +- **implements the Deep Potential series models**, which have been successfully applied to finite and extended systems, including organic molecules, metals, semiconductors, insulators, etc. +- **implements MPI and GPU supports**, making it highly efficient for high-performance parallel and distributed computing. +- **highly modularized**, easy to adapt to different descriptors for deep learning-based potential energy models. ### License and credits + The project DeePMD-kit is licensed under [GNU LGPLv3.0](./LICENSE). If you use this code in any future publications, please cite the following publications for general purpose: + - Han Wang, Linfeng Zhang, Jiequn Han, and Weinan E. "DeePMD-kit: A deep learning package for many-body potential energy representation and molecular dynamics." Computer Physics Communications 228 (2018): 178-184. -[![doi:10.1016/j.cpc.2018.03.016](https://img.shields.io/badge/DOI-10.1016%2Fj.cpc.2018.03.016-blue)](https://doi.org/10.1016/j.cpc.2018.03.016) -[![Citations](https://citations.njzjz.win/10.1016/j.cpc.2018.03.016)](https://badge.dimensions.ai/details/doi/10.1016/j.cpc.2018.03.016) + [![doi:10.1016/j.cpc.2018.03.016](https://img.shields.io/badge/DOI-10.1016%2Fj.cpc.2018.03.016-blue)](https://doi.org/10.1016/j.cpc.2018.03.016) + [![Citations](https://citations.njzjz.win/10.1016/j.cpc.2018.03.016)](https://badge.dimensions.ai/details/doi/10.1016/j.cpc.2018.03.016) - Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang. "DeePMD-kit v2: A software package for deep potential models." J. Chem. Phys. 159 (2023): 054801. -[![doi:10.1063/5.0155600](https://img.shields.io/badge/DOI-10.1063%2F5.0155600-blue)](https://doi.org/10.1063/5.0155600) -[![Citations](https://citations.njzjz.win/10.1063/5.0155600)](https://badge.dimensions.ai/details/doi/10.1063/5.0155600) + [![doi:10.1063/5.0155600](https://img.shields.io/badge/DOI-10.1063%2F5.0155600-blue)](https://doi.org/10.1063/5.0155600) + [![Citations](https://citations.njzjz.win/10.1063/5.0155600)](https://badge.dimensions.ai/details/doi/10.1063/5.0155600) In addition, please follow [the bib file](CITATIONS.bib) to cite the methods you used. ### Highlights in major versions #### Initial version -The goal of Deep Potential is to employ deep learning techniques and realize an inter-atomic potential energy model that is general, accurate, computationally efficient and scalable. The key component is to respect the extensive and symmetry-invariant properties of a potential energy model by assigning a local reference frame and a local environment to each atom. Each environment contains a finite number of atoms, whose local coordinates are arranged in a symmetry-preserving way. These local coordinates are then transformed, through a sub-network, to so-called *atomic energy*. Summing up all the atomic energies gives the potential energy of the system. -The initial proof of concept is in the [Deep Potential][1] paper, which employed an approach that was devised to train the neural network model with the potential energy only. With typical *ab initio* molecular dynamics (AIMD) datasets this is insufficient to reproduce the trajectories. The Deep Potential Molecular Dynamics ([DeePMD][2]) model overcomes this limitation. In addition, the learning process in DeePMD improves significantly over the Deep Potential method thanks to the introduction of a flexible family of loss functions. The NN potential constructed in this way reproduces accurately the AIMD trajectories, both classical and quantum (path integral), in extended and finite systems, at a cost that scales linearly with system size and is always several orders of magnitude lower than that of equivalent AIMD simulations. +The goal of Deep Potential is to employ deep learning techniques and realize an inter-atomic potential energy model that is general, accurate, computationally efficient and scalable. The key component is to respect the extensive and symmetry-invariant properties of a potential energy model by assigning a local reference frame and a local environment to each atom. Each environment contains a finite number of atoms, whose local coordinates are arranged in a symmetry-preserving way. These local coordinates are then transformed, through a sub-network, to so-called _atomic energy_. Summing up all the atomic energies gives the potential energy of the system. + +The initial proof of concept is in the [Deep Potential][1] paper, which employed an approach that was devised to train the neural network model with the potential energy only. With typical _ab initio_ molecular dynamics (AIMD) datasets this is insufficient to reproduce the trajectories. The Deep Potential Molecular Dynamics ([DeePMD][2]) model overcomes this limitation. In addition, the learning process in DeePMD improves significantly over the Deep Potential method thanks to the introduction of a flexible family of loss functions. The NN potential constructed in this way reproduces accurately the AIMD trajectories, both classical and quantum (path integral), in extended and finite systems, at a cost that scales linearly with system size and is always several orders of magnitude lower than that of equivalent AIMD simulations. Although highly efficient, the original Deep Potential model satisfies the extensive and symmetry-invariant properties of a potential energy model at the price of introducing discontinuities in the model. This has negligible influence on a trajectory from canonical sampling but might not be sufficient for calculations of dynamical and mechanical properties. These points motivated us to develop the Deep Potential-Smooth Edition ([DeepPot-SE][3]) model, which replaces the non-smooth local frame with a smooth and adaptive embedding network. DeepPot-SE shows great ability in modeling many kinds of systems that are of interest in the fields of physics, chemistry, biology, and materials science. @@ -48,27 +53,27 @@ In addition to building up potential energy models, DeePMD-kit can also be used #### v1 -* Code refactor to make it highly modularized. -* GPU support for descriptors. +- Code refactor to make it highly modularized. +- GPU support for descriptors. #### v2 -* Model compression. Accelerate the efficiency of model inference 4-15 times. -* New descriptors. Including `se_e2_r`, `se_e3`, and `se_atten` (DPA-1). -* Hybridization of descriptors. Hybrid descriptor constructed from the concatenation of several descriptors. -* Atom type embedding. Enable atom-type embedding to decline training complexity and refine performance. -* Training and inference of the dipole (vector) and polarizability (matrix). -* Split of training and validation dataset. -* Optimized training on GPUs, including CUDA and ROCm. -* Non-von-Neumann. -* C API to interface with the third-party packages. +- Model compression. Accelerate the efficiency of model inference 4-15 times. +- New descriptors. Including `se_e2_r`, `se_e3`, and `se_atten` (DPA-1). +- Hybridization of descriptors. Hybrid descriptor constructed from the concatenation of several descriptors. +- Atom type embedding. Enable atom-type embedding to decline training complexity and refine performance. +- Training and inference of the dipole (vector) and polarizability (matrix). +- Split of training and validation dataset. +- Optimized training on GPUs, including CUDA and ROCm. +- Non-von-Neumann. +- C API to interface with the third-party packages. See [our latest paper](https://doi.org/10.1063/5.0155600) for details of all features until v2.2.3. #### v3 -* Multiple backends supported. Add a PyTorch backend. -* The DPA-2 model. +- Multiple backends supported. Add a PyTorch backend. +- The DPA-2 model. ## Install and use DeePMD-kit @@ -78,16 +83,16 @@ Please read the [online documentation](https://deepmd.readthedocs.io/) for how t The code is organized as follows: -* `examples`: examples. -* `deepmd`: DeePMD-kit python modules. -* `source/lib`: source code of the core library. -* `source/op`: Operator (OP) implementation. -* `source/api_cc`: source code of DeePMD-kit C++ API. -* `source/api_c`: source code of the C API. -* `source/nodejs`: source code of the Node.js API. -* `source/ipi`: source code of i-PI client. -* `source/lmp`: source code of Lammps module. -* `source/gmx`: source code of Gromacs plugin. +- `examples`: examples. +- `deepmd`: DeePMD-kit python modules. +- `source/lib`: source code of the core library. +- `source/op`: Operator (OP) implementation. +- `source/api_cc`: source code of DeePMD-kit C++ API. +- `source/api_c`: source code of the C API. +- `source/nodejs`: source code of the Node.js API. +- `source/ipi`: source code of i-PI client. +- `source/lmp`: source code of Lammps module. +- `source/gmx`: source code of Gromacs plugin. # Contributing diff --git a/doc/_static/css/custom.css b/doc/_static/css/custom.css index 8894f47813..d0b761e71d 100644 --- a/doc/_static/css/custom.css +++ b/doc/_static/css/custom.css @@ -1,20 +1,22 @@ /* * SPDX-License-Identifier: LGPL-3.0-or-later */ -pre{ - overflow: auto; +pre { + overflow: auto; } -.wy-side-nav-search .wy-dropdown > a img.logo, .wy-side-nav-search > a img.logo { - width: 275px; +.wy-side-nav-search .wy-dropdown > a img.logo, +.wy-side-nav-search > a img.logo { + width: 275px; } img.platform-icon { - height: 2ex; + height: 2ex; } @media (prefers-color-scheme: dark) { - .wy-side-nav-search .wy-dropdown > a img.logo, .wy-side-nav-search > a img.logo { - content: url("../logo-dark.svg"); - } - img.platform-icon { - filter: invert(1); - } + .wy-side-nav-search .wy-dropdown > a img.logo, + .wy-side-nav-search > a img.logo { + content: url("../logo-dark.svg"); + } + img.platform-icon { + filter: invert(1); + } } diff --git a/doc/data/data-conv.md b/doc/data/data-conv.md index e8464b1ea9..7634daf5e6 100644 --- a/doc/data/data-conv.md +++ b/doc/data/data-conv.md @@ -5,6 +5,7 @@ Two binary formats, NumPy and HDF5, are supported for training. The raw format i ## NumPy format In a system with the Numpy format, the system properties are stored as text files ending with `.raw`, such as `type.raw` and `type_map.raw`, under the system directory. If one needs to train a non-periodic system, an empty `nopbc` file should be put under the system directory. Both input and labeled frame properties are saved as the [NumPy binary data (NPY) files](https://numpy.org/doc/stable/reference/generated/numpy.lib.format.html#npy-format) ending with `.npy` in each of the `set.*` directories. Take an example, a system may contain the following files: + ``` type.raw type_map.raw @@ -18,16 +19,19 @@ set.001/force.npy ``` We assume that the atom types do not change in all frames. It is provided by `type.raw`, which has one line with the types of atoms written one by one. The atom types should be integers. For example the `type.raw` of a system that has 2 atoms with 0 and 1: + ```bash $ cat type.raw 0 1 ``` Sometimes one needs to map the integer types to atom names. The mapping can be given by the file `type_map.raw`. For example + ```bash $ cat type_map.raw O H ``` + The type `0` is named by `"O"` and the type `1` is named by `"H"`. For training models with descriptor `se_atten`, a [new system format](../model/train-se-atten.md#data-format) is supported to put together the frame-sparse systems with the same atom number. @@ -35,9 +39,11 @@ For training models with descriptor `se_atten`, a [new system format](../model/t ## HDF5 format A system with the HDF5 format has the same structure as the Numpy format, but in an HDF5 file, a system is organized as an [HDF5 group](https://docs.h5py.org/en/stable/high/group.html). The file name of a Numpy file is the key in an HDF5 file, and the data is the value of the key. One needs to use `#` in a DP path to divide the path to the HDF5 file and the HDF5 path: + ``` /path/to/data.hdf5#/H2O ``` + Here, `/path/to/data.hdf5` is the file path and `/H2O` is the HDF5 path. All HDF5 paths should start with `/`. There should be some data in the `H2O` group, such as `/H2O/type.raw` and `/H2O/set.000/force.npy`. An HDF5 file with a large number of systems has better performance than multiple NumPy files in a large cluster. @@ -47,15 +53,18 @@ An HDF5 file with a large number of systems has better performance than multiple A raw file is a plain text file with each information item written in one file and one frame written on one line. **It's not directly supported**, but we provide a tool to convert them. In the raw format, the property of one frame is provided per line, ending with `.raw`. Take an example, the default files that provide box, coordinate, force, energy and virial are `box.raw`, `coord.raw`, `force.raw`, `energy.raw` and `virial.raw`, respectively. Here is an example of `force.raw`: + ```bash $ cat force.raw -0.724 2.039 -0.951 0.841 -0.464 0.363 6.737 1.554 -5.587 -2.803 0.062 2.222 -1.968 -0.163 1.020 -0.225 -0.789 0.343 ``` + This `force.raw` contains 3 frames with each frame having the forces of 2 atoms, thus it has 3 lines and 6 columns. Each line provides all the 3 force components of 2 atoms in 1 frame. The first three numbers are the 3 force components of the first atom, while the second three numbers are the 3 force components of the second atom. Other files are organized similarly. The number of lines of all raw files should be identical. One can use the script `$deepmd_source_dir/data/raw/raw_to_set.sh` to convert the prepared raw files to the NumPy format. For example, if we have a raw file that contains 6000 frames, + ```bash $ ls box.raw coord.raw energy.raw force.raw type.raw virial.raw @@ -69,4 +78,5 @@ making set 2 ... $ ls box.raw coord.raw energy.raw force.raw set.000 set.001 set.002 type.raw virial.raw ``` + It generates three sets `set.000`, `set.001` and `set.002`, with each set containing 2000 frames in the Numpy format. diff --git a/doc/data/dpdata.md b/doc/data/dpdata.md index 9b1a27ce82..63fe4f39c3 100644 --- a/doc/data/dpdata.md +++ b/doc/data/dpdata.md @@ -3,16 +3,19 @@ One can use a convenient tool [`dpdata`](https://github.com/deepmodeling/dpdata) to convert data directly from the output of first principle packages to the DeePMD-kit format. To install one can execute + ```bash pip install dpdata ``` An example of converting data [VASP](https://www.vasp.at/) data in `OUTCAR` format to DeePMD-kit data can be found at + ``` $deepmd_source_dir/examples/data_conv ``` Switch to that directory, then one can convert data by using the following python script + ```python import dpdata diff --git a/doc/data/system.md b/doc/data/system.md index 0ecd0e9119..6ca044f1c9 100644 --- a/doc/data/system.md +++ b/doc/data/system.md @@ -4,44 +4,44 @@ DeePMD-kit takes a **system** as the data structure. A snapshot of a system is c A system should contain system properties, input frame properties, and labeled frame properties. The system property contains the following property: -ID | Property | Raw file | Required/Optional | Shape | Description --------- | ---------------------- | ------------ | -------------------- | ----------------------- | ----------- -type | Atom type indexes | type.raw | Required | Natoms | Integers that start with 0. If both the training parameter {ref}`type_map ` is set and `type_map.raw` is provided, the system atom type should be mapped to `type_map.raw` in `type.raw` and will be mapped to the model atom type when training; otherwise, the system atom type will be always mapped to the model atom type (whether {ref}`type_map ` is set or not) -type_map | Atom type names | type_map.raw | Optional | Ntypes | Atom names that map to atom type, which is unnecessary to be contained in the periodic table. Only works when the training parameter {ref}`type_map ` is set -nopbc | Non-periodic system | nopbc | Optional | 1 | If True, this system is non-periodic; otherwise it's periodic +| ID | Property | Raw file | Required/Optional | Shape | Description | +| -------- | ------------------- | ------------ | ----------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| type | Atom type indexes | type.raw | Required | Natoms | Integers that start with 0. If both the training parameter {ref}`type_map ` is set and `type_map.raw` is provided, the system atom type should be mapped to `type_map.raw` in `type.raw` and will be mapped to the model atom type when training; otherwise, the system atom type will be always mapped to the model atom type (whether {ref}`type_map ` is set or not) | +| type_map | Atom type names | type_map.raw | Optional | Ntypes | Atom names that map to atom type, which is unnecessary to be contained in the periodic table. Only works when the training parameter {ref}`type_map ` is set | +| nopbc | Non-periodic system | nopbc | Optional | 1 | If True, this system is non-periodic; otherwise it's periodic | The input frame properties contain the following property, the first axis of which is the number of frames: -ID | Property | Raw file | Unit | Required/Optional | Shape | Description --------- | ---------------------- | -------------- | ---- | -------------------- | ----------------------- | ----------- -coord | Atomic coordinates | coord.raw | Å | Required | Nframes \* Natoms \* 3 | -box | Boxes | box.raw | Å | Required if periodic | Nframes \* 3 \* 3 | in the order `XX XY XZ YX YY YZ ZX ZY ZZ` -fparam | Extra frame parameters | fparam.raw | Any | Optional | Nframes \* Any | -aparam | Extra atomic parameters | aparam.raw | Any | Optional | Nframes \* aparam \* Any | -numb_copy | Each frame is copied by the `numb_copy` (int) times | prob.raw | 1 | Optional | Nframes | Integer; Default is 1 for all frames +| ID | Property | Raw file | Unit | Required/Optional | Shape | Description | +| --------- | --------------------------------------------------- | ---------- | ---- | -------------------- | ------------------------ | ----------------------------------------- | +| coord | Atomic coordinates | coord.raw | Å | Required | Nframes \* Natoms \* 3 | +| box | Boxes | box.raw | Å | Required if periodic | Nframes \* 3 \* 3 | in the order `XX XY XZ YX YY YZ ZX ZY ZZ` | +| fparam | Extra frame parameters | fparam.raw | Any | Optional | Nframes \* Any | +| aparam | Extra atomic parameters | aparam.raw | Any | Optional | Nframes \* aparam \* Any | +| numb_copy | Each frame is copied by the `numb_copy` (int) times | prob.raw | 1 | Optional | Nframes | Integer; Default is 1 for all frames | The labeled frame properties are listed as follows, all of which will be used for training if and only if the loss function contains such property: -ID | Property | Raw file | Unit | Shape | Description ----------------------- | ----------------------- | ------------------------ | ---- | ----------------------- | ----------- -energy | Frame energies | energy.raw | eV | Nframes | -force | Atomic forces | force.raw | eV/Å | Nframes \* Natoms \* 3 | -virial | Frame virial | virial.raw | eV | Nframes \* 9 | in the order `XX XY XZ YX YY YZ ZX ZY ZZ` -atom_ener | Atomic energies | atom_ener.raw | eV | Nframes \* Natoms | -atom_pref | Weights of atomic forces | atom_pref.raw | 1 | Nframes \* Natoms | -dipole | Frame dipole | dipole.raw | Any | Nframes \* 3 | -atomic_dipole | Atomic dipole | atomic_dipole.raw | Any | Nframes \* Natoms \* 3 | -polarizability | Frame polarizability | polarizability.raw | Any | Nframes \* 9 | in the order `XX XY XZ YX YY YZ ZX ZY ZZ` -atomic_polarizability | Atomic polarizability | atomic_polarizability.raw| Any | Nframes \* Natoms \* 9 | in the order `XX XY XZ YX YY YZ ZX ZY ZZ` -drdq | Partial derivative of atomic coordinates with respect to generalized coordinates | drdq.raw | 1 | Nframes \* Natoms \* 3 \* Ngen_coords | +| ID | Property | Raw file | Unit | Shape | Description | +| --------------------- | -------------------------------------------------------------------------------- | ------------------------- | ---- | ------------------------------------- | ----------------------------------------- | +| energy | Frame energies | energy.raw | eV | Nframes | +| force | Atomic forces | force.raw | eV/Å | Nframes \* Natoms \* 3 | +| virial | Frame virial | virial.raw | eV | Nframes \* 9 | in the order `XX XY XZ YX YY YZ ZX ZY ZZ` | +| atom_ener | Atomic energies | atom_ener.raw | eV | Nframes \* Natoms | +| atom_pref | Weights of atomic forces | atom_pref.raw | 1 | Nframes \* Natoms | +| dipole | Frame dipole | dipole.raw | Any | Nframes \* 3 | +| atomic_dipole | Atomic dipole | atomic_dipole.raw | Any | Nframes \* Natoms \* 3 | +| polarizability | Frame polarizability | polarizability.raw | Any | Nframes \* 9 | in the order `XX XY XZ YX YY YZ ZX ZY ZZ` | +| atomic_polarizability | Atomic polarizability | atomic_polarizability.raw | Any | Nframes \* Natoms \* 9 | in the order `XX XY XZ YX YY YZ ZX ZY ZZ` | +| drdq | Partial derivative of atomic coordinates with respect to generalized coordinates | drdq.raw | 1 | Nframes \* Natoms \* 3 \* Ngen_coords | In general, we always use the following convention of units: -Property | Unit ----------| ---- -Time | ps -Length | Å -Energy | eV -Force | eV/Å -Virial | eV -Pressure | Bar +| Property | Unit | +| -------- | ---- | +| Time | ps | +| Length | Å | +| Energy | eV | +| Force | eV/Å | +| Virial | eV | +| Pressure | Bar | diff --git a/doc/development/cmake.md b/doc/development/cmake.md index 3073327856..f8508d8992 100644 --- a/doc/development/cmake.md +++ b/doc/development/cmake.md @@ -9,11 +9,13 @@ find_package(DeePMD REQUIRED) Note that you may need to add ${deepmd_root} to the cached CMake variable `CMAKE_PREFIX_PATH`. To link against the C interface library, using + ```cmake target_link_libraries(some_library PRIVATE DeePMD::deepmd_c) ``` To link against the C++ interface library, using + ```cmake target_link_libraries(some_library PRIVATE DeePMD::deepmd_cc) ``` diff --git a/doc/development/create-a-model-pt.md b/doc/development/create-a-model-pt.md index 6fcddd33d8..35d81b364a 100644 --- a/doc/development/create-a-model-pt.md +++ b/doc/development/create-a-model-pt.md @@ -3,6 +3,7 @@ If you'd like to create a new model that isn't covered by the existing DeePMD-kit library, but reuse DeePMD-kit's other efficient modules such as data processing, trainner, etc, you may want to read this section. To incorporate your custom model you'll need to: + 1. Register and implement new components (e.g. descriptor) in a Python file. 2. Register new arguments for user inputs. 3. Package new codes into a Python package. @@ -72,7 +73,6 @@ The serialize and deserialize methods are important for cross-backend model conv In many instances, there is no requirement to create a new fitting net. For fitting user-defined scalar properties, the {py:class}`deepmd.pt.model.task.ener.InvarFitting` class can be utilized. However, if there is a need for a new fitting net, one should inherit from both the {py:class}`deepmd.pt.model.task.base_fitting.BaseFitting` class and the {py:class}`torch.nn.Module` class. Alternatively, for a more straightforward approach, inheritance from the {py:class}`deepmd.pt.model.task.fitting.GeneralFitting` class is also an option. - ```py from deepmd.pt.model.task.fitting import ( GeneralFitting, @@ -104,10 +104,12 @@ class SomeFittingNet(GeneralFitting): def output_def(self) -> FittingOutputDef: pass ``` + ### New models + The PyTorch backend's model architecture is meticulously structured with multiple layers of abstraction, ensuring a high degree of flexibility. Typically, the process commences with an atomic model responsible for atom-wise property calculations. This atomic model inherits from both the {py:class}`deepmd.pt.model.atomic_model.base_atomic_model.BaseAtomicModel` class and the {py:class}`torch.nn.Module` class. -Subsequently, the `AtomicModel` is encapsulated using the `make_model(AtomicModel)` function, which leverages the `deepmd.pt.model.model.make_model.make_model` function. The purpose of the `make_model` wrapper is to facilitate the translation between atomic property predictions and the extended property predictions and differentiation , e.g. the reduction of atomic energy contribution and the autodiff for calculating the forces and virial. The developers usually need to implement an `AtomicModel` not a `Model`. +Subsequently, the `AtomicModel` is encapsulated using the `make_model(AtomicModel)` function, which leverages the `deepmd.pt.model.model.make_model.make_model` function. The purpose of the `make_model` wrapper is to facilitate the translation between atomic property predictions and the extended property predictions and differentiation , e.g. the reduction of atomic energy contribution and the autodiff for calculating the forces and virial. The developers usually need to implement an `AtomicModel` not a `Model`. ```py from deepmd.pt.model.atomic_model.base_atomic_model import ( diff --git a/doc/development/create-a-model-tf.md b/doc/development/create-a-model-tf.md index 7c4f5335ec..b39313a8d3 100644 --- a/doc/development/create-a-model-tf.md +++ b/doc/development/create-a-model-tf.md @@ -3,6 +3,7 @@ If you'd like to create a new model that isn't covered by the existing DeePMD-kit library, but reuse DeePMD-kit's other efficient modules such as data processing, trainner, etc, you may want to read this section. To incorporate your custom model you'll need to: + 1. Register and implement new components (e.g. descriptor) in a Python file. You may also want to register new TensorFlow OPs if necessary. 2. Register new arguments for user inputs. 3. Package new codes into a Python package. @@ -13,6 +14,7 @@ To incorporate your custom model you'll need to: When creating a new component, take descriptor as the example, one should inherit from the {py:class}`deepmd.tf.descriptor.descriptor.Descriptor` class and override several methods. Abstract methods such as {py:class}`deepmd.tf.descriptor.descriptor.Descriptor.build` must be implemented and others are not. You should keep arguments of these methods unchanged. After implementation, you need to register the component with a key: + ```py from deepmd.tf.descriptor import Descriptor diff --git a/doc/development/type-embedding.md b/doc/development/type-embedding.md index 5919d6c944..10eeed6ee9 100644 --- a/doc/development/type-embedding.md +++ b/doc/development/type-embedding.md @@ -1,11 +1,15 @@ # Atom Type Embedding + ## Overview + Here is an overview of the DeePMD-kit algorithm. Given a specific centric atom, we can obtain the matrix describing its local environment, named $\mathcal R$. It consists of the distance between the centric atom and its neighbors, as well as a direction vector. We can embed each distance into a vector of $M_1$ dimension by an `embedding net`, so the environment matrix $\mathcal R$ can be embedded into matrix $\mathcal G$. We can thus extract a descriptor vector (of $M_1 \times M_2$ dim) of the centric atom from the $\mathcal G$ by some matrix multiplication, and put the descriptor into `fitting net` to get the predicted energy $E$. The vanilla version of DeePMD-kit builds `embedding net` and `fitting net` relying on the atom type, resulting in $O(N)$ memory usage. After applying atom type embedding, in DeePMD-kit v2.0, we can share one `embedding net` and one `fitting net` in total, which reduces training complexity largely. ## Preliminary + In the following chart, you can find the meaning of symbols used to clarify the atom-type embedding algorithm. + $i$: Type of centric atom $j$: Type of neighbor atom @@ -40,8 +44,10 @@ $$E = F( [ \text{Multi}( \mathcal G( [s_{ij}, A(j)] ) ), A(j)] )$$ The difference between the two variants above is whether using the information of centric atom when generating the descriptor. Users can choose by modifying the `type_one_side` hyper-parameter in the input JSON file. ## How to use + A detailed introduction can be found at [`se_e2_a_tebd`](../model/train-se-e2-a-tebd.md). Looking for a fast start-up, you can simply add a `type_embedding` section in the input JSON file as displayed in the following, and the algorithm will adopt the atom type embedding algorithm automatically. An example of `type_embedding` is like + ```json "type_embedding":{ "neuron": [2, 4, 8], @@ -50,19 +56,26 @@ An example of `type_embedding` is like } ``` - ## Code Modification + Atom-type embedding can be applied to varied `embedding net` and `fitting net`, as a result, we build a class `TypeEmbedNet` to support this free combination. In the following, we will go through the execution process of the code to explain our code modification. ### trainer (train/trainer.py) + In trainer.py, it will parse the parameter from the input JSON file. If a `type_embedding` section is detected, it will build a `TypeEmbedNet`, which will be later input in the `model`. `model` will be built in the function `_build_network`. + ### model (model/ener.py) + When building the operation graph of the `model` in `model.build`. If a `TypeEmbedNet` is detected, it will build the operation graph of `type embed net`, `embedding net` and `fitting net` by order. The building process of `type embed net` can be found in `TypeEmbedNet.build`, which output the type embedding vector of each atom type (of [$\text{ntypes} \times \text{nchanl}$] dimensions). We then save the type embedding vector into `input_dict`, so that they can be fetched later in `embedding net` and `fitting net`. -### embedding net (descriptor/se*.py) + +### embedding net (descriptor/se\*.py) + In `embedding net`, we shall take local environment $\mathcal R$ as input and output matrix $\mathcal G$. Functions called in this process by the order is + ``` build -> _pass_filter -> _filter -> _filter_lower ``` + `_pass_filter`: It will first detect whether an atom type embedding exists, if so, it will apply atom type embedding algorithm and doesn't divide the input by type. `_filter`: It will call `_filter_lower` function to obtain the result of matrix multiplication ($\mathcal G^T\cdot \mathcal R$), do further multiplication involved in $\text{Multi}(\cdot)$, and finally output the result of descriptor vector of $M_1 \times M_2$ dim. @@ -70,8 +83,8 @@ build -> _pass_filter -> _filter -> _filter_lower `_filter_lower`: The main function handling input modification. If type embedding exists, it will call `_concat_type_embedding` function to concat the first column of input $\mathcal R$ (the column of $s_{ij}$) with the atom type embedding information. It will decide whether to use the atom type embedding vector of the centric atom according to the value of `type_one_side` (if set **True**, then we only use the vector of the neighbor atom). The modified input will be put into the `fitting net` to get $\mathcal G$ for further matrix multiplication stage. ### fitting net (fit/ener.py) -In `fitting net`, it takes the descriptor vector as input, whose dimension is [natoms, $M_1\times M_2$]. Because we need to involve information on the centric atom in this step, we need to generate a matrix named `atype_embed` (of dim [natoms, nchanl]), in which each row is the type embedding vector of the specific centric atom. The input is sorted by type of centric atom, we also know the number of a particular atom type (stored in `natoms[2+i]`), thus we get the type vector of the centric atom. In the build phase of the fitting net, it will check whether type embedding exists in `input_dict` and fetch them. After that, call `embed_atom_type` function to look up the embedding vector for the type vector of the centric atom to obtain `atype_embed`, and concat input with it ([input, atype_embed]). The modified input goes through `fitting` net` to get predicted energy. +In `fitting net`, it takes the descriptor vector as input, whose dimension is [natoms, $M_1\times M_2$]. Because we need to involve information on the centric atom in this step, we need to generate a matrix named `atype_embed` (of dim [natoms, nchanl]), in which each row is the type embedding vector of the specific centric atom. The input is sorted by type of centric atom, we also know the number of a particular atom type (stored in `natoms[2+i]`), thus we get the type vector of the centric atom. In the build phase of the fitting net, it will check whether type embedding exists in `input_dict` and fetch them. After that, call `embed_atom_type` function to look up the embedding vector for the type vector of the centric atom to obtain `atype_embed`, and concat input with it ([input, atype_embed]). The modified input goes through `fitting` net` to get predicted energy. :::{note} You can't apply the compression method while using atom-type embedding. diff --git a/doc/environment.yml b/doc/environment.yml index 635f24fe1e..85d5a97c5b 100644 --- a/doc/environment.yml +++ b/doc/environment.yml @@ -7,7 +7,7 @@ dependencies: - python=3.9 - pip>=20.1 - pip: - - ..[docs,cpu,torch] - - "exhale @ https://github.com/svenevs/exhale/archive/2759a394268307b88f5440487ae0920ee4ebf81e.zip" - # https://github.com/mcmtroffaes/sphinxcontrib-bibtex/issues/309 - - docutils!=0.18.*,!=0.19.* + - ..[docs,cpu,torch] + - "exhale @ https://github.com/svenevs/exhale/archive/2759a394268307b88f5440487ae0920ee4ebf81e.zip" + # https://github.com/mcmtroffaes/sphinxcontrib-bibtex/issues/309 + - docutils!=0.18.*,!=0.19.* diff --git a/doc/freeze/compress.md b/doc/freeze/compress.md index b6c8966c60..01cc9fa3a8 100644 --- a/doc/freeze/compress.md +++ b/doc/freeze/compress.md @@ -11,37 +11,46 @@ The compression of the DP model uses three techniques, tabulated inference, oper For better performance, the NN inference can be replaced by tabulated function evaluations if the input of the NN is of dimension one. The idea is to approximate the output of the NN by a piece-wise polynomial fitting. The input domain (a compact domain in $\mathbb R$) is divided into $L_c$ equally spaced intervals, in which we apply a fifth-order polynomial $g^l_m(x)$ approximation of the $m$-th output component of the NN function: + ```math g^l_m(x) = a^l_m x^5 + b^l_m x^4 + c^l_m x^3 + d^l_m x^2 + e^l_m x + f^l_m,\quad x \in [x_l, x_{l+1}), ``` + where $l=1,2,\dots,L_c$ is the index of the intervals, $x_1, \dots, x_{L_c}, x_{L_c+1}$ are the endpoints of the intervals, and $a^l_m$, $b^l_m$, $c^l_m$, $d^l_m$, $e^l_m$, and $f^l_m$ are the fitting parameters. The fitting parameters can be computed by the equations below: + ```math a^l_m = \frac{1}{2\Delta x_l^5}[12h_{m,l}-6(y'_{m,l+1}+y'_{m,l})\Delta x_l + (y''_{m,l+1}-y''_{m,l})\Delta x_l^2], ``` + ```math b^l_m = \frac{1}{2\Delta x_l^4}[-30h_{m,l} +(14y'_{m,l+1}+16y'_{m,l})\Delta x_l + (-2y''_{m,l+1}+3y''_{m,l})\Delta x_l^2], ``` + ```math c^l_m = \frac{1}{2\Delta x_l^3}[20h_{m,l}-(8y'_{m,l+1}+12y'_{m,l})\Delta x_l + (y''_{m,l+1}-3y''_{m,l})\Delta x_l^2], ``` + ```math d^l_m = \frac{1}{2}y''_{m,l}, ``` + ```math e^l_m = y_{m,l}', ``` + ```math f^l_m = y_{m,l}, ``` + where $\Delta x_l=x_{l+1}-x_l$ denotes the size of the interval. $h_{m,l}=y_{m,l+1}-y_{m,l}$. $y_{m,l} = y_m(x_l)$, $y'_{m,l} = y'_m(x_l)$ and $y''_{m,l} = y''_m(x_l)$ are the value, the first-order derivative, and the second-order derivative of the $m$-th component of the target NN function at the interval point $x_l$, respectively. The first and second-order derivatives are easily calculated by the back-propagation of the NN functions. -In the standard DP model inference, taking the [two-body embedding descriptor](../model/train-se-e2-a.md) as an example, the matrix product $(\mathcal G^i)^T \mathcal R$ requires the transfer of the tensor $\mathcal G^i$ between the register and the host/device memories, which usually becomes the bottle-neck of the computation due to the relatively small memory bandwidth of the GPUs. +In the standard DP model inference, taking the [two-body embedding descriptor](../model/train-se-e2-a.md) as an example, the matrix product $(\mathcal G^i)^T \mathcal R$ requires the transfer of the tensor $\mathcal G^i$ between the register and the host/device memories, which usually becomes the bottle-neck of the computation due to the relatively small memory bandwidth of the GPUs. The compressed DP model merges the matrix multiplication $(\mathcal G^i)^T \mathcal R$ with the tabulated inference step. More specifically, once one column of the $(\mathcal G^i)^T$ is evaluated, it is immediately multiplied with one row of the environment matrix in the register, and the outer product is deposited to the result of $(\mathcal G^i)^T \mathcal R$. -By the operator merging technique, the allocation of $\mathcal G^i$ and the memory movement between register and host/device memories is avoided. +By the operator merging technique, the allocation of $\mathcal G^i$ and the memory movement between register and host/device memories is avoided. The operator merging of the three-body embedding can be derived analogously. The first dimension, $N_c$, of the environment ($\mathcal R^i$) and embedding ($\mathcal G^i$) matrices is the expected maximum number of neighbors. @@ -49,19 +58,24 @@ If the number of neighbors of an atom is smaller than $N_c$, the corresponding p In practice, if the real number of neighbors is significantly smaller than $N_c$, a notable operation is spent on the multiplication of padding zeros. In the compressed DP model, the number of neighbors is precisely indexed at the tabulated inference stage, further saving computational costs.[^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). ## Instructions Once the frozen model is obtained from DeePMD-kit, we can get the neural network structure and its parameters (weights, biases, etc.) from the trained model, and compress it in the following way: + ```bash dp compress -i graph.pb -o graph-compress.pb ``` + where `-i` gives the original frozen model, `-o` gives the compressed model. Several other command line options can be passed to `dp compress`, which can be checked with + ```bash $ dp compress --help ``` + An explanation will be provided + ``` usage: dp compress [-h] [-v {DEBUG,3,INFO,2,WARNING,1,ERROR,0}] [-l LOG_PATH] [-m {master,collect,workers}] [-i INPUT] [-o OUTPUT] @@ -118,11 +132,12 @@ optional arguments: The training script of the input frozen model (default: None) ``` + **Parameter explanation** Model compression, which includes tabulating the embedding net. -The table is composed of fifth-order polynomial coefficients and is assembled from two sub-tables. For model descriptor with `se_e2_a` type, the first sub-table takes the stride(parameter) as its uniform stride, while the second sub-table takes 10 * stride as its uniform stride; For model descriptor with `se_e3` type, the first sub-table takes 10 * stride as it's uniform stride, while the second sub-table takes 100 * stride as it's uniform stride. -The range of the first table is automatically detected by DeePMD-kit, while the second table ranges from the first table's upper boundary(upper) to the extrapolate(parameter) * upper. +The table is composed of fifth-order polynomial coefficients and is assembled from two sub-tables. For model descriptor with `se_e2_a` type, the first sub-table takes the stride(parameter) as its uniform stride, while the second sub-table takes 10 _ stride as its uniform stride; For model descriptor with `se_e3` type, the first sub-table takes 10 _ stride as it's uniform stride, while the second sub-table takes 100 _ stride as it's uniform stride. +The range of the first table is automatically detected by DeePMD-kit, while the second table ranges from the first table's upper boundary(upper) to the extrapolate(parameter) _ upper. Finally, we added a check frequency parameter. It indicates how often the program checks for overflow(if the input environment matrix overflows the first or second table range) during the MD inference. **Justification of model compression** @@ -131,14 +146,14 @@ Model compression, with little loss of accuracy, can greatly speed up MD inferen **Acceptable original model version** -The model compression interface requires the version of DeePMD-kit used in the original model generation should be `2.0.0-alpha.0` or above. If one has a frozen 1.2 or 1.3 model, one can upgrade it through the `dp convert-from` interface. (eg: ```dp convert-from 1.2/1.3 -i old_frozen_model.pb -o new_frozen_model.pb```) +The model compression interface requires the version of DeePMD-kit used in the original model generation should be `2.0.0-alpha.0` or above. If one has a frozen 1.2 or 1.3 model, one can upgrade it through the `dp convert-from` interface. (eg: `dp convert-from 1.2/1.3 -i old_frozen_model.pb -o new_frozen_model.pb`) **Acceptable descriptor type** Descriptors with `se_e2_a`, `se_e3`, `se_e2_r` and `se_atten_v2` types are supported by the model compression feature. `Hybrid` mixed with the above descriptors is also supported. - **Available activation functions for descriptor:** + - tanh - gelu - relu diff --git a/doc/freeze/freeze.md b/doc/freeze/freeze.md index 151c0b3b44..b80928a119 100644 --- a/doc/freeze/freeze.md +++ b/doc/freeze/freeze.md @@ -9,6 +9,7 @@ The trained neural network is extracted from a checkpoint and dumped into a prot ```bash $ dp freeze -o model.pb ``` + in the folder where the model is trained. The output model is called `model.pb`. ::: @@ -18,6 +19,7 @@ in the folder where the model is trained. The output model is called `model.pb`. ```bash $ dp --pt freeze -o model.pth ``` + in the folder where the model is trained. The output model is called `model.pth`. ::: @@ -25,8 +27,9 @@ in the folder where the model is trained. The output model is called `model.pth` :::: In [multi-task mode](../train/multi-task-training.md): + - This process will in default output several models, each of which contains the common descriptor and -one of the user-defined fitting nets in {ref}`fitting_net_dict `, let's name it `fitting_key`, together frozen in `graph_{fitting_key}.pb`. -Those frozen models are exactly the same as single-task output with fitting net `fitting_key`. + one of the user-defined fitting nets in {ref}`fitting_net_dict `, let's name it `fitting_key`, together frozen in `graph_{fitting_key}.pb`. + Those frozen models are exactly the same as single-task output with fitting net `fitting_key`. - If you add `--united-model` option in this situation, -the total multi-task model will be frozen into one unit `graph.pb`, which is mainly for multi-task initialization and can not be used directly for inference. + the total multi-task model will be frozen into one unit `graph.pb`, which is mainly for multi-task initialization and can not be used directly for inference. diff --git a/doc/inference/cxx.md b/doc/inference/cxx.md index cc7e7be540..58c74df068 100644 --- a/doc/inference/cxx.md +++ b/doc/inference/cxx.md @@ -1,6 +1,9 @@ # C/C++ interface + ## C++ interface + The C++ interface of DeePMD-kit is also available for the model interface, which is considered faster than the Python interface. An example `infer_water.cpp` is given below: + ```cpp #include "deepmd/DeepPot.h" @@ -14,14 +17,18 @@ int main(){ dp.compute (e, f, v, coord, atype, cell); } ``` + where `e`, `f` and `v` are predicted energy, force and virial of the system, respectively. See {cpp:class}`deepmd::DeepPot` for details. You can compile `infer_water.cpp` using `gcc`: + ```sh gcc infer_water.cpp -L $deepmd_root/lib -L $tensorflow_root/lib -I $deepmd_root/include -Wl,--no-as-needed -ldeepmd_cc -lstdc++ -ltensorflow_cc -Wl,-rpath=$deepmd_root/lib -Wl,-rpath=$tensorflow_root/lib -o infer_water ``` + and then run the program: + ```sh ./infer_water ``` @@ -31,6 +38,7 @@ and then run the program: Although C is harder to write, the C library will not be affected by different versions of C++ compilers. An example `infer_water.c` is given below: + ```cpp #include #include @@ -71,10 +79,13 @@ where `e`, `f` and `v` are predicted energy, force and virial of the system, res See {cpp:func}`DP_DeepPotCompute` for details. You can compile `infer_water.c` using `gcc`: + ```sh gcc infer_water.c -L $deepmd_root/lib -L $tensorflow_root/lib -I $deepmd_root/include -Wl,--no-as-needed -ldeepmd_c -Wl,-rpath=$deepmd_root/lib -Wl,-rpath=$tensorflow_root/lib -o infer_water ``` + and then run the program: + ```sh ./infer_water ``` @@ -103,10 +114,13 @@ Note that the feature of the header-only C++ library is still limited compared t See {cpp:class}`deepmd::hpp::DeepPot` for details. You can compile `infer_water_hpp.cpp` using `gcc`: + ```sh gcc infer_water_hpp.cpp -L $deepmd_root/lib -L $tensorflow_root/lib -I $deepmd_root/include -Wl,--no-as-needed -ldeepmd_c -Wl,-rpath=$deepmd_root/lib -Wl,-rpath=$tensorflow_root/lib -o infer_water_hpp ``` + and then run the program: + ```sh ./infer_water_hpp ``` diff --git a/doc/inference/nodejs.md b/doc/inference/nodejs.md index 72bfa6f9d9..8d58881898 100644 --- a/doc/inference/nodejs.md +++ b/doc/inference/nodejs.md @@ -9,9 +9,9 @@ const deepmd = require("deepmd-kit"); const dp = new deepmd.DeepPot("graph.pb"); -const coord = [1., 0., 0., 0., 0., 1.5, 1., 0., 3.]; +const coord = [1, 0, 0, 0, 0, 1.5, 1, 0, 3]; const atype = [1, 0, 1]; -const cell = [10., 0., 0., 0., 10., 0., 0., 0., 10.]; +const cell = [10, 0, 0, 0, 10, 0, 0, 0, 10]; const v_coord = new deepmd.vectord(coord.length); const v_atype = new deepmd.vectori(atype.length); @@ -20,15 +20,21 @@ for (var i = 0; i < coord.length; i++) v_coord.set(i, coord[i]); for (var i = 0; i < atype.length; i++) v_atype.set(i, atype[i]); for (var i = 0; i < cell.length; i++) v_cell.set(i, cell[i]); -var energy = 0.0 +var energy = 0.0; var v_forces = new deepmd.vectord(); var v_virials = new deepmd.vectord(); energy = dp.compute(energy, v_forces, v_virials, v_coord, v_atype, v_cell); console.log("energy:", energy); -console.log("forces:", [...Array(v_forces.size()).keys()].map(i => v_forces.get(i))); -console.log("virials:", [...Array(v_virials.size()).keys()].map(i => v_virials.get(i))); +console.log( + "forces:", + [...Array(v_forces.size()).keys()].map((i) => v_forces.get(i)), +); +console.log( + "virials:", + [...Array(v_virials.size()).keys()].map((i) => v_virials.get(i)), +); ``` Energy, forces, and virials will be printed to the screen. diff --git a/doc/inference/python.md b/doc/inference/python.md index db61cd7843..73faa2b329 100644 --- a/doc/inference/python.md +++ b/doc/inference/python.md @@ -1,6 +1,7 @@ # Python interface One may use the python interface of DeePMD-kit for model inference, an example is given as follows + ```python from deepmd.infer import DeepPot import numpy as np @@ -11,9 +12,11 @@ cell = np.diag(10 * np.ones(3)).reshape([1, -1]) atype = [1, 0, 1] e, f, v = dp.eval(coord, cell, atype) ``` + where `e`, `f` and `v` are predicted energy, force and virial of the system, respectively. Furthermore, one can use the python interface to calculate model deviation. + ```python from deepmd.infer import calc_model_devi from deepmd.infer import DeepPot as DP diff --git a/doc/install/build-conda.md b/doc/install/build-conda.md index fee9f77acc..14dee5c263 100644 --- a/doc/install/build-conda.md +++ b/doc/install/build-conda.md @@ -16,6 +16,7 @@ For example, if one wants to turn on `MPIIO` package in LAMMPS, go to [`lammps-f ``` This requires that Docker has been installed. After the building, the packages will be generated in `build_artifacts/linux-64` and `build_artifacts/noarch`, and then one can install then executing + ```sh conda create -n deepmd lammps -c file:///path/to/build_artifacts -c https://conda.deepmodeling.com -c nvidia ``` diff --git a/doc/install/easy-install.md b/doc/install/easy-install.md index 6acfd98cb0..0c56fdb0c5 100644 --- a/doc/install/easy-install.md +++ b/doc/install/easy-install.md @@ -18,21 +18,24 @@ Python 3.8 or above is required for Python interface. - [Install with docker](#install-with-docker) - [Install Python interface with pip](#install-python-interface-with-pip) - ## Install off-line packages + Both CPU and GPU version offline packages are available on [the Releases page](https://github.com/deepmodeling/deepmd-kit/releases). Some packages are split into two files due to the size limit of GitHub. One may merge them into one after downloading: + ```bash cat deepmd-kit-2.2.9-cuda118-Linux-x86_64.sh.0 deepmd-kit-2.2.9-cuda118-Linux-x86_64.sh.1 > deepmd-kit-2.2.9-cuda118-Linux-x86_64.sh ``` One may enable the environment using + ```bash conda activate /path/to/deepmd-kit ``` ## Install with conda + DeePMD-kit is available with [conda](https://github.com/conda/conda). Install [Anaconda](https://www.anaconda.com/distribution/#download-section), [Miniconda](https://docs.conda.io/en/latest/miniconda.html), or [miniforge](https://conda-forge.org/download/) first. You can refer to [DeepModeling conda FAQ](https://docs.deepmodeling.com/faq/conda.html) for how to setup a conda environment. @@ -58,35 +61,43 @@ Maintainers will build packages in the conda-forge organization together with ot :::: One may create an environment that contains the CPU version of DeePMD-kit and LAMMPS: + ```bash conda create -n deepmd deepmd-kit=*=*cpu libdeepmd=*=*cpu lammps -c https://conda.deepmodeling.com -c defaults ``` Or one may want to create a GPU environment containing [CUDA Toolkit](https://docs.nvidia.com/deploy/cuda-compatibility/index.html#binary-compatibility__table-toolkit-driver): + ```bash conda create -n deepmd deepmd-kit=*=*gpu libdeepmd=*=*gpu lammps cudatoolkit=11.6 horovod -c https://conda.deepmodeling.com -c defaults ``` + One could change the CUDA Toolkit version from `10.2` or `11.6`. One may specify the DeePMD-kit version such as `2.2.9` using + ```bash conda create -n deepmd deepmd-kit=2.2.9=*cpu libdeepmd=2.2.9=*cpu lammps horovod -c https://conda.deepmodeling.com -c defaults ``` One may enable the environment using + ```bash conda activate deepmd ``` ## Install with docker + A docker for installing the DeePMD-kit is available [here](https://github.com/deepmodeling/deepmd-kit/pkgs/container/deepmd-kit). To pull the CPU version: + ```bash docker pull ghcr.io/deepmodeling/deepmd-kit:2.2.8_cpu ``` To pull the GPU version: + ```bash docker pull ghcr.io/deepmodeling/deepmd-kit:2.2.8_cuda12.0_gpu ``` @@ -109,15 +120,18 @@ pip install deepmd-kit-cu11[gpu,cu11] ``` Or install the CPU version without CUDA supported: + ```bash pip install torch --index-url https://download.pytorch.org/whl/cpu pip install deepmd-kit[cpu] ``` [The LAMMPS module](../third-party/lammps-command.md) and [the i-Pi driver](../third-party/ipi.md) are only provided on Linux and macOS for the TensorFlow backend. To install LAMMPS and/or i-Pi, add `lmp` and/or `ipi` to extras: + ```bash pip install deepmd-kit[gpu,cu12,torch,lmp,ipi] ``` + MPICH is required for parallel running. (The macOS arm64 package doesn't support MPI yet.) It is suggested to install the package into an isolated environment. diff --git a/doc/install/install-from-source.md b/doc/install/install-from-source.md index 389cc78c9f..8676928e09 100644 --- a/doc/install/install-from-source.md +++ b/doc/install/install-from-source.md @@ -3,21 +3,26 @@ Please follow our [GitHub](https://github.com/deepmodeling/deepmd-kit) webpage to download the [latest released version](https://github.com/deepmodeling/deepmd-kit/tree/master) and [development version](https://github.com/deepmodeling/deepmd-kit/tree/devel). Or get the DeePMD-kit source code by `git clone` + ```bash cd /some/workspace git clone https://github.com/deepmodeling/deepmd-kit.git deepmd-kit ``` For convenience, you may want to record the location of the source to a variable, saying `deepmd_source_dir` by + ```bash cd deepmd-kit deepmd_source_dir=`pwd` ``` ## Install the Python interface + ### Install Backend's Python interface + First, check the Python version on your machine. Python 3.8 or above is required. + ```bash python --version ``` @@ -36,16 +41,19 @@ pip install --upgrade pip :::{tab-item} TensorFlow {{ tensorflow_icon }} The full instruction to install TensorFlow can be found on the official [TensorFlow website](https://www.tensorflow.org/install/pip). TensorFlow 2.2 or later is supported. + ```bash pip install --upgrade tensorflow ``` If one does not need the GPU support of DeePMD-kit and is concerned about package size, the CPU-only version of TensorFlow should be installed by + ```bash pip install --upgrade tensorflow-cpu ``` To verify the installation, run + ```bash python -c "import tensorflow as tf;print(tf.reduce_sum(tf.random.normal([1000, 1000])))" ``` @@ -69,17 +77,23 @@ Follow [PyTorch documentation](https://pytorch.org/get-started/locally/) to inst :::: It is important that every time a new shell is started and one wants to use `DeePMD-kit`, the virtual environment should be activated by + ```bash source $deepmd_venv/bin/activate ``` + if one wants to skip out of the virtual environment, he/she can do + ```bash deactivate ``` + If one has multiple python interpreters named something like python3.x, it can be specified by, for example + ```bash virtualenv -p python3.8 $deepmd_venv ``` + One should remember to activate the virtual environment every time he/she uses DeePMD-kit. ### Install the DeePMD-kit's python interface @@ -103,6 +117,7 @@ Note that TensorFlow may have specific requirements for the compiler version to :::: Execute + ```bash cd $deepmd_source_dir pip install . @@ -110,26 +125,31 @@ pip install . One may set the following environment variables before executing `pip`: -| Environment variables | Allowed value | Default value | Usage | -| --------------------- | ---------------------- | ------------- | -------------------------- | -| DP_VARIANT | `cpu`, `cuda`, `rocm` | `cpu` | Build CPU variant or GPU variant with CUDA or ROCM support. | -| CUDAToolkit_ROOT | Path | Detected automatically | The path to the CUDA toolkit directory. CUDA 9.0 or later is supported. NVCC is required. | -| ROCM_ROOT | Path | Detected automatically | The path to the ROCM toolkit directory. | -| DP_ENABLE_TENSORFLOW | 0, 1 | 1 | {{ tensorflow_icon }} Enable the TensorFlow backend. -| TENSORFLOW_ROOT | Path | Detected automatically | {{ tensorflow_icon }} The path to TensorFlow Python library. By default the installer only finds TensorFlow under user site-package directory (`site.getusersitepackages()`) or system site-package directory (`sysconfig.get_path("purelib")`) due to limitation of [PEP-517](https://peps.python.org/pep-0517/). If not found, the latest TensorFlow (or the environment variable `TENSORFLOW_VERSION` if given) from PyPI will be built against.| -| DP_ENABLE_NATIVE_OPTIMIZATION | 0, 1 | 0 | Enable compilation optimization for the native machine's CPU type. Do not enable it if generated code will run on different CPUs. | -| CMAKE_ARGS | str | - | Additional CMake arguments | -| <LANG>FLAGS (``=`CXX`, `CUDA` or `HIP`) | str | - | Default compilation flags to be used when compiling `` files. See [CMake documentation](https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_FLAGS.html). | +| Environment variables | Allowed value | Default value | Usage | +| --------------------------------------------------- | --------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| DP_VARIANT | `cpu`, `cuda`, `rocm` | `cpu` | Build CPU variant or GPU variant with CUDA or ROCM support. | +| CUDAToolkit_ROOT | Path | Detected automatically | The path to the CUDA toolkit directory. CUDA 9.0 or later is supported. NVCC is required. | +| ROCM_ROOT | Path | Detected automatically | The path to the ROCM toolkit directory. | +| DP_ENABLE_TENSORFLOW | 0, 1 | 1 | {{ tensorflow_icon }} Enable the TensorFlow backend. | +| TENSORFLOW_ROOT | Path | Detected automatically | {{ tensorflow_icon }} The path to TensorFlow Python library. By default the installer only finds TensorFlow under user site-package directory (`site.getusersitepackages()`) or system site-package directory (`sysconfig.get_path("purelib")`) due to limitation of [PEP-517](https://peps.python.org/pep-0517/). If not found, the latest TensorFlow (or the environment variable `TENSORFLOW_VERSION` if given) from PyPI will be built against. | +| DP_ENABLE_NATIVE_OPTIMIZATION | 0, 1 | 0 | Enable compilation optimization for the native machine's CPU type. Do not enable it if generated code will run on different CPUs. | +| CMAKE_ARGS | str | - | Additional CMake arguments | +| <LANG>FLAGS (``=`CXX`, `CUDA` or `HIP`) | str | - | Default compilation flags to be used when compiling `` files. See [CMake documentation](https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_FLAGS.html). | To test the installation, one should first jump out of the source directory + ``` cd /some/other/workspace ``` + then execute + ```bash dp -h ``` + It will print the help information like + ```text usage: dp [-h] {train,freeze,test} ... @@ -149,12 +169,14 @@ Valid subcommands: ### Install horovod and mpi4py {{ tensorflow_icon }} [Horovod](https://github.com/horovod/horovod) and [mpi4py](https://github.com/mpi4py/mpi4py) are used for parallel training. For better performance on GPU, please follow the tuning steps in [Horovod on GPU](https://github.com/horovod/horovod/blob/master/docs/gpus.rst). + ```bash # With GPU, prefer NCCL as a communicator. HOROVOD_WITHOUT_GLOO=1 HOROVOD_WITH_TENSORFLOW=1 HOROVOD_GPU_OPERATIONS=NCCL HOROVOD_NCCL_HOME=/path/to/nccl pip install horovod mpi4py ``` If your work in a CPU environment, please prepare runtime as below: + ```bash # By default, MPI is used as communicator. HOROVOD_WITHOUT_GLOO=1 HOROVOD_WITH_TENSORFLOW=1 pip install horovod mpi4py @@ -218,6 +240,7 @@ You can also download libtorch prebuilt library from the [PyTorch website](https ### Install DeePMD-kit's C++ interface Now go to the source code directory of DeePMD-kit and make a building place. + ```bash cd $deepmd_source_dir/source mkdir build @@ -238,6 +261,7 @@ If you enable two or more backends, these backend libraries must be built in a c :::{tab-item} TensorFlow {{ tensorflow_icon }} I assume you have activated the TensorFlow Python environment and want to install DeePMD-kit into path `$deepmd_root`, then execute CMake + ```bash cmake -DENABLE_TENSORFLOW=TRUE -DUSE_TF_PYTHON_LIBS=TRUE -DCMAKE_INSTALL_PREFIX=$deepmd_root .. ``` @@ -249,38 +273,43 @@ If you specify `-DUSE_TF_PYTHON_LIBS=FALSE`, you need to give the location where :::{tab-item} PyTorch {{ pytorch_icon }} I assume you have installed the PyTorch (either Python or C++ interface) to `$torch_root`, then execute CMake + ```bash cmake -DENABLE_PYTORCH=TRUE -DCMAKE_PREFIX_PATH=$torch_root -DCMAKE_INSTALL_PREFIX=$deepmd_root .. ``` + ::: :::: One may add the following arguments to `cmake`: -| CMake Aurgements | Allowed value | Default value | Usage | -| ------------------------ | ------------------- | ------------- | ------------------------| -| -DENABLE_TENSORFLOW=<value> | `TRUE` or `FALSE` | `FALSE` | {{ tensorflow_icon }} Whether building the TensorFlow backend. | -| -DENABLE_PYTORCH=<value> | `TRUE` or `FALSE` | `FALSE` | {{ pytorch_icon }} Whether building the PyTorch backend. | -| -DTENSORFLOW_ROOT=<value> | Path | - | {{ tensorflow_icon }} The Path to TensorFlow's C++ interface. | -| -DCMAKE_INSTALL_PREFIX=<value> | Path | - | The Path where DeePMD-kit will be installed. | -| -DUSE_CUDA_TOOLKIT=<value> | `TRUE` or `FALSE` | `FALSE` | If `TRUE`, Build GPU support with CUDA toolkit. | -| -DCUDAToolkit_ROOT=<value> | Path | Detected automatically | The path to the CUDA toolkit directory. CUDA 9.0 or later is supported. NVCC is required. | -| -DUSE_ROCM_TOOLKIT=<value> | `TRUE` or `FALSE` | `FALSE` | If `TRUE`, Build GPU support with ROCM toolkit. | -| -DCMAKE_HIP_COMPILER_ROCM_ROOT=<value> | Path | Detected automatically | The path to the ROCM toolkit directory. | -| -DLAMMPS_SOURCE_ROOT=<value> | Path | - | Only neccessary for LAMMPS plugin mode. The path to the [LAMMPS source code](install-lammps.md). LAMMPS 8Apr2021 or later is supported. If not assigned, the plugin mode will not be enabled. | -| -DUSE_TF_PYTHON_LIBS=<value> | `TRUE` or `FALSE` | `FALSE` | {{ tensorflow_icon }} If `TRUE`, Build C++ interface with TensorFlow's Python libraries (TensorFlow's Python Interface is required). And there's no need for building TensorFlow's C++ interface.| -| -DENABLE_NATIVE_OPTIMIZATION=<value> | `TRUE` or `FALSE` | `FALSE` | Enable compilation optimization for the native machine's CPU type. Do not enable it if generated code will run on different CPUs. | -| -DCMAKE_<LANG>_FLAGS=<value> (``=`CXX`, `CUDA` or `HIP`) | str | - | Default compilation flags to be used when compiling `` files. See [CMake documentation](https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_FLAGS.html). | +| CMake Aurgements | Allowed value | Default value | Usage | +| ---------------------------------------------------------------------------- | ----------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| -DENABLE_TENSORFLOW=<value> | `TRUE` or `FALSE` | `FALSE` | {{ tensorflow_icon }} Whether building the TensorFlow backend. | +| -DENABLE_PYTORCH=<value> | `TRUE` or `FALSE` | `FALSE` | {{ pytorch_icon }} Whether building the PyTorch backend. | +| -DTENSORFLOW_ROOT=<value> | Path | - | {{ tensorflow_icon }} The Path to TensorFlow's C++ interface. | +| -DCMAKE_INSTALL_PREFIX=<value> | Path | - | The Path where DeePMD-kit will be installed. | +| -DUSE_CUDA_TOOLKIT=<value> | `TRUE` or `FALSE` | `FALSE` | If `TRUE`, Build GPU support with CUDA toolkit. | +| -DCUDAToolkit_ROOT=<value> | Path | Detected automatically | The path to the CUDA toolkit directory. CUDA 9.0 or later is supported. NVCC is required. | +| -DUSE_ROCM_TOOLKIT=<value> | `TRUE` or `FALSE` | `FALSE` | If `TRUE`, Build GPU support with ROCM toolkit. | +| -DCMAKE_HIP_COMPILER_ROCM_ROOT=<value> | Path | Detected automatically | The path to the ROCM toolkit directory. | +| -DLAMMPS_SOURCE_ROOT=<value> | Path | - | Only neccessary for LAMMPS plugin mode. The path to the [LAMMPS source code](install-lammps.md). LAMMPS 8Apr2021 or later is supported. If not assigned, the plugin mode will not be enabled. | +| -DUSE_TF_PYTHON_LIBS=<value> | `TRUE` or `FALSE` | `FALSE` | {{ tensorflow_icon }} If `TRUE`, Build C++ interface with TensorFlow's Python libraries (TensorFlow's Python Interface is required). And there's no need for building TensorFlow's C++ interface. | +| -DENABLE_NATIVE_OPTIMIZATION=<value> | `TRUE` or `FALSE` | `FALSE` | Enable compilation optimization for the native machine's CPU type. Do not enable it if generated code will run on different CPUs. | +| -DCMAKE\_<LANG>\_FLAGS=<value> (``=`CXX`, `CUDA` or `HIP`) | str | - | Default compilation flags to be used when compiling `` files. See [CMake documentation](https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_FLAGS.html). | If the CMake has been executed successfully, then run the following make commands to build the package: + ```bash make -j4 make install ``` + Option `-j4` means using 4 processes in parallel. You may want to use a different number according to your hardware. If everything works fine, you will have the executable and libraries installed in `$deepmd_root/bin` and `$deepmd_root/lib` + ```bash $ ls $deepmd_root/bin $ ls $deepmd_root/lib diff --git a/doc/install/install-gromacs.md b/doc/install/install-gromacs.md index 758ad7784a..147822cf17 100644 --- a/doc/install/install-gromacs.md +++ b/doc/install/install-gromacs.md @@ -3,11 +3,14 @@ Before following this section, [DeePMD-kit C++ interface](install-from-source.md) should have be installed. ## Patch source code of GROMACS + Download the source code of a supported GROMACS version (2020.2) from https://manual.gromacs.org/2020.2/download.html. Run the following command: + ```bash export PATH=$PATH:$deepmd_kit_root/bin dp_gmx_patch -d $gromacs_root -v $version -p ``` + where `deepmd_kit_root` is the directory where the latest version of DeePMD-kit is installed, and `gromacs_root` refers to the source code directory of GROMACS. And `version` represents the version of GROMACS, where **only 2020.2 is supported now**. If attempting to patch another version of GROMACS you will still need to set `version` to `2020.2` as this is the only supported version, we cannot guarantee that patching other versions of GROMACS will work. ## Compile GROMACS with deepmd-kit + The C++ interface of `Deepmd-kit 2.x` and `TensorFlow 2.x` are required. And be aware that only DeePMD-kit with **high precision** is supported now since we cannot ensure single precision is enough for a GROMACS simulation. Here is a sample compile script: + ```bash #!/bin/bash export CC=/usr/bin/gcc diff --git a/doc/install/install-ipi.md b/doc/install/install-ipi.md index 1f4de7474c..3dd45d6749 100644 --- a/doc/install/install-ipi.md +++ b/doc/install/install-ipi.md @@ -1,11 +1,14 @@ # Install i-PI + The i-PI works in a client-server model. The i-PI provides the server for integrating the replica positions of atoms, while the DeePMD-kit provides a client named `dp_ipi` that computes the interactions (including energy, forces and virials). The server and client communicate via the Unix domain socket or the Internet socket. Full documentation for i-PI can be found [here](http://ipi-code.org/). The source code and a complete installation guide for i-PI can be found [here](https://github.com/i-pi/i-pi). To use i-PI with already existing drivers, install and update using Pip: + ```bash pip install -U i-PI ``` Test with Pytest: + ```bash pip install pytest pytest --pyargs ipi.tests diff --git a/doc/install/install-lammps.md b/doc/install/install-lammps.md index 21e1e72dd1..c24bfac06b 100644 --- a/doc/install/install-lammps.md +++ b/doc/install/install-lammps.md @@ -3,6 +3,7 @@ There are two ways to install LAMMPS: the built-in mode and the plugin mode. The built-in mode builds LAMMPS along with the DeePMD-kit and DeePMD-kit will be loaded automatically when running LAMMPS. The plugin mode builds LAMMPS and a plugin separately, so one needs to use `plugin load` command to load the DeePMD-kit's LAMMPS plugin library. ## Install LAMMPS's DeePMD-kit module (built-in mode) + Before following this section, [DeePMD-kit C++ interface](install-from-source.md) should have be installed. DeePMD-kit provides a module for running MD simulations with LAMMPS. Now make the DeePMD-kit module for LAMMPS. @@ -11,12 +12,15 @@ DeePMD-kit provides a module for running MD simulations with LAMMPS. Now make th cd $deepmd_source_dir/source/build make lammps ``` + DeePMD-kit will generate a module called `USER-DEEPMD` in the `build` directory, which supports either double or single float precision interface. Now download the LAMMPS code, and uncompress it. + ```bash cd /some/workspace wget https://github.com/lammps/lammps/archive/stable_2Aug2023_update3.tar.gz tar xf stable_2Aug2023_update3.tar.gz ``` + The source code of LAMMPS is stored in the directory `lammps-stable_2Aug2023_update3`. Then, you can [build LAMMPS](https://docs.lammps.org/Build.html) with either make or CMake. @@ -24,6 +28,7 @@ Then, you can [build LAMMPS](https://docs.lammps.org/Build.html) with either mak ### With make Now go into the LAMMPS code and copy the DeePMD-kit module like this + ```bash cd lammps-stable_2Aug2023_update3/src/ cp -r $deepmd_source_dir/source/build/USER-DEEPMD . @@ -31,17 +36,21 @@ make yes-kspace make yes-extra-fix make yes-user-deepmd ``` + You can enable any other package you want. Now build LAMMPS + ```bash make mpi -j4 ``` If everything works fine, you will end up with an executable `lmp_mpi`. + ```bash ./lmp_mpi -h ``` The DeePMD-kit module can be removed from the LAMMPS source code by + ```bash make no-user-deepmd ``` @@ -64,6 +73,7 @@ echo "include(${deepmd_source_dir}/source/lmp/builtin.cmake)" >> ../cmake/CMakeL It's expected to see one extra line in the end of `CMakeLists.txt`. Now build LAMMPS. You can install any other package you want. + ```bash cmake -D LAMMPS_INSTALL_RPATH=ON -D BUILD_SHARED_LIBS=yes -D CMAKE_INSTALL_PREFIX=${deepmd_root} -DCMAKE_PREFIX_PATH=${deepmd_root} ../cmake make -j4 @@ -71,14 +81,17 @@ make install ``` If everything works fine, you will end up with an executable `${deepmd_root}/bin/lmp`. + ```bash ${deepmd_root}/bin/lmp -h ``` ## Install LAMMPS (plugin mode) + Starting from `8Apr2021`, LAMMPS also provides a plugin mode, allowing one to build LAMMPS and a plugin separately. Now download the LAMMPS code (`8Apr2021` or later), and uncompress it: + ```bash cd /some/workspace wget https://github.com/lammps/lammps/archive/stable_2Aug2023_update3.tar.gz @@ -91,7 +104,9 @@ The source code of LAMMPS is stored in the directory `lammps-stable_2Aug2023_upd mkdir -p lammps-stable_2Aug2023_update3/build/ cd lammps-stable_2Aug2023_update3/build/ ``` + Now build LAMMPS. Note that `PLUGIN` must be enabled, and `BUILD_SHARED_LIBS` must be set to `yes`. You can install any other package you want. + ```bash cmake -D PKG_PLUGIN=ON -D LAMMPS_INSTALL_RPATH=ON -D BUILD_SHARED_LIBS=yes -D CMAKE_INSTALL_PREFIX=${deepmd_root} -D CMAKE_INSTALL_LIBDIR=lib -D CMAKE_INSTALL_FULL_LIBDIR=${deepmd_root}/lib ../cmake make -j4 @@ -99,6 +114,7 @@ make install ``` If everything works fine, you will end up with an executable `${deepmd_root}/bin/lmp`. + ```bash ${deepmd_root}/bin/lmp -h ``` @@ -109,4 +125,5 @@ If `${tensorflow_root}`, `${deepmd_root}`, or the path to TensorFlow Python pack ```sh patchelf --add-rpath "${tensorflow_root}/lib" liblammps.so ``` + ::: diff --git a/doc/install/install-tf.1.12.md b/doc/install/install-tf.1.12.md index f4009405d7..13abd8f7a7 100644 --- a/doc/install/install-tf.1.12.md +++ b/doc/install/install-tf.1.12.md @@ -1,5 +1,7 @@ # Install TensorFlow's C++ interface + The TensorFlow's C++ interface will be compiled from the source code. Firstly one installs bazel. It is highly recommended that the bazel version 0.15.0 is used. A full instruction of bazel installation can be found [here](https://docs.bazel.build/versions/master/install.html). + ```bash cd /some/workspace wget https://github.com/bazelbuild/bazel/releases/download/0.15.0/bazel-0.15.0-dist.zip @@ -11,6 +13,7 @@ export PATH=`pwd`/output:$PATH ``` Firstly get the source code of the TensorFlow + ```bash cd /some/workspace git clone https://github.com/tensorflow/tensorflow tensorflow -b v1.12.0 --depth=1 @@ -18,26 +21,35 @@ cd tensorflow ``` DeePMD-kit is compiled by CMake, so we need to compile and integrate TensorFlow with CMake projects. The rest of this section follows [the instruction provided by Tuatini](http://tuatini.me/building-tensorflow-as-a-standalone-project/). Now execute + ```bash ./configure ``` + You will answer a list of questions that help configure the building of TensorFlow. It is recommended to build for Python3. You may want to answer the question like this (please replace `$tensorflow_venv` with the virtual environment directory): + ```bash Please specify the location of python. [Default is $tensorflow_venv/bin/python]: ``` + The library path for Python should be set accordingly. Now build the shared library of TensorFlow: + ```bash bazel build -c opt --verbose_failures //tensorflow:libtensorflow_cc.so ``` -You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue for your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. + +You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue for your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. Now I assume you want to install TensorFlow in directory `$tensorflow_root`. Create the directory if it does not exist + ```bash mkdir -p $tensorflow_root ``` + Before moving on, we need to compile the dependencies of TensorFlow, including Protobuf, Eigen, nsync and absl. Firstly, protobuf + ```bash mkdir /tmp/proto sed -i 's;PROTOBUF_URL=.*;PROTOBUF_URL=\"https://mirror.bazel.build/github.com/google/protobuf/archive/v3.6.0.tar.gz\";g' tensorflow/contrib/makefile/download_dependencies.sh @@ -48,7 +60,9 @@ cd tensorflow/contrib/makefile/downloads/protobuf/ make make install ``` + Then Eigen + ```bash mkdir /tmp/eigen cd ../eigen @@ -57,7 +71,9 @@ cd build_dir cmake -DCMAKE_INSTALL_PREFIX=/tmp/eigen/ ../ make install ``` + nsync + ```bash mkdir /tmp/nsync cd ../../nsync @@ -67,7 +83,9 @@ cmake -DCMAKE_INSTALL_PREFIX=/tmp/nsync/ ../ make make install ``` + And absl + ```bash cd ../../absl bazel build @@ -75,7 +93,9 @@ mkdir -p $tensorflow_root/include/ rsync -avzh --include '*/' --include '*.h' --exclude '*' absl $tensorflow_root/include/ cd ../../../../.. ``` + Now, copy the libraries to the tensorflow's installation directory: + ```bash mkdir $tensorflow_root/lib cp bazel-bin/tensorflow/libtensorflow_cc.so $tensorflow_root/lib/ @@ -83,7 +103,9 @@ cp bazel-bin/tensorflow/libtensorflow_framework.so $tensorflow_root/lib/ cp /tmp/proto/lib/libprotobuf.a $tensorflow_root/lib/ cp /tmp/nsync/lib64/libnsync.a $tensorflow_root/lib/ ``` + Then copy the headers + ```bash mkdir -p $tensorflow_root/include/tensorflow cp -r bazel-genfiles/* $tensorflow_root/include/ @@ -94,12 +116,16 @@ cp -r /tmp/proto/include/* $tensorflow_root/include cp -r /tmp/eigen/include/eigen3/* $tensorflow_root/include cp -r /tmp/nsync/include/*h $tensorflow_root/include ``` + Now clean up the source files in the header directories: + ```bash cd $tensorflow_root/include find . -name "*.cc" -type f -delete ``` + The temporary installation directories for the dependencies can be removed: + ```bash rm -fr /tmp/proto /tmp/eigen /tmp/nsync ``` diff --git a/doc/install/install-tf.1.14-gpu.md b/doc/install/install-tf.1.14-gpu.md index 4e9fcaf7fc..5850af24ba 100644 --- a/doc/install/install-tf.1.14-gpu.md +++ b/doc/install/install-tf.1.14-gpu.md @@ -1,5 +1,7 @@ # Install TensorFlow-GPU's C++ interface + TensorFlow's C++ interface will be compiled from the source code. Firstly one installs Bazel. It is highly recommended that the Bazel version 0.24.1 is used. Full instructions on Bazel installation can be found [here](https://docs.bazel.build/versions/master/install.html). + ```bash cd /some/workspace wget https://github.com/bazelbuild/bazel/releases/download/0.24.1/bazel-0.24.1-dist.zip @@ -11,6 +13,7 @@ export PATH=`pwd`/output:$PATH ``` Firstly get the source code of the TensorFlow + ```bash cd /some/workspace git clone https://github.com/tensorflow/tensorflow tensorflow -b v1.14.0 --depth=1 @@ -20,6 +23,7 @@ cd tensorflow DeePMD-kit is compiled by CMake, so we need to compile and integrate TensorFlow with CMake projects. The rest of this section follows [the instruction provided by Tuatini](http://tuatini.me/building-tensorflow-as-a-standalone-project/). Now execute You will answer a list of questions that help configure the building of TensorFlow. It is recommended to build for Python3. You may want to answer the question like this (please replace `$tensorflow_venv` with the virtual environment directory): + ```bash ./configure Please specify the location of python. [Default is xxx]: @@ -93,23 +97,30 @@ Configuration finished The library path for Python should be set accordingly. Now build the shared library of TensorFlow: + ```bash bazel build -c opt --verbose_failures //tensorflow:libtensorflow_cc.so ``` -You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue for your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. + +You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue for your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. Now I assume you want to install TensorFlow in directory `$tensorflow_root`. Create the directory if it does not exist + ```bash mkdir -p $tensorflow_root ``` + Now, copy the libraries to the TensorFlow's installation directory: + ```bash mkdir $tensorflow_root/lib cp -d bazel-bin/tensorflow/libtensorflow_cc.so* $tensorflow_root/lib/ cp -d bazel-bin/tensorflow/libtensorflow_framework.so* $tensorflow_root/lib/ cp -d $tensorflow_root/lib/libtensorflow_framework.so.1 $tensorflow_root/lib/libtensorflow_framework.so ``` + Then copy the headers + ```bash mkdir -p $tensorflow_root/include/tensorflow cp -r bazel-genfiles/* $tensorflow_root/include/ @@ -121,16 +132,20 @@ cp -r bazel-tensorflow/external/eigen_archive/unsupported/ $tensorflow_root/incl rsync -avzh --include '*/' --include '*.h' --include '*.inc' --exclude '*' bazel-tensorflow/external/protobuf_archive/src/ $tensorflow_root/include/ rsync -avzh --include '*/' --include '*.h' --include '*.inc' --exclude '*' bazel-tensorflow/external/com_google_absl/absl/ $tensorflow_root/include/absl ``` + Now clean up the source files in the header directories: + ```bash cd $tensorflow_root/include find . -name "*.cc" -type f -delete ``` # Troubleshooting + ```bash git: unknown command -C ... ``` + This may be your git version issue because the low version of Git does not support this command. Upgrading your Git may be helpful. ```bash @@ -139,9 +154,11 @@ Please set them or make sure they are set and tested correctly in the CMake file FFTW_LIB (ADVANCED) linked by target "FFTW" in directory xxx ``` + Currently, when building the Eigen package, you can delete the FFTW in the CMake file. ```bash fatal error: absl/numeric/int128_have_intrinsic.inc: No such file or directory ``` + Basically, you could build an empty file named "int128_have_intrinsic.inc" in the same directory of "int128.h". diff --git a/doc/install/install-tf.1.14.md b/doc/install/install-tf.1.14.md index 065df9cad9..6457d484ad 100644 --- a/doc/install/install-tf.1.14.md +++ b/doc/install/install-tf.1.14.md @@ -1,5 +1,7 @@ # Install tensorflow's C++ interface + The tensorflow's C++ interface will be compiled from the source code. Firstly one installs bazel. It is highly recommended that the bazel version 0.24.1 is used. A full instruction of bazel installation can be found [here](https://docs.bazel.build/versions/master/install.html). + ```bash cd /some/workspace wget https://github.com/bazelbuild/bazel/releases/download/0.24.1/bazel-0.24.1-dist.zip @@ -11,6 +13,7 @@ export PATH=`pwd`/output:$PATH ``` Firstly get the source code of the tensorflow + ```bash cd /some/workspace git clone https://github.com/tensorflow/tensorflow tensorflow -b v1.14.0 --depth=1 @@ -18,33 +21,44 @@ cd tensorflow ``` DeePMD-kit is compiled by cmake, so we need to compile and integrate tensorflow with cmake projects. The rest of this section basically follows [the instruction provided by Tuatini](http://tuatini.me/building-tensorflow-as-a-standalone-project/). Now execute + ```bash ./configure ``` + You will answer a list of questions that help configure the building of tensorflow. It is recommended to build for Python3. You may want to answer the question like this (please replace `$tensorflow_venv` by the virtual environment directory): + ```bash Please specify the location of python. [Default is $tensorflow_venv/bin/python]: ``` + The library path for Python should be set accordingly. Now build the shared library of tensorflow: + ```bash bazel build -c opt --verbose_failures //tensorflow:libtensorflow_cc.so ``` -You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue of your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. + +You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue of your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. Now I assume you want to install tensorflow in directory `$tensorflow_root`. Create the directory if it does not exists + ```bash mkdir -p $tensorflow_root ``` + Now, copy the libraries to the tensorflow's installation directory: + ```bash mkdir $tensorflow_root/lib cp -d bazel-bin/tensorflow/libtensorflow_cc.so* $tensorflow_root/lib/ cp -d bazel-bin/tensorflow/libtensorflow_framework.so* $tensorflow_root/lib/ cp -d $tensorflow_root/lib/libtensorflow_framework.so.1 $tensorflow_root/lib/libtensorflow_framework.so ``` + Then copy the headers + ```bash mkdir -p $tensorflow_root/include/tensorflow cp -r bazel-genfiles/* $tensorflow_root/include/ @@ -56,7 +70,9 @@ cp -r bazel-tensorflow/external/eigen_archive/unsupported/ $tensorflow_root/incl rsync -avzh --include '*/' --include '*.h' --include '*.inc' --exclude '*' bazel-tensorflow/external/protobuf_archive/src/ $tensorflow_root/include/ rsync -avzh --include '*/' --include '*.h' --include '*.inc' --exclude '*' bazel-tensorflow/external/com_google_absl/absl/ $tensorflow_root/include/absl ``` + Now clean up the source files in the header directories: + ```bash cd $tensorflow_root/include find . -name "*.cc" -type f -delete diff --git a/doc/install/install-tf.1.8.md b/doc/install/install-tf.1.8.md index bfc1a616d4..f9554f9348 100644 --- a/doc/install/install-tf.1.8.md +++ b/doc/install/install-tf.1.8.md @@ -1,5 +1,7 @@ # Install tensorflow's C++ interface + The tensorflow's C++ interface will be compiled from the source code. Firstly one installs bazel. It is highly recommended that the bazel version 0.10.0 is used. A full instruction of bazel installation can be found [here](https://docs.bazel.build/versions/master/install.html). + ```bash cd /some/workspace wget https://github.com/bazelbuild/bazel/releases/download/0.10.0/bazel-0.10.0-dist.zip @@ -11,6 +13,7 @@ export PATH=`pwd`/output:$PATH ``` Firstly get the source code of the TensorFlow + ```bash cd /some/workspace git clone https://github.com/tensorflow/tensorflow tensorflow -b v1.8.0 --depth=1 @@ -18,26 +21,35 @@ cd tensorflow ``` DeePMD-kit is compiled by CMake, so we need to compile and integrate TensorFlow with CMake projects. The rest of this section basically follows [the instruction provided by Tuatini](http://tuatini.me/building-tensorflow-as-a-standalone-project/). Now execute + ```bash ./configure ``` + You will answer a list of questions that help configure the building of TensorFlow. It is recommended to build for Python3. You may want to answer the question like this (please replace `$tensorflow_venv` with the virtual environment directory): + ```bash Please specify the location of python. [Default is $tensorflow_venv/bin/python]: ``` + The library path for Python should be set accordingly. Now build the shared library of TensorFlow: + ```bash bazel build -c opt --verbose_failures //tensorflow:libtensorflow_cc.so ``` -You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue of your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. + +You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue of your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. Now I assume you want to install TensorFlow in directory `$tensorflow_root`. Create the directory if it does not exist + ```bash mkdir -p $tensorflow_root ``` + Before moving on, we need to compile the dependencies of TensorFlow, including Protobuf, Eigen and nsync. Firstly, protobuf + ```bash mkdir /tmp/proto tensorflow/contrib/makefile/download_dependencies.sh @@ -47,7 +59,9 @@ cd tensorflow/contrib/makefile/downloads/protobuf/ make make install ``` + Then Eigen + ```bash mkdir /tmp/eigen cd ../eigen @@ -56,7 +70,9 @@ cd build_dir cmake -DCMAKE_INSTALL_PREFIX=/tmp/eigen/ ../ make install ``` + And nsync + ```bash mkdir /tmp/nsync cd ../../nsync @@ -67,7 +83,9 @@ make make install cd ../../../../../.. ``` + Now, copy the libraries to the TensorFlow's installation directory: + ```bash mkdir $tensorflow_root/lib cp bazel-bin/tensorflow/libtensorflow_cc.so $tensorflow_root/lib/ @@ -75,7 +93,9 @@ cp bazel-bin/tensorflow/libtensorflow_framework.so $tensorflow_root/lib/ cp /tmp/proto/lib/libprotobuf.a $tensorflow_root/lib/ cp /tmp/nsync/lib/libnsync.a $tensorflow_root/lib/ ``` + Then copy the headers + ```bash mkdir -p $tensorflow_root/include/tensorflow cp -r bazel-genfiles/* $tensorflow_root/include/ @@ -86,12 +106,16 @@ cp -r /tmp/proto/include/* $tensorflow_root/include cp -r /tmp/eigen/include/eigen3/* $tensorflow_root/include cp -r /tmp/nsync/include/*h $tensorflow_root/include ``` + Now clean up the source files in the header directories: + ```bash cd $tensorflow_root/include find . -name "*.cc" -type f -delete ``` + The temporary installation directories for the dependencies can be removed: + ```bash rm -fr /tmp/proto /tmp/eigen /tmp/nsync ``` diff --git a/doc/install/install-tf.2.12.md b/doc/install/install-tf.2.12.md index dce0c224d5..8523345d3d 100644 --- a/doc/install/install-tf.2.12.md +++ b/doc/install/install-tf.2.12.md @@ -1,4 +1,5 @@ # Install TensorFlow's C++ interface + TensorFlow's C++ interface will be compiled from the source code. In this manual, we install TensorFlow 2.12.0. It is noted that the source code of TensorFlow 2.12.0 uses C++ 17, so one needs a C++ compiler that supports C++ 17. Firstly one installs Bazel. [bazelisk](https://github.com/bazelbuild/bazelisk) can be lanuched to use [bazel](https://github.com/bazelbuild/bazel). @@ -10,6 +11,7 @@ export PATH=/some/workspace/bazel/bin:$PATH ``` Firstly get the source code of the TensorFlow + ```bash git clone https://github.com/tensorflow/tensorflow tensorflow -b v2.12.0 --depth=1 cd tensorflow @@ -76,23 +78,30 @@ Configuration finished The library path for Python should be set accordingly. Now build the shared library of TensorFlow: + ```bash bazel build -c opt --verbose_failures //tensorflow:libtensorflow_cc.so ``` -You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue for your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. If you want to enable [oneDNN optimization](https://www.oneapi.io/blog/tensorflow-and-onednn-in-partnership/), add `--config=mkl`. + +You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue for your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. If you want to enable [oneDNN optimization](https://www.oneapi.io/blog/tensorflow-and-onednn-in-partnership/), add `--config=mkl`. Now I assume you want to install TensorFlow in directory `$tensorflow_root`. Create the directory if it does not exist + ```bash mkdir -p $tensorflow_root ``` + Now, copy the libraries to the TensorFlow's installation directory: + ```bash mkdir -p $tensorflow_root/lib cp -d bazel-bin/tensorflow/libtensorflow_cc.so* $tensorflow_root/lib/ cp -d bazel-bin/tensorflow/libtensorflow_framework.so* $tensorflow_root/lib/ cp -d $tensorflow_root/lib/libtensorflow_framework.so.2 $tensorflow_root/lib/libtensorflow_framework.so ``` + Then copy the headers + ```bash mkdir -p $tensorflow_root/include/tensorflow rsync -avzh --exclude '_virtual_includes/' --include '*/' --include '*.h' --include '*.inc' --exclude '*' bazel-bin/ $tensorflow_root/include/ @@ -107,12 +116,15 @@ rsync -avzh --include '*/' --include '*.h' --include '*.inc' --exclude '*' bazel ``` If you've enabled oneDNN, also copy `libiomp5.so`: + ```bash cp -d bazel-out/k8-opt/bin/external/llvm_openmp/libiomp5.so $tensorflow_root/lib/ ``` # Troubleshooting + ```bash git: unknown command -C ... ``` + This may be an issue with your Git version issue. Early versions of Git do not support this command, in this case upgrading your Git to a newer version may resolve any issues. diff --git a/doc/install/install-tf.2.3.md b/doc/install/install-tf.2.3.md index e538607db0..2fc7b35f2c 100644 --- a/doc/install/install-tf.2.3.md +++ b/doc/install/install-tf.2.3.md @@ -1,5 +1,7 @@ # Install TensorFlow's C++ interface + The tensorflow's C++ interface will be compiled from the source code. Firstly one installs bazel. The bazel version 3.1.0 should be used. A full instruction of bazel installation can be found [here](https://docs.bazel.build/versions/master/install.html). + ```bash cd /some/workspace wget https://github.com/bazelbuild/bazel/releases/download/3.1.0/bazel-3.1.0-installer-linux-x86_64.sh @@ -9,6 +11,7 @@ export PATH=/some/workspace/bazel/bin:$PATH ``` Firstly get the source code of the TensorFlow + ```bash git clone https://github.com/tensorflow/tensorflow tensorflow -b v2.3.0 --depth=1 cd tensorflow @@ -75,23 +78,30 @@ Configuration finished The library path for Python should be set accordingly. Now build the shared library of tensorflow: + ```bash bazel build -c opt --verbose_failures //tensorflow:libtensorflow_cc.so ``` -You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue of your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. + +You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue of your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. Now I assume you want to install TensorFlow in directory `$tensorflow_root`. Create the directory if it does not exist + ```bash mkdir -p $tensorflow_root ``` + Now, copy the libraries to the tensorflow's installation directory: + ```bash mkdir -p $tensorflow_root/lib cp -d bazel-bin/tensorflow/libtensorflow_cc.so* $tensorflow_root/lib/ cp -d bazel-bin/tensorflow/libtensorflow_framework.so* $tensorflow_root/lib/ cp -d $tensorflow_root/lib/libtensorflow_framework.so.2 $tensorflow_root/lib/libtensorflow_framework.so ``` + Then copy the headers + ```bash mkdir -p $tensorflow_root/include/tensorflow rsync -avzh --exclude '_virtual_includes/' --include '*/' --include '*.h' --include '*.inc' --exclude '*' bazel-bin/ $tensorflow_root/include/ @@ -105,7 +115,9 @@ rsync -avzh --include '*/' --include '*.h' --include '*.inc' --exclude '*' bazel ``` # Troubleshooting + ```bash git: unknown command -C ... ``` + This may be an issue with your git version issue. Early versions of git do not support this command, in this case upgrading your git to a newer version may resolve any issues. diff --git a/doc/install/install-tf.2.8.md b/doc/install/install-tf.2.8.md index da1f299131..4145ba01d1 100644 --- a/doc/install/install-tf.2.8.md +++ b/doc/install/install-tf.2.8.md @@ -1,4 +1,5 @@ # Install TensorFlow's C++ interface + TensorFlow's C++ interface will be compiled from the source code. Firstly one installs Bazel. [bazelisk](https://github.com/bazelbuild/bazelisk) can be lanuched to use [bazel](https://github.com/bazelbuild/bazel). ```bash @@ -8,6 +9,7 @@ export PATH=/some/workspace/bazel/bin:$PATH ``` Firstly get the source code of the TensorFlow + ```bash git clone https://github.com/tensorflow/tensorflow tensorflow -b v2.8.0 --depth=1 cd tensorflow @@ -74,23 +76,30 @@ Configuration finished The library path for Python should be set accordingly. Now build the shared library of TensorFlow: + ```bash bazel build -c opt --verbose_failures //tensorflow:libtensorflow_cc.so ``` -You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue for your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. If you want to enable [oneDNN optimization](https://www.oneapi.io/blog/tensorflow-and-onednn-in-partnership/), add `--config=mkl`. + +You may want to add options `--copt=-msse4.2`, `--copt=-mavx`, `--copt=-mavx2` and `--copt=-mfma` to enable SSE4.2, AVX, AVX2 and FMA SIMD accelerations, respectively. It is noted that these options should be chosen according to the CPU architecture. If the RAM becomes an issue for your machine, you may limit the RAM usage by using `--local_resources 2048,.5,1.0`. If you want to enable [oneDNN optimization](https://www.oneapi.io/blog/tensorflow-and-onednn-in-partnership/), add `--config=mkl`. Now I assume you want to install TensorFlow in directory `$tensorflow_root`. Create the directory if it does not exist + ```bash mkdir -p $tensorflow_root ``` + Now, copy the libraries to the TensorFlow's installation directory: + ```bash mkdir -p $tensorflow_root/lib cp -d bazel-bin/tensorflow/libtensorflow_cc.so* $tensorflow_root/lib/ cp -d bazel-bin/tensorflow/libtensorflow_framework.so* $tensorflow_root/lib/ cp -d $tensorflow_root/lib/libtensorflow_framework.so.2 $tensorflow_root/lib/libtensorflow_framework.so ``` + Then copy the headers + ```bash mkdir -p $tensorflow_root/include/tensorflow rsync -avzh --exclude '_virtual_includes/' --include '*/' --include '*.h' --include '*.inc' --exclude '*' bazel-bin/ $tensorflow_root/include/ @@ -104,12 +113,15 @@ rsync -avzh --include '*/' --include '*.h' --include '*.inc' --exclude '*' bazel ``` If you've enabled oneDNN, also copy `libiomp5.so`: + ```bash cp -d bazel-out/k8-opt/bin/external/llvm_openmp/libiomp5.so $tensorflow_root/lib/ ``` # Troubleshooting + ```bash git: unknown command -C ... ``` + This may be an issue with your Git version issue. Early versions of Git do not support this command, in this case upgrading your Git to a newer version may resolve any issues. diff --git a/doc/logo.md b/doc/logo.md index 420f378336..67c303f651 100644 --- a/doc/logo.md +++ b/doc/logo.md @@ -1,5 +1,5 @@ -# Logo - -DeePMD-kit logo - -The logo of DeePMD-kit is a beaver. Beavers were widely distributed in Europe and Asia but became nearly extinct due to hunting. Listed as a first-class state-protected animal in China, the population of beavers in China is less than the giant pandas. We hope that users of DeePMD-kit can enhance the awareness to protect beavers. +# Logo + +DeePMD-kit logo + +The logo of DeePMD-kit is a beaver. Beavers were widely distributed in Europe and Asia but became nearly extinct due to hunting. Listed as a first-class state-protected animal in China, the population of beavers in China is less than the giant pandas. We hope that users of DeePMD-kit can enhance the awareness to protect beavers. diff --git a/doc/model/dplr.md b/doc/model/dplr.md index 317630ebe5..ec95f9f424 100644 --- a/doc/model/dplr.md +++ b/doc/model/dplr.md @@ -13,33 +13,42 @@ In the following, we take the DPLR model for example to introduce the training a ## Theory The Deep Potential Long Range (DPLR) model adds the electrostatic energy to the total energy: + ```math E=E_{\text{DP}} + E_{\text{ele}}, ``` + where $E_{\text{DP}}$ is the short-range contribution constructed as the [standard energy model](./train-energy.md) that is fitted against $(E^\ast-E_{\text{ele}})$. $E_{\text{ele}}$ is the electrostatic energy introduced by a group of Gaussian distributions that is an approximation of the electronic structure of the system, and is calculated in Fourier space by + ```math E_{\text{ele}} = \frac{1}{2\pi V}\sum_{m \neq 0, \|m\|\leq L} \frac{\exp({-\pi ^2 m^2/\beta ^2})}{m^2}S^2(m), ``` + where $\beta$ is a freely tunable parameter that controls the spread of the Gaussians. $L$ is the cutoff in Fourier space and $S(m)$, the structure factor, is given by + ```math S(m)=\sum_i q_i e^{-2\pi \imath m \boldsymbol r_i} + \sum_n q_n e^{-2\pi \imath m \boldsymbol W_n}, ``` + where $\imath = \sqrt{-1}$ denotes the imaginary unit, $\boldsymbol r_i$ indicates ion coordinates, $q_i$ is the charge of the ion $i$, and $W_n$ is the $n$-th Wannier centroid (WC) which can be obtained from a separated [dipole model](./train-fitting-tensor.md). It can be proved that the error in the electrostatic energy introduced by the Gaussian approximations is dominated by a summation of dipole-quadrupole interactions that decay as $r^{-4}$, where $r$ is the distance between the dipole and quadrupole.[^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). ## Train a deep Wannier model for Wannier centroids We use the deep Wannier model (DW) to represent the relative position of the Wannier centroid (WC) with the atom with which it is associated. One may consult the introduction of the [dipole model](train-fitting-tensor.md) for a detailed introduction. An example input `wc.json` and a small dataset `data` for tutorial purposes can be found in + ```bash $deepmd_source_dir/examples/water/dplr/train/ ``` + It is noted that **the tutorial dataset is not enough for training a productive model**. Two settings make the training input script different from an energy training input: + ```json "fitting_net": { "type": "dipole", @@ -48,8 +57,10 @@ Two settings make the training input script different from an energy training in "seed": 1 }, ``` + The type of fitting is set to {ref}`dipole `. The dipole is associated with type 0 atoms (oxygens), by the setting `"dipole_type": [0]`. What we trained is the displacement of the WC from the corresponding oxygen atom. It shares the same training input as the atomic dipole because both are 3-dimensional vectors defined on atoms. The loss section is provided as follows + ```json "loss": { "type": "tensor", @@ -57,9 +68,11 @@ The loss section is provided as follows "pref_atomic": 1.0 }, ``` + so that the atomic dipole is trained as labels. Note that the NumPy compressed file `atomic_dipole.npy` should be provided in each dataset. The training and freezing can be started from the example directory by + ```bash dp train dw.json && dp freeze -o dw.pb ``` @@ -67,6 +80,7 @@ dp train dw.json && dp freeze -o dw.pb ## Train the DPLR model The training of the DPLR model is very similar to the standard short-range DP models. An example input script can be found in the example directory. The following section is introduced to compute the long-range energy contribution of the DPLR model, and modify the short-range DP model by this part. + ```json "modifier": { "type": "dipole_charge", @@ -77,8 +91,10 @@ The training of the DPLR model is very similar to the standard short-range DP mo "ewald_beta": 0.40 }, ``` -The {ref}`model_name ` specifies which DW model is used to predict the position of WCs. {ref}`model_charge_map ` gives the amount of charge assigned to WCs. {ref}`sys_charge_map ` provides the nuclear charge of oxygen (type 0) and hydrogen (type 1) atoms. {ref}`ewald_beta ` (unit $\text{Å}^{-1}$) gives the spread parameter controls the spread of Gaussian charges, and {ref}`ewald_h ` (unit Å) assigns the grid size of Fourier transformation. + +The {ref}`model_name ` specifies which DW model is used to predict the position of WCs. {ref}`model_charge_map ` gives the amount of charge assigned to WCs. {ref}`sys_charge_map ` provides the nuclear charge of oxygen (type 0) and hydrogen (type 1) atoms. {ref}`ewald_beta ` (unit $\text{Å}^{-1}$) gives the spread parameter controls the spread of Gaussian charges, and {ref}`ewald_h ` (unit Å) assigns the grid size of Fourier transformation. The DPLR model can be trained and frozen by (from the example directory) + ```bash dp train ener.json && dp freeze -o ener.pb ``` @@ -88,11 +104,13 @@ dp train ener.json && dp freeze -o ener.pb In MD simulations, the long-range part of the DPLR is calculated by the LAMMPS `kspace` support. Then the long-range interaction is back-propagated to atoms by DeePMD-kit. This setup is commonly used in classical molecular dynamics simulations as the "virtual site". Unfortunately, LAMMPS does not natively support virtual sites, so we have to hack the LAMMPS code, which makes the input configuration and script a little wired. An example of an input configuration file and script can be found in + ```bash $deepmd_source_dir/examples/water/dplr/lmp/ ``` We use `atom_style full` for DPLR simulations. the coordinates of the WCs are explicitly written in the configuration file. Moreover, a virtual bond is established between the oxygens and the WCs to indicate they are associated together. The configuration file containing 128 H2O molecules is thus written as + ``` 512 atoms @@ -127,13 +145,17 @@ Bonds 2 1 2 386 ... ``` + The oxygens and hydrogens are assigned with atom types 1 and 2 (corresponding to training atom types 0 and 1), respectively. The WCs are assigned with atom type 3. We want to simulate heavy water so the mass of hydrogens is set to 2. An example input script is provided in + ```bash $deepmd_source_dir/examples/water/dplr/lmp/in.lammps ``` + Here are some explanations + ```lammps # groups of real and virtual atoms group real_atom type 1 2 @@ -148,6 +170,7 @@ bond_style zero bond_coeff * special_bonds lj/coul 1 1 1 angle no ``` + Type 1 and 2 (O and H) are `real_atom`s, while type 3 (WCs) are `virtual_atom`s. The model file `ener.pb` stores both the DW and DPLR models, so the position of WCs and the energy can be inferred from it. A virtual bond type is specified by `bond_style zero`. The `special_bonds` command switches off the exclusion of intramolecular interactions. ```lammps @@ -157,19 +180,22 @@ Type 1 and 2 (O and H) are `real_atom`s, while type 3 (WCs) are `virtual_atom`s. kspace_style pppm/dplr 1e-5 kspace_modify gewald ${BETA} diff ik mesh ${KMESH} ${KMESH} ${KMESH} ``` + The long-range part is calculated by the `kspace` support of LAMMPS. The `kspace_style` `pppm/dplr` is required. The spread parameter set by variable `BETA` should be set the same as that used in training. The `KMESH` should be set dense enough so the long-range calculation is converged. ### fix dplr command **Syntax** - ``` fix ID group-ID style_name keyword value ... ``` -* ID, group-ID are documented in :doc:`fix ` command -* style_name = *dplr* -* three or more keyword/value pairs may be appended + + + +- ID, group-ID are documented in :doc:`fix ` command +- style\_name = _dplr_ +- three or more keyword/value pairs may be appended ``` keyword = *model* or *type_associate* or *bond_type* or *efield* @@ -201,6 +227,7 @@ The atom names specified in [pair_style `deepmd`](../third-party/lammps-command. If it is not set, the training parameter {ref}`type_map ` will be mapped to LAMMPS atom types. To use a time-dependent electric field, LAMMPS's `variable` feature can be utilized: + ```lammps variable EFIELD_Z equal 2*sin(2*PI*time/0.006) fix 0 all dplr model ener.pb type_associate 1 3 bond_type 1 efield 0 0 v_EFIELD_Z @@ -216,21 +243,23 @@ compute real_press all pressure real_temp fix 1 real_atom nvt temp ${TEMP} ${TEMP} ${TAU_T} fix_modify 1 temp real_temp ``` + The temperature of the system should be computed from the real atoms. The kinetic contribution in the pressure tensor is also computed from the real atoms. The thermostat is applied to only real atoms. The computed temperature and pressure of real atoms can be accessed by, e.g. + ```lammps fix thermo_print all print ${THERMO_FREQ} "$(step) $(pe) $(ke) $(etotal) $(enthalpy) $(c_real_temp) $(c_real_press) $(vol) $(c_real_press[1]) $(c_real_press[2]) $(c_real_press[3])" append thermo.out screen no title "# step pe ke etotal enthalpy temp press vol pxx pyy pzz" ``` The LAMMPS simulation can be started from the example directory by + ```bash lmp -i in.lammps ``` + If LAMMPS complains that no model file `ener.pb` exists, it can be copied from the training example directory. The MD simulation lasts for only 20 steps. If one runs a longer simulation, it will blow up, because the model is trained with a very limited dataset for very short training steps, thus is of poor quality. Another restriction that should be noted is that the energies printed at the zero steps are not correct. This is because at the zero steps the position of the WC has not been updated with the DW model. The energies printed in later steps are correct. - - [1]: https://arxiv.org/abs/2112.13327 diff --git a/doc/model/dprc.md b/doc/model/dprc.md index 4699db77d0..33dde237d7 100644 --- a/doc/model/dprc.md +++ b/doc/model/dprc.md @@ -15,6 +15,7 @@ E=E_\text{QM}(\mathbf R; \mathbf P) + E_\text{QM/MM}(\mathbf R; \mathbf P) + E_ Deep Potential - Range Correction (DPRc) was initially designed to correct the potential energy from a fast, linear-scaling low-level semiempirical QM/MM theory to a high-level ''ab initio'' QM/MM theory in a range-correction way to quantitatively correct short and mid-range non-bonded interactions leveraging the non-bonded lists routinely used in molecular dynamics simulations using molecular mechanical force fields such as AMBER. In this way, long-ranged electrostatic interactions can be modeled efficiently using the particle mesh Ewald method or its extensions for multipolar and QM/MM potentials. In a DPRc model, the switch function is modified to disable MM-MM interaction: + ```math s_\text{DPRc}(r_{ij}) = \begin{cases} @@ -22,12 +23,16 @@ In a DPRc model, the switch function is modified to disable MM-MM interaction: s(r_{ij}), &\text{otherwise}, \end{cases} ``` + where $s_\text{DPRc}(r_{ij})$ is the new switch function and $s(r_{ij})$ is the old one. This ensures the forces between MM atoms are zero, i.e. + ```math {\boldsymbol F}_{ij} = - \frac{\partial E}{\partial \boldsymbol r_{ij}} = 0, \quad i \in \text{MM} \land j \in \text{MM}. ``` + The fitting network is revised to remove energy bias from MM atoms: + ```math E_i= \begin{cases} @@ -35,10 +40,11 @@ The fitting network is revised to remove energy bias from MM atoms: \mathcal{F}_0(\mathcal{D}^i) - \mathcal{F}_0(\mathbf{0}), &\text{if $i \in \text{MM}$}, \end{cases} ``` + where $\mathbf{0}$ is a zero matrix. It is worth mentioning that usage of DPRc is not limited to its initial design for QM/MM correction and can be expanded to any similar interaction.[^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). See the [JCTC paper](https://doi.org/10.1021/acs.jctc.1c00201) for details. @@ -135,6 +141,7 @@ As described in the paper, the DPRc model only corrects $E_\text{QM}$ and $E_\te :::: {ref}`exclude_types ` can be generated by the following Python script: + ```py from itertools import combinations_with_replacement, product @@ -181,7 +188,7 @@ The DPRc model has the best practices with the [AMBER](../third-party/out-of-dee If one wants to correct from a low-level method into a full DFT level, and the system is too large to do full DFT calculation, one may try the experimental pairwise DPRc model. In a pairwise DPRc model, the total energy is divided into QM internal energy and the sum of QM/MM energy for each MM residue $l$: -$$ E = E_\text{QM} + \sum_{l} E_{\text{QM/MM},l} $$ +$$ E = E*\text{QM} + \sum*{l} E\_{\text{QM/MM},l} $$ In this way, the interaction between the QM region and each MM fragmentation can be computed and trained separately. Thus, the pairwise DPRc model is divided into two sub-[DPRc models](./dprc.md). @@ -193,32 +200,19 @@ It is noted that the [`se_atten` descriptor](./train-se-atten.md) should be used { "model": { "type": "pairwise_dprc", - "type_map": [ - "C", - "P", - "O", - "H", - "OW", - "HW" - ], + "type_map": ["C", "P", "O", "H", "OW", "HW"], "type_embedding": { - "neuron": [ - 8 - ], + "neuron": [8], "precision": "float32" }, "qm_model": { "descriptor": { "type": "se_atten_v2", "sel": 24, - "rcut_smth": 0.50, - "rcut": 9.00, + "rcut_smth": 0.5, + "rcut": 9.0, "attn_layer": 0, - "neuron": [ - 25, - 50, - 100 - ], + "neuron": [25, 50, 100], "resnet_dt": false, "axis_neuron": 12, "precision": "float32", @@ -226,21 +220,10 @@ It is noted that the [`se_atten` descriptor](./train-se-atten.md) should be used }, "fitting_net": { "type": "ener", - "neuron": [ - 240, - 240, - 240 - ], + "neuron": [240, 240, 240], "resnet_dt": true, "precision": "float32", - "atom_ener": [ - null, - null, - null, - null, - 0.0, - 0.0 - ], + "atom_ener": [null, null, null, null, 0.0, 0.0], "seed": 1 } }, @@ -248,92 +231,38 @@ It is noted that the [`se_atten` descriptor](./train-se-atten.md) should be used "descriptor": { "type": "se_atten_v2", "sel": 27, - "rcut_smth": 0.50, - "rcut": 6.00, + "rcut_smth": 0.5, + "rcut": 6.0, "attn_layer": 0, - "neuron": [ - 25, - 50, - 100 - ], + "neuron": [25, 50, 100], "resnet_dt": false, "axis_neuron": 12, "set_davg_zero": true, "exclude_types": [ - [ - 0, - 0 - ], - [ - 0, - 1 - ], - [ - 0, - 2 - ], - [ - 0, - 3 - ], - [ - 1, - 1 - ], - [ - 1, - 2 - ], - [ - 1, - 3 - ], - [ - 2, - 2 - ], - [ - 2, - 3 - ], - [ - 3, - 3 - ], - [ - 4, - 4 - ], - [ - 4, - 5 - ], - [ - 5, - 5 - ] + [0, 0], + [0, 1], + [0, 2], + [0, 3], + [1, 1], + [1, 2], + [1, 3], + [2, 2], + [2, 3], + [3, 3], + [4, 4], + [4, 5], + [5, 5] ], "precision": "float32", "seed": 1 }, "fitting_net": { "type": "ener", - "neuron": [ - 240, - 240, - 240 - ], + "neuron": [240, 240, 240], "resnet_dt": true, "seed": 1, "precision": "float32", - "atom_ener": [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ] + "atom_ener": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] } } } diff --git a/doc/model/overall.md b/doc/model/overall.md index f8fb2fa151..102a8fc671 100644 --- a/doc/model/overall.md +++ b/doc/model/overall.md @@ -16,17 +16,20 @@ The indices of the neighboring atoms (i.e. atoms within a certain cutoff radius) Note that the Cartesian coordinates can be either under the periodic boundary condition (PBC) or in vacuum (under the open boundary condition). The network parameters are denoted by $\boldsymbol \theta = \{\boldsymbol \theta_d, \boldsymbol \theta_f\}$, where $\boldsymbol \theta_d$ and $\boldsymbol\theta_f$ yield the network parameters of the descriptor (if any) and those of the fitting network, respectively. From the above equation, one may compute the global property of the system by + ```math \boldsymbol y = \sum_{i=1}^N \boldsymbol y_i, ``` + where $N$ is the number of atoms in a frame. For example, if $y_i$ represents the potential energy contribution of atom $i$, then $y$ gives the total potential energy of the frame.[^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). ## Instructions A model has two parts, a descriptor that maps atomic configuration to a set of symmetry invariant features, and a fitting net that takes descriptor as input and predicts the atomic contribution to the target physical property. It's defined in the {ref}`model ` section of the `input.json`, for example, + ```json "model": { "type_map": ["O", "H"], @@ -38,11 +41,13 @@ A model has two parts, a descriptor that maps atomic configuration to a set of s } } ``` + The two subsections, {ref}`descriptor ` and {ref}`fitting_net `, define the descriptor and the fitting net, respectively. The {ref}`type_map ` is optional, which provides the element names (but not necessarily same as the actual name of the element) of the corresponding atom types. A water model, as in this example, has two kinds of atoms. The atom types are internally recorded as integers, e.g., `0` for oxygen and `1` for hydrogen here. A mapping from the atom type to their names is provided by {ref}`type_map `. DeePMD-kit implements the following descriptors: + 1. [`se_e2_a`](train-se-e2-a.md): DeepPot-SE constructed from all information (both angular and radial) of atomic configurations. The embedding takes the distance between atoms as input. 2. [`se_e2_r`](train-se-e2-r.md): DeepPot-SE constructed from radial information of atomic configurations. The embedding takes the distance between atoms as input. 3. [`se_e3`](train-se-e3.md): DeepPot-SE constructed from all information (both angular and radial) of atomic configurations. The embedding takes angles between two neighboring atoms as input. @@ -51,6 +56,7 @@ DeePMD-kit implements the following descriptors: 6. [`hybrid`](train-hybrid.md): Concate a list of descriptors to form a new descriptor. The fitting of the following physical properties is supported + 1. [`ener`](train-energy.md): Fit the energy of the system. The force (derivative with atom positions) and the virial (derivative with the box tensor) can also be trained. 2. [`dipole`](train-fitting-tensor.md): The dipole moment. 3. [`polar`](train-fitting-tensor.md): The polarizability. diff --git a/doc/model/pairtab.md b/doc/model/pairtab.md index fee4d754a6..c8763705f7 100644 --- a/doc/model/pairtab.md +++ b/doc/model/pairtab.md @@ -5,17 +5,23 @@ ::: ## Theory + In applications like the radiation damage simulation, the interatomic distance may become too close, so that the DFT calculations fail. In such cases, the DP model that is an approximation of the DFT potential energy surface is usually replaced by an empirical potential, like the Ziegler-Biersack-Littmark (ZBL) screened nuclear repulsion potential in the radiation damage simulations. The DeePMD-kit package supports the interpolation between DP and an empirical pairwise potential + ```math E_i = (1-w_i) E_i^{\mathrm{DP}} + w_i (E_i^0 + E_i^{\mathrm{pair}}), ``` + where the $w_i$ is the interpolation weight and the $E_i^{\mathrm{pair}} $ is the atomic contribution due to the pairwise potential $u^{\mathrm{pair}}(r)$, i.e. + ```math E_i^{\mathrm{pair}} = \sum_{j\in n(i)} u^{\mathrm{pair}}(r_{ij}). ``` + The interpolation weight $w_i$ is defined by + ```math w_i = \begin{cases} @@ -24,19 +30,22 @@ The interpolation weight $w_i$ is defined by 0, & \sigma_i \geq r_b, \end{cases} ``` + where $u_i = (\sigma_i - r_a ) / (r_b - r_a)$. $E_i^0$ is the atom energy bias. In the range $[r_a, r_b]$, the DP model smoothly switched off and the pairwise potential smoothly switched on from $r_b$ to $r_a$. The $\sigma_i$ is the softmin of the distance between atom $i$ and its neighbors, + ```math \sigma_i = \dfrac {\sum\limits_{j\in n(i)} r_{ij} e^{-r_{ij} / \alpha_s}} {\sum\limits_{j\in n(i)} e^{-r_{ij} / \alpha_s}}, ``` + where the scale $\alpha_s$ is a tunable scale of the interatomic distance $r_{ij}$. The pairwise potential $u^{\textrm{pair}}(r)$ is defined by a user-defined table that provides the value of $u^{\textrm{pair}}$ on an evenly discretized grid from 0 to the cutoff distance.[^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). DeePMD-kit also supports combination with a pairwise potential: diff --git a/doc/model/sel.md b/doc/model/sel.md index f4a3cf6c09..8455c242a9 100644 --- a/doc/model/sel.md +++ b/doc/model/sel.md @@ -5,9 +5,11 @@ All descriptors require to set `sel`, which means the expected maximum number of `sel` should not be too large or too small. If `sel` is too large, the computing will become much slower and cost more memory. If `sel` is not enough, the energy will be not conserved, making the accuracy of the model worse. To determine a proper `sel`, one can calculate the neighbor stat of the training data before training: + ```sh dp neighbor-stat -s data -r 6.0 -t O H ``` + where `data` is the directory of data, `6.0` is the cutoff radius, and `O` and `H` is the type map. The program will give the `max_nbor_size`. For example, `max_nbor_size` of the water example is `[38, 72]`, meaning an atom may have 38 O neighbors and 72 H neighbors in the training data. The `sel` should be set to a higher value than that of the training data, considering there may be some extreme geometries during MD simulations. As a result, we set `sel` to `[46, 92]` in the water example. diff --git a/doc/model/train-energy-spin.md b/doc/model/train-energy-spin.md index e0b3968c09..3eb589590b 100644 --- a/doc/model/train-energy-spin.md +++ b/doc/model/train-energy-spin.md @@ -9,6 +9,7 @@ In this section, we will take `$deepmd_source_dir/examples/NiO/se_e2_a/input.jso ## Spin The construction of the fitting net is give by section {ref}`spin ` + ```json "spin" : { "use_spin": [true, false], @@ -16,9 +17,10 @@ The construction of the fitting net is give by section {ref}`spin ` "spin_norm": [1.2737], }, ``` -* {ref}`use_spin ` determines whether to turn on the magnetism of the atoms.The index of this option matches option `type_map `. -* {ref}`virtual_len ` specifies the distance between virtual atom and the belonging real atom. -* {ref}`spin_norm ` gives the magnitude of the magnetic moment for each magnatic atom. + +- {ref}`use_spin ` determines whether to turn on the magnetism of the atoms.The index of this option matches option `type_map `. +- {ref}`virtual_len ` specifies the distance between virtual atom and the belonging real atom. +- {ref}`spin_norm ` gives the magnitude of the magnetic moment for each magnatic atom. ## Spin Loss @@ -33,11 +35,13 @@ The prefectors may not be a constant, rather it changes linearly with the learni $$p_{fr}(t) = p_{fr}^0 \frac{ \alpha(t) }{ \alpha(0) } + p_{fr}^\infty ( 1 - \frac{ \alpha(t) }{ \alpha(0) })$$ where $\alpha(t)$ denotes the learning rate at step $t$. $p_{fr}^0$ and $p_{fr}^\infty$ specifies the $p_f$ at the start of the training and at the limit of $t \to \infty$ (set by {ref}`start_pref_fr ` and {ref}`limit_pref_f `, respectively), i.e. + ```math pref_fr(t) = start_pref_fr * ( lr(t) / start_lr ) + limit_pref_fr * ( 1 - lr(t) / start_lr ) ``` The {ref}`loss ` section in the `input.json` is + ```json "loss" :{ "type": "ener_spin", @@ -51,6 +55,7 @@ The {ref}`loss ` section in the `input.json` is "limit_pref_v": 0, }, ``` + The options {ref}`start_pref_e `, {ref}`limit_pref_e `, {ref}`start_pref_fr `, {ref}`limit_pref_fm `, {ref}`start_pref_v ` and {ref}`limit_pref_v ` determine the start and limit prefactors of energy, atomic force, magnatic force and virial, respectively. If one does not want to train with virial, then he/she may set the virial prefactors {ref}`start_pref_v ` and {ref}`limit_pref_v ` to 0. diff --git a/doc/model/train-energy.md b/doc/model/train-energy.md index bfe304b5d2..c1da1f4c1f 100644 --- a/doc/model/train-energy.md +++ b/doc/model/train-energy.md @@ -8,63 +8,79 @@ In this section, we will take `$deepmd_source_dir/examples/water/se_e2_a/input.j ## Theory -In the DP model, we let the fitting network $\mathcal{F}_ 0$ maps the descriptor $\mathcal{D}^i$ to a scalar, where the subscript $0$ means that the output is a zero-order tensor (i.e. scalar). The model can then be used to predict the total potential energy of the system by +In the DP model, we let the fitting network $\mathcal{F}_ 0$ maps the descriptor $\mathcal{D}^i$ to a scalar, where the subscript $0$ means that the output is a zero-order tensor (i.e. scalar). The model can then be used to predict the total potential energy of the system by + ```math E = \sum_i E_i = \sum_i \mathcal F_0 (\mathcal D^i), ``` + where the output of the fitting network is treated as the atomic potential energy contribution, i.e. $E_i$. The output scalar can also be treated as other scalar properties defined on an atom, for example, the partial charge of atom $i$. -In some cases, atomic-specific or frame-specific parameters, such as electron temperature, may be treated as extra input to the fitting network. +In some cases, atomic-specific or frame-specific parameters, such as electron temperature, may be treated as extra input to the fitting network. We denote the atomic and frame-specific parameters by $\boldsymbol{P}^i\in \mathbb{R}^{N_p}$ (with $N_p$ being the dimension) and $\boldsymbol{Q}\in \mathbb{R}^{N_q}$ (with $N_q$ being the dimension), respectively. + ```math E_i=\mathcal{F}_0(\{\mathcal{D}^i, \boldsymbol{P}^i, \boldsymbol Q\}). ``` The atomic force $\boldsymbol{F}_ {i}$ and the virial tensor $\boldsymbol{\Xi} = (\Xi_{\alpha\beta})$ (if PBC is applied) can be derived from the potential energy $E$: + ```math F_{i,\alpha}=-\frac{\partial E}{\partial r_{i,\alpha}}, ``` + ```math \Xi_{\alpha\beta}=-\sum_{\gamma} \frac{\partial E}{\partial h_{\gamma\alpha}} h_{\gamma\beta}, ``` + where $r_{i,\alpha}$ and $F_{i,\alpha}$ denotes the $\alpha$-th component of the coordinate and force of atom $i$. $h_{\alpha\beta}$ is the $\beta$-th component of the $\alpha$-th basis vector of the simulation region. The properties $\eta$ of the energy loss function could be energy $E$, force $\boldsymbol{F}$, virial $\boldsymbol{\Xi}$, relative energy $\Delta E$, or any combination among them, and the loss functions of them are + ```math L_E(\boldsymbol{x};\boldsymbol{\theta})=\frac{1}{N}(E(\boldsymbol{x};\boldsymbol{\theta})-E^*)^2, ``` + ```math L_F(\boldsymbol{x};\boldsymbol{\theta})=\frac{1}{3N}\sum_{k=1}^{N}\sum_{\alpha=1}^3(F_{k,\alpha}(\boldsymbol{x};\boldsymbol{\theta})-F_{k,\alpha}^*)^2, ``` + ```math L_\Xi(\boldsymbol{x};\boldsymbol{\theta})=\frac{1}{9N}\sum_{\alpha,\beta=1}^{3}(\Xi_{\alpha\beta}(\boldsymbol{x};\boldsymbol{\theta})-\Xi_{\alpha\beta}^*)^2, ``` + ```math L_{\Delta E}(\boldsymbol{x};\boldsymbol{\theta})=\frac{1}{N}({\Delta E}(\boldsymbol{x};\boldsymbol{\theta})-{\Delta E}^*)^2, ``` + where $F_{k,\alpha}$ is the $\alpha$-th component of the force on atom $k$, and the superscript $\ast$ indicates the label of the property that should be provided in advance. Using $N$ ensures that each loss of fitting property is averaged over atomic contributions before they contribute to the total loss by weight. If part of atoms is more important than others, for example, certain atoms play an essential role when calculating free energy profiles or kinetic isotope effects, the MSE of atomic forces with prefactors $q_{k}$ can also be used as the loss function: + ```math L_F^p(\mathbf{x};\boldsymbol{\theta})=\frac{1}{3N}\sum_{k=1}^{N} \sum_{\alpha} q_{k} (F_{k,\alpha}(\mathbf{x};\boldsymbol{\theta})-F_{k,\alpha}^*)^2. ``` + The atomic forces with larger prefactors will be fitted more accurately than those in other atoms. If some forces are quite large, for example, forces can be greater than 60 eV/Å in high-temperature reactive simulations, one may also prefer the force loss is relative to the magnitude: + ```math L^r_F(\boldsymbol{x};\boldsymbol{\theta})=\frac{1}{3N}\sum_{k=1}^{N}\sum_\alpha \left(\frac{F_{k,\alpha}(\boldsymbol{x};\boldsymbol{\theta})-F_{k,\alpha}^*}{\lvert\boldsymbol{F}^\ast_k\lvert + \nu}\right)^2. ``` + where $\nu$ is a small constant used to protect an atom where the magnitude of $\boldsymbol{F}^\ast_k$ is small from having a large $L^r_F$. Benefiting from the relative force loss, small forces can be fitted more accurately.[^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). ## The fitting network The construction of the fitting net is given by section {ref}`fitting_net ` + ```json "fitting_net" : { "neuron": [240, 240, 240], @@ -72,9 +88,10 @@ The construction of the fitting net is given by section {ref}`fitting_net ` specifies the size of the fitting net. If two neighboring layers are of the same size, then a [ResNet architecture](https://arxiv.org/abs/1512.03385) is built between them. -* If the option {ref}`resnet_dt ` is set to `true`, then a timestep is used in the ResNet. -* {ref}`seed ` gives the random seed that is used to generate random numbers when initializing the model parameters. + +- {ref}`neuron ` specifies the size of the fitting net. If two neighboring layers are of the same size, then a [ResNet architecture](https://arxiv.org/abs/1512.03385) is built between them. +- If the option {ref}`resnet_dt ` is set to `true`, then a timestep is used in the ResNet. +- {ref}`seed ` gives the random seed that is used to generate random numbers when initializing the model parameters. ## Loss @@ -87,11 +104,13 @@ where $L_e$, $L_f$, and $L_v$ denote the loss in energy, forces and virials, res $$p_f(t) = p_f^0 \frac{ \alpha(t) }{ \alpha(0) } + p_f^\infty ( 1 - \frac{ \alpha(t) }{ \alpha(0) })$$ where $\alpha(t)$ denotes the learning rate at step $t$. $p_f^0$ and $p_f^\infty$ specifies the $p_f$ at the start of the training and the limit of $t \to \infty$ (set by {ref}`start_pref_f ` and {ref}`limit_pref_f `, respectively), i.e. + ```math pref_f(t) = start_pref_f * ( lr(t) / start_lr ) + limit_pref_f * ( 1 - lr(t) / start_lr ) ``` The {ref}`loss ` section in the `input.json` is + ```json "loss" : { "start_pref_e": 0.02, @@ -102,6 +121,7 @@ The {ref}`loss ` section in the `input.json` is "limit_pref_v": 0 } ``` + The options {ref}`start_pref_e `, {ref}`limit_pref_e `, {ref}`start_pref_f `, {ref}`limit_pref_f `, {ref}`start_pref_v ` and {ref}`limit_pref_v ` determine the start and limit prefactors of energy, force and virial, respectively. If one does not want to train with virial, then he/she may set the virial prefactors {ref}`start_pref_v ` and {ref}`limit_pref_v ` to 0. diff --git a/doc/model/train-fitting-dos.md b/doc/model/train-fitting-dos.md index b74ab3acf7..7b68525a45 100644 --- a/doc/model/train-fitting-dos.md +++ b/doc/model/train-fitting-dos.md @@ -36,9 +36,9 @@ The JSON of `dos` type should be provided like }, ``` -- `type` specifies which type of fitting net should be used. It should be `dos`. -- `numb_dos` specifies the length of output vector (density of states), which the same as the `NEDOS` set in VASP software, this argument defines the output length of the neural network. We note that the length of `dos` provided in training set should be the same. -- The rest arguments have the same meaning as they do in `ener` mode. +- `type` specifies which type of fitting net should be used. It should be `dos`. +- `numb_dos` specifies the length of output vector (density of states), which the same as the `NEDOS` set in VASP software, this argument defines the output length of the neural network. We note that the length of `dos` provided in training set should be the same. +- The rest arguments have the same meaning as they do in `ener` mode. ## Loss @@ -66,13 +66,12 @@ The loss section should be provided like }, ``` -- {ref}`type ` should be written as `dos` as a distinction from `ener` mode. -- `pref_dos` and `pref_ados`, respectively specify the weight of global and atomic loss. If set to 0, the corresponding label will not be included in the training process. -- We also provides a combination training of vector and its cumulative distribution function `cdf`, which can be defined as +- {ref}`type ` should be written as `dos` as a distinction from `ener` mode. +- `pref_dos` and `pref_ados`, respectively specify the weight of global and atomic loss. If set to 0, the corresponding label will not be included in the training process. +- We also provides a combination training of vector and its cumulative distribution function `cdf`, which can be defined as $$D(\epsilon) = \int_{e_{min}}^{\epsilon} g(\epsilon')d\epsilon'$$ - ## Training Data Preparation The global label should be named `dos.npy/raw`, while the atomic label should be named `atomic_dos.npy/raw`. If wrongly named, DP will report an error. diff --git a/doc/model/train-fitting-tensor.md b/doc/model/train-fitting-tensor.md index 0c9c0f492c..4d5cb22707 100644 --- a/doc/model/train-fitting-tensor.md +++ b/doc/model/train-fitting-tensor.md @@ -1,236 +1,243 @@ -# Fit `tensor` like `Dipole` and `Polarizability` {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} - -:::{note} -**Supported backends**: TensorFlow {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} -::: - -Unlike `energy`, which is a scalar, one may want to fit some high dimensional physical quantity, like `dipole` (vector) and `polarizability` (matrix, shorted as `polar`). Deep Potential has provided different APIs to do this. In this example, we will show you how to train a model to fit a water system. A complete training input script of the examples can be found in - -::::{tab-set} - -:::{tab-item} TensorFlow {{ tensorflow_icon }} - -```bash -$deepmd_source_dir/examples/water_tensor/dipole/dipole_input.json -$deepmd_source_dir/examples/water_tensor/polar/polar_input.json -``` - -::: - -:::{tab-item} PyTorch {{ pytorch_icon }} - -```bash -$deepmd_source_dir/examples/water_tensor/dipole/dipole_input_torch.json -$deepmd_source_dir/examples/water_tensor/polar/polar_input_torch.json -``` - -::: - -:::: - -The training and validation data are also provided our examples. But note that **the data provided along with the examples are of limited amount, and should not be used to train a production model.** - -Similar to the `input.json` used in `ener` mode, training JSON is also divided into {ref}`model `, {ref}`learning_rate `, {ref}`loss ` and {ref}`training `. Most keywords remain the same as `ener` mode, and their meaning can be found [here](train-se-e2-a.md). To fit a tensor, one needs to modify {ref}`model/fitting_net ` and {ref}`loss `. - -## Theory - -To represent the first-order tensorial properties (i.e. vector properties), we let the fitting network, denoted by $\mathcal F_{1}$, output an $M$-dimensional vector; then we have the representation, - -```math -(T_i^{(1)})_\alpha = -\frac{1}{N_c} -\sum_{j=1}^{N_c}\sum_{m=1}^M (\mathcal G^i)_{jm} (\mathcal R^i)_{j,\alpha+1} -(\mathcal F_{1}(\mathcal D^i))_m, \ \alpha=1,2,3. -``` -We let the fitting network $\mathcal F_{2}$ output an $M$-dimensional vector, and the second-order tensorial properties (matrix properties) are formulated as -```math -(T_i^{(2)})_{\alpha\beta} = -\frac{1}{N_c^2} -\sum_{j=1}^{N_c}\sum_{k=1}^{N_c}\sum_{m=1}^M -(\mathcal G^i)_{jm} -(\mathcal R^i)_{j,\alpha+1} -(\mathcal R^i)_{k,\beta+1} -(\mathcal G^i)_{km} -(\mathcal F_{2}(\mathcal D^i))_m, -\ \alpha,\beta=1,2,3, -``` - -where $\mathcal{G}^i$ and $\mathcal{R}^i$ can be found in [`se_e2_a`](./train-se-e2-a.md). -Thus, the tensor fitting network requires the descriptor to have the same or similar form as the DeepPot-SE descriptor. -$\mathcal{F}_1$ and $\mathcal F_2$ are the neural network functions. -The total tensor $\boldsymbol{T}$ (total dipole $\boldsymbol{T}^{(1)}$ or total polarizability $\boldsymbol{T}^{(2)}$) is the sum of the atomic tensor: -```math - \boldsymbol{T} = \sum_i \boldsymbol{T}_i. -``` -The tensorial models can be used to calculate IR spectrum and Raman spectrum.[^1] - -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). - -## The fitting Network - -The {ref}`fitting_net ` section tells DP which fitting net to use. - -::::{tab-set} - -:::{tab-item} TensorFlow {{ tensorflow_icon }} - -The JSON of `dipole` type should be provided like - -```json - "fitting_net" : { - "type": "dipole", - "sel_type": [0], - "neuron": [100,100,100], - "resnet_dt": true, - "seed": 1, - }, -``` - -The JSON of `polar` type should be provided like - -```json - "fitting_net" : { - "type": "polar", - "sel_type": [0], - "neuron": [100,100,100], - "resnet_dt": true, - "seed": 1, - }, -``` - -- `type` specifies which type of fitting net should be used. It should be either `dipole` or `polar`. Note that `global_polar` mode in version 1.x is already **deprecated** and is merged into `polar`. To specify whether a system is global or atomic, please see [here](train-se-e2-a.md). -- `sel_type` is a list specifying which type of atoms have the quantity you want to fit. For example, in the water system, `sel_type` is `[0]` since `0` represents atom `O`. If left unset, all types of atoms will be fitted. -- The rest arguments have the same meaning as they do in `ener` mode. - -::: - -:::{tab-item} PyTorch {{ pytorch_icon }} - -The JSON of `dipole` type should be provided like -```json - "atom_exclude_types": [ - 1 - ], - "fitting_net" : { - "type": "dipole", - "neuron": [100,100,100], - "resnet_dt": true, - "seed": 1, - }, -``` - -The JSON of `polar` type should be provided like - -```json - "atom_exclude_types": [ - 1 - ], - "fitting_net" : { - "type": "polar", - "neuron": [100,100,100], - "resnet_dt": true, - "seed": 1, - }, -``` -- `type` specifies which type of fitting net should be used. It should be either `dipole` or `polar`. Note that `global_polar` mode in version 1.x is already **deprecated** and is merged into `polar`. To specify whether a system is global or atomic, please see [here](train-se-e2-a.md). -- `atom_exclude_types` is a list specifying the which type of atoms have the quantity you want to set to zero. For example, in the water system, `atom_exclude_types` is `[1]` since `1` represents atom `H`. -- The rest arguments have the same meaning as they do in `ener` mode. -::: - -:::: - - - -## Loss - -DP supports a combinational training of the global system (only a global `tensor` label, i.e. dipole or polar, is provided in a frame) and atomic system (labels for **each** atom included in `sel_type`/ not included in `atom_exclude_types` are provided). In a global system, each frame has just **one** `tensor` label. For example, when fitting `polar`, each frame will just provide a `1 x 9` vector which gives the elements of the polarizability tensor of that frame in order XX, XY, XZ, YX, YY, YZ, XZ, ZY, ZZ. By contrast, in an atomic system, each atom in `sel_type` has a `tensor` label. For example, when fitting a dipole, each frame will provide a `#sel_atom x 3` matrices, where `#sel_atom` is the number of atoms whose type are in `sel_type`. - -The {ref}`loss ` section tells DP the weight of these two kinds of loss, i.e. - -```python -loss = pref * global_loss + pref_atomic * atomic_loss -``` - -The loss section should be provided like - -```json - "loss" : { - "type": "tensor", - "pref": 1.0, - "pref_atomic": 1.0 - }, -``` - -- {ref}`type ` should be written as `tensor` as a distinction from `ener` mode. -- {ref}`pref ` and {ref}`pref_atomic ` respectively specify the weight of global loss and atomic loss. It can not be left unset. If set to 0, the corresponding label will NOT be included in the training process. - -## Training Data Preparation - -In tensor mode, the identification of the label's type (global or atomic) is derived from the file name. The global label should be named `dipole.npy/raw` or `polarizability.npy/raw`, while the atomic label should be named `atomic_dipole.npy/raw` or `atomic_polarizability.npy/raw`. If wrongly named, DP will report an error - -```bash -ValueError: cannot reshape array of size xxx into shape (xx,xx). This error may occur when your label mismatch it's name, i.e. you might store global tensor in `atomic_tensor.npy` or atomic tensor in `tensor.npy`. -``` - -In this case, please check the file name of the label. - -## Train the Model - -The training command is the same as `ener` mode, i.e. - -::::{tab-set} - -:::{tab-item} TensorFlow {{ tensorflow_icon }} - -```bash -dp train input.json -``` -::: - -:::{tab-item} PyTorch {{ pytorch_icon }} - -```bash -dp --pt train input.json -``` -::: - -:::: - - -The detailed loss can be found in `lcurve.out`: - -``` -# step rmse_val rmse_trn rmse_lc_val rmse_lc_trn rmse_gl_val rmse_gl_trn lr - 0 8.34e+00 8.26e+00 8.34e+00 8.26e+00 0.00e+00 0.00e+00 1.0e-02 - 100 3.51e-02 8.55e-02 0.00e+00 8.55e-02 4.38e-03 0.00e+00 5.0e-03 - 200 4.77e-02 5.61e-02 0.00e+00 5.61e-02 5.96e-03 0.00e+00 2.5e-03 - 300 5.68e-02 1.47e-02 0.00e+00 0.00e+00 7.10e-03 1.84e-03 1.3e-03 - 400 3.73e-02 3.48e-02 1.99e-02 0.00e+00 2.18e-03 4.35e-03 6.3e-04 - 500 2.77e-02 5.82e-02 1.08e-02 5.82e-02 2.11e-03 0.00e+00 3.2e-04 - 600 2.81e-02 5.43e-02 2.01e-02 0.00e+00 1.01e-03 6.79e-03 1.6e-04 - 700 2.97e-02 3.28e-02 2.03e-02 0.00e+00 1.17e-03 4.10e-03 7.9e-05 - 800 2.25e-02 6.19e-02 9.05e-03 0.00e+00 1.68e-03 7.74e-03 4.0e-05 - 900 3.18e-02 5.54e-02 9.93e-03 5.54e-02 2.74e-03 0.00e+00 2.0e-05 - 1000 2.63e-02 5.02e-02 1.02e-02 5.02e-02 2.01e-03 0.00e+00 1.0e-05 - 1100 3.27e-02 5.89e-02 2.13e-02 5.89e-02 1.43e-03 0.00e+00 5.0e-06 - 1200 2.85e-02 2.42e-02 2.85e-02 0.00e+00 0.00e+00 3.02e-03 2.5e-06 - 1300 3.47e-02 5.71e-02 1.07e-02 5.71e-02 3.00e-03 0.00e+00 1.3e-06 - 1400 3.13e-02 5.76e-02 3.13e-02 5.76e-02 0.00e+00 0.00e+00 6.3e-07 - 1500 3.34e-02 1.11e-02 2.09e-02 0.00e+00 1.57e-03 1.39e-03 3.2e-07 - 1600 3.11e-02 5.64e-02 3.11e-02 5.64e-02 0.00e+00 0.00e+00 1.6e-07 - 1700 2.97e-02 5.05e-02 2.97e-02 5.05e-02 0.00e+00 0.00e+00 7.9e-08 - 1800 2.64e-02 7.70e-02 1.09e-02 0.00e+00 1.94e-03 9.62e-03 4.0e-08 - 1900 3.28e-02 2.56e-02 3.28e-02 0.00e+00 0.00e+00 3.20e-03 2.0e-08 - 2000 2.59e-02 5.71e-02 1.03e-02 5.71e-02 1.94e-03 0.00e+00 1.0e-08 -``` - -One may notice that in each step, some of the local loss and global loss will be `0.0`. This is because our training data and validation data consist of the global system and atomic system, i.e. -``` - --training_data - >atomic_system - >global_system - --validation_data - >atomic_system - >global_system -``` -During training, at each step when the `lcurve.out` is printed, the system used for evaluating the training (validation) error may be either with only global or only atomic labels, thus the corresponding atomic or global errors are missing and are printed as zeros. +# Fit `tensor` like `Dipole` and `Polarizability` {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} + +:::{note} +**Supported backends**: TensorFlow {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} +::: + +Unlike `energy`, which is a scalar, one may want to fit some high dimensional physical quantity, like `dipole` (vector) and `polarizability` (matrix, shorted as `polar`). Deep Potential has provided different APIs to do this. In this example, we will show you how to train a model to fit a water system. A complete training input script of the examples can be found in + +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} + +```bash +$deepmd_source_dir/examples/water_tensor/dipole/dipole_input.json +$deepmd_source_dir/examples/water_tensor/polar/polar_input.json +``` + +::: + +:::{tab-item} PyTorch {{ pytorch_icon }} + +```bash +$deepmd_source_dir/examples/water_tensor/dipole/dipole_input_torch.json +$deepmd_source_dir/examples/water_tensor/polar/polar_input_torch.json +``` + +::: + +:::: + +The training and validation data are also provided our examples. But note that **the data provided along with the examples are of limited amount, and should not be used to train a production model.** + +Similar to the `input.json` used in `ener` mode, training JSON is also divided into {ref}`model `, {ref}`learning_rate `, {ref}`loss ` and {ref}`training `. Most keywords remain the same as `ener` mode, and their meaning can be found [here](train-se-e2-a.md). To fit a tensor, one needs to modify {ref}`model/fitting_net ` and {ref}`loss `. + +## Theory + +To represent the first-order tensorial properties (i.e. vector properties), we let the fitting network, denoted by $\mathcal F_{1}$, output an $M$-dimensional vector; then we have the representation, + +```math +(T_i^{(1)})_\alpha = +\frac{1}{N_c} +\sum_{j=1}^{N_c}\sum_{m=1}^M (\mathcal G^i)_{jm} (\mathcal R^i)_{j,\alpha+1} +(\mathcal F_{1}(\mathcal D^i))_m, \ \alpha=1,2,3. +``` + +We let the fitting network $\mathcal F_{2}$ output an $M$-dimensional vector, and the second-order tensorial properties (matrix properties) are formulated as + +```math +(T_i^{(2)})_{\alpha\beta} = +\frac{1}{N_c^2} +\sum_{j=1}^{N_c}\sum_{k=1}^{N_c}\sum_{m=1}^M +(\mathcal G^i)_{jm} +(\mathcal R^i)_{j,\alpha+1} +(\mathcal R^i)_{k,\beta+1} +(\mathcal G^i)_{km} +(\mathcal F_{2}(\mathcal D^i))_m, +\ \alpha,\beta=1,2,3, +``` + +where $\mathcal{G}^i$ and $\mathcal{R}^i$ can be found in [`se_e2_a`](./train-se-e2-a.md). +Thus, the tensor fitting network requires the descriptor to have the same or similar form as the DeepPot-SE descriptor. +$\mathcal{F}_1$ and $\mathcal F_2$ are the neural network functions. +The total tensor $\boldsymbol{T}$ (total dipole $\boldsymbol{T}^{(1)}$ or total polarizability $\boldsymbol{T}^{(2)}$) is the sum of the atomic tensor: + +```math + \boldsymbol{T} = \sum_i \boldsymbol{T}_i. +``` + +The tensorial models can be used to calculate IR spectrum and Raman spectrum.[^1] + +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). + +## The fitting Network + +The {ref}`fitting_net ` section tells DP which fitting net to use. + +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} + +The JSON of `dipole` type should be provided like + +```json + "fitting_net" : { + "type": "dipole", + "sel_type": [0], + "neuron": [100,100,100], + "resnet_dt": true, + "seed": 1, + }, +``` + +The JSON of `polar` type should be provided like + +```json + "fitting_net" : { + "type": "polar", + "sel_type": [0], + "neuron": [100,100,100], + "resnet_dt": true, + "seed": 1, + }, +``` + +- `type` specifies which type of fitting net should be used. It should be either `dipole` or `polar`. Note that `global_polar` mode in version 1.x is already **deprecated** and is merged into `polar`. To specify whether a system is global or atomic, please see [here](train-se-e2-a.md). +- `sel_type` is a list specifying which type of atoms have the quantity you want to fit. For example, in the water system, `sel_type` is `[0]` since `0` represents atom `O`. If left unset, all types of atoms will be fitted. +- The rest arguments have the same meaning as they do in `ener` mode. + +::: + +:::{tab-item} PyTorch {{ pytorch_icon }} + +The JSON of `dipole` type should be provided like + +```json + "atom_exclude_types": [ + 1 + ], + "fitting_net" : { + "type": "dipole", + "neuron": [100,100,100], + "resnet_dt": true, + "seed": 1, + }, +``` + +The JSON of `polar` type should be provided like + +```json + "atom_exclude_types": [ + 1 + ], + "fitting_net" : { + "type": "polar", + "neuron": [100,100,100], + "resnet_dt": true, + "seed": 1, + }, +``` + +- `type` specifies which type of fitting net should be used. It should be either `dipole` or `polar`. Note that `global_polar` mode in version 1.x is already **deprecated** and is merged into `polar`. To specify whether a system is global or atomic, please see [here](train-se-e2-a.md). +- `atom_exclude_types` is a list specifying the which type of atoms have the quantity you want to set to zero. For example, in the water system, `atom_exclude_types` is `[1]` since `1` represents atom `H`. +- The rest arguments have the same meaning as they do in `ener` mode. + ::: + +:::: + +## Loss + +DP supports a combinational training of the global system (only a global `tensor` label, i.e. dipole or polar, is provided in a frame) and atomic system (labels for **each** atom included in `sel_type`/ not included in `atom_exclude_types` are provided). In a global system, each frame has just **one** `tensor` label. For example, when fitting `polar`, each frame will just provide a `1 x 9` vector which gives the elements of the polarizability tensor of that frame in order XX, XY, XZ, YX, YY, YZ, XZ, ZY, ZZ. By contrast, in an atomic system, each atom in `sel_type` has a `tensor` label. For example, when fitting a dipole, each frame will provide a `#sel_atom x 3` matrices, where `#sel_atom` is the number of atoms whose type are in `sel_type`. + +The {ref}`loss ` section tells DP the weight of these two kinds of loss, i.e. + +```python +loss = pref * global_loss + pref_atomic * atomic_loss +``` + +The loss section should be provided like + +```json + "loss" : { + "type": "tensor", + "pref": 1.0, + "pref_atomic": 1.0 + }, +``` + +- {ref}`type ` should be written as `tensor` as a distinction from `ener` mode. +- {ref}`pref ` and {ref}`pref_atomic ` respectively specify the weight of global loss and atomic loss. It can not be left unset. If set to 0, the corresponding label will NOT be included in the training process. + +## Training Data Preparation + +In tensor mode, the identification of the label's type (global or atomic) is derived from the file name. The global label should be named `dipole.npy/raw` or `polarizability.npy/raw`, while the atomic label should be named `atomic_dipole.npy/raw` or `atomic_polarizability.npy/raw`. If wrongly named, DP will report an error + +```bash +ValueError: cannot reshape array of size xxx into shape (xx,xx). This error may occur when your label mismatch it's name, i.e. you might store global tensor in `atomic_tensor.npy` or atomic tensor in `tensor.npy`. +``` + +In this case, please check the file name of the label. + +## Train the Model + +The training command is the same as `ener` mode, i.e. + +::::{tab-set} + +:::{tab-item} TensorFlow {{ tensorflow_icon }} + +```bash +dp train input.json +``` + +::: + +:::{tab-item} PyTorch {{ pytorch_icon }} + +```bash +dp --pt train input.json +``` + +::: + +:::: + +The detailed loss can be found in `lcurve.out`: + +``` +# step rmse_val rmse_trn rmse_lc_val rmse_lc_trn rmse_gl_val rmse_gl_trn lr + 0 8.34e+00 8.26e+00 8.34e+00 8.26e+00 0.00e+00 0.00e+00 1.0e-02 + 100 3.51e-02 8.55e-02 0.00e+00 8.55e-02 4.38e-03 0.00e+00 5.0e-03 + 200 4.77e-02 5.61e-02 0.00e+00 5.61e-02 5.96e-03 0.00e+00 2.5e-03 + 300 5.68e-02 1.47e-02 0.00e+00 0.00e+00 7.10e-03 1.84e-03 1.3e-03 + 400 3.73e-02 3.48e-02 1.99e-02 0.00e+00 2.18e-03 4.35e-03 6.3e-04 + 500 2.77e-02 5.82e-02 1.08e-02 5.82e-02 2.11e-03 0.00e+00 3.2e-04 + 600 2.81e-02 5.43e-02 2.01e-02 0.00e+00 1.01e-03 6.79e-03 1.6e-04 + 700 2.97e-02 3.28e-02 2.03e-02 0.00e+00 1.17e-03 4.10e-03 7.9e-05 + 800 2.25e-02 6.19e-02 9.05e-03 0.00e+00 1.68e-03 7.74e-03 4.0e-05 + 900 3.18e-02 5.54e-02 9.93e-03 5.54e-02 2.74e-03 0.00e+00 2.0e-05 + 1000 2.63e-02 5.02e-02 1.02e-02 5.02e-02 2.01e-03 0.00e+00 1.0e-05 + 1100 3.27e-02 5.89e-02 2.13e-02 5.89e-02 1.43e-03 0.00e+00 5.0e-06 + 1200 2.85e-02 2.42e-02 2.85e-02 0.00e+00 0.00e+00 3.02e-03 2.5e-06 + 1300 3.47e-02 5.71e-02 1.07e-02 5.71e-02 3.00e-03 0.00e+00 1.3e-06 + 1400 3.13e-02 5.76e-02 3.13e-02 5.76e-02 0.00e+00 0.00e+00 6.3e-07 + 1500 3.34e-02 1.11e-02 2.09e-02 0.00e+00 1.57e-03 1.39e-03 3.2e-07 + 1600 3.11e-02 5.64e-02 3.11e-02 5.64e-02 0.00e+00 0.00e+00 1.6e-07 + 1700 2.97e-02 5.05e-02 2.97e-02 5.05e-02 0.00e+00 0.00e+00 7.9e-08 + 1800 2.64e-02 7.70e-02 1.09e-02 0.00e+00 1.94e-03 9.62e-03 4.0e-08 + 1900 3.28e-02 2.56e-02 3.28e-02 0.00e+00 0.00e+00 3.20e-03 2.0e-08 + 2000 2.59e-02 5.71e-02 1.03e-02 5.71e-02 1.94e-03 0.00e+00 1.0e-08 +``` + +One may notice that in each step, some of the local loss and global loss will be `0.0`. This is because our training data and validation data consist of the global system and atomic system, i.e. + +``` + --training_data + >atomic_system + >global_system + --validation_data + >atomic_system + >global_system +``` + +During training, at each step when the `lcurve.out` is printed, the system used for evaluating the training (validation) error may be either with only global or only atomic labels, thus the corresponding atomic or global errors are missing and are printed as zeros. diff --git a/doc/model/train-hybrid.md b/doc/model/train-hybrid.md index c82fe8e961..c0a55d9eb5 100644 --- a/doc/model/train-hybrid.md +++ b/doc/model/train-hybrid.md @@ -1,4 +1,4 @@ -# Descriptor `"hybrid"` {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} +# Descriptor `"hybrid"` {{ tensorflow_icon }} {{ pytorch_icon }} {{ dpmodel_icon }} :::{note} **Supported backends**: TensorFlow {{ tensorflow_icon }}, PyTorch {{ pytorch_icon }}, DP {{ dpmodel_icon }} @@ -9,6 +9,7 @@ This descriptor hybridizes multiple descriptors to form a new descriptor. For ex ## Theory A hybrid descriptor $\mathcal{D}^i_\text{hyb}$ concatenates multiple kinds of descriptors into one descriptor: + ```math \mathcal{D}^{i}_\text{hyb} = \{ \begin{array}{cccc} @@ -16,14 +17,16 @@ A hybrid descriptor $\mathcal{D}^i_\text{hyb}$ concatenates multiple kinds of de \end{array} \}. ``` + The list of descriptors can be different types or the same descriptors with different parameters. This way, one can set the different cutoff radii for different descriptors.[^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). ## Instructions To use the descriptor in DeePMD-kit, one firstly set the {ref}`type ` to {ref}`hybrid `, then provide the definitions of the descriptors by the items in the `list`, + ```json "descriptor" :{ "type": "hybrid", @@ -41,6 +44,7 @@ To use the descriptor in DeePMD-kit, one firstly set the {ref}`type `. An example of the descriptor is provided as follows + ```json "descriptor" :{ "type": "se_a_mask", @@ -39,15 +42,17 @@ The construction of the descriptor is given by section {ref}`descriptor ` of the descriptor is set to `"se_a_mask"`. -* {ref}`sel ` gives the maximum number of atoms in input coordinates. It is a list, the length of which is the same as the number of atom types in the system, and `sel[i]` denotes the maximum number of atoms with type `i`. -* The {ref}`neuron ` specifies the size of the embedding net. From left to right the members denote the sizes of each hidden layer from the input end to the output end, respectively. If the outer layer is twice the size of the inner layer, then the inner layer is copied and concatenated, then a [ResNet architecture](https://arxiv.org/abs/1512.03385) is built between them. -* The {ref}`axis_neuron ` specifies the size of the submatrix of the embedding matrix, the axis matrix as explained in the [DeepPot-SE paper](https://arxiv.org/abs/1805.09003) -* If the option {ref}`type_one_side ` is set to `true`, the embedding network parameters vary by types of neighbor atoms only, so there will be $N_\text{types}$ sets of embedding network parameters. Otherwise, the embedding network parameters vary by types of centric atoms and types of neighbor atoms, so there will be $N_\text{types}^2$ sets of embedding network parameters. -* If the option {ref}`resnet_dt ` is set to `true`, then a timestep is used in the ResNet. -* {ref}`seed ` gives the random seed that is used to generate random numbers when initializing the model parameters. + +- The {ref}`type ` of the descriptor is set to `"se_a_mask"`. +- {ref}`sel ` gives the maximum number of atoms in input coordinates. It is a list, the length of which is the same as the number of atom types in the system, and `sel[i]` denotes the maximum number of atoms with type `i`. +- The {ref}`neuron ` specifies the size of the embedding net. From left to right the members denote the sizes of each hidden layer from the input end to the output end, respectively. If the outer layer is twice the size of the inner layer, then the inner layer is copied and concatenated, then a [ResNet architecture](https://arxiv.org/abs/1512.03385) is built between them. +- The {ref}`axis_neuron ` specifies the size of the submatrix of the embedding matrix, the axis matrix as explained in the [DeepPot-SE paper](https://arxiv.org/abs/1805.09003) +- If the option {ref}`type_one_side ` is set to `true`, the embedding network parameters vary by types of neighbor atoms only, so there will be $N_\text{types}$ sets of embedding network parameters. Otherwise, the embedding network parameters vary by types of centric atoms and types of neighbor atoms, so there will be $N_\text{types}^2$ sets of embedding network parameters. +- If the option {ref}`resnet_dt ` is set to `true`, then a timestep is used in the ResNet. +- {ref}`seed ` gives the random seed that is used to generate random numbers when initializing the model parameters. To make the `aparam.npy` used for descriptor `se_a_mask`, two variables in `fitting_net` section are needed. + ```json "fitting_net" :{ "neuron": [240, 240, 240], @@ -57,14 +62,16 @@ To make the `aparam.npy` used for descriptor `se_a_mask`, two variables in `fitt "use_aparam_as_mask": true } ``` -* `neuron`, `resnet_dt` and `seed` are the same as the {ref}`fitting_net ` section for fitting energy. -* {ref}`numb_aparam ` gives the dimesion of the `aparam.npy` file. In this example, it is set to 1 and stores the real/virtual sign of the atoms. For real/virtual atoms, the corresponding sign in `aparam.npy` is set to 1/0. -* {ref}`use_aparam_as_mask ` is set to `true` to use the `aparam.npy` as the mask of the atoms in the descriptor `se_a_mask`. + +- `neuron`, `resnet_dt` and `seed` are the same as the {ref}`fitting_net ` section for fitting energy. +- {ref}`numb_aparam ` gives the dimesion of the `aparam.npy` file. In this example, it is set to 1 and stores the real/virtual sign of the atoms. For real/virtual atoms, the corresponding sign in `aparam.npy` is set to 1/0. +- {ref}`use_aparam_as_mask ` is set to `true` to use the `aparam.npy` as the mask of the atoms in the descriptor `se_a_mask`. Finally, to make a reasonable fitting task with `se_a_mask` descriptor for DP/MM simulations, the loss function with `se_a_mask` is designed to include the atomic forces difference in specific atoms of the input particles only. More details about the selection of the specific atoms can be found in paper [DP/MM](left to be filled). Thus, `atom_pref.npy` ( [ nframes * natoms ] ) is required as the indicator of the specific atoms in the input particles. And the `loss` section in the training input script should be set as follows. + ```json "loss": { "type": "ener", diff --git a/doc/model/train-se-atten.md b/doc/model/train-se-atten.md index 745c0d1720..364d35805b 100644 --- a/doc/model/train-se-atten.md +++ b/doc/model/train-se-atten.md @@ -19,43 +19,53 @@ Attention-based descriptor $\mathcal{D}^i \in \mathbb{R}^{M \times M_{<}}$, whic ```math \mathcal{D}^i = \frac{1}{N_c^2}(\hat{\mathcal{G}}^i)^T \mathcal{R}^i (\mathcal{R}^i)^T \hat{\mathcal{G}}^i_<, ``` + where $\hat{\mathcal{G}}^i$ represents the embedding matrix $\mathcal{G}^i$ after additional self-attention mechanism and $\mathcal{R}^i$ is defined by the full case in the [`se_e2_a`](./train-se-e2-a.md). Note that we obtain $\mathcal{G}^i$ using the type embedding method by default in this descriptor. To perform the self-attention mechanism, the queries $\mathcal{Q}^{i,l} \in \mathbb{R}^{N_c\times d_k}$, keys $\mathcal{K}^{i,l} \in \mathbb{R}^{N_c\times d_k}$, and values $\mathcal{V}^{i,l} \in \mathbb{R}^{N_c\times d_v}$ are first obtained: + ```math \left(\mathcal{Q}^{i,l}\right)_{j}=Q_{l}\left(\left(\mathcal{G}^{i,l-1}\right)_{j}\right), ``` + ```math \left(\mathcal{K}^{i,l}\right)_{j}=K_{l}\left(\left(\mathcal{G}^{i,l-1}\right)_{j}\right), ``` + ```math \left(\mathcal{V}^{i,l}\right)_{j}=V_{l}\left(\left(\mathcal{G}^{i,l-1}\right)_{j}\right), ``` + where $Q_{l}$, $K_{l}$, $V_{l}$ represent three trainable linear transformations that output the queries and keys of dimension $d_k$ and values of dimension $d_v$, and $l$ is the index of the attention layer. -The input embedding matrix to the attention layers, denoted by $\mathcal{G}^{i,0}$, is chosen as the two-body embedding matrix. +The input embedding matrix to the attention layers, denoted by $\mathcal{G}^{i,0}$, is chosen as the two-body embedding matrix. Then the scaled dot-product attention method is adopted: + ```math A(\mathcal{Q}^{i,l}, \mathcal{K}^{i,l}, \mathcal{V}^{i,l}, \mathcal{R}^{i,l})=\varphi\left(\mathcal{Q}^{i,l}, \mathcal{K}^{i,l},\mathcal{R}^{i,l}\right)\mathcal{V}^{i,l}, ``` + where $\varphi\left(\mathcal{Q}^{i,l}, \mathcal{K}^{i,l},\mathcal{R}^{i,l}\right) \in \mathbb{R}^{N_c\times N_c}$ is attention weights. In the original attention method, one typically has $\varphi\left(\mathcal{Q}^{i,l}, \mathcal{K}^{i,l}\right)=\mathrm{softmax}\left(\frac{\mathcal{Q}^{i,l} (\mathcal{K}^{i,l})^{T}}{\sqrt{d_{k}}}\right)$, with $\sqrt{d_{k}}$ being the normalization temperature. This is slightly modified to incorporate the angular information: + ```math \varphi\left(\mathcal{Q}^{i,l}, \mathcal{K}^{i,l},\mathcal{R}^{i,l}\right) = \mathrm{softmax}\left(\frac{\mathcal{Q}^{i,l} (\mathcal{K}^{i,l})^{T}}{\sqrt{d_{k}}}\right) \odot \hat{\mathcal{R}}^{i}(\hat{\mathcal{R}}^{i})^{T}, ``` + where $\hat{\mathcal{R}}^{i} \in \mathbb{R}^{N_c\times 3}$ denotes normalized relative coordinates , $\hat{\mathcal{R}}^{i}_{j} = \frac{\boldsymbol{r}_{ij}}{\lVert \boldsymbol{r}_{ij} \lVert}$ and $\odot$ means element-wise multiplication. Then layer normalization is added in a residual way to finally obtain the self-attention local embedding matrix $\hat{\mathcal{G}}^{i} = \mathcal{G}^{i,L_a}$ after $L_a$ attention layers:[^1] + ```math \mathcal{G}^{i,l} = \mathcal{G}^{i,l-1} + \mathrm{LayerNorm}(A(\mathcal{Q}^{i,l}, \mathcal{K}^{i,l}, \mathcal{V}^{i,l}, \mathcal{R}^{i,l})). ``` -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). - +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). ## Introduction to new features of DPA-1 + Next, we will list the detailed settings in input.json and the data format, especially for large systems with dozens of elements. An example of DPA-1 input can be found [here](../../examples/water/se_atten/input.json). ### Descriptor `"se_atten"` @@ -63,10 +73,12 @@ Next, we will list the detailed settings in input.json and the data format, espe The notation of `se_atten` is short for the smooth edition of Deep Potential with an attention mechanism. This descriptor was described in detail in [the DPA-1 paper](https://arxiv.org/abs/2208.08236) and the images above. -In this example, we will train a DPA-1 model for a water system. A complete training input script of this example can be found in the directory: +In this example, we will train a DPA-1 model for a water system. A complete training input script of this example can be found in the directory: + ```bash $deepmd_source_dir/examples/water/se_atten/input.json ``` + With the training input script, data are also provided in the example directory. One may train the model with the DeePMD-kit from the directory. An example of the DPA-1 descriptor is provided as follows @@ -92,17 +104,17 @@ An example of the DPA-1 descriptor is provided as follows } ``` -* The {ref}`type ` of the descriptor is set to `"se_atten"`, which will use DPA-1 structures. -* {ref}`rcut ` is the cut-off radius for neighbor searching, and the {ref}`rcut_smth ` gives where the smoothing starts. -* **{ref}`sel `** gives the maximum possible number of neighbors in the cut-off radius. It is an int. Note that this number highly affects the efficiency of training, which we usually use less than 200. (We use 120 for training 56 elements in [OC2M dataset](https://github.com/Open-Catalyst-Project/ocp/blob/main/DATASET.md)) -* The {ref}`neuron ` specifies the size of the embedding net. From left to right the members denote the sizes of each hidden layer from the input end to the output end, respectively. If the outer layer is twice the size of the inner layer, then the inner layer is copied and concatenated, then a [ResNet architecture](https://arxiv.org/abs/1512.03385) is built between them. -* The {ref}`axis_neuron ` specifies the size of the submatrix of the embedding matrix, the axis matrix as explained in the [DeepPot-SE paper](https://arxiv.org/abs/1805.09003) -* If the option {ref}`resnet_dt ` is set to `true`, then a timestep is used in the ResNet. -* {ref}`seed ` gives the random seed that is used to generate random numbers when initializing the model parameters. -* {ref}`attn ` sets the length of a hidden vector during scale-dot attention computation. -* {ref}`attn_layer ` sets the number of layers in attention mechanism. -* {ref}`attn_mask ` determines whether to mask the diagonal in the attention weights and False is recommended. -* {ref}`attn_dotr ` determines whether to dot the relative coordinates on the attention weights as a gated scheme, True is recommended. +- The {ref}`type ` of the descriptor is set to `"se_atten"`, which will use DPA-1 structures. +- {ref}`rcut ` is the cut-off radius for neighbor searching, and the {ref}`rcut_smth ` gives where the smoothing starts. +- **{ref}`sel `** gives the maximum possible number of neighbors in the cut-off radius. It is an int. Note that this number highly affects the efficiency of training, which we usually use less than 200. (We use 120 for training 56 elements in [OC2M dataset](https://github.com/Open-Catalyst-Project/ocp/blob/main/DATASET.md)) +- The {ref}`neuron ` specifies the size of the embedding net. From left to right the members denote the sizes of each hidden layer from the input end to the output end, respectively. If the outer layer is twice the size of the inner layer, then the inner layer is copied and concatenated, then a [ResNet architecture](https://arxiv.org/abs/1512.03385) is built between them. +- The {ref}`axis_neuron ` specifies the size of the submatrix of the embedding matrix, the axis matrix as explained in the [DeepPot-SE paper](https://arxiv.org/abs/1805.09003) +- If the option {ref}`resnet_dt ` is set to `true`, then a timestep is used in the ResNet. +- {ref}`seed ` gives the random seed that is used to generate random numbers when initializing the model parameters. +- {ref}`attn ` sets the length of a hidden vector during scale-dot attention computation. +- {ref}`attn_layer ` sets the number of layers in attention mechanism. +- {ref}`attn_mask ` determines whether to mask the diagonal in the attention weights and False is recommended. +- {ref}`attn_dotr ` determines whether to dot the relative coordinates on the attention weights as a gated scheme, True is recommended. ::: @@ -125,37 +137,42 @@ An example of the DPA-1 descriptor is provided as follows } ``` -* The {ref}`type ` of the descriptor is set to `"dpa1"`, which will use DPA-1 structures. -* {ref}`rcut ` is the cut-off radius for neighbor searching, and the {ref}`rcut_smth ` gives where the smoothing starts. -* **{ref}`sel `** gives the maximum possible number of neighbors in the cut-off radius. It is an int. Note that this number highly affects the efficiency of training, which we usually use less than 200. (We use 120 for training 56 elements in [OC2M dataset](https://github.com/Open-Catalyst-Project/ocp/blob/main/DATASET.md)) -* The {ref}`neuron ` specifies the size of the embedding net. From left to right the members denote the sizes of each hidden layer from the input end to the output end, respectively. If the outer layer is twice the size of the inner layer, then the inner layer is copied and concatenated, then a [ResNet architecture](https://arxiv.org/abs/1512.03385) is built between them. -* The {ref}`tebd_dim ` specifies the dimension of the type embedding. -* The {ref}`axis_neuron ` specifies the size of the submatrix of the embedding matrix, the axis matrix as explained in the [DeepPot-SE paper](https://arxiv.org/abs/1805.09003) -* {ref}`attn ` sets the length of a hidden vector during scale-dot attention computation. -* {ref}`attn_layer ` sets the number of layers in attention mechanism. -* {ref}`attn_mask ` determines whether to mask the diagonal in the attention weights and False is recommended. -* {ref}`attn_dotr ` determines whether to dot the relative coordinates on the attention weights as a gated scheme, True is recommended. -* {ref}`post_ln ` determines whether to perform post layer norm. +- The {ref}`type ` of the descriptor is set to `"dpa1"`, which will use DPA-1 structures. +- {ref}`rcut ` is the cut-off radius for neighbor searching, and the {ref}`rcut_smth ` gives where the smoothing starts. +- **{ref}`sel `** gives the maximum possible number of neighbors in the cut-off radius. It is an int. Note that this number highly affects the efficiency of training, which we usually use less than 200. (We use 120 for training 56 elements in [OC2M dataset](https://github.com/Open-Catalyst-Project/ocp/blob/main/DATASET.md)) +- The {ref}`neuron ` specifies the size of the embedding net. From left to right the members denote the sizes of each hidden layer from the input end to the output end, respectively. If the outer layer is twice the size of the inner layer, then the inner layer is copied and concatenated, then a [ResNet architecture](https://arxiv.org/abs/1512.03385) is built between them. +- The {ref}`tebd_dim ` specifies the dimension of the type embedding. +- The {ref}`axis_neuron ` specifies the size of the submatrix of the embedding matrix, the axis matrix as explained in the [DeepPot-SE paper](https://arxiv.org/abs/1805.09003) +- {ref}`attn ` sets the length of a hidden vector during scale-dot attention computation. +- {ref}`attn_layer ` sets the number of layers in attention mechanism. +- {ref}`attn_mask ` determines whether to mask the diagonal in the attention weights and False is recommended. +- {ref}`attn_dotr ` determines whether to dot the relative coordinates on the attention weights as a gated scheme, True is recommended. +- {ref}`post_ln ` determines whether to perform post layer norm. ::: :::: ### Descriptor `"se_atten_v2"` + We highly recommend using the version 2.0 of the attention-based descriptor `"se_atten_v2"`, which is inherited from `"se_atten"` but with the following parameter modifications: + ```json "stripped_type_embedding": true, "smooth_type_embdding": true, "set_davg_zero": false ``` -Practical evidence demonstrates that `"se_atten_v2"` offers better and more stable performance compared to `"se_atten"`. +Practical evidence demonstrates that `"se_atten_v2"` offers better and more stable performance compared to `"se_atten"`. ### Fitting `"ener"` + DPA-1 only supports `"ener"` fitting type, and you can refer [here](train-energy.md) for detailed information. ### Type embedding + DPA-1 only supports models with type embeddings. And the default setting is as follows: + ```json "type_embedding":{ "neuron": [8], @@ -163,11 +180,13 @@ DPA-1 only supports models with type embeddings. And the default setting is as f "seed": 1 } ``` -You can add these settings in input.json if you want to change the default ones, see [here](train-se-e2-a-tebd.md) for detailed information. +You can add these settings in input.json if you want to change the default ones, see [here](train-se-e2-a-tebd.md) for detailed information. ### Type map + For training large systems, especially those with dozens of elements, the {ref}`type ` determines the element index of training data: + ```json "type_map": [ "Mg", @@ -175,8 +194,11 @@ For training large systems, especially those with dozens of elements, the {ref}` "Cu" ] ``` + which should include all the elements in the dataset you want to train on. + ## Data format + DPA-1 supports the standard data format, which is detailed in [data-conv.md](../data/data-conv.md) and [system.md](../data/system.md). Note that in this format, only those frames with the same fingerprint (i.e. the number of atoms of different elements) can be put together as a unified system. This may lead to sparse frame numbers in those rare systems. @@ -184,6 +206,7 @@ This may lead to sparse frame numbers in those rare systems. An ideal way is to put systems with the same total number of atoms together, which is the way we trained DPA-1 on [OC2M](https://github.com/Open-Catalyst-Project/ocp/blob/main/DATASET.md). This system format, which is called `mixed_type`, is proper to put frame-sparse systems together and is slightly different from the standard one. Take an example, a `mixed_type` may contain the following files: + ``` type.raw type_map.raw @@ -193,13 +216,14 @@ set.*/energy.npy set.*/force.npy set.*/real_atom_types.npy ``` + This system contains `Nframes` frames with the same atom number `Natoms`, the total number of element types contained in all frames is `Ntypes`. Most files are the same as those in [standard formats](../data/system.md), here we only list the distinct ones: -ID | Property | File | Required/Optional | Shape | Description ----------- | -------------------------------- | ------------------- | -------------------- | ----------------------- | ----------- -/ | Atom type indexes (place holder) | type.raw | Required | Natoms | All zeros to fake the type input -type_map | Atom type names | type_map.raw | Required | Ntypes | Atom names that map to atom type contained in all the frames, which is unnecessart to be contained in the periodic table -type | Atom type indexes of each frame | real_atom_types.npy | Required | Nframes \* Natoms | Integers that describe atom types in each frame, corresponding to indexes in type_map. `-1` means virtual atoms. +| ID | Property | File | Required/Optional | Shape | Description | +| -------- | -------------------------------- | ------------------- | ----------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------ | +| / | Atom type indexes (place holder) | type.raw | Required | Natoms | All zeros to fake the type input | +| type_map | Atom type names | type_map.raw | Required | Ntypes | Atom names that map to atom type contained in all the frames, which is unnecessart to be contained in the periodic table | +| type | Atom type indexes of each frame | real_atom_types.npy | Required | Nframes \* Natoms | Integers that describe atom types in each frame, corresponding to indexes in type_map. `-1` means virtual atoms. | With these edited files, one can put together frames with the same `Natoms`, instead of the same formula (like `H2O`). Note that this `mixed_type` format only supports `se_atten` descriptor. @@ -208,6 +232,7 @@ To put frames with different `Natoms` into the same system, one can pad systems The API to generate or transfer to `mixed_type` format is available on [dpdata](https://github.com/deepmodeling/dpdata) for a more convenient experience. ## Training example + Here we upload the AlMgCu example shown in the paper, you can download it here: [Baidu disk](https://pan.baidu.com/s/1Mk9CihPHCmf8quwaMhT-nA?pwd=d586); [Google disk](https://drive.google.com/file/d/11baEpRrvHoqxORFPSdJiGWusb3Y4AnRE/view?usp=sharing). diff --git a/doc/model/train-se-e2-a-tebd.md b/doc/model/train-se-e2-a-tebd.md index 7797a8f3c0..a6291bb238 100644 --- a/doc/model/train-se-e2-a-tebd.md +++ b/doc/model/train-se-e2-a-tebd.md @@ -16,6 +16,7 @@ Usually, when the type embedding approach is not enabled, for a system with mult (\mathcal{G}^i)_j = \mathcal{N}^{\alpha_i, \alpha_j}_{e,2}(s(r_{ij})) \quad \mathrm{or}\quad (\mathcal{G}^i)_j = \mathcal{N}^{ \alpha_j}_{e,2}(s(r_{ij})), ``` + ```math (\mathcal{G}^i)_{jk} =\mathcal{N}^{\alpha_j, \alpha_k}_{e,3}((\theta_i)_{jk}). ``` @@ -28,6 +29,7 @@ The limitation of this approach is that when there are large numbers of chemical Similar to the embedding networks, if the type embedding approach is not used, the fitting network parameters are chemical-species-wise, and there are $N_t$ sets of fitting network parameters. For performance, atoms are sorted by their chemical species $\alpha_i$ in advance. Take an example, the atomic energy $E_i$ is represented as follows: + ```math E_i=\mathcal{F}_0^{\alpha_i}(\mathcal{D}^i). ``` @@ -46,21 +48,25 @@ The type embeddings of central and neighboring atoms $\mathcal{A}^i$ and $\mathc (\mathcal{G}^i)_j = \mathcal{N}_{e,2}(\{s(r_{ij}), \mathcal{A}^i, \mathcal{A}^j\}) \quad \mathrm{or}\quad (\mathcal{G}^i)_j = \mathcal{N}_{e,2}(\{s(r_{ij}), \mathcal{A}^j\}) , ``` + ```math (\mathcal{G}^i)_{jk} =\mathcal{N}_{e,3}(\{(\theta_i)_{jk}, \mathcal{A}^j, \mathcal{A}^k\}). ``` In fitting networks, the type embedding is inserted into the input of the fitting networks: + ```math E_i=\mathcal{F}_0(\{\mathcal{D}^i, \mathcal{A}^i\}). ``` In this way, all chemical species share the same network parameters through the type embedding.[^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). ## Instructions + The {ref}`model ` defines how the model is constructed, adding a section of type embedding net: + ```json "model": { "type_map": ["O", "H"], @@ -75,9 +81,11 @@ The {ref}`model ` defines how the model is constructed, adding a section } } ``` + The model will automatically apply the type embedding approach and generate type embedding vectors. If the type embedding vector is detected, the descriptor and fitting net would take it as a part of the input. The construction of type embedding net is given by {ref}`type_embedding `. An example of {ref}`type_embedding ` is provided as follows + ```json "type_embedding":{ "neuron": [2, 4, 8], @@ -85,15 +93,17 @@ The construction of type embedding net is given by {ref}`type_embedding ` specifies the size of the type embedding net. From left to right the members denote the sizes of each hidden layer from the input end to the output end, respectively. It takes a one-hot vector as input and output dimension equals to the last dimension of the {ref}`neuron ` list. If the outer layer is twice the size of the inner layer, then the inner layer is copied and concatenated, then a [ResNet architecture](https://arxiv.org/abs/1512.03385) is built between them. -* If the option {ref}`resnet_dt ` is set to `true`, then a timestep is used in the ResNet. -* {ref}`seed ` gives the random seed that is used to generate random numbers when initializing the model parameters. +- The {ref}`neuron ` specifies the size of the type embedding net. From left to right the members denote the sizes of each hidden layer from the input end to the output end, respectively. It takes a one-hot vector as input and output dimension equals to the last dimension of the {ref}`neuron ` list. If the outer layer is twice the size of the inner layer, then the inner layer is copied and concatenated, then a [ResNet architecture](https://arxiv.org/abs/1512.03385) is built between them. +- If the option {ref}`resnet_dt ` is set to `true`, then a timestep is used in the ResNet. +- {ref}`seed ` gives the random seed that is used to generate random numbers when initializing the model parameters. A complete training input script of this example can be found in the directory. + ```bash $deepmd_source_dir/examples/water/se_e2_a_tebd/input.json ``` + See [here](../development/type-embedding.md) for further explanation of `type embedding`. :::{note} diff --git a/doc/model/train-se-e2-a.md b/doc/model/train-se-e2-a.md index e99f14518e..2412bbc64e 100644 --- a/doc/model/train-se-e2-a.md +++ b/doc/model/train-se-e2-a.md @@ -43,10 +43,10 @@ where $\boldsymbol{r}_{ij}=\boldsymbol{r}_j-\boldsymbol{r}_i = (x_{ij}, y_{ij}, \end{cases} ``` -where $x=\frac{r - r_s}{ r_c - r_s}$ switches from 1 at $r_s$ to 0 at the cutoff radius $r_c$. +where $x=\frac{r - r_s}{ r_c - r_s}$ switches from 1 at $r_s$ to 0 at the cutoff radius $r_c$. The switching function $s(r)$ is smooth in the sense that the second-order derivative is continuous. -Each row of the embedding matrix $\mathcal{G}^i \in \mathbb{R}^{N_c \times M}$ consists of $M$ nodes from the output layer of an NN function $\mathcal{N}_ {g}$ of $s(r_{ij})$: +Each row of the embedding matrix $\mathcal{G}^i \in \mathbb{R}^{N_c \times M}$ consists of $M$ nodes from the output layer of an NN function $\mathcal{N}_ {g}$ of $s(r_{ij})$: ```math (\mathcal{G}^i)_j = \mathcal{N}_{e,2}(s(r_{ij})), @@ -58,17 +58,20 @@ $\mathcal{G}^i_< \in \mathbb{R}^{N_c \times M_<}$ only takes first $M_<$ columns $r_s$, $r_c$, $M$ and $M_<$ are hyperparameters provided by the user. The DeepPot-SE is continuous up to the second-order derivative in its domain.[^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). ## Instructions -In this example, we will train a DeepPot-SE model for a water system. A complete training input script of this example can be found in the directory. +In this example, we will train a DeepPot-SE model for a water system. A complete training input script of this example can be found in the directory. + ```bash $deepmd_source_dir/examples/water/se_e2_a/input.json ``` + With the training input script, data are also provided in the example directory. One may train the model with the DeePMD-kit from the directory. The construction of the descriptor is given by section {ref}`descriptor `. An example of the descriptor is provided as follows + ```json "descriptor" :{ "type": "se_e2_a", @@ -82,11 +85,12 @@ The construction of the descriptor is given by section {ref}`descriptor ` of the descriptor is set to `"se_e2_a"`. -* {ref}`rcut ` is the cut-off radius for neighbor searching, and the {ref}`rcut_smth ` gives where the smoothing starts. -* {ref}`sel ` gives the maximum possible number of neighbors in the cut-off radius. It is a list, the length of which is the same as the number of atom types in the system, and `sel[i]` denotes the maximum possible number of neighbors with type `i`. -* The {ref}`neuron ` specifies the size of the embedding net. From left to right the members denote the sizes of each hidden layer from the input end to the output end, respectively. If the outer layer is twice the size of the inner layer, then the inner layer is copied and concatenated, then a [ResNet architecture](https://arxiv.org/abs/1512.03385) is built between them. -* If the option {ref}`type_one_side ` is set to `true`, the embedding network parameters vary by types of neighbor atoms only, so there will be $N_\text{types}$ sets of embedding network parameters. Otherwise, the embedding network parameters vary by types of centric atoms and types of neighbor atoms, so there will be $N_\text{types}^2$ sets of embedding network parameters. -* The {ref}`axis_neuron ` specifies the size of the submatrix of the embedding matrix, the axis matrix as explained in the [DeepPot-SE paper](https://arxiv.org/abs/1805.09003) -* If the option {ref}`resnet_dt ` is set to `true`, then a timestep is used in the ResNet. -* {ref}`seed ` gives the random seed that is used to generate random numbers when initializing the model parameters. + +- The {ref}`type ` of the descriptor is set to `"se_e2_a"`. +- {ref}`rcut ` is the cut-off radius for neighbor searching, and the {ref}`rcut_smth ` gives where the smoothing starts. +- {ref}`sel ` gives the maximum possible number of neighbors in the cut-off radius. It is a list, the length of which is the same as the number of atom types in the system, and `sel[i]` denotes the maximum possible number of neighbors with type `i`. +- The {ref}`neuron ` specifies the size of the embedding net. From left to right the members denote the sizes of each hidden layer from the input end to the output end, respectively. If the outer layer is twice the size of the inner layer, then the inner layer is copied and concatenated, then a [ResNet architecture](https://arxiv.org/abs/1512.03385) is built between them. +- If the option {ref}`type_one_side ` is set to `true`, the embedding network parameters vary by types of neighbor atoms only, so there will be $N_\text{types}$ sets of embedding network parameters. Otherwise, the embedding network parameters vary by types of centric atoms and types of neighbor atoms, so there will be $N_\text{types}^2$ sets of embedding network parameters. +- The {ref}`axis_neuron ` specifies the size of the submatrix of the embedding matrix, the axis matrix as explained in the [DeepPot-SE paper](https://arxiv.org/abs/1805.09003) +- If the option {ref}`resnet_dt ` is set to `true`, then a timestep is used in the ResNet. +- {ref}`seed ` gives the random seed that is used to generate random numbers when initializing the model parameters. diff --git a/doc/model/train-se-e2-r.md b/doc/model/train-se-e2-r.md index c543df6b22..f427310196 100644 --- a/doc/model/train-se-e2-r.md +++ b/doc/model/train-se-e2-r.md @@ -18,7 +18,7 @@ where $N_c$ is the expected maximum number of neighboring atoms, which is the same constant for all atoms over all frames. A matrix with a dimension of $N_c$ will be padded if the number of neighboring atoms is less than $N_c$. -Each row of the embedding matrix $\mathcal{G}^i \in \mathbb{R}^{N_c \times M}$ consists of $M$ nodes from the output layer of an NN function $\mathcal{N}_ {g}$ of $s(r_{ij})$: +Each row of the embedding matrix $\mathcal{G}^i \in \mathbb{R}^{N_c \times M}$ consists of $M$ nodes from the output layer of an NN function $\mathcal{N}_ {g}$ of $s(r_{ij})$: ```math (\mathcal{G}^i)_j = \mathcal{N}_{e,2}(s(r_{ij})), @@ -35,23 +35,25 @@ where $\boldsymbol{r}_ {ij}=\boldsymbol{r}_ j-\boldsymbol{r}_ i = (x_{ij}, y_{ij \end{cases} ``` -where $x=\frac{r - r_s}{ r_c - r_s}$ switches from 1 at $r_s$ to 0 at the cutoff radius $r_c$. +where $x=\frac{r - r_s}{ r_c - r_s}$ switches from 1 at $r_s$ to 0 at the cutoff radius $r_c$. The switching function $s(r)$ is smooth in the sense that the second-order derivative is continuous. In the above equations, the network parameters are not explicitly written. $r_s$, $r_c$ and $M$ are hyperparameters provided by the user. The DeepPot-SE is continuous up to the second-order derivative in its domain.[^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). ## Instructions A complete training input script of this example can be found in the directory + ```bash $deepmd_source_dir/examples/water/se_e2_r/input.json ``` The training input script is very similar to that of [`se_e2_a`](train-se-e2-a.md). The only difference lies in the {ref}`descriptor ` section + ```json "descriptor": { "type": "se_e2_r", @@ -65,4 +67,5 @@ The training input script is very similar to that of [`se_e2_a`](train-se-e2-a.m "_comment": " that's all" }, ``` + The type of the descriptor is set by the key {ref}`type `. diff --git a/doc/model/train-se-e3.md b/doc/model/train-se-e3.md index 4eb35357a0..3a0c1a9547 100644 --- a/doc/model/train-se-e3.md +++ b/doc/model/train-se-e3.md @@ -9,9 +9,11 @@ The notation of `se_e3` is short for the Deep Potential Smooth Edition (DeepPot- ## Theory The three-body embedding DeepPot-SE descriptor incorporates bond-angle information, making the model more accurate. The descriptor $\mathcal{D}^i$ can be represented as + ```math \mathcal{D}^i = \frac{1}{N_c^2}(\mathcal{R}^i(\mathcal{R}^i)^T):\mathcal{G}^i, ``` + where $N_c$ is the expected maximum number of neighboring atoms, which is the same constant for all atoms over all frames. $\mathcal{R}^i$ is constructed as @@ -24,6 +26,7 @@ $\mathcal{R}^i$ is constructed as \end{array} \}, ``` + Currently, only the full information case of $\mathcal{R}^i$ is supported by the three-body embedding. Each element of $\mathcal{G}^i \in \mathbb{R}^{N_c \times N_c \times M}$ comes from $M$ nodes from the output layer of an NN $\mathcal{N}_{e,3}$ function: @@ -34,16 +37,18 @@ Each element of $\mathcal{G}^i \in \mathbb{R}^{N_c \times N_c \times M}$ comes f where $(\theta_i)_ {jk} = (\mathcal{R}^i)_ {j,\\{2,3,4\\}}\cdot (\mathcal{R}^i)_ {k,\\{2,3,4\\}}$ considers the angle form of two neighbours ($j$ and $k$). The notation $:$ in the equation indicates the contraction between matrix $\mathcal{R}^i(\mathcal{R}^i)^T$ and the first two dimensions of tensor $\mathcal{G}^i$.[^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). ## Instructions A complete training input script of this example can be found in the directory + ```bash $deepmd_source_dir/examples/water/se_e3/input.json ``` The training input script is very similar to that of [`se_e2_a`](train-se-e2-a.md). The only difference lies in the `descriptor ` section + ```json "descriptor": { "type": "se_e3", @@ -56,4 +61,5 @@ The training input script is very similar to that of [`se_e2_a`](train-se-e2-a.m "_comment": " that's all" }, ``` + The type of the descriptor is set by the key {ref}`type `. diff --git a/doc/nvnmd/nvnmd.md b/doc/nvnmd/nvnmd.md index 7c00baad27..67cfb5e22d 100644 --- a/doc/nvnmd/nvnmd.md +++ b/doc/nvnmd/nvnmd.md @@ -33,7 +33,6 @@ where `$dataset` is the path to the data set and `$workspace` is the path to the Create and go to the training directory. - ```bash mkdir train cd train @@ -50,10 +49,10 @@ The structure of the input script is as follows ```json { - "nvnmd" : {}, - "learning_rate" : {}, - "loss" : {}, - "training": {} + "nvnmd": {}, + "learning_rate": {}, + "loss": {}, + "training": {} } ``` @@ -63,29 +62,30 @@ The "nvnmd" section is defined as ```json { - "version": 0, - "max_nnei":128, - "net_size":128, - "sel":[60, 60], - "rcut":6.0, - "rcut_smth":0.5, - "type_map": ["Ge", "Te"] + "version": 0, + "max_nnei": 128, + "net_size": 128, + "sel": [60, 60], + "rcut": 6.0, + "rcut_smth": 0.5, + "type_map": ["Ge", "Te"] } ``` where items are defined as: -| Item | Mean | Optional Value | -| --------- | --------------------------- | --------------------------------------------- | -| version | the version of network structure | 0 or 1 | -| max_nnei | the maximum number of neighbors that do not distinguish element types | 128 or 256 | -| net_size | the size of nueral network | 128 | -| sel | the number of neighbors | version 0: integer list of lengths 1 to 4 are acceptable; version 1: integer | -| rcut | the cutoff radial | (0, 8.0] | -| rcut_smth | the smooth cutoff parameter | (0, 8.0] | -| type_map | mapping atom type to the name (str) of the type | string list, optional | +| Item | Mean | Optional Value | +| --------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| version | the version of network structure | 0 or 1 | +| max_nnei | the maximum number of neighbors that do not distinguish element types | 128 or 256 | +| net_size | the size of nueral network | 128 | +| sel | the number of neighbors | version 0: integer list of lengths 1 to 4 are acceptable; version 1: integer | +| rcut | the cutoff radial | (0, 8.0] | +| rcut_smth | the smooth cutoff parameter | (0, 8.0] | +| type_map | mapping atom type to the name (str) of the type | string list, optional | Multiple versions of the nvnmd model correspond to different network structures. `nvnmd-v0` and `nvnmd-v1` differ in the following ways: + 1. `nvnmd-v0` and `nvnmd-v1` use the `se_a` descriptor and `se_atten` descriptor, respectively 2. `nvnmd-v0` has 1 set of parameters for each element and supports up to 4 element types. `nvnmd-v1` shares 1 set of parameters for each element and supports up to 31 types. 3. `nvnmd-v0` distinguishes between neighboring atoms, so `sel` is a list of integers. `nvnmd-v1` does not distinguish between neighboring atoms, so `sel` is an integer. @@ -96,20 +96,20 @@ The "learning_rate" section is defined as ```json { - "type":"exp", - "start_lr": 1e-3, - "stop_lr": 3e-8, - "decay_steps": 5000 + "type": "exp", + "start_lr": 1e-3, + "stop_lr": 3e-8, + "decay_steps": 5000 } ``` where items are defined as: -| Item | Mean | Optional Value | -| ----------- | ------------------------------------------------------------ | ---------------------- | -| type | learning rate variant type | exp | -| start_lr | the learning rate at the beginning of the training | a positive real number | -| stop_lr | the desired learning rate at the end of the training | a positive real number | +| Item | Mean | Optional Value | +| ----------- | ---------------------------------------------------------------- | ---------------------- | +| type | learning rate variant type | exp | +| start_lr | the learning rate at the beginning of the training | a positive real number | +| stop_lr | the desired learning rate at the end of the training | a positive real number | | decay_stops | the learning rate is decaying every {decay_stops} training steps | a positive integer | ### loss @@ -118,12 +118,12 @@ The "loss" section is defined as ```json { - "start_pref_e": 0.02, - "limit_pref_e": 2, - "start_pref_f": 1000, - "limit_pref_f": 1, - "start_pref_v": 0, - "limit_pref_v": 0 + "start_pref_e": 0.02, + "limit_pref_e": 2, + "start_pref_f": 1000, + "limit_pref_f": 1, + "start_pref_v": 0, + "limit_pref_v": 0 } ``` @@ -145,17 +145,17 @@ The "training" section is defined as ```json { "seed": 1, - "stop_batch": 1000000, - "numb_test": 1, - "disp_file": "lcurve.out", - "disp_freq": 1000, - "save_ckpt": "model.ckpt", - "save_freq": 10000, - "training_data":{ - "systems":["system1_path", "system2_path", "..."], - "set_prefix": "set", - "batch_size": ["batch_size_of_system1", "batch_size_of_system2", "..."] - } + "stop_batch": 1000000, + "numb_test": 1, + "disp_file": "lcurve.out", + "disp_freq": 1000, + "save_ckpt": "model.ckpt", + "save_freq": 10000, + "training_data": { + "systems": ["system1_path", "system2_path", "..."], + "set_prefix": "set", + "batch_size": ["batch_size_of_system1", "batch_size_of_system2", "..."] + } } ``` @@ -189,20 +189,19 @@ After the training process, you will get two folders: `nvnmd_cnn` and `nvnmd_qnn You can also restart the CNN training from the path prefix of checkpoint files (`nvnmd_cnn/model.ckpt`) by -``` bash +```bash dp train-nvnmd train_cnn.json -r nvnmd_cnn/model.ckpt -s s1 ``` You can also initialize the CNN model and train it by -``` bash +```bash mv nvnmd_cnn nvnmd_cnn_bck cp train_cnn.json train_cnn2.json # please edit train_cnn2.json dp train-nvnmd train_cnn2.json -s s1 -i nvnmd_cnn_bck/model.ckpt ``` - # Testing The frozen model can be used in many ways. The most straightforward testing can be invoked by @@ -215,6 +214,7 @@ dp test -m ./nvnmd_qnn/frozen_model.pb -s path/to/system -d ./test/detail -n 999 where the frozen model file to import is given via the `-m` command line flag, the path to the testing data set is given via the `-s` command line flag, and the file containing details of energy, forces and virials accuracy is given via the `-d` command line flag, the amount of data for testing is given via the `-n` command line flag. # Running MD in Bohrium + After CNN and QNN training, you can upload the ML model to our online NVNMD system and run MD there through Bohrium (https://bohrium.dp.tech). Bohrium is a research platfrom designed for AI for Science Era. For more information, please refer to [Bohrium Introduction](https://bohrium-doc.dp.tech/en/docs/WhatIsBohrium/). ## Registration @@ -251,30 +251,30 @@ Then you need prepare the configuration file `job.json`, the configuration file ```json { - "job_name": "test", - "command": "/usr/bin/lmp_mpi < in.lmp;", - "log_file": "OUTCAR", - "machine_type": "c4_m16_cpu", - "job_type": "container", - "image_name": "lammps_dp:29Sep2021", - "platform": "hnugba", - "region": "default", - "project_id": 0000 + "job_name": "test", + "command": "/usr/bin/lmp_mpi < in.lmp;", + "log_file": "OUTCAR", + "machine_type": "c4_m16_cpu", + "job_type": "container", + "image_name": "lammps_dp:29Sep2021", + "platform": "hnugba", + "region": "default", + "project_id": 0000 } ``` where items are defined as: -| Item | Mean | Optional Value | -| ------------ | -------------------------------------------------------------------------------------------------------------------------- | -------------- | -| job_name | the name of computing job, which can be named freely | a string | -| command | the command to be executed on the computing node | a string | -| log_file | the log file that can be viewed at any time during the calculation process, which can be viewed on the Bohrium "Jobs" page | a string | -| machine_type | the machine type used for the job | "c1_m4_cpu", "c4_m16_cpu", "c8_m32_cpu" | -| job_type | the job type | "container" | -| image_name | the image name used for the job | "lammps_dp:29Sep2021"| -| platform | resource provider | "hnugba" | -| project_id | the project ID to which the job belongs, which can be viewed on the "Projects" page | a integer | +| Item | Mean | Optional Value | +| ------------ | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | +| job_name | the name of computing job, which can be named freely | a string | +| command | the command to be executed on the computing node | a string | +| log_file | the log file that can be viewed at any time during the calculation process, which can be viewed on the Bohrium "Jobs" page | a string | +| machine_type | the machine type used for the job | "c1_m4_cpu", "c4_m16_cpu", "c8_m32_cpu" | +| job_type | the job type | "container" | +| image_name | the image name used for the job | "lammps_dp:29Sep2021" | +| platform | resource provider | "hnugba" | +| project_id | the project ID to which the job belongs, which can be viewed on the "Projects" page | a integer | Notice:The task will use 4 CPU cores for computation, so do not repeatedly use the `mpirun` command, otherwise an error will be reported. All 0000 after "project_id" need to be replaced with your own project ID, which can be viewed on the "Projects" page. Also, the JSON file format requires that no commas be added after the last field within the {}, otherwise, there will be a syntax error. Please check the [documentation](https://github.com/LiuGroupHNU/md-data/blob/master/code/doc/mdpu/hardware.md) for the latest hardware configuration information. diff --git a/doc/test/model-deviation.md b/doc/test/model-deviation.md index a59696c5ee..441d1aabc6 100644 --- a/doc/test/model-deviation.md +++ b/doc/test/model-deviation.md @@ -6,50 +6,61 @@ Model deviation $\epsilon_y$ is the standard deviation of properties $\boldsymbo The DeePMD-kit supports $\boldsymbol y$ to be the atomic force $\boldsymbol F_i$ and the virial tensor $\boldsymbol \Xi$. The model deviation is used to estimate the error of a model at a certain data frame, denoted by $\boldsymbol x$, containing the coordinates and chemical species of all atoms. We present the model deviation of the atomic force and the virial tensor + ```math \epsilon_{\boldsymbol{F},i} (\boldsymbol x)= \sqrt{\langle \lVert \boldsymbol F_i(\boldsymbol x; \boldsymbol \theta_k)-\langle \boldsymbol F_i(\boldsymbol x; \boldsymbol \theta_k) \rangle \rVert^2 \rangle}, ``` + ```math \epsilon_{\boldsymbol{\Xi},{\alpha \beta}} (\boldsymbol x)= \frac{1}{N} \sqrt{\langle ( {\Xi}_{\alpha \beta}(\boldsymbol x; \boldsymbol \theta_k)-\langle {\Xi}_{\alpha \beta}(\boldsymbol x; \boldsymbol \theta_k) \rangle )^2 \rangle}, ``` + where $\boldsymbol \theta_k$ is the parameters of the model $\mathcal M_k$, and the ensemble average $\langle\cdot\rangle$ is estimated by + ```math \langle \boldsymbol y(\boldsymbol x; \boldsymbol \theta_k) \rangle = \frac{1}{n_m} \sum_{k=1}^{n_m} \boldsymbol y(\boldsymbol x; \boldsymbol \theta_k). ``` + Small $\epsilon_{\boldsymbol{F},i}$ means the model has learned the given data; otherwise, it is not covered, and the training data needs to be expanded. If the magnitude of $\boldsymbol F_i$ or $\boldsymbol \Xi$ is quite large, a relative model deviation $\epsilon_{\boldsymbol{F},i,\text{rel}}$ or $\epsilon_{\boldsymbol{\Xi},\alpha\beta,\text{rel}}$ can be used instead of the absolute model deviation: + ```math \epsilon_{\boldsymbol{F},i,\text{rel}} (\boldsymbol x) = \frac{\lvert \epsilon_{\boldsymbol{F},i} (\boldsymbol x) \lvert} {\lvert \langle \boldsymbol F_i (\boldsymbol x; \boldsymbol \theta_k) \rangle \lvert + \nu}, ``` + ```math \epsilon_{\boldsymbol{\Xi},\alpha\beta,\text{rel}} (\boldsymbol x) = \frac{ \epsilon_{\boldsymbol{\Xi},\alpha\beta} (\boldsymbol x) } {\lvert \langle \boldsymbol \Xi (\boldsymbol x; \boldsymbol \theta_k) \rangle \lvert + \nu}, ``` + where $\nu$ is a small constant used to protect an atom where the magnitude of $\boldsymbol{F}_i$ or $\boldsymbol{\Xi}$ is small from having a large model deviation. Statistics of $\epsilon_{\boldsymbol{F},i}$ and $\epsilon_{\boldsymbol{\Xi},{\alpha \beta}}$ can be provided, including the maximum, average, and minimal model deviation over the atom index $i$ and over the component index $\alpha,\beta$, respectively. The maximum model deviation of forces $\epsilon_{\boldsymbol F,\text{max}}$ in a frame was found to be the best error indicator in a concurrent or active learning algorithm.[^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). ## Instructions One can also use a subcommand to calculate the deviation of predicted forces or virials for a bunch of models in the following way: + ```bash dp model-devi -m graph.000.pb graph.001.pb graph.002.pb graph.003.pb -s ./data -o model_devi.out ``` + where `-m` specifies graph files to be calculated, `-s` gives the data to be evaluated, `-o` the file to which model deviation results is dumped. Here is more information on this sub-command: + ```bash usage: dp model-devi [-h] [-v {DEBUG,3,INFO,2,WARNING,1,ERROR,0}] [-l LOG_PATH] [-m MODELS [MODELS ...]] [-s SYSTEM] diff --git a/doc/test/test.md b/doc/test/test.md index c206e8d777..251a12c7e2 100644 --- a/doc/test/test.md +++ b/doc/test/test.md @@ -1,14 +1,19 @@ # Test a model The frozen model can be used in many ways. The most straightforward test can be performed using `dp test`. A typical usage of `dp test` is + ```bash dp test -m graph.pb -s /path/to/system -n 30 ``` + where `-m` gives the tested model, `-s` the path to the tested system and `-n` the number of tested frames. Several other command line options can be passed to `dp test`, which can be checked with + ```bash $ dp test --help ``` + An explanation will be provided + ``` usage: dp test [-h] [-m MODEL] [-s SYSTEM] [-S SET_PREFIX] [-n NUMB_TEST] [-r RAND_SEED] [--shuffle-test] [-d DETAIL_FILE] diff --git a/doc/third-party/ase.md b/doc/third-party/ase.md index ac65fc926e..76371a3197 100644 --- a/doc/third-party/ase.md +++ b/doc/third-party/ase.md @@ -1,6 +1,7 @@ # Use deep potential with ASE Deep potential can be set up as a calculator with ASE to obtain potential energies and forces. + ```python from ase import Atoms from deepmd.calculator import DP @@ -16,6 +17,7 @@ print(water.get_forces()) ``` Optimization is also available: + ```python from ase.optimize import BFGS diff --git a/doc/third-party/gromacs.md b/doc/third-party/gromacs.md index 672fb693b9..c9779611e7 100644 --- a/doc/third-party/gromacs.md +++ b/doc/third-party/gromacs.md @@ -1,10 +1,15 @@ # Running MD with GROMACS + ## DP/MM Simulation + This part gives a simple tutorial on how to run a DP/MM simulation for methane in water, which means using DP for methane and TIP3P for water. All relevant files can be found in `examples/methane`. + ### Topology Preparation + Similar to QM/MM simulation, the internal interactions (including bond, angle, dihedrals, LJ, Columb) of the region described by a neural network potential (NNP) have to be **turned off**. In GROMACS, bonded interactions can be turned off by modifying `[ bonds ]`, `[ angles ]`, `[ dihedrals ]` and `[ pairs ]` sections. And LJ and Columb interactions must be turned off by `[ exclusions ]` section. For example, if one wants to simulate ethane in water, using DeepPotential for methane and TIP3P for water, the topology of methane should be like the following (as presented in `examples/methane/methane.itp`): + ``` [ atomtypes ] ;name btype mass charge ptype sigma epsilon @@ -38,7 +43,9 @@ For example, if one wants to simulate ethane in water, using DeepPotential for m 4 1 2 3 5 5 1 2 3 4 ``` + For comparison, the original topology file generated by `acpype` will be: + ``` ; methane_GMX.itp created by acpype (v: 2021-02-05T22:15:50CET) on Wed Sep 8 01:21:53 2021 @@ -75,45 +82,60 @@ For comparison, the original topology file generated by `acpype` will be: 3 1 5 1 1.0758e+02 3.2635e+02 ; H2 - C1 - H4 4 1 5 1 1.0758e+02 3.2635e+02 ; H3 - C1 - H4 ``` + ### DeepMD Settings + Before running simulations, we need to tell GROMACS to use DeepPotential by setting the environment variable `GMX_DEEPMD_INPUT_JSON`: + ```bash export GMX_DEEPMD_INPUT_JSON=input.json ``` + Then, in your working directories, we have to write `input.json` file: + ```json { - "graph_file": "/path/to/graph.pb", - "type_file": "type.raw", - "index_file": "index.raw", - "lambda": 1.0, - "pbc": false + "graph_file": "/path/to/graph.pb", + "type_file": "type.raw", + "index_file": "index.raw", + "lambda": 1.0, + "pbc": false } ``` + Here is an explanation for these settings: -+ `graph_file` : The graph file (with suffix .pb) generated by `dp freeze` command -+ `type_file` : File to specify DP atom types (in space-separated format). Here, `type.raw` looks like + +- `graph_file` : The graph file (with suffix .pb) generated by `dp freeze` command +- `type_file` : File to specify DP atom types (in space-separated format). Here, `type.raw` looks like + ``` 1 0 0 0 0 ``` -+ `index_file` : File containing indices of DP atoms (in space-separated format), which should be consistent with the indices' order in .gro file but **starting from zero**. Here, `index.raw` looks like + +- `index_file` : File containing indices of DP atoms (in space-separated format), which should be consistent with the indices' order in .gro file but **starting from zero**. Here, `index.raw` looks like + ``` 0 1 2 3 4 ``` -+ `lambda`: Optional, default 1.0. Used in alchemical calculations. -+ `pbc`: Optional, default true. If true, the GROMACS periodic condition is passed to DeepMD. + +- `lambda`: Optional, default 1.0. Used in alchemical calculations. +- `pbc`: Optional, default true. If true, the GROMACS periodic condition is passed to DeepMD. ### Run Simulation + Finally, you can run GROMACS using `gmx mdrun` as usual. ## All-atom DP Simulation + This part gives an example of how to simulate all atoms described by a DeepPotential with Gromacs, taking water as an example. Instead of using `[ exclusions ]` to turn off the non-bonded energies, we can simply do this by setting LJ parameters (i.e. epsilon and sigma) and partial charges to 0, as shown in `examples/water/gmx/water.top`: + ``` [ atomtypes ] ; name at.num mass charge ptype sigma epsilon HW 1 1.008 0.0000 A 0.00000e+00 0.00000e+00 OW 8 16.00 0.0000 A 0.00000e+00 0.00000e+00 ``` + As mentioned in the above section, `input.json` and relevant files (`index.raw`, `type.raw`) should also be created. Then, we can start the simulation under the NVT ensemble and plot the radial distribution function (RDF) by `gmx rdf` command. We can see that the RDF given by Gromacs+DP matches perfectly with Lammps+DP, which further provides an evidence on the validity of our simulation. ![rdf](../../examples/water/gmx/rdf.png) diff --git a/doc/third-party/ipi.md b/doc/third-party/ipi.md index 59decdf3bb..84a972d885 100644 --- a/doc/third-party/ipi.md +++ b/doc/third-party/ipi.md @@ -1,30 +1,36 @@ # Run path-integral MD with i-PI + The i-PI works in a client-server model. The i-PI provides the server for integrating the replica positions of atoms, while the DeePMD-kit provides a client named `dp_ipi` that computes the interactions (including energy, forces and virials). The server and client communicate via the Unix domain socket or the Internet socket. Installation instructions for i-PI can be found [here](../install/install-ipi.md). The client can be started by + ```bash i-pi input.xml & dp_ipi water.json ``` + It is noted that multiple instances of the client allow for computing, in parallel, the interactions of multiple replicas of the path-integral MD. `water.json` is the parameter file for the client `dp_ipi`, and an example is provided: + ```json { - "verbose": false, - "use_unix": true, - "port": 31415, - "host": "localhost", - "graph_file": "graph.pb", - "coord_file": "conf.xyz", - "atom_type" : { - "OW": 0, - "HW1": 1, - "HW2": 1 - } + "verbose": false, + "use_unix": true, + "port": 31415, + "host": "localhost", + "graph_file": "graph.pb", + "coord_file": "conf.xyz", + "atom_type": { + "OW": 0, + "HW1": 1, + "HW2": 1 + } } ``` + The option **`use_unix`** is set to `true` to activate the Unix domain socket, otherwise, the Internet socket is used. The option **`port`** should be the same as that in input.xml: + ```xml 31415 ``` diff --git a/doc/third-party/lammps-command.md b/doc/third-party/lammps-command.md index 150d755795..63f9d8e3bd 100644 --- a/doc/third-party/lammps-command.md +++ b/doc/third-party/lammps-command.md @@ -1,6 +1,7 @@ # Run MD with LAMMPS ## units + All units in LAMMPS except `lj` are supported. `lj` is not supported. The most commonly used units are `metal`, since the internal units of distance, energy, force, and charge in DeePMD-kit are `\AA`, `eV`, `eV / \AA`, and `proton charge`, respectively. These units are consistent with the `metal` units in LAMMPS. @@ -34,11 +35,12 @@ The DeePMD-kit package provides the pair_style `deepmd` ```lammps pair_style deepmd models ... keyword value ... ``` + - deepmd = style of this pair_style - models = frozen model(s) to compute the interaction. -If multiple models are provided, then only the first model serves to provide energy and force prediction for each timestep of molecular dynamics, -and the model deviation will be computed among all models every `out_freq` timesteps. -- keyword = *out_file* or *out_freq* or *fparam* or *fparam_from_compute* or *aparam_from_compute* or *atomic* or *relative* or *relative_v* or *aparam* or *ttm* + If multiple models are provided, then only the first model serves to provide energy and force prediction for each timestep of molecular dynamics, + and the model deviation will be computed among all models every `out_freq` timesteps. +- keyword = _out_file_ or _out_freq_ or _fparam_ or _fparam_from_compute_ or _aparam_from_compute_ or _atomic_ or _relative_ or _relative_v_ or _aparam_ or _ttm_
     out_file value = filename
         filename = The file name for the model deviation output. Default is model_devi.out
@@ -63,6 +65,7 @@ and the model deviation will be computed among all models every `out_freq` times
 
### Examples + ```lammps pair_style deepmd graph.pb pair_style deepmd graph.pb fparam 1.2 @@ -77,6 +80,7 @@ compute 1 all ke/atom ``` ### Description + Evaluate the interaction of the system by using [Deep Potential][DP] or [Deep Potential Smooth Edition][DP-SE]. It is noticed that deep potential is not a "pairwise" interaction, but a multi-body interaction. This pair style takes the deep potential defined in a model file that usually has the .pb extension. The model can be trained and frozen by package [DeePMD-kit](https://github.com/deepmodeling/deepmd-kit), which can have either double or single float precision interface. @@ -107,8 +111,8 @@ If the training parameter {ref}`type_map ` is not set, atom name Spin is specified by keywords `virtual_len` and `spin_norm`. If the keyword `virtual_len` is set, the distance between virtual atom and its corresponding real atom for each type of magnetic atoms will be fed to the model as the spin parameters. If the keyword `spin_norm` is set, the magnitude of the magnetic moment for each type of magnetic atoms will be fed to the model as the spin parameters. ### Restrictions -- The `deepmd` pair style is provided in the USER-DEEPMD package, which is compiled from the DeePMD-kit, visit the [DeePMD-kit website](https://github.com/deepmodeling/deepmd-kit) for more information. +- The `deepmd` pair style is provided in the USER-DEEPMD package, which is compiled from the DeePMD-kit, visit the [DeePMD-kit website](https://github.com/deepmodeling/deepmd-kit) for more information. ## Compute tensorial properties @@ -117,6 +121,7 @@ The DeePMD-kit package provides the compute `deeptensor/atom` for computing atom ```lammps compute ID group-ID deeptensor/atom model_file ``` + - ID: user-assigned name of the computation - group-ID: ID of the group of atoms to compute - deeptensor/atom: the style of this compute @@ -125,27 +130,33 @@ compute ID group-ID deeptensor/atom model_file At this time, the training parameter {ref}`type_map ` will be mapped to LAMMPS atom types. ### Examples + ```lammps compute dipole all deeptensor/atom dipole.pb ``` + The result of the compute can be dumped to trajectory file by + ```lammps dump 1 all custom 100 water.dump id type c_dipole[1] c_dipole[2] c_dipole[3] ``` ### Restrictions + - The `deeptensor/atom` compute is provided in the USER-DEEPMD package, which is compiled from the DeePMD-kit, visit the [DeePMD-kit website](https://github.com/deepmodeling/deepmd-kit) for more information. - For the issue of using a unit style for `compute deeptensor/atom`, refer to the discussions in [units](#units) of this page. - ## Long-range interaction + The reciprocal space part of the long-range interaction can be calculated by LAMMPS command `kspace_style`. To use it with DeePMD-kit, one writes + ```lammps pair_style deepmd graph.pb pair_coeff * * kspace_style pppm 1.0e-5 kspace_modify gewald 0.45 ``` + Please notice that the DeePMD does nothing to the direct space part of the electrostatic interaction, because this part is assumed to be fitted in the DeePMD model (the direct space cut-off is thus the cut-off of the DeePMD model). The splitting parameter `gewald` is modified by the `kspace_modify` command. ## Use of the centroid/stress/atom to get the full 3x3 "atomic-virial" @@ -157,9 +168,11 @@ $$dvatom=-\sum_{m}( \mathbf{r}_n- \mathbf{r}_m) \frac{de_m}{d\mathbf{r}_n}$$ Where $\mathbf{r}_n$ is the atomic position of nth atom, $\mathbf{v}_n$ velocity of the atom and $\frac{de_m}{d\mathbf{r}_n}$ the derivative of the atomic energy. In LAMMPS one can get the per-atom stress using the command `centroid/stress/atom`: + ```lammps compute ID group-ID centroid/stress/atom NULL virial ``` + see [LAMMPS doc page](https://docs.lammps.org/compute_stress_atom.html#thompson2) for more details on the meaning of the keywords. :::{versionchanged} v2.2.3 @@ -167,20 +180,25 @@ v2.2.2 or previous versions passed per-atom stress (`cvatom`) with the per-atom ::: ### Examples + In order of computing the 9-component per-atom stress + ```lammps compute stress all centroid/stress/atom NULL virial ``` + Thus `c_stress` is an array with 9 components in the order `xx,yy,zz,xy,xz,yz,yx,zx,zy`. If you use this feature please cite [D. Tisi, L. Zhang, R. Bertossa, H. Wang, R. Car, S. Baroni - arXiv preprint arXiv:2108.10850, 2021](https://arxiv.org/abs/2108.10850) ## Computation of heat flux + Using a per-atom stress tensor one can, for example, compute the heat flux defined as: $$\mathbf J = \sum_n e_n \mathbf v_n + \sum_{n,m} ( \mathbf r_m- \mathbf r_n) \frac{de_m}{d\mathbf r_n} \mathbf v_n$$ to compute the heat flux with LAMMPS: + ```lammps compute ke_ID all ke/atom compute pe_ID all pe/atom @@ -196,10 +214,10 @@ compute pe all pe/atom compute stress all centroid/stress/atom NULL virial compute flux all heat/flux ke pe stress ``` + `c_flux` is a global vector of length 6. The first three components are the $x$, $y$ and $z$ components of the full heat flux vector. The others are the components of the so-called convective portion, see [LAMMPS doc page](https://docs.lammps.org/compute_heat_flux.html) for more detailes. If you use these features please cite [D. Tisi, L. Zhang, R. Bertossa, H. Wang, R. Car, S. Baroni - arXiv preprint arXiv:2108.10850, 2021](https://arxiv.org/abs/2108.10850) - -[DP]:https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.120.143001 -[DP-SE]:https://dl.acm.org/doi/10.5555/3327345.3327356 +[DP]: https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.120.143001 +[DP-SE]: https://dl.acm.org/doi/10.5555/3327345.3327356 diff --git a/doc/third-party/out-of-deepmd-kit.md b/doc/third-party/out-of-deepmd-kit.md index 2ebed6fb46..3eb722c040 100644 --- a/doc/third-party/out-of-deepmd-kit.md +++ b/doc/third-party/out-of-deepmd-kit.md @@ -11,6 +11,7 @@ An [OpenMM](https://github.com/openmm/openmm) plugin is provided from [JingHuang Starting from [AmberTools24](https://ambermd.org/), `sander` includes an interface to the DeePMD-kit, which implements the [Deep Potential Range Corrected (DPRc) correction](../model/dprc.md). The DPRc model and the interface were developed by the [York Lab](https://theory.rutgers.edu/) from Rutgers University. More details are available in + - [Amber Reference Manuals](https://ambermd.org/Manuals.php), providing documentation for how to enable the interface and the `&dprc` namelist; - [GitLab RutgersLBSR/AmberDPRc](https://gitlab.com/RutgersLBSR/AmberDPRc/), providing examples mdin files; - [DP-Amber](https://github.com/njzjz/dpamber/), a tiny tool to convert Amber trajectory to DPRc training data; diff --git a/doc/train/finetuning.md b/doc/train/finetuning.md index e4fa00e23d..011db0bf9f 100644 --- a/doc/train/finetuning.md +++ b/doc/train/finetuning.md @@ -36,6 +36,7 @@ such as {ref}`descriptor `, {ref}`fitting_net ` part in `input.json` to perform finetuning: + ```json "model": { "type_map": ["O", "H"], diff --git a/doc/train/gpu-limitations.md b/doc/train/gpu-limitations.md index dee606c2a3..92577fd65c 100644 --- a/doc/train/gpu-limitations.md +++ b/doc/train/gpu-limitations.md @@ -1,6 +1,7 @@ # Known limitations of using GPUs {{ tensorflow_icon }} If you use DeePMD-kit in a GPU environment, the acceptable value range of some variables is additionally restricted compared to the CPU environment due to the software's GPU implementations: + 1. The number of atom types of a given system must be less than 128. 2. The maximum distance between an atom and its neighbors must be less than 128. It can be controlled by setting the rcut value of training parameters. 3. Theoretically, the maximum number of atoms that a single GPU can accept is about 10,000,000. However, this value is limited by the GPU memory size currently, usually within 1000,000 atoms even in the model compression mode. diff --git a/doc/train/multi-task-training-pt.md b/doc/train/multi-task-training-pt.md index d1d0f9cd0d..284ecf9a27 100644 --- a/doc/train/multi-task-training-pt.md +++ b/doc/train/multi-task-training-pt.md @@ -3,6 +3,7 @@ :::{note} **Supported backends**: PyTorch {{ pytorch_icon }} ::: + ## Theory @@ -10,6 +11,7 @@ The multi-task training process can simultaneously handle different datasets with properties that cannot be fitted in one network (e.g. properties from DFT calculations under different exchange-correlation functionals or different basis sets). These datasets are denoted by $\boldsymbol x^{(1)}, \dots, \boldsymbol x^{(n_t)}$. For each dataset, a training task is defined as + ```math \min_{\boldsymbol \theta} L^{(t)} (\boldsymbol x^{(t)}; \boldsymbol \theta^{(t)}, \tau), \quad t=1, \dots, n_t. ``` @@ -29,6 +31,7 @@ In particular, it makes multi-GPU parallel training and even tasks beyond DFT po enabling larger-scale and more general multi-task training to obtain more general pre-trained models. ## Perform the multi-task training using PyTorch + Training on multiple data sets (each data set contains several data systems) can be performed in multi-task mode, typically with one common descriptor and multiple specific fitting nets for each data set. To proceed, one need to change the representation of the model definition in the input script. @@ -37,34 +40,35 @@ define the shared parameters of the model part {ref}`shared_dict `: The parameter definition of the shared part, including various descriptors, -type maps (or even fitting nets can be shared). Each module can be defined with a user-defined `part_key`, such as `my_descriptor`. -The content needs to align with the corresponding definition in the single-task training model component, such as the definition of the descriptor. + type maps (or even fitting nets can be shared). Each module can be defined with a user-defined `part_key`, such as `my_descriptor`. + The content needs to align with the corresponding definition in the single-task training model component, such as the definition of the descriptor. - {ref}`model/model_dict `: The core definition of the model part and the explanation of sharing rules, -starting with user-defined model name keys `model_key`, such as `my_model_1`. -Each model part needs to align with the components of the single-task training {ref}`model `, but with the following sharing rules: + starting with user-defined model name keys `model_key`, such as `my_model_1`. + Each model part needs to align with the components of the single-task training {ref}`model `, but with the following sharing rules: - - If you want to share the current model component with other tasks, which should be part of the {ref}`model/shared_dict `, -you can directly fill in the corresponding `part_key`, such as -```"descriptor": "my_descriptor", ``` -to replace the previous detailed parameters. Here, you can also specify the shared_level, such as -```"descriptor": "my_descriptor:shared_level", ``` -and use the user-defined integer `shared_level` in the code to share the corresponding module to varying degrees -(default is to share all parameters, i.e., `shared_level`=0). -The parts that are exclusive to each model can be written following the previous definition. + you can directly fill in the corresponding `part_key`, such as + `"descriptor": "my_descriptor", ` + to replace the previous detailed parameters. Here, you can also specify the shared_level, such as + `"descriptor": "my_descriptor:shared_level", ` + and use the user-defined integer `shared_level` in the code to share the corresponding module to varying degrees + (default is to share all parameters, i.e., `shared_level`=0). + The parts that are exclusive to each model can be written following the previous definition. - {ref}`loss_dict `: The loss settings corresponding to each task model, specified by the `model_key`. -Each {ref}`loss_dict/model_key ` contains the corresponding loss settings, -which are the same as the definition in single-task training {ref}``. + Each {ref}`loss_dict/model_key ` contains the corresponding loss settings, + which are the same as the definition in single-task training {ref}``. - {ref}`training/data_dict `: The data settings corresponding to each task model, specified by the `model_key`. -Each `training/data_dict/model_key` contains the corresponding `training_data` and `validation_data` settings, -which are the same as the definition in single-task training {ref}`training_data ` and {ref}`validation_data `. + Each `training/data_dict/model_key` contains the corresponding `training_data` and `validation_data` settings, + which are the same as the definition in single-task training {ref}`training_data ` and {ref}`validation_data `. - (Optional) {ref}`training/model_prob `: The sampling weight settings corresponding to each `model_key`, i.e., the probability weight in the training step. -You can specify any positive real number weight for each task. The higher the weight, the higher the probability of being sampled in each training. -This setting is optional, and if not set, tasks will be sampled with equal weights. + You can specify any positive real number weight for each task. The higher the weight, the higher the probability of being sampled in each training. + This setting is optional, and if not set, tasks will be sampled with equal weights. An example input for multi-task training two models in water system is shown as following: + ```{literalinclude} ../../examples/water_multi_task/pytorch_example/input_torch.json :language: json :linenos: @@ -76,6 +80,7 @@ To finetune based on the checkpoint `model.pt` after the multi-task pre-training users only need to prepare the normal input for single-task training `input_single.json`, and then select one of the trained model's task names `model_key`. Run the following command: + ```bash $ dp --pt train input_single.json --finetune model.pt --model-branch model_key ``` diff --git a/doc/train/multi-task-training-tf.md b/doc/train/multi-task-training-tf.md index 47fb1cc1da..0f745958eb 100644 --- a/doc/train/multi-task-training-tf.md +++ b/doc/train/multi-task-training-tf.md @@ -3,6 +3,7 @@ :::{note} **Supported backends**: TensorFlow {{ tensorflow_icon }} ::: + ## Theory @@ -10,6 +11,7 @@ The multi-task training process can simultaneously handle different datasets with properties that cannot be fitted in one network (e.g. properties from DFT calculations under different exchange-correlation functionals or different basis sets). These datasets are denoted by $\boldsymbol x^{(1)}, \dots, \boldsymbol x^{(n_t)}$. For each dataset, a training task is defined as + ```math \min_{\boldsymbol \theta} L^{(t)} (\boldsymbol x^{(t)}; \boldsymbol \theta^{(t)}, \tau), \quad t=1, \dots, n_t. ``` @@ -20,24 +22,26 @@ At each training step, a task is randomly picked from ${1, \dots, n_t}$, and the If different fitting networks have the same architecture, they can share the parameters of some layers to improve training efficiency.[^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). ## Perform the multi-task training + Training on multiple data sets (each data set contains several data systems) can be performed in multi-task mode, with one common descriptor and multiple specific fitting nets for each data set. One can simply switch the following parameters in training input script to perform multi-task mode: + - {ref}`fitting_net ` --> {ref}`fitting_net_dict `, -each key of which can be one individual fitting net. -- {ref}`training_data `, {ref}`validation_data ` ---> {ref}`data_dict `, each key of which can be one individual data set contains -several data systems for corresponding fitting net, the keys must be consistent with those in -{ref}`fitting_net_dict `. + each key of which can be one individual fitting net. +- {ref}`training_data `, {ref}`validation_data ` + --> {ref}`data_dict `, each key of which can be one individual data set contains + several data systems for corresponding fitting net, the keys must be consistent with those in + {ref}`fitting_net_dict `. - {ref}`loss ` --> {ref}`loss_dict `, each key of which can be one individual loss setting -for corresponding fitting net, the keys must be consistent with those in -{ref}`fitting_net_dict `, if not set, the corresponding fitting net will use the default loss. + for corresponding fitting net, the keys must be consistent with those in + {ref}`fitting_net_dict `, if not set, the corresponding fitting net will use the default loss. - (Optional) {ref}`fitting_weight `, each key of which can be a non-negative integer or float, -deciding the chosen probability for corresponding fitting net in training, if not set or invalid, -the corresponding fitting net will not be used. + deciding the chosen probability for corresponding fitting net in training, if not set or invalid, + the corresponding fitting net will not be used. The training procedure will automatically choose single-task or multi-task mode, based on the above parameters. Note that parameters of single-task mode and multi-task mode can not be mixed. @@ -45,6 +49,7 @@ Note that parameters of single-task mode and multi-task mode can not be mixed. An example input for training energy and dipole in water system can be found here: [multi-task input on water](../../examples/water_multi_task/ener_dipole/input.json). The supported descriptors for multi-task mode are listed: + - {ref}`se_a (se_e2_a) ` - {ref}`se_r (se_e2_r) ` - {ref}`se_at (se_e3) ` @@ -53,6 +58,7 @@ The supported descriptors for multi-task mode are listed: - {ref}`hybrid ` The supported fitting nets for multi-task mode are listed: + - {ref}`ener ` - {ref}`dipole ` - {ref}`polar ` @@ -60,12 +66,14 @@ The supported fitting nets for multi-task mode are listed: The output of `dp freeze` command in multi-task mode can be seen in [freeze command](../freeze/freeze.md). ## Initialization from pretrained multi-task model + For advance training in multi-task mode, one can first train the descriptor on several upstream datasets and then transfer it on new downstream ones with newly added fitting nets. At the second step, you can also inherit some fitting nets trained on upstream datasets, by merely adding fitting net keys in {ref}`fitting_net_dict ` and optional fitting net weights in {ref}`fitting_weight `. Take [multi-task input on water](../../examples/water_multi_task/ener_dipole/input.json) again for example. You can first train a multi-task model using input script with the following {ref}`model ` part: + ```json "model": { "type_map": ["O", "H"], @@ -89,12 +97,16 @@ You can first train a multi-task model using input script with the following {re }, } ``` + After training, you can freeze this multi-task model into one unit graph: + ```bash $ dp freeze -o graph.pb --united-model ``` + Then if you want to transfer the trained descriptor and some fitting nets (take `water_ener` for example) to newly added datasets with new fitting net `water_ener_2`, you can modify the {ref}`model ` part of the new input script in a more simplified way: + ```json "model": { "type_map": ["O", "H"], @@ -108,12 +120,14 @@ you can modify the {ref}`model ` part of the new input script in a more s }, } ``` + It will autocomplete the configurations according to the frozen graph. Note that for newly added fitting net keys, other parts in the input script, including {ref}`data_dict ` and {ref}`loss_dict ` (optionally {ref}`fitting_weight `), should be set explicitly. While for old fitting net keys, it will inherit the old configurations if not set. Finally, you can perform the modified multi-task training from the frozen model with command: + ```bash $ dp train input.json --init_frz_model graph.pb ``` @@ -125,6 +139,7 @@ In this situation, one can set {ref}`model/fitting_net[ener]/layer_name>` to sha The architecture of the layers with the same name should be the same. For example, if one want to share the first and the third layers for two three-hidden-layer fitting networks, the following parameters should be set. + ```json "fitting_net_dict": { "ccsd": { diff --git a/doc/train/parallel-training.md b/doc/train/parallel-training.md index aba2836250..9ea92b4751 100644 --- a/doc/train/parallel-training.md +++ b/doc/train/parallel-training.md @@ -5,6 +5,7 @@ ::: ## TensorFlow Implementation {{ tensorflow_icon }} + Currently, parallel training in tensorflow version is enabled in a synchronized way with help of [Horovod](https://github.com/horovod/horovod). Depending on the number of training processes (according to MPI context) and the number of GPU cards available, DeePMD-kit will decide whether to launch the training in parallel (distributed) mode or in serial mode. Therefore, no additional options are specified in your JSON/YAML input file. @@ -15,6 +16,7 @@ Horovod works in the data-parallel mode, resulting in a larger global batch size The number of decay steps required to achieve the same accuracy can decrease by the number of cards (e.g., 1/2 of steps in the above case), but needs to be scaled manually in the input file. In some cases, it won't work well when scaling the learning rate by worker count in a `linear` way. Then you can try `sqrt` or `none` by setting argument {ref}`scale_by_worker ` like below. + ```json "learning_rate" :{ "scale_by_worker": "none", @@ -27,11 +29,11 @@ In some cases, it won't work well when scaling the learning rate by worker count Testing `examples/water/se_e2_a` on an 8-GPU host, linear acceleration can be observed with the increasing number of cards. | Num of GPU cards | Seconds every 100 samples | Samples per second | Speed up | -| -- | -- | -- | -- | -| 1 | 1.4515 | 68.89 | 1.00 | -| 2 | 1.5962 | 62.65*2 | 1.82 | -| 4 | 1.7635 | 56.71*4 | 3.29 | -| 8 | 1.7267 | 57.91*8 | 6.72 | +| ---------------- | ------------------------- | ------------------ | -------- | +| 1 | 1.4515 | 68.89 | 1.00 | +| 2 | 1.5962 | 62.65\*2 | 1.82 | +| 4 | 1.7635 | 56.71\*4 | 3.29 | +| 8 | 1.7267 | 57.91\*8 | 6.72 | ### How to use @@ -47,13 +49,16 @@ Need to mention, the environment variable `CUDA_VISIBLE_DEVICES` must be set to To maximize the performance, one should follow [FAQ: How to control the parallelism of a job](../troubleshooting/howtoset_num_nodes.md) to control the number of threads. When using MPI with Horovod, `horovodrun` is a simple wrapper around `mpirun`. In the case where fine-grained control over options is passed to `mpirun`, [`mpirun` can be invoked directly](https://horovod.readthedocs.io/en/stable/mpi_include.html), and it will be detected automatically by Horovod, e.g., + ```bash CUDA_VISIBLE_DEVICES=4,5,6,7 mpirun -l -launcher=fork -hosts=localhost -np 4 \ dp train --mpi-log=workers input.json ``` + this is sometimes necessary for an HPC environment. Whether distributed workers are initiated can be observed in the "Summary of the training" section in the log (`world size` > 1, and `distributed`). + ``` [0] DEEPMD INFO ---Summary of the training--------------------------------------- [0] DEEPMD INFO distributed @@ -72,6 +77,7 @@ Whether distributed workers are initiated can be observed in the "Summary of the ### Logging What's more, 2 command-line arguments are defined to control the logging behavior when performing parallel training with MPI. + ``` optional arguments: -l LOG_PATH, --log-path LOG_PATH @@ -88,19 +94,20 @@ optional arguments: ## PyTorch Implementation {{ pytorch_icon }} -Currently, parallel training in pytorch version is implemented in the form of PyTorch Distributed Data Parallelism [DDP](https://pytorch.org/docs/stable/generated/torch.nn.parallel.DistributedDataParallel.html). +Currently, parallel training in pytorch version is implemented in the form of PyTorch Distributed Data Parallelism [DDP](https://pytorch.org/docs/stable/generated/torch.nn.parallel.DistributedDataParallel.html). DeePMD-kit will decide whether to launch the training in parallel (distributed) mode or in serial mode depending on your execution command. ### Dataloader and Dataset + One of the major differences between two backends during training is that the PyTorch version employs a multi-threaded data loading utility [DataLoader](https://pytorch.org/docs/stable/data.html). We utilize the PyTorch framework and have designed and implemented a multiprocessing data processing and loading system called DpLoaderSet based on torch DataLoader and Dataset. - First, we establish a DeepmdData class for each system, which is consistent with the TensorFlow version in this level. Then, we create a dataloader for each system, resulting in the same number of dataloaders as the number of systems. Next, we create a dataset for the dataloaders obtained in the previous step. This allows us to query the data for each system through this dataset, while the iteration pointers for each system are maintained by their respective dataloaders. Finally, a dataloader is created for the outermost dataset. -We achieve custom sampling methods using a weighted sampler. The length of the sampler is set to total_batch_num * num_workers.The parameter "num_workers" defines the number of threads involved in multi-threaded loading, which can be modified by setting the environment variable NUM_WORKERS (default: min(8, ncpus)). +We achieve custom sampling methods using a weighted sampler. The length of the sampler is set to total_batch_num \* num_workers.The parameter "num_workers" defines the number of threads involved in multi-threaded loading, which can be modified by setting the environment variable NUM_WORKERS (default: min(8, ncpus)). > **Note** The underlying dataloader will use a distributed sampler to ensure that each GPU receives batches with different content in parallel mode, which will use sequential sampler in serial mode. In the TensorFlow version, Horovod shuffles the dataset using different random seeds for the same purpose.. + ```mermaid flowchart LR @@ -174,6 +181,7 @@ torchrun --rdzv_endpoint=node0:12321 --nnodes=2 --nproc_per_node=4 --node_rank=0 # On node 1: torchrun --rdzv_endpoint=node0:12321 --nnodes=2 --nproc_per_node=4 --node_rank=1 --no_python dp --pt train tests/water/se_e2_a.json ``` + > **Note** Set environment variables to tune [CPU specific optimizations](https://pytorch.org/tutorials/recipes/recipes/tuning_guide.html#cpu-specific-optimizations) in advance. > **Note** for developers: `torchrun` by default passes settings as environment variables [(list here)](https://pytorch.org/docs/stable/elastic/run.html#environment-variables). diff --git a/doc/train/tensorboard.md b/doc/train/tensorboard.md index a6cfdccb68..7b41c004ce 100644 --- a/doc/train/tensorboard.md +++ b/doc/train/tensorboard.md @@ -12,10 +12,10 @@ experimentation. Full instructions for TensorBoard can be found DeePMD-kit can now use most of the interesting features enabled by TensorBoard! -* **Tracking and visualizing metrics,** such as l2_loss, l2_energy_loss and l2_force_loss -* **Visualizing the model graph** (ops and layers) -* **Viewing histograms of weights, biases, or other tensors as they change over time.** -* **Viewing summaries of trainable variables** +- **Tracking and visualizing metrics,** such as l2_loss, l2_energy_loss and l2_force_loss +- **Visualizing the model graph** (ops and layers) +- **Viewing histograms of weights, biases, or other tensors as they change over time.** +- **Viewing summaries of trainable variables** @@ -84,6 +84,7 @@ tensorboard --logdir path/to/logs ![DeePMD-kit distribution](../images/tensorboard-distribution.png) ### Viewing summaries of trainable variables + ![DeePMD-kit scalar](../images/tensorboard-scalar.png) ## Attention diff --git a/doc/train/training-advanced.md b/doc/train/training-advanced.md index 4940b77fa7..a0f6759256 100644 --- a/doc/train/training-advanced.md +++ b/doc/train/training-advanced.md @@ -7,21 +7,26 @@ In this section, we will take `$deepmd_source_dir/examples/water/se_e2_a/input.j ### Theory The learning rate $\gamma$ decays exponentially: + ```math \gamma(\tau) = \gamma^0 r ^ {\lfloor \tau/s \rfloor}, ``` + where $\tau \in \mathbb{N}$ is the index of the training step, $\gamma^0 \in \mathbb{R}$ is the learning rate at the first step, and the decay rate $r$ is given by + ```math r = {\left(\frac{\gamma^{\text{stop}}}{\gamma^0}\right )} ^{\frac{s}{\tau^{\text{stop}}}}, ``` + where $\tau^{\text{stop}} \in \mathbb{N}$, $\gamma^{\text{stop}} \in \mathbb{R}$, and $s \in \mathbb{N}$ are the stopping step, the stopping learning rate, and the decay steps, respectively, all of which are hyperparameters provided in advance. [^1] -[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). +[^1]: This section is built upon Jinzhe Zeng, Duo Zhang, Denghui Lu, Pinghui Mo, Zeyu Li, Yixiao Chen, Marián Rynik, Li'ang Huang, Ziyao Li, Shaochen Shi, Yingze Wang, Haotian Ye, Ping Tuo, Jiabin Yang, Ye Ding, Yifan Li, Davide Tisi, Qiyu Zeng, Han Bao, Yu Xia, Jiameng Huang, Koki Muraoka, Yibo Wang, Junhan Chang, Fengbo Yuan, Sigbjørn Løland Bore, Chun Cai, Yinnian Lin, Bo Wang, Jiayan Xu, Jia-Xin Zhu, Chenxing Luo, Yuzhi Zhang, Rhys E. A. Goodall, Wenshuo Liang, Anurag Kumar Singh, Sikai Yao, Jingchao Zhang, Renata Wentzcovitch, Jiequn Han, Jie Liu, Weile Jia, Darrin M. York, Weinan E, Roberto Car, Linfeng Zhang, Han Wang, [J. Chem. Phys. 159, 054801 (2023)](https://doi.org/10.1063/5.0155600) licensed under a [Creative Commons Attribution (CC BY) license](http://creativecommons.org/licenses/by/4.0/). ### Instructions The {ref}`learning_rate ` section in `input.json` is given as follows + ```json "learning_rate" :{ "type": "exp", @@ -31,17 +36,19 @@ The {ref}`learning_rate ` section in `input.json` is given as fol "_comment": "that's all" } ``` -* {ref}`start_lr ` gives the learning rate at the beginning of the training. -* {ref}`stop_lr ` gives the learning rate at the end of the training. It should be small enough to ensure that the network parameters satisfactorily converge. -* During the training, the learning rate decays exponentially from {ref}`start_lr ` to {ref}`stop_lr ` following the formula: - ``` - lr(t) = start_lr * decay_rate ^ ( t / decay_steps ) - ``` +- {ref}`start_lr ` gives the learning rate at the beginning of the training. +- {ref}`stop_lr ` gives the learning rate at the end of the training. It should be small enough to ensure that the network parameters satisfactorily converge. +- During the training, the learning rate decays exponentially from {ref}`start_lr ` to {ref}`stop_lr ` following the formula: + + ``` + lr(t) = start_lr * decay_rate ^ ( t / decay_steps ) + ``` ## Training parameters Other training parameters are given in the {ref}`training ` section. + ```json "training": { "training_data": { @@ -65,15 +72,18 @@ Other training parameters are given in the {ref}`training ` section. "save_freq": 1000 } ``` + The sections {ref}`training_data ` and {ref}`validation_data ` give the training dataset and validation dataset, respectively. Taking the training dataset for example, the keys are explained below: -* {ref}`systems ` provide paths of the training data systems. DeePMD-kit allows you to provide multiple systems with different numbers of atoms. This key can be a `list` or a `str`. - * `list`: {ref}`systems ` gives the training data systems. - * `str`: {ref}`systems ` should be a valid path. DeePMD-kit will recursively search all data systems in this path. -* At each training step, DeePMD-kit randomly picks {ref}`batch_size ` frame(s) from one of the systems. The probability of using a system is by default in proportion to the number of batches in the system. More options are available for automatically determining the probability of using systems. One can set the key {ref}`auto_prob ` to - * `"prob_uniform"` all systems are used with the same probability. - * `"prob_sys_size"` the probability of using a system is proportional to its size (number of frames). - * `"prob_sys_size; sidx_0:eidx_0:w_0; sidx_1:eidx_1:w_1;..."` the `list` of systems is divided into blocks. Block `i` has systems ranging from `sidx_i` to `eidx_i`. The probability of using a system from block `i` is proportional to `w_i`. Within one block, the probability of using a system is proportional to its size. -* An example of using `"auto_prob"` is given below. The probability of using `systems[2]` is 0.4, and the sum of the probabilities of using `systems[0]` and `systems[1]` is 0.6. If the number of frames in `systems[1]` is twice of `system[0]`, then the probability of using `system[1]` is 0.4 and that of `system[0]` is 0.2. + +- {ref}`systems ` provide paths of the training data systems. DeePMD-kit allows you to provide multiple systems with different numbers of atoms. This key can be a `list` or a `str`. + - `list`: {ref}`systems ` gives the training data systems. + - `str`: {ref}`systems ` should be a valid path. DeePMD-kit will recursively search all data systems in this path. +- At each training step, DeePMD-kit randomly picks {ref}`batch_size ` frame(s) from one of the systems. The probability of using a system is by default in proportion to the number of batches in the system. More options are available for automatically determining the probability of using systems. One can set the key {ref}`auto_prob ` to + - `"prob_uniform"` all systems are used with the same probability. + - `"prob_sys_size"` the probability of using a system is proportional to its size (number of frames). + - `"prob_sys_size; sidx_0:eidx_0:w_0; sidx_1:eidx_1:w_1;..."` the `list` of systems is divided into blocks. Block `i` has systems ranging from `sidx_i` to `eidx_i`. The probability of using a system from block `i` is proportional to `w_i`. Within one block, the probability of using a system is proportional to its size. +- An example of using `"auto_prob"` is given below. The probability of using `systems[2]` is 0.4, and the sum of the probabilities of using `systems[0]` and `systems[1]` is 0.6. If the number of frames in `systems[1]` is twice of `system[0]`, then the probability of using `system[1]` is 0.4 and that of `system[0]` is 0.2. + ```json "training_data": { "systems": ["../data_water/data_0/", "../data_water/data_1/", "../data_water/data_2/"], @@ -81,7 +91,9 @@ The sections {ref}`training_data ` and {ref}`validation_ "batch_size": "auto" } ``` -* The probability of using systems can also be specified explicitly with key {ref}`sys_probs ` which is a list having the length of the number of systems. For example + +- The probability of using systems can also be specified explicitly with key {ref}`sys_probs ` which is a list having the length of the number of systems. For example + ```json "training_data": { "systems": ["../data_water/data_0/", "../data_water/data_1/", "../data_water/data_2/"], @@ -89,34 +101,40 @@ The sections {ref}`training_data ` and {ref}`validation_ "batch_size": "auto:32" } ``` -* The key {ref}`batch_size ` specifies the number of frames used to train or validate the model in a training step. It can be set to - * `list`: the length of which is the same as the {ref}`systems`. The batch size of each system is given by the elements of the list. - * `int`: all systems use the same batch size. - * `"auto"`: the same as `"auto:32"`, see `"auto:N"` - * `"auto:N"`: automatically determines the batch size so that the {ref}`batch_size ` times the number of atoms in the system is no less than `N`. -* The key {ref}`numb_batch ` in {ref}`validate_data ` gives the number of batches of model validation. Note that the batches may not be from the same system + +- The key {ref}`batch_size ` specifies the number of frames used to train or validate the model in a training step. It can be set to + - `list`: the length of which is the same as the {ref}`systems`. The batch size of each system is given by the elements of the list. + - `int`: all systems use the same batch size. + - `"auto"`: the same as `"auto:32"`, see `"auto:N"` + - `"auto:N"`: automatically determines the batch size so that the {ref}`batch_size ` times the number of atoms in the system is no less than `N`. +- The key {ref}`numb_batch ` in {ref}`validate_data ` gives the number of batches of model validation. Note that the batches may not be from the same system The section {ref}`mixed_precision ` specifies the mixed precision settings, which will enable the mixed precision training workflow for DeePMD-kit. The keys are explained below: -* {ref}`output_prec ` precision used in the output tensors, only `float32` is supported currently. -* {ref}`compute_prec ` precision used in the computing tensors, only `float16` is supported currently. -Note there are several limitations about mixed precision training: -* Only {ref}`se_e2_a ` type descriptor is supported by the mixed precision training workflow. -* The precision of the embedding net and the fitting net are forced to be set to `float32`. + +- {ref}`output_prec ` precision used in the output tensors, only `float32` is supported currently. +- {ref}`compute_prec ` precision used in the computing tensors, only `float16` is supported currently. + Note there are several limitations about mixed precision training: +- Only {ref}`se_e2_a ` type descriptor is supported by the mixed precision training workflow. +- The precision of the embedding net and the fitting net are forced to be set to `float32`. Other keys in the {ref}`training ` section are explained below: -* {ref}`numb_steps ` The number of training steps. -* {ref}`seed ` The random seed for getting frames from the training data set. -* {ref}`disp_file ` The file for printing learning curve. -* {ref}`disp_freq ` The frequency of printing learning curve. Set in the unit of training steps -* {ref}`save_freq ` The frequency of saving checkpoint. + +- {ref}`numb_steps ` The number of training steps. +- {ref}`seed ` The random seed for getting frames from the training data set. +- {ref}`disp_file ` The file for printing learning curve. +- {ref}`disp_freq ` The frequency of printing learning curve. Set in the unit of training steps +- {ref}`save_freq ` The frequency of saving checkpoint. ## Options and environment variables Several command line options can be passed to `dp train`, which can be checked with + ```bash $ dp train --help ``` + An explanation will be provided + ``` positional arguments: INPUT the input json database @@ -146,16 +164,16 @@ To maximize the performance, one should follow [FAQ: How to control the parallel One can set other environmental variables: -| Environment variables | Allowed value | Default value | Usage | -| --------------------- | ---------------------- | ------------- | -------------------------- | -| DP_INTERFACE_PREC | `high`, `low` | `high` | Control high (double) or low (float) precision of training. | -| DP_AUTO_PARALLELIZATION | 0, 1 | 0 | Enable auto parallelization for CPU operators. | -| DP_JIT | 0, 1 | 0 | Enable JIT. Note that this option may either improve or decrease the performance. Requires TensorFlow supports JIT. | - +| Environment variables | Allowed value | Default value | Usage | +| ----------------------- | ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- | +| DP_INTERFACE_PREC | `high`, `low` | `high` | Control high (double) or low (float) precision of training. | +| DP_AUTO_PARALLELIZATION | 0, 1 | 0 | Enable auto parallelization for CPU operators. | +| DP_JIT | 0, 1 | 0 | Enable JIT. Note that this option may either improve or decrease the performance. Requires TensorFlow supports JIT. | ## Adjust `sel` of a frozen model One can use `--init-frz-model` features to adjust (increase or decrease) [`sel`](../model/sel.md) of a existing model. Firstly, one needs to adjust [`sel`](./train-input.rst) in `input.json`. For example, adjust from `[46, 92]` to `[23, 46]`. + ```json "model": { "descriptor": { @@ -163,7 +181,9 @@ One can use `--init-frz-model` features to adjust (increase or decrease) [`sel`] } } ``` + To obtain the new model at once, [`numb_steps`](./train-input.rst) should be set to zero: + ```json "training": { "numb_steps": 0 @@ -171,6 +191,7 @@ To obtain the new model at once, [`numb_steps`](./train-input.rst) should be set ``` Then, one can initialize the training from the frozen model and freeze the new model at once: + ```sh dp train input.json --init-frz-model frozen_model.pb dp freeze -o frozen_model_adjusted_sel.pb diff --git a/doc/train/training.md b/doc/train/training.md index c1e5b89a84..5b7bbd32a8 100644 --- a/doc/train/training.md +++ b/doc/train/training.md @@ -1,17 +1,21 @@ # Train a model Several examples of training can be found in the `examples` directory: + ```bash $ cd $deepmd_source_dir/examples/water/se_e2_a/ ``` After switching to that directory, the training can be invoked by + ```bash $ dp train input.json ``` + where `input.json` is the name of the input script. By default, the verbosity level of the DeePMD-kit is `INFO`, one may see a lot of important information on the code and environment showing on the screen. Among them two pieces of information regarding data systems are worth special notice. + ```bash DEEPMD INFO ---Summary of DataSystem: training ----------------------------------------------- DEEPMD INFO found 3 system(s): @@ -26,9 +30,11 @@ DEEPMD INFO system natoms bch_sz n_bc DEEPMD INFO ../data_water/data_3 192 1 80 1.000 T DEEPMD INFO -------------------------------------------------------------------------------------- ``` + The DeePMD-kit prints detailed information on the training and validation data sets. The data sets are defined by {ref}`training_data ` and {ref}`validation_data ` defined in the {ref}`training ` section of the input script. The training data set is composed of three data systems, while the validation data set is composed by one data system. The number of atoms, batch size, the number of batches in the system and the probability of using the system are all shown on the screen. The last column presents if the periodic boundary condition is assumed for the system. During the training, the error of the model is tested every {ref}`disp_freq ` training steps with the batch used to train the model and with {ref}`numb_btch ` batches from the validating data. The training error and validation error are printed correspondingly in the file {ref}`disp_file ` (default is `lcurve.out`). The batch size can be set in the input script by the key {ref}`batch_size ` in the corresponding sections for the training and validation data set. An example of the output + ```bash # step rmse_val rmse_trn rmse_e_val rmse_e_trn rmse_f_val rmse_f_trn lr 0 3.33e+01 3.41e+01 1.03e+01 1.03e+01 8.39e-01 8.72e-01 1.0e-03 @@ -38,6 +44,7 @@ During the training, the error of the model is tested every {ref}`disp_freq
Release notes

Sourced from pypa/cibuildwheel's releases.

v2.17.0

  • 🌟 Adds the ability to inherit configuration in TOML overrides. This makes certain configurations much simpler. If you're overriding an option like before-build or environment, and you just want to add an extra command or environment variable, you can just append (or prepend) to the previous config. See the docs for more information. (#1730)
  • 🌟 Adds official support for native arm64 macOS GitHub runners. To use them, just specify macos-14 as an os of your job in your workflow file. You can also keep macos-13 in your build matrix to build x86_64. Check out the new GitHub Actions example config.
  • ✨ You no longer need to specify --platform to run cibuildwheel locally! Instead it will detect your platform automatically. This was a safety feature, no longer necessary. (#1727)
  • 🛠 Removed setuptools and wheel pinned versions. This only affects old-style projects without a pyproject.toml, projects with pyproject.toml are already getting fresh versions of their build-system.requires installed into an isolated environment. (#1725)
  • 🛠 Improve how the GitHub Action passes arguments (#1757)
  • 🛠 Remove a system-wide install of pipx in the GitHub Action (#1745)
  • 🐛 No longer will cibuildwheel override the PIP_CONSTRAINT environment variable when using the build frontend. Instead it will be extended. (#1675)
  • 🐛 Fix a bug where building and testing both x86_86 and arm64 wheels on the same runner caused the wrong architectures in the test environment (#1750)
  • 🐛 Fix a bug that prevented testing a CPython 3.8 wheel targeting macOS 11+ on x86_64 (#1768)
  • 📚 Moved the docs onto the official PyPA domain - they're now available at https://cibuildwheel.pypa.io . (#1775)
  • 📚 Docs and examples improvements (#1762, #1734)

v2.16.5

  • 🐛 Fix an incompatibility with the GitHub Action and new GitHub Runner images for Windows that bundle Powershell 7.3+ (#1741)
  • 🛠 Preliminary support for new macos-14 arm64 runners (#1743)

v2.16.4

🛠 Update manylinux pins to upgrade from a problematic PyPy version. (#1737)

v2.16.3

  • 🐛 Fix a bug when building from sdist, where relative paths to files in the package didn't work because the working directory was wrong (#1687)
  • 🛠 Adds the ability to disable mounting the host filesystem in containers to /host, through the disable_host_mount suboption on CIBW_CONTAINER_ENGINE.
  • 📚 A lot of docs improvements! (#1708, #1705, #1686, #1679, #1667, #1665)

v2.16.2

  • 🛠 Updates CPython 3.12 version to 3.12.0, final release (#1635)
  • ✨ Adds a debug option CIBW_DEBUG_KEEP_CONTAINER to stop cibuildwheel deleting build containers after the build finishes. (#1620)
  • 📚 Adds support for [tool.cibuildwheel] checking by adding a schema compatible with the validate-pyproject tool (#1622, #1628, #1629)
  • 🐛 Fix parsing of CIBW_CONTAINER_ENGINE and CIBW_BUILD_FRONTEND options to not break arguments on : characters (#1621)
  • 🐛 Fix the evaluation order of CIBW_ENVIRONMENT and CIBW_ENVIRONMENT_PASS so that CIBW_ENVIRONMENT assignments can reference environment variables passed through from the host machine. (#1617)
  • 🛠 Supports manylinux images' deferred installation of interpreters through the manylinux-interpreters tool (#1630)

v2.16.1

  • 🛠 Updates the prerelease CPython 3.12 version to 3.12.0rc3 (#1625)
  • 🛠 Only calls linux32 in containers when necessary (#1599)

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 R z71dij-+VIk06M34;0^M{dDk1APikEa;`&(Bx9s~OT-Aa{Y!v-n^5H|xvl*aIUNTSZ zxwhoT-QaD^FVY9cJlTABL;RrYkqzcNPPuMZezTjn)asf$WqrdaJ`d&r@|{EXWlQfg zVx9?J<_vX(4*PYxpyy-$!+HJ=`FI=YDr3H~ai8^U%%QV?)Pa~UsCVuP@g-kg={{cP zC*Z`0<9e6-QU6jteJ^+r&nd~0pZIf#mkyu6;0@i6qVw4$p1%lPJ#z&4@A=*@eSYh@ z3VMbS^5VnPV>^DgxKFN=Jz$P@zIdp*L&y3W)^&*ba92zWKENqV}JsUdg^Kvu{)Mo7d{#EaMFG!SKEL-Y@mF zId0ge=Q#21Bf8~&@YllokAm+Oj>;G3Rd2Pw{FBhLDGp8%H$2bxG5PXy^E=F&%9p2@ zPx2gW^Z?YAH>ukV@Y#4>$*<$3`v>r~%oh)(<8nXVA>+gNs(z`Vp49s&ym7K}Ka$_W zEMM*CDjxK)_ao{M(xdqPyeIE`4sZ1VUMgRlH%?MKI30Zt;3dvq-+XA~)6#ux5mD_?uz_bct(TgAgZc;YVn?c;)egL*^PJU8$3!=if2 z7WSx6DD&nQ_WuBX*Poi_^IDUt7*Mb*)22a@M9bV$=sd%3i@AJh&^JuR3i_d)m zkMl8fuF5x$lSfa7zE`|QzWmF)jrE`U`yVuaQQ4nfeK`x@;r$Ws=l@^t_ecIuzWoy3 z*jHDd$_nS~LHPFQKJA$2wvH%xIO(4A=~C=FXZ@Y<$1B$z!Y5jFhjYx;dqdti32&b- z&MSXx^Et1?{Sgk_k6ylu@8Je@qGitEEBuP`okR5tdCuG|^wq`hSLid~McKzY=qaDa z{Gx1eUO2B;**{9Ug;o65W{dPUZQ}ni^5pmNZ=KJ4`(=F;@z8Z+=lMl*JG;jB@bRtS zJLE&1&(B=(u0y?RI{JmgyU>>p@+qQ0GcaYLW4OMdvq{ym-xP-)#gc={Rlnr2K0L3!Hshowar_o_^OwbXZ0Zeuzraxq{15Zx%X)9EFFY5% z8`_5-s8hOc`w(-p=BQ^LFZ#KBbqDFAcHj@^iRYu>_3zPXcwSo}o{Yf1wdmu>_kIa4 z7$1)Exeo|+@LO`9Vk`7h;1Bq!eDAV&ji?_N&zDq z71sjyIv$3fo_QVjeSxz=FXj80E&q}}+5E*b;(5#QF7Ui{==KS4e9`^OcfU6O_U8(h z`^oR3vzaU8SM+z(>h!VkUe6TW%kApt+xg%BeZDwve{8QqIQWp)7di&(&UJ2&P)|8s zX?^+DrTwbasRM;h179rs%-xqW8S?{OPm+EyAMUj;5B&}J0zN11jl+l9SAU%M`eN{; ziYtHQ-OCd_+;2|bKe%GQ^@i{RZTyJm0ym1^ZdB5#=ZhQCISl8*`23mgbzG4T>?HBz z82QN$(T|>Ir}RmbFLtfZb9^V?pXT2^`k@x=dt-eRxIQ1=Q2g$Io5tWp-*dmtGhgYG z`vxlI&+p&~=IKMv*YEI4Ui2FGYXp7X0`b0&|J5dVWHkI!mEYvOU-nT}zV*QQGkpL% z#J{G`CvjyHzAfK5RGhG1$`jXt3VBQDd-ZPLW?fCzbE2qjo)7ohpG|n;uJd8PPnG8i zTK}s!v%-Gfg-6YIzmzYU-xzUS-0xQrA0Pdd)N7y9mvfEv=i?8R|97Z6yr!Vd{#DmD8Hy)C7$yG`SB$4z1~&wD{A8d^E7xx^8osqZ@-iu z+Bf8T@c9dH`)%}xp%;Y@z5$+p&iv_o@3P)w>y}S|GscPQSMk~C@$XaMjdSq5hv^H* zw=U_b?Gt@HbVBe?%u5-7D~?iMeh@k%#g}|_2jdO-hdc-OU5b4XKR6&id+75?ztIeR z0a-Yp;`T<{?8RFwl z@I3kW4dZcmY;f!g;`|so`X2SWjnHe`$6a|#J{_#y<*s$u_M3r_LA0L(f$e-#Cr`$UWZae0in%Q)+ed)b6vi&Q< zv+AR)>*C$@d(1f;!RNLEFSX`=0M_@k=pl$oPjJuZuX#p3)-;<4ez~rvey3I9_X+r$yZ8e1&@;TFesqs_-~;zDU!Ef4;eNc!%7dGst5koA z=cgRC?*aMe5!RhA&Wm5_(*N0v_%5HKE#Bom{KgbKqWx9p;konem*y`I^X18np9O9W z`jl75M+U@;^YLye|9Vo1N65z?>T|jODOonZ$n-UK0JW`e7<*C{mZpF zx@7w-nXksD*uIbZ(Me5MhaUc;+2Xw8hU2&MV(Xags|sEWyoE0lb%%U$!+t5^VV=RC z9p*ev#@u$Dzr%s!{co)=A8#W)_Kxu^eA8I?q)O*ds|y!ySjR@c_C}H3G+(|fUCn@Z zcAEEi-gWU}|54S+F7b2ch#ULSx#i<~rElm}davFG&d{9s?P8oI+;|G!;}BmD6rJOI z_sjZt@o8tmmrZeEvT}YUp1u)zrQ?hADf!+nfx8tYRah=cjkoAwi_vYg_q|X?@Ydj7;8+FMdfA&uB+vG3t4oD}F zZ@*LrYLoB3W6a`?e8v3E{$gxYxc1^rsZvpL5`)yZFMi;W4%ySIEQO z6!9bZ;)eZT?boY1!WQ|-%OZVLA6#`Ced3yVDSROF?U(eT?oYl&{bw%tZ25p$C+j|7 z^VzOz=G!mzZ}s2-$H4Q)=`*}l>~E-g>(NT*^;o>muKQ)nzZ4HT!F&3BvOZM%<+!qh z9_JMMIuY}xvaL(^t7km_XZ~wipLMcN&}TQpH|ux1#9}_)MtI$N>r=&jl)JVX|KJ{R z&^+5C^Y+F0v*)m8+b{d2xG$S{Z{L^jJ@6cQ-!rXylm5PZaYOlHtxnwmpE>(1ov*YX z1NZS!pIikWE*8%z$+lnim)Cx@z!Br{P%ZNOcIYS-|8D-Oo|G?N*5~YcJ|*#fnz*pa ze(eOG2)|R2f8oQFZ(ZWoI`FpcTRw(fayEQm&;t-JPMRl$|Hy|ogy(B@@uMw#DQ3;r z7wZM^A4TW(jD6fL=2f%ZFYz-2;=q;A{aerWA9sm(^8gExUs~5Jf6{!sjq8=t1#CON zWSv`afABAZSKFk2ev>|leD}+K=Hk(o3iIfS_UT#loBO}p&s^)APCHLZJZ?YydZkC) zwl56lvKx7_^Z~Dl^N-+n^6i)ESk}{wL?4RyfLr8U&c9mWAE$$-C#^BRp9wI+C;=V=dWcluw^@G&;sYgC{ zT?K!eT3vDTv30VplTjbb7Z3gZ5a-}Uo9IGsFn6<0-Q=z7EbLpONT-)iAEkPTd6}Ek z74Gr5NAZ*Dqe~fQA4l-7m?-wa&33<}tKJFz%=xh5_z83;{qVO@oIe@yLq4AQ?w4?s z`z!5#eG-290rC7(_#F7W(nnBv?nSo!vVWB4y(sR_hkuQEQSpc$;v5R!zeR785BIu$ zY+a?|$f9{t>dbB8`5E(v)`h|2=c|8-f9Z$c2K&Xkb)EWu*ZHL5yx(c&)#tlk%Hw-c zPcS~TK3Tl!pz>T|y>k=b;a1wbQtOC!mwao6KD;ULV%K$9{(GY+Zob6lHs8DK{X$^1eu3!#&;-?IB4#9KXZzY2bB`Q9(xmx1E~ z_{i~Mi$2&kK0aIQ)3o&g#L0a7CB1yl_2hV$j05eL0$;vvt{r4Y;i~06T@2%&kPNB0dfun@4T&I0a9<$)O1oOZ0?U(LX ztuCI%dGY~tZ0j)}1^y|{pW@9H^AYpC%X+82tCMGp;J-;+oS=?w|EQDbZ|3Qry3D(f zFCJPar2BLQere0Tjr0-4{33J?A1cQc&xgpCe-#OHLyndy8WX*Us@SyggYn(;=KaBpo8TT*SeyNYc^BZnQUg>(JeN*Kl@QVFf z4c}PZzkK&g^K3ix7jDu=*+943sx;SjUtRI?5byBSt z?&0s)3O$79#k#*aTinp!zYiW<2d^K&U!#TYt4-cA27a8R-{WEV%`QQZZ@+%27ySu5 zJBdF2Rb^i~#h1;{=Se^7@8?^W^j$mVmxvc9jBD{<+d;4WmUs9%IQS>(wL?6T8^4egKn z@iyYUHUDnCo8zR`KjJu#UN0Zs5MCJS*{s*Ij@*7kiksKrb#8(Cr>Lvs)1@eH>{!nm z{n_Zw=?m?JABS-MB>Q?6{a!x3sC-g9M|BF{vPJ6U_Sta#`8xRU9(>J3@|b+*Q2B1V zQUgsZ>R$F!68@{z{Ia@?_&*&q$iLoFf64cLiFc~i#iwnAZ;$e> z*WmVAoqkdGp+}q-UdfkNiYM$_-+-R02`{r=%x@?jHiGvxpQ!kfZ@)C(a9BTf9|ODq ze3Si%mMg7q6n)W3<+??d`0mePpFh{}{NHttCh*@FP+xf-by~%b7CMN0@3Qp(*1>u% ziggU)f4i*fK0I#&UUZ5)E#JDN1L}ew&Jnj;@J?5GkIhG&Fl*-YEBLD*Csx)|bU!*XoKB zhl%fZ;4SWg@4NVqZsCK_D7@R`q50k~`xm<3-+C_ND&a!waa!i*&G*93=fl0KC+w^1 z9a`laKC}J_om89uev$WUKl?c!{pfxlvV65~fpF|y?#(IYDo;~?==YmU zp1$4<*VQZQ>$BV|e-869)Vs)sXYI!k{axzcUI$-Y3Ef^+zE{3jeenA!?+f_oOr<`J zcKE|;{Y~QFa?#%>ACA&K3_RBi{nK_)9b%!VPhcnN4en#cf0VjXzIC~tue{@X>l?sF z_kst|T&8#K`{LfcLcf#j+^4Y+@;;r)Sm-X|~SKgui zIANUlt9ghw;1fqVuh)3b^Q}v`uOIz|dY7NFAI;Dos-84wJQDas`w6Gow@2Rum9|Ri6_XHe<^MZ z`$EC-ULSqFefF)1u4A13j z`wzJ;UeNavKYHkg>?gX2{?&8b^Sxi%2kXpNi+za1$8Un??R#)F;-vXu_mBC$X8Ef6 ztLG4}5bstaZ?XQ@eF6A_1y3&@_k3}~^RQhn@mvStsblaM-3q;Byw~t;k#}aE%)4Fkex~K4d_U+*fe&0G&l*Q(?m6MsQO@B*^nrId zpDso7+B)!SKcKU@Udg{=p8T>;JUkxrVjTBX7t5EY{C4*ub**y<{L9=q@Kc98dK|s> zW$V#-zw_Zk<%@04$)Z1F#(mjEy2@V6L6EL!Ir`4!gOD#zao#B0w;FlAbwuhzY@7dZ roe2HV4S1Y<@3MJK)lDXdZ_kMf)6^OIF~>~%^Q0)hS%TlrcfbA~p$nL> literal 0 HcmV?d00001 diff --git a/examples/water_tensor/polar/validation_data_reformat/atomic_system/set.000/box.npy b/examples/water_tensor/polar/validation_data_reformat/atomic_system/set.000/box.npy new file mode 100644 index 0000000000000000000000000000000000000000..a3809a0db562e2ebdf1b8a1a095f672b6a369924 GIT binary patch literal 2288 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 Iy1*{HK`#iq<-s8RPvCF&mx9{nk6B&-2 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 mqr`K}=gj6oBC|gTiPCY7<0Ug1QkwGRc zDuQYg1^p}<+P0C3AP{WPqD@ed2vaClh(rYG&UbiM-Q9ENaPB$ZcMfx-?ePAC9W}B3 z*g%+ziYE%;wjkV*ZweDZnC~r|D`d}RdJDN|{(U-orWnn=FZN{nqB+-Z*_;TrHY9>l z@V`%8>fWwoY{6rW-`b5B^WtJFV$7>LQ;0FweA|l{^U#4bV$2Pr?8{uNiZMrhtjBzN z6MHwW-@XSmo4Y#LmwDq8#+WZOvL5qv$U8O9R@l3FbRX+654~nR=1Wt&FZ1#e^O$Wm z7mRmo{`Zk~<}n|6!&x;S9cInuXYZNEJl@7LG}o4x$9!lVW6aH6{FeDtKkwZ<@|nGx zpMPOJ=E)i6F+Zx>Q*-A;68mL-ca!y)@BiYSnk#YcjyX5Oddy4nJZJNrS)PwMvxI$_ z2Wxrn<`rf3Zoc@JyJJ57o9APWA7jntHDio1|M|ce^UHeHY#y6t9&`H(-l=)nSLQLN zx_KAosq@TZo~YUjb4wlbm``5ijG0^R@(j)8PyCkoM-OM-+*slKm>;KkhUS$&*_XNW zku{sYx3YKhhH=h@dANx+o8Mkz@8(6#%wyg;z+E<%SF?BXgF$}Fd^yA3&H2;JV@_86 Zzne2x*q8a?E8ds+`cvjHubpIH=6_axjGX`g literal 0 HcmV?d00001 diff --git a/examples/water_tensor/dipole/training_data_reformat/global_system/set.000/coord.npy b/examples/water_tensor/dipole/training_data_reformat/global_system/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..4f6c37e77a3ecd7729cce35d5982e87f7777cf45 GIT binary patch literal 184448 zcmbSS_ghZ?|8J+g)82az?Ydu&XH&}FgpA01TcII)6cSRB5fzb;jP{VMN)kzmhLvQr zwD3Kjf8pCt-S>5MU8mPM=lOih6O(36oHCc6Zwp_f@v@LjOE((3>KMB(A7^Z#W4!#o zjhiB`mULV4JHgUGG&~b2bG_}y#uJix<5nr;c0q?$a z;^Z1d+NPR^>h6A6NotaHO&b>22~prGN%FoJh-1d?WMU?Qr&B#4z0HlLN%){Yzye2) zj3>R}_v|Q?Y5ng?1XhG#%ppZu8<&HK5l1xBSbBTE4eHBhA^FvRh%#$}-M{DT%A1wI z^FdTtePVv|Jg_mX3Sn78EKOA-}j9o z$L20>HMO8)K#WRvAI9%61KP;O(aiGa5QsCTD+`UtJhvGUwMtZJVNdMgBdnI30;dg@ zw7yE5$~=OgTIo*P=gCsgr)aEEGNk`91gPp<81_HT$H%Y2bf9HE&c8{4h4(1NS$o1d z{3-NBMQC04Vf=7E!GtR`sYoXS|K)YE2kB}w>+~_q>)*$$lPu_3uL;G7#j_&{5+vrQ zNmswU-*R6){s3WCq>PFxD*NDh4BmVN=cv6y$)#lFh z_3u+&lT0$ibfqZZ%D0knHnCV&CQakN4DlW+XCQ3jUpRCaky^3>#R;m@l1FOfqNGY^ zF6xs;jvak?u1JqU^+`3`n4(o95w(AehfKRE74JHWfmiE`K1bS;anWXshIz6pUrG@Z z6N6p9bJ5sw8U3Nb7%D2pM$sIkzDUNw{?q9ECqwRoSFp}TfO1R(>GHNLnC}!NiE%14 zr|m3oL7Z9=|DYge6;EFw8zFa_QSd&9C&ZtL9v2Z(xwW6oSbq(I39f8%_&b(rbQrS@ zuCe9Cs#vh-AWn!|vj?3oSYSscmL{8F+L>**BFdxx{Ffo_(;67P_alER16)W;#{2C= zzqhE+A$xBYs3MK%EBX{=k<2Twv4KXmD*bsX%G}Gcm}AosyvxpH>(}CL zKOeRSoW$@FORQORjR^+Dvj{^w3OD$GbunY$+hs+=rah1n6lJSt7*hVUPtfp@#&X>< zc<)cZ33Ut1X{g8VJ(uxHxR2GB)ZvNXPDHl6#!6>RXuljw$!}glsA?WWC8VgP^b@Yj zxZsnrAvw#hMQ*)3sUNo@*YhXgW~D%efev*3=32~c*Q5uN^k`H6Z=8S3-6vN|suFHP z@_KtZqijS6%DZqQPn+%^ccd-{Ta2H+04m!;u%yfew_YEBSMh$hFAs+9)}`30z;N~G zJ1nyX9lfdmE%g!PnvN&)N!s|b;u&g=`%=9nkS=}@9)G^z_rPN~-A;$h^iG(U8 z8EDw(ioI@@v~lPn-f54cpqU;tb7eSkj4kMeq%(EYCSy3(m0B+ZP+QkQEZp4yqcTY< zejm^KH}(^D`u;;ovNmtpy3d#rsz~+?p=?)I0u$j06S$mr#}np%M%elQRGMc}NLXps=1kwANHZca|EJ zpY6xl!W{G#DA8o$H|X2nfEqVL{D@E>6N?M1Hjsyjp~}Sj?amwgPZv)F^eFvEB^!~H z@%Zy2(EX3LIW0|`!S)$Sv(m@5dv@8TgHviRb}CgMJn57e_hYaPrDNYw#eqpwQ z3aOe7;QmQ|nrJCa!|iR@xAiaL+;nMGq%et2_ar60O0@lWih!&s^x|{@ChF#6ZM-vy zZ@rGI?IoD{br-hX(SywC5)8c!g~38&+{|x6_|_08R(U~2<|?`$xl@JJe<+Mr!}+=1 z)U+!RN&iV<>O4=9F7$?1#{`6X>|~)~;?zESDmr)EVN3d@snJ9Y@#&Y?*!yy{{#i6m z_?l3WloWY)?&kKEKIM(7)9aRSOv-npo-jF5$le8yr$Q{dshBBF{fHu73d`3}gkniM z4rzt3zW2IpsJR2Tb~Q7lX^m`Qv^&ppegiX|Ylad3E{`0$IJVTgnQib`!TvjQ38N0` z9!{zms7uen;fdmq~>u(1nSMh@ea*=&^lnLr^&HPLB03t0*R~Z?I`kCiu>hDk(&7&3F-Er*XmeNSoTcyMY90?_mYpYY`^j*@0O;gX#?=wmk5s(cRKu4o((#0 z<*vh%3R2dy_rsS^v(kfRJh{dSl)f;9U+)pFu13K%vKUkK4~>pG#P?qcTRH6~Vzgu^ zCC`y63bv!WOpX$2EXiLX8ZuL~=~k#6F-IT3Uy39&MJRmpbe_%CaddarFb3SAaxY&kN=G)S#(I%{zmV~9k zGGrNAkB|p<5t5`p{{r75+BXrO&i}=o*k%;!k0GC9nHX3%idtbYIuM?XYZlLNphSw+ z*~cSs(1xVMyl83b1xVUB(&Pm^YHcjVZdqgMJ2{CKl&9fg-3i2=aG=+7#At2dO{j?3 z)2$4C64-nUj&Ho_b+0tH8`j{${2atJjwSyAD+~#i;f}U4@f}%((1a7{t5TuwYvX`{ zo9wc%B_$4C#jT~E*|VQU)P@sKx~Ic7UUnq47BhNfn8SAL(fx|OJ=&C(8~29v&SnD7qk=sHB+fXVJZF=m?KQugZ#r}$^P9GX7ekTH5sc@ z{$kCNIFA-KHd&I~)x_EK9uv0Wzf6cG+QEH-7;OKP;K$sF$U3PH;gdHZtJlx?FA2dV z$`kdIOK(u+|<7?Rk!o3U!`vr6z>Ef53g~L0%b2 zVMpp?X!;lNEN@Q4kw1qacH|SD^nAopzfA6&dm(cEEq<9Fgz>&sI6L!`=KZmBaDqIE zF6?A^n|z$(H;GqpW&dStvNE8U;2fNCID&p*B`*Du|ANR`3l0#XYovZ240xnLuXG3j`E$rmIcX( zihKw=t=-V5ser)rCamYhqjz&KIu_ZGmVF8;ANph6FpsW`y@+uo)9~_-DLv`qFqoY+ zweGd0;>SjiwzHs@!H%SQeJPgRaHT&doT+J(IA&fi-{hq4nVv?yaloT;!{W`0VYis}v*r#y{ideUZ8Q+0qH7BZ(kF+Wz(XhN6%j-yV$ zJ8U4+p3WB7(2FZ6Z0!;?nmQWE#*dajV#z3bU7pN>HQm2HM zmXD?5U-XHeenNi#T$XnB5Ecex;e}HU8`_qEqUwu?+G$a|OfLyJ`?J75KL&zbVrWRN z!t$cExId~71G9UuPTPd&nGe_*hqq{IR^#Q~3c|n7icsS5v7v$(+)YkU%YP1(% z$qHW_v?*g#M3l+UV*xIt3gFTd9a=DbGkSt@*{de*Of?MgwAGa;OpwH@Ijn2%L#y2X z;(~lG6c&4vj-EKZ5sroTD|^=X!JR@3-yvz^ZFZUY(q@wZs9iq6UhK4{y{`2z`}3IB zy-W~4?Y$^0ez72oj#wcL$B5zW}|ZfrUz~z&ZCa4;$CQt5_G>*nB#!2aWBc17PY>@ zj{UD8FkY8V-mApAz#fbg*wgaT&-keR3%}>@8$A=EuY6 zd47P|b*e(>NfKihM5r-E9InsAu|h_OraW{;n0qYyIZvJv9*EQ8^&cVI;znm)$&k{y zQ5?>0 zp+-r~6>c9s7ADo(pLo@Ox=^*R8=ZI0f*0ydhYvsCIj^{c2N?l$y)lKkbsWK}bSLs~ z1n*hfAk*B`kI7m_6m>3xy)^j)t#9gdr)4Z+RfXwb$NzS^8-1=%#FQ%<)L7_3K`&3? zsgV`=j&Y*gq>VT@s!D5~%FsNEe4g26TMD?PO;Q&Nc%I)l3}P-p#+Kvk{js978;-Q}?+r{; zGN5PciS9XPAxy!M3UWN?gm)#{k0xQT-iGw~6luLu8sZ8q$ltOIB)9vC2s((sfop(SYX6NkRRmWo$M# zXM;konDo)FFJV-3&C%eyt=;GoB&@X+0 zh^TR-Q=83U$O^1jY(%c{HQ4eZ8;ew2Nh0q(o8g)OVMigl^?3_B=6w$C#S-*k=rmJ9 z3{E`w1t&2MOGhZt+;|1ro$WyFGsaRvq#hl6Wk7i!bjjwAEVZuCrwsv_D4hP>BVd&& z6>aA5jr%-SA*M+a))zrCL9uk#o2R&39Swoy4^Z^_7SjJrg|lrn!Vf%v#)9Lh_?d-> znaXt6A{#$dhM~Prgm&z?0{Z|#n*T$KJXs+;szqtBvMfEgGoFq9mw`q*Sy~x)ljYw% z44aJt^wcPnX|?BIgKi4Due*@dEZdFBurfBfr=VEr(?Q(7G|c*&3Yhuxop_k;i)a-B z+w4tlS-a50=a02Mfs~Q90f+aSg72X-mG2dy+a%2V-;}ZRVKLg9HkL_Um0-ReGIZ8W zkhfum7-V*(qU_{!I34a`i*t){=+Gui@ch7%deRa7a4goX6hMTtE*?(Nr6(Z+IC;ky zhhyY9&9D!aw}Ff>cL^bX2^#Zz<)vm zzA1ZR|0_LO@$dt>9Q|=|svWgoYry=P{}AwZEUC4HBhF5pRK#4UEhYgi2?{hTj!1Rs zUX0tONd`&g^tI+Eg3Gk%v8Vz0X}m@r_r7*Xm{H5nAMCLs}%{CxNhHuUJwh+Hz%);vcWr-3x6E=AIZTNwHB360B+ zngj#^>()mubmr9)TW6WdP~LrsO#E`;~Na{m)JTXZ0G+#xJT%S6Bb zBgj=OLCP@?{F`7&@lP(o&N%{x3v6ju)d^^R@x}73=G42S46O}DBp72u^h*QYV~uIj z5>87qp8@$jcGR-aj&^_k%XDvKBSuJw3{)quZ3`>#Pe+`-t*&6bx+VDURX;{zT|K-N zvzf#84EQyH-fR&^rNALvwkPUnsbbNiWe^*3rgd7%bo7c9_Eh+x_N_9t%ojtRrV>`G z>C^0!f%vb%4`vd|w13$Meq=pC?{hgS+4~lP91m}*(k6vJqp-2K3I7Y#?6->+xi$MS z2Qf_~DQVK$*7LkMpI>r=JCmTY8k=&&h9)0(rm*9endD6anpp!5pB!iY zn_cMbn5k^*iDHZq5W&j4D0Z zBM#uh$pVZ7&STM)nYg?=4{up{Nl9P?`cCFSX=XG+J>{Y6S_Zz##ps*n1}Eq1Fdv+Z z;NzlLzO5e0TNo*9Uj*?&YfhW@qxIsGA=D9uqGm4=UJ!%L1{N5OzR#3iN>Xj_bZ(jb zWUIYZDJ*Okx~A2#fEERcA!P)=aiNxJJuneChBHBar1kJ0UKzwde4ht3o_!6gS4ZLM z{U7hPxev`hHHvDh1H1=|9O>5-5jwg)jv03Plf1eBh3pY!FI2{$;N*MWZQE9lrVU0| zTX~$RR^DW7-V$(|^^MoEI3F_~j$tnzUB-`Pr}1#|KGrUB6L)@ALiLJuNypTDZ1sPQ z?Ho?~v{fJK0zJ?@uS4%!e=+~9PZ7Jtn!9FWlrO2mcTaPArSlSFK7Pi*P&3ZAXvVMW zPY}xKy}_0h7(OpR3l1#94jWIpeQgLrPLWV@ol1eWDg8_u&85z4gOYAhZkX+U#`(=?Lh$Zb~#x_Zq>?uQz=+VGLAwE?wH6QUi%oqz#1 zbAEk}L%&ybVOxG6UMZPS+5kVbd^HB&W;d$#9YW@`6-s4`&;f^0K{!D1h^WBxRJ@nl2d%Egr#1QY`&! zCP7>EBx#%J2@H=>){mIlN=vGfv!K04jOg3b7~an*ZghZq z21W!kip2h!Q#$80G>7jkq1W%>w(%N_u8C30;1k5lB_Q;IAk8xW0j7BcPOi%2=Jys# z_l2o|oxs$+e;}YThBj!PhVSiqTo#fg<&|3ydeMOMjht!1lmc|8t5Y6)Y1W(qq@>!@ zrekjOL9PVqpORpG$b#N(RG{ns9fOv#2f030qA6FeB3s{tR>+Hzy~=X<_2$A|L7vLG zUC}Rl3hNfjQcRaC_#GZYaAYjGmhD5k%Ub5M%ZTJmkHEb6H&24o-*Y(r+Pt@v$!Hi- z+I?-}eUZhyBYdSBcM4J5*B5M&)GoAr|I9s0I`maX zfgaea5}%v_&EBfX;Z1!iZC9k1l4EJ&dwCLRG^J&m(z*ZHk?s$>kX_+bG)Q<*v4ak^ zR2E_2A9oE)uVI^C44&S9iRVkQajy0hmR!rjdCt4K+cXcCd3A6!R;KNW6*%zX3(Sgz z=|g-TR2;rxWS<@tmEVDt)F2+FsgnQePi*|)MVOHUv42BMVewT|MX6KDhq2ga8wYKF z0W@#h#S={52eV3bxZF9(8UzmFV(?cs>Q(D@Iye>0mY$fuEfmqG9BIm*b!eLIiIRnu z)V6Q|7XDg{1;#!c#*il-JIrif)UfS|x^(28CNsIy#e(={DS2Wz^O>p4d<|nEF6)a$ zCg0dM`#9tr6u@B1OU8dA6YAq+lhox5Y@d7|CU@#!t-2P$^W8A>>ocsJaS}V1 zPlNZipYS*pjJAbt7%9Am`DJCew{{GdX8hvr!bW5(IQ^PO?+swq=~&Q6lv*#>*qM^(CbV$g0?{{+>H{& zIB!hQ230o4u#}0A*~v&2`XwEed9q}dRn9!5(ou5#FNTZScy8I6L_g;%lZ^BnsI}^2af}W*E!+d|av4ol2yi%#Hk;cQBaS~BYN~DMTA63t|LkNHtQWwp|ghyc%L0bpdYA6B`!%N z=V$(8sV5C-+;nR;Qff(&TR-uh-qxno+qs$Ru#uUcl%qQ*ohf$lHkRvPL=P$znD4r5 zTu?ATcA7GCQptq&o5w6_)s*6Nx(CAJNGBzAXg{*4~1t{9%MlNn>?$D`6+Y`A-re7|6JVoIjz+by~&*?$sgc zm=!b0vqGtbI>HXfvoW2UP@-spcSF{^_8XE|_Z~G1bO{xf%+I4` z*IvTh<{(p1;zhqr>K35Fk{-`}*xAv)RRIv(=8Q?Un@AEto{tQx3AN z4NuvUUPlsb=ls}ru55nNRUG?y2Zh10ETA_VQ|EHJtE*DU!-yRCCcj4ebQLO@BmiaZ z%r)oBQO3Yy_GU^e28NC4La_|a*_EI?-F-XnbEC?mF9L~E+3IrH|8C;rufgYRI#y_%aZ6)Ip;^U%Gaab zPKkspFXLE2BmU!4punh4n789ALgS>!Ch!bvp1zY;P4;kTkz;D+Ynj@`iAZnbgIdE> z7CB^u{0)cL2v^k!b^?vZg}xFUgpul?R@n6 zV-|)wy4@GAJHo=}-@yE1`6b(Z7x1M1+(yDeiPB8|HrCznft_#XqrN^)^EDQQ_B2_l zTP4KhU|zG@vH=9id`JFOJ9?HEi||)U^j?YEJ!5Bbet|5FSlLpU*eUoQGoYdsYUF#@ zg%0akvflFsB&g)W`On$B?%T4osn3au^ez-{I_5?B8Jgs`(UFX8vepRunsw^ej!r(Al#gr zQJW=5Ki|znY`_;34D{mEa4HtPS0{6SZTietj<#TV+N5bou@g(-B4J8C{d)BBeKkb# zlaRQ^j=nl;(rv{YT)%5f{z)n%yX7qG#Xae{w=P}g{GWo>fR?doLnbV}Gh#HErVa%!(;!zZ8!|A_ zp^l&C^if=gT6tC^I$52z&N8DfwrQBH>q(ctI8a4cDgtLrrqmgFG~b{EOWrc#>Al38 z`7;pl@ETql^};TjyEQkmP^MP`A4^j%)02Y-m1^X*Cl|lJe8b##QXC)Uw7c#>D75KQ z_spwU%<-nccz%kCnCkIzRx{pT6{6ecEm&OP17z5KMC2Zu;wr^PgkSFB<^P=p^^#SH z+NWJ?vSSr?)f`7r=T&B0wjOh|R%7S670BqHjMvA_Y1_iXD6F@Dk*Y13`2Gil9?s{# zV^8zuC{V4UAJrC4!@USSIuqnY#c^wJpA z%tkta>9s~dF(;lm+^%95lK}Nc#uTN%M+r6@F9@=ubCm*QdOHOA@#f^OBuSBbH6Rh& zg^RQgmtJ_|4)r5iAO)qS0+=buPw|bbaQI_4ywlWCBcVdQnxAp|m>1%|h>`ldKFqS@ zG7R~86kj|Ct42)8Wt}(aAKMMl5>2`l;6)2cz-$ zt`S9e+HkYJ6?c?OX}rBF8BOK*@s(vb6|)A``$J&5#Sv@O8E(&9i+qQpa9*(lo+?ey zs2@kS`IcbOr5CVIbRdWNIGmdF8&TfQPBp(pOK_GK=VlzkQQ>F?M%Q7_We zOoH7<0rVExa~^OJF6?!oQi1VQzB>s=Klo6&38U%ONmw<&dEj-^NU$ao!P=$hiIpWI zEh5k020T=kru?gcw)`pZHnj~<;{Ei z8lJlLl)+P^e;8sz1Lx5*SBbWF7PH`Ud$7zyo$$^9SB6qKKS7kzIUiN=;vUU@&ryD-az z^0$UDS9u<}HhNQz&0{v)<4voB-D%39U99YzHN|EPvhKwV7<_k@`SI%5`SCp{JhqA5 zd?bxI>Ma=AB8tgMDl~b&FZCzNlfzL7%3C>+JayGMj3!4i-~H+PA3?I-CQ6<+gy@g2 zG$~d}(}fGY2)HXiH`gdoypkM6)k<^O2_7BFR3(WW!>DVvp^u+cX-4E2dN{?8ayXuT z{Hg#+R;_}7c?-M!r4ZrQgJHZz6(9E2A=Gdgq#nw_Ks*f#+xGBo)Foh#!*RAlNQmc_ zcL<+dgoOT zeNQEux}t!6=&#}OqD9PgvpR$}`O@5pN0|MpsqlR^g(O}KGV#6kXmN0%as7AjNzQ}* zo_Pcjq0^|8ain~e2Fyyn!oAA@^fjpz#g9H=<11;(*gwF!^95;fx*Vn3e`D%09f%Sx2|=m!c4Kce%Z9JSpLr5kwyv@Ui%^3sSxbw?2p zwgOX^Poe#nWy$XG9?(ZG3ZAiB`O>5v;z;H?e87SB z-pgQrbjHvF!(Rvwc@0&Lml{ub2UELN*zGZ=G}})|f0U0`cJ3q@uF2jxA4ia(1PyCX zDET^^2&ESa)Kwe6?s{Y*NI;NM@0rsNer5WgqequZooKayKJ_;klJ;gTYW%K2ueRya z#~w{;Xg!Z8iCmAV9>x^B1van{_do$1Ep*5+aFcs^>DYGq%FINrx`@WIPA5L-6`Y4ZNmJ;4+=`PQL$#EFzP zOo2hvR9~j)WPN0s#JP-p}FpGnW0TX zCHd}QIsHsLM`O}b3xv;;L5}hkc*uTax}rmD2ggmD`!%8RTmjW2 zjs1I^X<+&guA~#Z&e+lYw`1tY-FB9-+MJ@@1?cM1378x40*U&E@Z8e|*QLAha>rUs zd$9^*E8k&G{W(1TT8l_|eVo3pOFPxyIUgByi(WCP`gzu}wwRUe> ze04M827jR=*@tGT_(EpfQEZgtBO{q=4yzr2q3l=Kc7K8lhdrME?dS5*96lb@rQaO? z`+JTbqGL>H;%7bT6dz`>94?FIiPL}5NoayV7cK=9Dyz95nJ6!3#JI%TdVMT@1Ax zXHVcKcaLku)sd=*Ey_XKIZHBF9$>YTe9*Z>i2`b7K`VA5gmUD`#QPUyQyZWlqelrDgAgdJ!LY6@r99$vpmp^) zS@?uWELn!~(sZ8Ie3tJC&u8IN$+*R0gZn7pNDvYwlcbi2%xwR2pus==N%OXaYE zL&oG+Oq5u4oITy{!s%J_86B%ZWwkIfUvroFEG-ea356xYLy#i|IkiqhsvsE@9N}ZNZ^OflR?y z0B>$D;YHqB!d9x&0)N|AIH&x4VfM8*d=H%$X9)tFgc{ z5&i*zbp2@o=2`qil7=GrOPk{K!Y?>;*^mwvnjuL~lzJRxX^M&|X0GJ=K2Hs3fb$>E zaaj2pH_uOBe2bJ<&#-8rJ{jHphE1Gjb;oW8+Dtr&XVZwt7Ag8Ye>Fb0+vBF7JYDP3q(;|a^gbqXT4O}}v-oL3 zq#dQbmZrdte;9V=k;dpqIRDvrHS$8)Y1b}1zo)q1KV*Zv3x${jG-P=oebedhR%Ee6FjXiIqk zI${Ly!EPzif?Ba5{vA7&;fc9B`N-u(2b*%m2f_=haCmtZJAaFZni_* z7Cs6Wks|?48&fbE{QT<#? z+Wl|~EX)l#-9VoX^Bicj^R@ePdmHlCb0@8K!P50XTxTrEk*>Kql*SYd@+JnTkb9E_ zZT665Ygcf6g)`<hA6p3v&KK)@ z(1$_KdK~rrh5o_>7^&6a!a`wMxNAGt|LDZ}MFTh!egz&XT6BJk4eb%lN9b2+T4Ur& z+l5PE^-zmmel(=5X${ciErXzu4LyG^Luxhq(X45~b(I9^F7?bg=w1@?YIzvPN!6~Tm8dJ*CccWPG=5345{wsbp&q6XWGYfNdIjT zDpo9I5vmRp^1+h)o*ZHpQ)Ma4T8q9dmt!luWvEWqQu}1niCpl~0R+RXiwB$bvGj1Y+jG zK#K1&r=Ez@aC7vc7xhJWA{>UPf|ucGeFIz19Yp7%Sp4?ShSU@%tjxNC-#?V7IrS2@ zza2n~f(V^Ik%WZKVJ>T?OXn^n<7%-0wKol8T&V$DI>O}-1V2J!kvflm^cLECM)6`( zHhZFR4Hk+v>}0+M)+d}o==>Y(T(=VrM8{(7Sy|R}-2ewJ-NxpJnZQw~SC`y^8G z2djN1v)NvpZ=2YIW%GscZeA_&&L4qApBDmsn;?~yf(wD4*{}Yu_^@jyjJLeT*DZSJ zaaE^l+dJ@H#T`dSP3V2f8*EuW0ZU_KY5cU+m?^4E?Z+KR!#Ek1T;FGKx(n?~T!uM` z+BBSJO!iVENRP6kz4Er4PyG==sZJC!YD7<sBMq;H zA2=Nuh$B0ASorz{GR_}GT(u5jPF~0N;G0PL=Lh{&C5SM7h-$-N?45HK=^INqj9>s; z?{;?RmnG%cp2gRvOWB~5D`kbH!20qB_P*AP3K!<%$y{er&HTf2$=rv-pF#dB&#_he zPvg6xEk(9u0DeLk{SvbUsS{73dZ zsRG7apYnS`BK!L6Hr6gNr6yNCoY!nak(CXdR*|4l{%QWneS zbHamphSWFQ&6>~KVF{NFQ*xGKE=kXL?xv|8eyQfvy(g9x9d&0}Vs;euLbCYDtk1l0 zp}KUsyoaZ;su-3@Pnem_R2H$e0^Q2WY@UfD@5`Jw*eCLh4JrP?+sqo?rGMPDU+u;D zjx?rMElauhgJok3S=+r4@N=CAk!9*M|Jx99Z*`z-vLT(kK86lDe?S0FmL^>8#-9VS zZ1nykoc#M7&2nd$__1n?pL!cXN8?IzqaIc=f)nw~qrFJw+?^33`Eju7N*O;2mjiGohn>qYfmo{BhB}r|rf9k+x!Ip{A z*4evYIm3iqxqp(G3V+1vBF4_P`tvkje}MS!n@sLXKAS%B6KlegnT(7a+%=DRc)!E$D+KUujUE0>6eueEbptmH-H3nSI5fVeaG5k;I=${Z(wAMprK8p)V}1@g zcHiM_B~Q}(EU{4g7yjAl()vz!1Sbm7*Rk@{yGaF4)3~{wt4aHNxZJeuHH_!_fr}j8 zA+zEj*ezKyKk*3qTZfUz5~&&?KBQeN8!0kd^DvSd7+vb9D|rVajb zed_58reJNmBJ$ce-Bwf@HM5uF?rTo(`D;Kk{&JmaZf7S6D$ul$X1GbY(6KxV`tk77j)(&Rd(X~%^iR=E_) ztQFG@-?81C9rW|tKWGYlW*sU%xVMAjOEa%BF-`|<{v<|)qXS6($;jTbA2T`s=eCVK zDY|l9{4fzN$9f(r!JrQnn(R@^A^aS2A@57+>|$~uW>p$4uev3i3;shZ#r?_;0}xprF6%m5|!CdZr> zm77xlyx4JmIqIL_LMo>k5O(Y&Oow`4Bvp@wxL9bU)q$TXxxUdge0bW&_2BB^BlHWW z!xK2HQHNKvg{f@JcI4=Bx$oTrn6cvuPS|Ksc7zQHWagp&lNK#&F{H}}UZeY$G|ek? zrOj80akhUcVr^_lRBD<%I`rilU39PTPW?|6RdPo05o zsplwY6eDq&6)5dE4_gU7S`(23wceRb=DQ(fMc#nKox5z^N*yY#NW{pTMJ)02IP&ka zB*EH4?4_S94Mu3uno=1yZLtg;@V2KVm*%m)TLN@Kr5_6x*Tep=1HGN}9z%1R@o1+h zi4^r9xa1!8?s6l6`cO6@Dg_^^q$$#AWATh@N3pb1hN^Qdn98lIcwH?>rouYZ7Oq6k zkEl?-nK_-GHCts{MLKtryN8@FsB+STJnACw{v@bH#e(=8C&Bk_API3;Xx-$~ zxc$b9JX?w&AQOfqP5(#HdHCi0wqdwEq`hdTUD|s+_j#$zvS(JvmhDHf$tW|Vl&q9Q zLuSZ`WECP&RFqX#W~IE>`v>5op6C0$@AEp&#{V24aW5!< z{Fiq4I35?D?wo>$m#dK7ZG>a%lMwQ~tJwOh2O4jmg_k%fO7y)^#~qEltO)!)_8&j@ zeQ1HvBseIfU>cu!hS#q`e*8lGz7#?UTf0&5qT#|gTp2S8tm)JmS#dtt3ajrM(TP$2 zBuzKZirVuD(A?&Q2g@Fb<7>8a=cGYQo_bRl=jLKpP+v@T{UfX-=fxCX2a+lLfL@Qa zV932jNlgdh^M{IR46Gf7J6e!Y$nvveCeek)Kjx`I7X!L?Z=rp*Fk!ua9XFQDQhggX}HcojADb?-pvoyo*)j0^a%fp<&z9`*v1v&{OxZ64z4?+*X z`1@6CRk!4>$7_)-=R_^a2XG^LmT*1hNB>%r@oMoKVOL;JSEm+Yt%T>| z4N4fDDm=HOV9P8QTGQ|>u&?3)ocVAIvjRE)iHQuHedj40HfqxH?JLB|^-q}~{D%X7 zrWOTUYvlKKF|HQ7QQ^S7B3$YWGYPiz*Fy@?8|t7lz=aB!!8BUwf=?zJVfDkRdo<~n^)KQ4Y9VfL$78fn2eSwdkvCF>yz5_M!L}OC{dLGLp&f(kufn;*5dUTw z(A0$|M9p&#q;=z-%Bp7ZD!~~qRvFW{rCo)6>OINjiu^$7vktVycB=?I8zQE1_HB19 z%{M-|R&xKd8I9BXCb^w+1yf(&7408~3Y*KtxV+3*Ts9RFvlY+LWy2RyFsK7H`|eBf z47*Z1XCF49C&cUw4Kn%C36ETB?oGBs#`g)X^f#n!Us^HdLj#ugu_UQ)N;F;d4Jx;4 zP)yk;T=vuu_sefXtL7ojEJ_vGP502hxB&N&T$H$_6cy>$;C?!mnV~A7*^m!hABSa= zYQ^)H`}iKS7=!W@5gJ#9h(^#OnuyKnau^cmPf{mC5hve^=OMi)sB{Mk&dH)8YN*J| z?@CK{j>3!j_2Tk1ZJM{*60>Hm6a7Z((ctrIpb=_ALnD>ww`&SM?lmXH@!d%6U?!q8 zTsSLJqWN)a(XMMR9`X$Q+`9(c)bJ27v%)03jy0gA$3@XJtWYeiZo%Vu$)f$9CIUk@ z2d0ndA$}e*hwQbPV*WB&G%j~RyTOhC)AzY>G4Us7UGC5tq~P=4-V~I55O<~>Lj8DW zI%t}P-wvPQYpq2On|tD|>32MPWKQF56YTi>(>+Uz@~ibAow)-|+l^`JR_=ih;vU@w z1G>4l7P$jA!y#9lQmt;IbVwUU2duz4BR{&{ph8MxH=!iXpEADwz}zEqu_n7ONxgMK z=Xn==8zoCuZ3iP^oIc*o)g+}J>hMyS2G>Yt{r_1~q0CF@I|NYj1Z^r^Rl|-QAF9de zN%G2-*skS7L)T_u-hdx?$FrjHx99M1%L^n4E5q_UgUB6b#DekkNQavvCv+JJqpAK~f`JzVs*Lz>=ge7Vz~zgHmH7Ckz$+=HI<^`^3? z-AI4GExk`4KmmECROsqTj(_%JKxw#G{KHvHdzS~(jmi?`2P?#{NvkkS%}kW9TO)?9 zZWQm5T9NjN$;mBEp3n|vxk=WJ+ z2i^x#p^hDWH>wbux-e7ZXEQig(MrY(KacJ9JlCEwbvcw9})CbN17DY8spx{ zBN*V~NL$X!!TQ2A?Dyh+`T{xJDcR1?^B!c}BNZ7>OGSRLE!{EQ2l7}c@-sbX-HEdp z{_4sdjW*#oNrompPRFvj1)RgU(XVam zab!vXiul=FqLYq|4hIm}FObB7LJ@j>GiFxGGxPXUGP2))XjM=kH@U~+#k%c?Ui<|M z{55I)eG7^-H=;$(?5jF%OIO)Zu)|%K=9s%sxU&TrhV~@W1DAo_9z^T@p9Nlq*DpUx zKgQhu#tKwzbEl_P_n@D$6w~Ay@TBxICR7E(Ij9kZPQ~c?Z8M@4+{3qNMih1MI&K^O zhR@orv~Oe)qH@~cn`%x`xASmNQ-R)jx>BvX4;~E2g7ZrcQoX$jPnREu>tB27sB%L8 z$=h+dUl-`)IEX&aH{fn<7x<2;mUIm}id~QFP@r-_Ebd&({V!8AyRSpdb3M_^VhC;> z+=vT%Hi_b%Cb0XHfb-YCT**A7Mww1QwB}i+7`ae|>aPV;=-HDJ&(Zp{!pEQD#A7k; z_Z8f)7=_snBgDsRcd)o@4D`SLp8+bsj`=3oJ#ea+_hc;EqiyJC_AeMqhoXY#sqLZz z?J|q;c9aX%KbEIc{G3XD(~3QEm!Z@!5R)pU$v5*f&JOBV8r-+%D4#Gmh= zRy1%Zd$xu~VSJ4vDHOJ2=ZuN4>@cK5DeEzIc{dV|eM#^4L8J`tPLKI#_NQYV3Yk~m z=FyKrw%voSsS#y#u^@FnKIf0^K{qFw)AK7uSm0wrZ;u&L7pHN|7l&eMm+d%F*%SZ$ zn~VF$4zRmt5ynQZ!nh^NP*w5>8`9kAOr8on^}nIxxT{J>G3YyA9S2~*i7{PEBxJLWMx>g6fyS%oG1gh=w17 z>5n1wvB-zomuy7vhb&kuu0+4f31XjSF+R36!d!8OIDRG>-}&E-?^-A6nR`NFviSyT zTrW%3n>vdHS@j4o$q!6ky++*eIfa-xFC=|4bSXNg3wH^9FbHOprKf`gNI`L&KIz3J zir4;teX}tIT$81FrbQUJ-;5H&KVspda-JbL&_-n$b~$7tQm#alC411Tb>5WxW3TYq z?M-uE22+HTAzJ3SQS3P{syOIRTVI?K-}_oputFc||2a~W_p~Rqq5vwtrV6=frZn#4 zIw9%t5Z4!mVZ|3K@$~mCWL)Tmbv;LkRfV5mx2!Lc7yW`qy{rh2e2cPp74rXhE$~=T z9VV>#h`1GHlBsHZZ|6B~?jC2_obwKuZ%nBDhz04FJVE~?d$J$pOVb@{@!C%owwE%w zFMJ)>O-ztBpU*-MO7T%PTTFVGgF2&gSS>gk54k2T9xvguKrEVPt750qIlO+i9u@OP zi7vU1@X*JZ)})R>X>x+-BCIJSbv~+fUrYX2d634QPIX0aCDfYzLTrEj##O*1X*ljGt#QLvjYgsNnD|``Y6-JU{+PR|0$ra(l ze@QOOCj}ah(ZGty4z`K<^&4SeZ4bM&{XKH#K* z0&Q5U0sW)$^azTyIOeyoNUMeGbZvS!@|Q>$e-OU$#x%O&1Wu$CLE)Ajee=&j-$4g3 zmV4N<-rdF;c8g>W;4`C4FkP!vqyBOE7;+_u_MUA;h|hjz^27M|p^Eo4wPMW573x^8P9wjXV1hBI>&Q~E`(~T?;xmx!Wv`0K zT4vZgTtb!G&5#qVPXTf_(EHJNC`EUpeV0!{CBzzklFcYjwHnU#lQ5=u0Xjcbu%qIi zh|h4u*;Q4TrqLm0p4A)LzS{G7|Djj2LI_^!8quA4x>_%^f+0cV& zRVa&_jREUisXkMSzHVHL<+q6@ab~(OgL93Ejud-FiTt$JLUYu8_Ms@!x~!SF{`v$v z7`srp<3Oy@e1&ppH7a=-kAGI%#EMJ{%5F}?>&|h)-O!DGbN6QG>NjG_8)MpW!+~Vw zHw#lob-LWggbG8)h+}<}sk6?Lj;N-I$<=cF^ZX8*argMV?MEqcA7I|E0;>Ca(%yB< z)z;<0+$E6OL$3$6i@iv(Ri&VP6R$4#$xfHD|MwSVippmlLz}b|jXdv2egoAhjST3+ zXcKB3twfKdm`C2`MgOK~keZw=X{%e&qHi%Mdb&gMkUNR$lOwQy_gbMo*@o&>51?7d zh|rfMP>+kle1)@^H75sSZD*m~G#~YerLdDfh)$L>b?RMRc#Yi(~e`ylNJ=Fsu!hg%EHN7U9tZ6 zGm$fG8F%lpBnHFYiU zXd%Hhox75|ij)R*r-l{IBFx2r8DMKNoN+`_*)bYZYt8AU!d^+r+uP#)j#)J}!t;wqTAcCOHJ~R(Hnc9ii6Ghg>lIC<{C{@R!&hIBi9l45?0Vnav z@DDrZ&fwy(SRC_z4T+=@Po}bqXeiGY-X@8n|J>+&1^>>sZxLGIHuRCX1zoRrk;$2G zuU<*$l79;8dzjF&_#DWVbKv;Zge>b0An#`>+%A8Cq7IpXW+d3eE6>R{!$(u%BN=2_?qPK}b)S$5x z#{x>QGg*;dcWR9CjUjb&3|eY5=;en_ zeEazbI}P+Hh0lLhZf9^eM2Gs;*5g@pHTFN~0q<}fYIw>}xQi}aLYd?18z<~d`atTD z7PaR842XXcOr7$kl7wAO)ILr^-{-oDAYTjGc&H!EUgRrYO!FlR*CnD)!!5kXY7{pg zZxjc2mg4b^A>tBEk`z67ith_$am!tv#$8X5{K=6empklys)`pMTa@Tcsti3CuwGKw z;~lo={KEMTV`{k40aLyoZ&;;G?&&S~;;O*iVCKNa%Fy2v?Ex`+im_+#W5kS)y!vdz zIT$HihhYf`n}lm{bGnA?_lqz;#uC=a*Kx~#1WYwO&^zKK_o4^EIz9jfN3&oU8Axlb z{?92JBKldszO+sBAA4WTh}T`7z^1-})1Nb<2eyLz4=wT3$~7AK(Djc1-`W$0Si1(Y6t z0ll|!E&6f@*cze_9UhCwB*Avc=fTxL9mG`cd*>-BNL~`*U$})?G=j3EeS0 zn0>FivoPs|2THgHnB28@5%)xL;oWYFweNkQTX7z<267Gm z0`mkj@8P|p26|2nrkB06Y4;Ro8dTMvT+>ZSLE4j!>G)AWfj)IvY(=AH?8Qw7TcLe5 zOk{B1FD$<%4ts&I7&tXyT_M;>jRp z#+G`}dqpqixcB161?~!}x>H2yA)M)Jz@0+}+OTU4UIi-9HD*wLFJ2?GfXDC$6#<*W+@Icywteke?c;rcNSBmcSDL_A-WqjJ^L)Du~IXjQX zkA9q6N=(Gxludj;R-$g^HR5yeerWIi0$V#RI&Nn{QPZvHwz2^!thJ=WAGIm_xi)Qx zWT&x$1G)Znrm~6xl=N_?KEG{f2H!&b#(PnGoj<#K3h>yE+4Pi)%#$s^kE;iB=7V|a1A9~QKbW4m3*n|HfxKm{v3H#fpP`dKDXne9xEY;ZnukiaKzRgd>>K}zv zeY@zE|69B~9gX$BG}!~b2!A8xM2d7D?&YmT`KE0`%AhA~M{UBZmo`NYNQ08veCY3x zL~)RNHGnHLv#p1=TbgZ~& zfL`zCiCxMOn7P$~ZfCdRgj!#GJ8nWRzsXQW(iXhh?MzR-Wyxq+hluViP5wg)F>tv% zu*9ocREs3&RzJ_7?ORZAvIGoN+OL({XO?X zjuc~YH#-WSszn!e_+s+q_r@Wzc1SGt9@fq5ExlnzmsTaH9p`2jyw}D|MJ=e}lih z7F9-?(D#Nm3<eJMeczK1!8Oz~=B;=v}WvaQ;m+N+m#7+8sgP9cb^#O^}mu2cG*;Qp9%LTs{l+ z@pg1EY6o6E2%)x2S9-||Tjvvy!U;c`o_YxDUJs?8+_SuRd^5uRlHm~b1xdOa#lVQ8 zSX1>J0ea<<4Ht8n;r{{exz|OrO{2v5FS9f;N+LJtlqhsL4uke{6946+MWrLp*nh5* z*zeb+{mC8Tu}1*raL#l8yaiN!@)JEzMniPU;naD z4>6}-t3iF_<>4J~igefXrj_|EsNuQL<|#h3Xm>MK2XsJj*dSVdwgx?gG2IHZC6n3~#Brv1 z`kotU%xJ_<9|Ib!W=!4reEg`#DKVlt1JCFZ?tUqgTs)Bvo8Nh8-drtSEh)ye^a5Cn z+l;@R|HQGdD@gCQ7}fdxR)4L3q%#3Y{19- z+al+-B~5-2h4A8Ml7ERZkd$iE?Uo|3R!$Akr@PaHLLCg5E{|Z&$lLc;i7r0`dsvj{ z$*P?&Ed&Jyb|WjSLQ20dDo9YE<;=;IM7b zARZcapk>-9p?~4MIB+gT;{587xb(#kpM29K1NZKf#7UXs`!WZS)4Lp}okFO8RvKpQ zxy7FaPf{74j@G-EA%A8NtxL^8;i7sx&sL)LTm$rTs>0GTJ_qo7aKs+=Z@YA%zc>Gh zlKNvM?FT-ZHCLLxzf64Rx*j#ANzLs+I8Ldd?_UvQED3}&~mm+=pgD~|Q zN~bPp(Zu2me45{jBKfR+r{s;;(5XTbiu{o5@J-xS?@nj3W#Mtr65}5#QXKcsVk3Uz zIrD`9W7X(WH1l7_o#_*I&-70<;jSAyahKJha*Q+;IVvy%$Krv4I*d^1LY9ADV@0wO zNuSlBUeqdl&VLTvbakV+c>RPZJUmq#*lZ}i%zq?ev=;|PFX|LK2fEVN**wP!8jU7f zA6i)V1Cl>$@$QQiIdQISImrzTv7_-i{5kH<9)#;9#t4nCf`)}RT_d(rv{7}K>v-?pl^@mD8n$2j;ePh=frNb@YQbI{Z=CxH*t`N%ASM5<{^^T z!7D|?pJVu^^XH0#&H<6Q=Z&zA|Hz!Q9jRC)3*RSyk-_)R?mG?fHL)F1^PK6&AaCl) z^VY6QG|5TMoh}zlL*O`NI=9o4p6q0QN)691E4YvJX>4GRa&HQHCr!nRL%0LrMW#G6 z`(ybl@UXc9y*uDSr*kYR@zW|vU?*sfvIVuhS`xTujSHoyyU?5WVIp(dJDg*0y=^cv z{hlxJfjevq%=BpTvkGire}11O9dQ5L01qQs8sM6W4cBETkr|V%`6n^+?;BJwgEBKd z8b{9bq_um4DZKA>?j{AWqo*&8D`noZmle4<4<{>jsttP2e8CcT+Eu1b;|C>U&n5|t zH|D-4&zs`Q9qF~I0y`rXKxcOmN)&a;QH*C+GaZ|jsnSKCJ~*v?3kA9+w7_XAX!1qz zCYjmKAvt{a>=4Do?z>fEQ=%o!9(oMDdbMNIKrN~%J%;2dHF!~?PVILMS?PHa z3s2~f#%WI|d{00|4tv(>y5R4W6G+d#CJf@sBt27Bqi4$&as1mKiTV9hTqYURtWOgn zYXv)LyihrU-{nIEX+)32zU?+xaL9w22adtDgg)>v4kH=Tr}%-r#TmIr!g#ALwLhqn z_SrfLTT*8U{!{PJbH#Tf5N9bI4 z^f79}@vmv{)2xTmZf(S}%PeWnPt?X6VLJ0)PHD|hf6@zzweB?6Wf}%u(xK`a4{G(_ zh#Ab_9grg0J31a??(scQ*MV$aHR00_~es|Kd*^P}k+mU<2j%KRtVumam>W^G_&YFjU z&71J^WCijZ;^F7715c~(aGn*74NpyBd%O(uN{*nU!v}rK`To4}Dvp}(#4?ZlaI9wE zRNE>%J9L9{rKhlQJA}pT0uNou&Og_USRdLSk2Kxr*^m{;YTAb6pSJX|Y&-PA+(`dC zd&lYqqjyCwnzJmB;&05vEguKE_s5Iuu5LtJpS=j`+JUhVBu0 zUgm$m`0A~aGbU?9@tZ6ZZwsLVQ(uVM!Mia0tf11eOrbZZ1X4dFq%uL1?u8A9=Obr) z`(euGy?Ge@#uq0#wWvu-7gJVR;7^hXol2?w^icVB5uR<%2TxX`ulCnO0!D{x; z^xbq!Ozv+%M=u4@uszbKzGy^=gZ$|3-`mWtIa2P6{~}3Wx?fDIF2Vd{J@nTtmYBb-!teV%ad*^e(J}ozVy@j3BePmi zZT3hK{6~>u4@gs}hRoH8r({Xb`~y->4Hp*9f3S2LvuPK2K9~I+DnWYmD8q>K&b-Cp z%iZb86Hgks_%$qk-xa+#rQq_YLdbk;5Ys1K!jPJaJYVttTu9AD^qOf zeF5u?XJcMUhnTcE9bPgk`L35Owmp4BXP%aJXoV!W4F%_Kxlf zxt)F(H6c-$J=I~a1@Adg{Vj^`8q%^a<8WYVk%;)AOb&w#uz#DrB!eBtYY!hs+ukNg zMZOjl_S?qinTbMkk}Us!&S2=*(V`DCwXSh>2z;_fgf#S}lj?sEv23>pD08K^o8RGO zgQ76m+9_fb$^!q4h?aDj+5^GAH}lskO$;fr#>$UtuWa@o30 zcS?(~_QMrOA2}p?+|0+EZm+O?s16;T-W}K4+ELg|lO{gy6jGYi2xp#Z#2*vnbbX4> zl~%Mc=>gX3e}`0-4UIU;T+Zm{IC-r*9r#^{s~X=RV;GMek->EEx+1wA-GwDGK@^=< z2m2{;2u~YAX96AwpRg-pl`S(vE|0|bgdT|9)Rp;AC2V51^)%=1Wc5Rr&R_V9!S?P{ z`jO96qrbz@-;P@O&NnGjmLA1A(TvtRNZ4}^TC;UYt*QdKhnsPpT{)SXj`Mf76sP$a ze<^pLME*=LjZ3hEpPs9Mp&cgq+dlQ(Y>kLMDEMZ3?lU#S5e%s6F(Obowa+7 zU0V|%4h*5Q+-*O)X9Wg7U4#4G4kK{ce#yaIQ!!`41sr+0L`<9Uf41@@F6Q`4lAmiy zDyRp2-lal;?!NQlI*BAHQHoIyUF@q9uNNgd{K#Lr;3r4v6zrdly;FYSXJ`X5f-B)SYcHmJ>csnBFAxxY464WKA;n$HNw(aL?X5!> z*}K#H{3`rHcs^8PM3>Xk@qk?gE7a{s_FO4em99gCgP<2<^y$-r9XRl;FNO8*K{tl% zW%eq7eM()avvMA;{<{YqHSTk7o5km^T(l(q!r38#?C^ZWGb+xZ4rb$t?Fmse--VVd z-^JHC$tgE9ZQY>LT=;;zChf-@v#!50#&L(F!Lcv0!u(R?TM)almfLp|ayR z5_}m(DSeAVJreMuvmSev@P3|wy7Y9PDW&p0tg89ewA<61!f)GB=sj(k@ywL8m@!D( zd<>2Cf}P%0RAPAzK7WYJx_MKC@lkvl#-5a&ck%q}DnuSBg8Z!GXpESG&TZ##l070d zlb54q(h(Rcbf*x_R5U#MfpzcL19OaLLA&MY?CCDFZSFSM_W2FT#}BZ*5KP#pXYT)xnj(afp`=>g4U$@V5{~F+|2i<532`ppL+r3 zJ(~=VwZmYv!iMj7QCJ(g7Cl#bP!x06VO91pzidyN19a$y$pB${&I9v$8ItxSS24K$ zz1aHDn3;Sx(Xe(ZD%$$Nu%%cO&iX4J$ob&1`4#bEQM@=78iUHeapL{vHc_qfMs)lM zlF0vm_nziJu`h0UQRPiOd*9-DL_tm9&4=>zzP}CTXurbZd9he`*9Kia{KLJ&EBVei z5L%oyZsgwg!iO!ea8u&*v=#m9{}1otY@rkFN%MHV@YBf!A08S|9sij}+!y@+x$f|? zM5w3glIs3GBp|nYO z;E9X3XQF=fXsjPB!PHVuoakzegD2-=?~3I-yBG&4yI1(w>_?j;`y+qQLrgdz=vVL> zSZDpkZ=N@d4GzX#?BnxL8&0geh~X{gF*5io2Cpr^$y2*9w!Q&{xfSU5%#L=pYty!U zsyv^tBD;Ocd?#S1QVPEt!_Dbi&zE9H1J4?U>_pz3G(Iz%k!MN*=Q`ZM9O+2CJks$= zW-sg;nYGcj$7GiZ@$c6wT<=bJYiY|o@CD|YJyA2gLX?j^jVW_d(6l&&{XjP{s`(Ut zO^m|bq5O>8e+&(4^`JT57H0wiNL%F?vikEbu#f)Ky675)M#^ENLof2Q+QTkUNBUSE zLO*pwgzx zX@V++PkSlsGfiRf;3vv9+f(?OUKlm87L6n9$#Aj@9u1MA`U(@WwveW!(|WU?ZZQrG z>OybQXW=_D{13c2hq%}c8T-c~C_ zj{Pcxlo?r>76|{wqoO0sp32j&iqomx`3%SNP+nztuD^uZou>q7s=8927o0a-_%2b~ zYfbGr!4!15kH~iOrrnwPIJo#BqAi|@t}>RKZ9YV2a8K?}%Rx7y2@|H>6zS|bnz*SK z_3qt`UhwnCY{Ed=C~HpA_KLL2hG-~14;v>oAuCRi>~oca^%U|hx^Alp|m6EJ+2tl;`cK4oci#tJ*Bfyub7OG zDhcv-C1Li{2(0hbEk^|3$x(UKT)W; zIkHEr8=Z3g4?m7OV);@N(rWj{%B2=C+iF42Ja%DJL@z3uu0)aV*W;IkAI;I}O7or% zWA1A(MXpdH^_7d5Y5q@=+Pz+g)uk}di4m)|pBHyO)*w2gPGWq=0+xT-ojbfpjJ~Ie z{mDd@7nBjv!v+rNp46}*UW|T2kX7!>zQN-d_&Huub@v#)tvZT;!}((4)MH?21>$?| zEBeY@4_bd>W~&AbI_ZH?+Q9aBi!F`Z+0f?eY9`Y7=4~KN3oT!Q|DfN~+(Q(I;F&u-7(Rt9Q|o7XsVT+>^;88BAl%5Q>dT#n>ucZt~D(Jm&P;r`SS zZPExd#W{H`T99B&I94dKCw8TTx$;!;(ULju`DiLOA@j>t6tv431q%!*zut=O7%f8W z7Dw7Pqz4uA{uJH9UbI$2m!w8{Qp9dwGMU8gn~@=O+|Q53%I8SdL>bdlIe%hyL}+a1 zetC_AyQ!}v&zXfC#vVSeo{yotcQ>k~s*Q3cm# zc4+NNWbXbOZpL?DT5}#|?yScsC*B1*HwI}j_Viz~J2mYu$KNm`lK#(|9f$RZ8tF#X zx2?&WnJV>`*_b`mm0sM@qBBP8kljo~F8VZcVLayW|LN38B^uGb2J+VTv0y*X;8x9o zhg>$^_;sP}fdlbAtOny}s?nk1c`3%4E0^l+6YE%!Psj*OJ2t-e1HGL4z1PG4GF_YQJCkD2-D zN$;2a#9GQ_<~V>Z>*WQec<)7mw<_H;7<)C2=S35CXj13=`YVkWIX9D$BB?@0@*l5G zHDLynvc`laY*M1aa0{BY)QfUrG-!g94OvL6sQd60XkWNnGOOK%5z6KVRue0t3I`I7MA=b`^af%5L{6pG%*p}JPl#nRt`Q@${DWC>wSHDoU@hZihOaW_72( z74__EFhkxUTgrUVfC}y2*xf^iJ=w9a_R%J5cBkLe+lZsQ?@8vHHC=sn2yz`-)H$5H z_t~9jaJA$eOZ>Tf_!mF+v%mYNDXo%j#He%=+GgucA0~Q1x;h-M11DpZ=R`OcMdJAf zH>jj~qVIT~?W|jkA04&0v^O(Qiuu`o#*g5?)1@eW&Jfsa?!>R!hCl~rBY0hoM>jri z`QN&VQ*tNa(Ec0uxC?(VbpyH_dX3qAD)F8jB)cSf6q}SRR6O12P^db2uHGy{oZ0QX z-;zp-V}&Hjg6>V{9dos(uza^M9XoiEJDiEoE;6Aar8L+szJvBFUm*Wz4bJS*#gmqI zXbn$AZMp|SV}9^1uIb!848p*bx6!2^@7Ri;1T))A6#HI7hRRR`-Y!GQ=T}&n8-q*d z1gd$qHTwHTq$!yrQLmT>VAD5jr{<7$7>_u4v0&%e;2uXT}Md@03$}Ag)!JegJ z_B75pUd2K8O@mN7q)bchTJrnRn!Zg^qOlg6@$0iWb?9kQ@BI;IndMHtO)Au~c_(}N zr$`)CZiu1YRnT3iEXED95+mcEVSJ&onEq7}{(afQvoA@EiTWd^?$_sTPpRnq?8ZKw z(~{ytR-*KrJdU2s7AEzXC|WHgc{cq3ifbFF=cVq*{WoT_w zcaoC|gnCg|`clA|;A=J9ll_CSO;)r)!wSL7L@wH8NtuatXnUA~7t^`3_TLjE?k>T& zx8{7OeFH0J?xS`afa)m0J2zG7sksk!@KNh~zh)Sz41k7zC_TJjf+hDO;P+dZxma&R z$%bM03*J>6P$G(S7NUND3RQ;KacBN9++;&&PiA-O-TDO8alL57M;CHo#%^r}@Bd18 zi+Q?IWNF;V&a_(Csj`RffeL?qc{kabdYm05MPb)!MZmhmz_yk%Vuwc|!`5wA-fiPu zzppllPX`r*;fD2M)lf%vFTKEOH$q9P3z-LggVV78*x_nTA9KGVSHT#kJeYZ}evjmq z!Dx7H1MT@$IPC1tnZ_DSsr!bynO3;b7DRznvXnGVg=)PAkhQrw^*Jp|vdg?le!UVo zoZxP%VFGHpS4n(VtP_*AWnzD#RFRV0T@gBeI(oXFk({?n5`W*^7L#Yb;~umvnf2Df zVBYcJqhU|4kBt_wv%ll1vKjqO^QH8myKrfl2K~rzCFdJUP|2OFcb*=!Dn1_LwwutW zrD_y)s~2gH2%z7FhID3oFimF$`0ZC2+V(G$c7I_WhVLhlKdtHFnL5e#YfcmiXR4ca zSlEsbv^&X~j8^QGJm~u#r4KW4K|zK#A9#%EnY%E-YwQWSM!GtBts9C*W2k~`rX47JQ@tm`=O&Hf4$EBIOP zpNBZ>b_=o{n|YrUcb_cOsN9va_;(?^qg|i7eyNb=bNx~FA8kFd2<5UjaOAiFvt<9l zzWxO7V*h_{kI?_?9Vo|}kdpl_46Mi$IyH8*KrIFTcdn|$_)!fzCem*=if(nxVli`^ zIo_Oo$ZgEB*;4lrcg62ha@5=2m|mOcBWvtCtg63*1@gD>M%9IyhG(J2iYqX3wWYj8 zm9SIF#Hz<`r1$r|P&AB1;$|5dq4!PLneD>z;P-g5c&ga#y9vqM*-89lLSIf8kg29Q z&HQdk%RH=UIy>J-W}8u;9i|k~=tN=5_&M5~fz(gT75I6O>#_`39R)p%;<;Vy6{rk# zqk#)gz@u^{EJr-$os`)aSsIU+!-Z%vK8OPDQtp3sA5xXP_p*Bd3@`Eyl@@h!@3_Fe zhc{42FsHi{v$1_tCtN&L$e3M&X;wMt{Qn&Kn;X2$_TcznS=v%oA_`XJp!obbp>sh& zypLUr@Qj(l(^NsMV}GFQ-j8C0?g`PrbB&gzu6W9OyH+a-nma23(`~&m@ryTQbRC1- zi4o|vtRD?e)}nO=LwO(1dvV3M2L=9#lq@>@OZ-UEB;&`@;=7DQ+<&|s_p}A~Fg}TU zRr`@OTN+d2TE(95Oz5kP$NM9Su=uHr6AyX6;|(cNP`EhtUUk|q`%_ow=zVYcE%097gfzW$lZ;%2-YnV6)v{)4S2Jz3*BI^Z_h`)W$XuJ50{1`6~@}4y~&PedFv6NITm5R128gjA~xxD#h;Ri z_&Ie8M&y@cM4mrA@6iL3I`82AH3@0lS-=b`=cd-6OeIxJ@!pJO!=)&trJX%$8+a$r z7v8<6Os+$YqW463$}w%n#S{b5@t38DoJ=wLt~KeLRio#7c8EA51A02>JDQ$1N)A4E zp{2Tekk^<9H$L+@oKC}y_vdhmS-_PA{8_NyibbhUk@;>rhUKfGXCv>Rmp_5lqamED ze&8O&atzD0g!1rn=oZ8L#IOX!@XXuM`3^o!+KhJ*rAW1Y2+6V}3^^9Td+qJ0b8-v} zIBPi*U`=Lj2e9R_1mn41YHqdy=Tn`j`(!VwOAEjdvjCdc;6#aLV{qJ)-LsD+G;hs3 ztYLSC%B^4U4;v&r*ya8pRfZh&kBikc7jWTOBaE#_NLD2F6jSCNMQknSy8gXH=j|J) zaPgr(wHL&y_+99J(T5#-?4(H#VOKpNu8{Y2FN|V-NrJa^s-(a^@g>sM?2|L3x(Bk9 z`|<%)J2;!=oMo@^WsLPQqVbPE;`<5Sts9;y+M?{Kk-Li-YIVYhXE9G4`qJk>8TLDH zRvqd~mt$SX)5%awa55w01=5*&@j`ZxE!lSZP{)96fn$g1(vFg2qQ~`8JlNYRI$p*| z^2_c+db1tu-o*-ojvV}2byN(KG&2vfM=~j2hIC6i(5CUdXoBfa%nfgZ_L^a0+pv!~ z)l-*la_%+J`zyi*=B39YTV{y|&MC~Doq$}ltf>5V3~SU5 z;(7E_@$K0eL>|sVl=M0{T3iv4?7XxaI~OU>{DkG?8vGeN5VLJtg*l&hr!gtvU`&}lQ?%E! zzb{jhpK;6qXUS3A!NbT=?MLA=yOQMoM!ekApS!=mp=EIrH+zMMEm=XdVL?60qIj2e zbQp!tZ^Mh6JYn+Cn@%OZ!T4#Ol5U@+5LOvPIjgpc)z@?|!H=EWiEc%2PdOk%eGs+i zKZV>)6;bY=jy*FfasS1YA~nrieB6BrA9L4>!`0ac4t|Zm;cDc$+!WK&JCQ4|M*qq? zM2}&Wxaev`DL(xCD7w%46fJ1^g6nwHR*QhO#`I_P3nY)IKqTjbMiX+e-n|nY&70t| zgf&LxEqp#;*6->-x=^A@bHcXpyDf-{T&3_N_>KrZphmwx0`52Nh(%{~*njv=Osud( zn|BvVW8cl!qaUEju9PKxb;zgnA0}=!Cj<6f-824(%lB-_I^rSR{yoCrxtvW$WMavk zY7DE;CL8TWY~Ij>SJoPoH|mJ+SvFcSK;jMyM+;%EA0xU=3`5vK861u8DQu3}0O_$} z=vU@BE)*l|@&gfjqneirWaH)D5t3r5S{SPp;+Yn+eR90FYKcE-CLO}%vfDT(C!uT2 zQ?aG%HEeS4!`+|ZLhfAvyMa|{|LZ2nzj9By=xRW1P8t%0(E+qyiCz8t{%Jp>6PRYP zRoGabg=eMuRhJp{VzX&3!Ut6qbsFrDm|MIRe|fi$=1_T>u}2jS++&;bUXH4GU(H@r-8qfsvLP;}L!W)Dwtx-}A4A869u2=2l3*bNm=ekQ9L(AHC&Tg17Gn)jBp zRhRed8r+ktG}Wa1!9H|R^7=}#e-Le7XH2=SUKDzM5c#ra{=2CM{Z8md1DD%Sx5I&S zm)_%l6rFcG)@>WcZIO|^Ws|IoQn;_Q-pz3VALp^}xNp{2dhGFnEvq#?AV zkP6YD@}BRX{quZ0-1qNyUFUfm-*1!UPP9J!fmOQCvF|8*24}F>{_H#a>YWJtnhIFC zKSq3G6BO6SU~}thSSZWTCC-Cby3}J0=QIDs?!(b5M$}x%Ufsm&xLBh_|Mxv-eB$?1 zw;mMoio3(ijeT_|1*4-~X^fN!{fIutoi97W2|ez2 zJY)hpR%%hrorvlY%@Lhf!6?ix;{XU3VgA=$z$#EKTm7GhMNBSP~) zUm7d%qS4V+%(YRc7^xwYwy|71PM4*V2VE$?GgYkAQ>8JzE3j>L0iLar(3iN+$ZFDt8)cDHQho$BlAtVE*5o@k}%Jy9a?1)anb!Kc6q;nQ}}ktf=S7|$5tQ>Q!94N za-T5Ym@*Qa>Ed@&s#JC$|3owX8Mqs^%7Dzr+taImnMkT&M(izjTC?^5l*a~=2JbBF z!U{2w?-82LtghL zP$wCfG6r=T^V!4TmA~*`IKD27LypH`F|2I?>;{LS$GO?4{4;|8HoOPA5zd~l%_yJh zMlW8+Lao9XbLyN)`;iv;y9}cK>qcSMCT&_VqCYKi)AO>mLol^3esN z--MsSAl?kQm2ZXixkS;4jri0%QE1D*6rX~DA5U!QLPRH?=4r!jnj0x`FJ_JBG-TBD zAme0Z`jBVD%$(0yczFv3l{AYf^)j^i%p5!!=ZkR;Kj9XbhOw`{!2hEmB0KfyWb{Ym zsu4EKQzCcmHasxn97-1p3K=sVU-NrV>27uvFWrpf=_b^Fl`okt6u{)3RQKA0f}=ZN zpU{hbe(<2@GRpL8K6CIEuwVG-2hOPX;9a99ee6CQ<9p45xy(Y;hR%eCswXtU1a{pG z$8wE0p3_c4^QQ{PsCiJDN+@&W%Hgfun=J1q;Pt?tIQxcs4^2~G+dmG$E2L?{*=+1g zID#dqzwqeu0R;Blik-<#aILtH2{}BY;u+(Qi$8?UW+S@d-jy!y4;QDeTT*x+cfFq0 ziqfDSRJ=X{>joXi>O(d(b$JRThl;tg(}U~=?8aE@M7XF`AuDb$cI(#**O}~h91wz& zk9%XzuUaIIRzo-0u24U*3wM@Y!J5v&$Qrc?84iyz?8Qt>Gd_#D`r9z77>~xom&F&; zzI3R8`|He}4QlB{M|S2R^6E!%;8B0lS(Jw0)qSa?ZV;`rNyRzOfxMIUp>1aI5t1zEuPQ)$Qa3UhL^M+NDc{xAXz`HYber-prjHEy|6sR+?P6iFq8Xbao#>3l zUSTw}1&!=lIC?vn8Hc=h}u+$!-9S3kRwSNt5|HvExj9O*y? zTi#2O)(+r~mmrIx>&#m1OG=Tego3?<^j&>uvf5VhrOAmd%<-jBqf3SUR6mmK|63d_ ze}=gH454zq3$(&2Ih)Acc=I=6t>t(0^9U23je3+kbs*`T(xk#1W#+s2Q@flAjbgV~ zcc&qwr6x67^_Hghl-V;ka4~ zO_M5^WpP-T2Khkc;~RKJJ{N8ai-eEbV=P>Bjo;mdFdsaS=C!RC*^(g0PZ~*1vVTPG z6(`uYdQv;{Y>YW?DrH=Xob02xSv{1Zqw=BEl+Jul52`YMf|RO{c+sv#mmd8P*LKU& z{zu*DYe=2=?No!{mB##8)q-8{L(IEiLU&5#NN2}KG;YwO2bUG;*y-!2@nN^_krsp> zk>UPzI8>sEvPN=Nx6d4Q%a5cD+tsPyegvQUd}&(m87TDFg4{8(v~bL9Jl?nf>J6IY z5@C&{#<5U_6tx&?(a+M)u$<^i7G7FZRPh@hxfk<$j2s=f{2HS&{3v|b2TZ-hj0;Uw zn&SOdl8zC);~ zcfN4m-JLEqKEv7@4j9J#&S#UpqwKFX2DDky@q4epEAKou^p#ih^i{)~)cyRU))^;xU*>LMO)*k1b?d@&w zYQNxf)!0Nb-{A=F6{HHfX7m=LJl4ZE7={0o3%u7XHbLHqR$Pj8r1W4bm}PdMXP+FY zpil~3i(8P_YDRyfJ*lg56xOEelj{;U>JhyY9an8={$N+STQw0=FKf`UI0f1}BS>hP z2^#xOho-A<5^tG5ae9w5C10r)5$@i!ZlMQll2}nP=j5ydJm_Y%8+p9$MfMp!yf-uF zT^a8VbJ@M7nu|f~h5g<38i8HULiblaHZmvsRc9H>uFBE*kxekD?Lv7vhY{>0PyJmL zDQPb6k+WM-1v~11j*2kU53>d`xc_-Qt>`cCWi-goh6*4RD zPNSVJixY#m%Rj0&&ApW&GPia@bK-M|@H@zm@*uy;2RO+bq(8-W^l^3tZY$?9dz#3> zK@GP~MDe^!kJ_B?hznBNVJp*>uEaKoPnR~aXTge2TRPH+p33xKnkG%UZ9`Ra_?*sj zvXDDH=|H0nedTA`{$b|SLg|=c=|?&}nM3kA7g6foG?H`Ddd)|;>l;LI_0O3_v;+Rm z7jbx7K4La6#ra8B`14W>rJ#K%dy|2jKIW8qrVuq{Kd_i*j;(>YaO?9At2*r{P4+y} zqUFi-suInsdL%wPJcT}?ZD{d}5i8ytN3Y>3)U5VP94pv?#v^CNddqB4G9e!2Ga7_T zn?9N|c4BP(O|eIFwYWTsxr-VSX7Y?-R)inna4-h08qT?7-VOaX5Aq`h;cd4-GTN$4 zvX)-LiFfFy`J6E6jh1t{rI{T&sq4_aHj z>Uq-vkCx`3P|cUD#Y|k>T7`=K{i&|9C$2r?Ufre!v?^9&YUDOJ4CFnp@*h}qq~KE! zGjUt)G4EWD{(aRUt?3WNstH$1)Ij^euEu<6yK&2d@48!spEvL=SVt`(72i>(0c%L+(dG@U9~oXgRpCrQJwyk8&4GgHH~xGO&0v||pg4ULc;jhhSlK>d*()jj3Dk9|4DpVB7Z z{l8FKRu7MXmh_CdS=-L#%S}#vG7F`tg_}N<$tUwPto(Zes58Q+K0W&Kp&_M+Xs!*W>2Pf)1 zR*Fogvj3|0?nFpM)W8+ z2l?Fl7+92qWm9zE@$DMsQ6!f1?iBA_UqB%u5-(dUaB;*Dto0#kYM+d}2OcP;;nWi1 z$83j5JfroaC#}pn^X`iSIDijBI74|vno=EhA%87F+9Vrrg*3SL!;+g;kk)*F2(bbu3Pe0nO%Q=z5vIU1v zY!T(}_>5|PK(f>ICTG`w2qT#q=ppRs`8u8< zYONCiBV5QZ%Y_~`S<|0isXp_Y1*MHMXU5?wp8!K=3gpgrx>NF_kI+Ci!|GlnfaZt{MlK8zg z-Z~ER^x27i%$Q!En}zgMc`)bhxZ4pY#F^Ydmor8*DRwGGHfG{qoHEVc5RC?%siJh+GdhFNvaLv&!%W&yqBg7UBCL6W>2| zp>_m{dfPhq1-4=NBLtF@>++Dsho z+EqM@>P~y(N+joGWSDswh1KT+nAcb{K_L$hj#rosTe&n*Dp9+QbKEeSSX~ zlcwf(EcxdHZ*_HA)6s&`JrZPnV^>p-F*3%AZY8|M!=qzyih1&_d~Z^bx5wiVx+EXRzA$FI*#2~;Bzg8< zF`sqbJP9?<-GJs@hUB-#k{w}<@XXVv|72X~_p3kbeEYw1!iGv_mEr1k6VfYpqJa7A z1nuID_Vv5DM`HuK0in3;bAvOAZ}{%%PitNELX*0(aWCn>*y(5 zw#>$vCC@SW$_nf};01*{4^jAJH=N&&!M}dZuvVOdCf-x7jJSjcvkKVP%KNgN`%yCX z4z_e%%}mMrs2^R9Yc&bTpB{ndMP~G3VhXaxOoF+VBQ02)fk&LNc9*xMK0%vNR^~)? zi4q$9W)M=69mqJwo6nA05N_Z}&+fX>zV!kcwn-S*ojcKO3&q>012_=ahA)RVN#-=1 zLNIq|4s6R1zH^t0n9egu=g;1|r~AarcX!!+=}j|_7K?EknX$+7+tga_Ii3x|?PMo# zx`H;`v4{82@1k)}cb?;KM8`uDjQgTbGoNu!Wls^VY}2O`oGDF;e1wayt!dFSWio8w z%+a_rBG28JR&RSE`OBWVpX{D6di1B@Zvda&(|eGwtE2e(vlorlmKF&c%*gJJ7e!2( zCK~TM&~0sZYDkI@Klpwx7iuZ)DV&4aFBiV|NhEzU9$;++A+p$AIOZJ3A(KRZq;OrSXCj6%(Tetk`CsprAubb(V>X4Ec9HmRD9w4 z$5o!m%s<;lIELxcM8u;1jR~Sn&X?MJULbCBx;P{~j2=}rVw&wbkrvN96809VtgDjf zH$E4;QrOFp79pk=FrUU%LWcV01^bgaMZx|6GH1v0utPE;N#ha*`RBuO3Gd>&WMk}u zr%*ncQ!xI>Z4_T%eqD$$W$7zpPs2<0IXRM&zBYoRS~#<C9x;{;v1nhPX=|&XSX&}htc_E;%bvFjo-ukJIN<#4054IFZHNI_8-=S zTalc*9MzV6fEV+7&nwoTa4eq`7jm3@fjV!oIE#v9QDvgXHI8Y3|N zX!1#kMxR$?Kll*geoK`GZ<3{?6><1IrrrAwpN~Skr=j0WIZ0sYPUg2Af}{C=1*EcD z?B@c|V(ItnAhx1EoqI&zz*gMcX~@HFvOYOy||7%XOFMHA)t!!rM z8<6wdL3DHXS>E?J(Vwz@oDD3*n!r>j-L|I(uKeuY$9`V!4{hdW-?c^iu;z^?*)yNA zlx87n=6R?*(V(o}{UCQR6$T2bH1joZ>_`dLJuxIDqbQ8mjT2c??0J6>39D_YVt}SA z`xXwuFX$3`r0i&qwi)f*ERS9u(iB{VsOC za;K^xsTe+)ohyYs=;nlM?rZ0w??@lgy?IKgMRI4C@2L0GzDUZO*THq40wuZa5jk)E z@0V`CsA;CuuB<_OcrGkI(~unbJo|g6I!#frpu3w4=*%ul3VGIp3_3Fr)6avv?D-ya z`6#}=@gVsIQ!+EiMdnCP(v3NX%u%uEF{~WTcTPbmXAj0Kzl>I^V{jWW1xLp)qvN0s zN#DGQsP_%npJ-0*%-DZiCr#-)6lwN5?k#rk47|*c@~BZswP=9F8v@mc{2ug>heV1j7|9+`Foj#Z-rgM=7_#qGcbLBf4ni6f#1Bd zF^m}wt0{iS+hR+v>q24nr9b4`1(mPYpwmAS#MZU9#pg6*n%SW)N``(FBVAO;qG705 zYu-;}+=<1cc|Mrd@l%{Ty`BHoits!3M67vz09OwOK=rl>w3A#>T3|?*p47vDGa}z2 zEXdWq6S)TlqvNI;P8sNVg zL!wRSd#MGvY=4Wvq(u?cF7&hDHu9M#WfSZ~y7jFX-;X&!W*&6)MF{px4B{n!5U60U(4^V&g7tV@IOi5{koY8Bf7i-h36J2S{fGja%gdruX8`8YY z`C=!t`!+NGtH4FctM z^PoEt4|soepaMltIy`b2j8`XMQluhXFjy!|ho404SQR?);4R;%7w~K2k+H`t_JWX-EiaHl7s>@QMFAkScVW&qUC$wSR@%w0MKPBvj>(N!c zTM|){C^l`irO!&fBHWLizMJ)FVfU*A(J2meqUn?5!^iGaWM)qZgEtAyJasxVs5dpv zPY~J7wq*QWNoevJPT{#B(hnF5B%eUXvTV_Alw<_0-%(U@P-k>A+ymJn%cI0qhxSd@!Y^rG$^400v_JhM8b|jM_WxC z4d>m$k%BVxe_~9y^%j?mzhQID7JS~k)MJtxE_dH1yk}^T%fULK^rut2m@7|>ay$cuwl3K_%nbk=)&!_td^%h6nuPJF*o#67Y)%vX@3OKt`1kyWQf3%jz*>#67$@kBBw@sTh~ zxhIxQG8Szw98p}DCRXJpi=_EG#llWYI_+KrU+<9+McwI*ZYAHB&2f{@1$&Fj@Rr}7 za^b-+P`!it{{m5-I}0Y7<&cxrMe8p;#Od5Y$*X~Qd)Jquei@O;5))cb$=o_mW7_{y zn@oQXqf6~pShMQNBBZ%zMBx=~g7 zaxC=Lp)k$?OFtl*e8!!&r)yAwRsj3xoyoMSC++w%fMT2+sbfwbx?pNd4pH62FdaX- zpkYUu{bman?$-@TG@#G1R|K9r1Qa%l(Ip2`TI013bfbUp4Wke|=_mnYq5l`4B`UNTQrmhWl@QgP$XIT105XDx>>qH)VdQTolE+Bp~9Ub|Ri zrh8I<=8@Ii;Cb_I1qw9jPEX|d+*$IId5o@fKjpbN*IR{lHQb%Wtux*-EC$+hgv(xati zM&vNth`Jr_M&HVq8TnL&>dN)W-o%!c{Ij6llXyP1^&)}`eaK_74ILQx1l!MglItB$ znr?m>p#!vmR zU-q3i=DiJ0wO!Eq=a4vjgV{7n(>SkOF3vCLho{lbl>es&V=wl^JvsK9KmCR=5mOLy ziWyk0zv0Sk>RtO=IIonCdR006oc9mujR(;3`J3oHz8ROczCf2Dov6t=EH-C3QkxTd zT281iyR;X5(ENpIVaG-52s1j+xf|qi;D_Q_|D(I6Qg+^ zeRBm8Ps!5c3(~xUVLzMmKa34kB~5=lo|klB?%!0Q*3FV)ytoT=^q`2kpiTq-{=|f; zVu>#2&E!9C!*`#P_{p82!(aE})ATsJI$}&aGScziu_BCiV>Uw0D1-%?;H&mq&atmX zjOIwFb@>7B5#6D8$sJRr3ehWfKWFWk_dQ`B{D^ngE2FSdgLel8c}P9JmGf7#*yCnT z^-a?e6Ccf4HglTaa|^uY4(nZSu$Xd?Qjx3J_~Lt?;$K4Sh^dLWGMa zj&-QAt5b%4y)8!MQ=SiJGmEBg9*TID|8w(aEaE)W#tID_VuyGzXK?$Sdo9A;`;yN# z&SNy#VfHr8!5K2^(?mvcw`i_dp=3pdRb`SZH@}IcnLX(J$_O#Bs#apY--7)2e3e+s ze8i_37Z_d{D<-LwqoA@~gy^{ysF#-EsnamT@;)W_eRpwJTb|jFGPEygbHS+q8CuJK z3;q5{JYQ}_Huu^tpS7o&8{e=ds0-!3Fr{tz6_6ikK+0Dg==ZKlxXxT4W?bEa*sbSr zvPmLNXJ5ung|ql;dcwQ=nFNfR&V7fc$?&dL#(r z*Pvn0hxSTwp3dv1SeoEUb(%ZjwMYZ++#PAj);ai?kRh@?zYG6DBigsBQ5^jBLZ~Nj z4)?AB`(F$Y&V9+&C_Tvfy3(9YGPJw?5H3~srsu(YM`9k-?rcGm7s*nWQ`hjS_goRu zcfMqZR~1%PR*SBA`wAS4`OXo$MaaFk6gOmkAkNBMTpYm6AN~ZMT6Re?w`M4m+(y!y z;rn=ZKLnjhJ~Z!UDLMi}eWZ@3(7-i6-a96}dWS`1G`-;)R<3=U|Hk9K_fF|eJ z6?vZg4VQ0sqXz3zaZ3FWl%>q+#8)MxLfkra=eyB67$zh;%Kg zDY^^yt_sxEZ#LGikML1((IkzMX#93)&YR+`Kpu*dvH$cY@zixFWc<~!iZcj>dkuJ| zAwljg6-xA60KWi5q^9dp^K%7SVf+Ju%x{TS_=$nLKVZNIJ8H~Tp=IULw0XKU{ZM`i zqn90=y;Gx6thfa2j<$T=TypY24}p zlUJ%j(|)X&Xm>@_O?Du&+{aLy(i7P|`MGiM6aKURAvUx+^ZVjG@_a&3d?OSGve21(#% zHM)D~2l{VGfzMuVifhmmzdTp+&(WW*?hO}j+K=MFTpt>B=ajh04i&BDH&|_CNK+ql zLzjD<+_5qvsYQFm+@L0GHC3m*mwVH#tLyRQx&|dZVCI_E2B-&i=X{+j`Sl*aB4t%l zHc_C}%Yr2??1{|f+*{nfSh3yGi3*tOzfSkLBx9f>b>n}ZxMj&%98by`=uJB(SkeE_ ztHBlybZwdo=}+uMyZ^kx?4VSH%Bz#(oTr$1I~OxkwWu@T4bS^hP`Cdda<)|=&ysUb z30e3N_6HB;l_+}kX`FI+i0A@U3Uk|ulwX_;Rr00Kw+Er((wjz1lF;jyh1lH9mRggC z)1(`RQ2Zp3S>PV@Vw@5!ymk)yN^W$Xcf&Hv641NCk6wINqn^VT;NZmb*v@moAwQh( zTlP9~y6ch@vw-F#C8G77K7C!dox`%+8IX6RlwT)sA@q}Y*knT=kO-YiW@5=Po>x@c z(=!x`thiQmyKYA{)$haxnP;fVGA4~| z(I(w;xPFSpP>*|fzW*93-_Av<+Fe-0KY=;Vw#?^T#gCP`w7BjH^Cf;@zm^sC(8)p6 z8!7r2#{HM;xw!sao>oM#i~fG6@a=IJlg=y9t=uj=hd&LS4_dT$v_76h#USNLfzTbk zU3?qA7r$Feg+_@oOmC(mT(U(}=1dSlC6_@F128#n0(xBZCHvC3*v`y5{|6)atj%1a zt39z?&4-;y-N@~Apm1rvC@O7@X#F&kg6%%lLN85)JPehEdyAczyC@5aDXyTQN^tvI zfN$f6;cT)c?vL7or-$s}ukjf7nWUq-wu(E)Hn_|#4yW&Lk!loz4-bvV z=f95_sKgl69v)Cy2ktDy*KfYXDc&vIs39>96B#@xAW9y=9)*M^K&R3oD1U4I=dkP;t^-c zu9S$!cy-H2qEuu8dB1P)UKt~~PO3a(E``aEJc;YMVTk`5$G(@3C~2uh=%6hAJ%5MX z{z^3WjDhWzxA63oCX<`Y0voDHij(Wbl^e#ib_4eSrPPrAP@iItXw#YxoKfA?gL^Z_ zaXji6mO5F{)~V-_>=BPPT~oRpbsQgka*!zX0V~JEf@b!?!FCy%|1_Grh6A9=okRJm z3_LU%g|Z%(P~TF31gS)Ge`&?%b!q zyhx5@%l*aMj|UKQ;46O5HkC+uoyM$Id8*GiB?c{y6xrXi;KyEP|MOdf(p&E4SJ~60 zskOrWWeR&4JSfOhpJG~u;$=@qEbPPV)Eook?)%O&25bKPjAh@M8pbr}QXcm;^*f4C z6st=Xv90(v^A*e=SW&!>>+zIY^h?lz*lCvo|Q?rswnYCOBX z7cJuCdNNOIkht~Ij*k9wr}{xT;s?7OGOoH%+3~$%;dDcq@Mol<$5h)@F7<`FR_DrFnRVs#JF4+BNcSX+Gi3n4#?nWpc#z|UWS&Ci=zA$ z|4xmqarbRMqUExrd_N8CZrsc9{)dB_7n%9)N9GpF%$?i@zh`dZCG%TmTEE8rCE4Pb z7|3^oAJAY%VA+4p6xr`7?EjWZzRZwClbbJf%>3vR`b`Iq<_D9?#00T!oe>tF@}~OT zPw?F|Lb#Y6gl+T%Z1;I0=1DkvTh6--@9jPc8W%aE{1RiCmouSR2iN`I0r$D5^O=3= zYCmvdMOV66D37r`3%Zori>ADNhK*6&F(1s?ze`033uxi&tsBiM{DfMgX0%P*!sm}! zl5c&OxxO|Azh-~-$;|l#liD~mJdPHw^#+i;lPUyfw_fg5M(>Bpn5`~H0mYtJ?!8TX zo1jUF%!Zs6UW;zkUX=b!owWA-LEIG=^0sJ4_0;#cGN=z7xljgq?y-E)R-w#WO{lrl z2rF-Ok~e&cJ#V|xu~Xa|DoYn$vAU9?k8i}iqCjzC6vWb36UDaBjbg!4Gx2#$PmJmA zLeHXKplozsEP853&Bl#T{HBb7Z+ek_$``EBn}t{XW?=LFM>ulb1Jf2qAZuGKzY~<1 z2kLN>ej;FnM&T>SQu0?Nid}zFa*m%%6$jhcWwR6UVLoK( z1zm`@@y4OTtk;#FW+!l%u`JV1#JDI5fIzCyKijOYGWbT;+Tj|l8=&9`OONRbj<}NMnhk;QA*czx$(jGfF z8_aiKSqE}?m5c?={e`l;7yHysz~WYwI8kax#>~HUUhtGVF~1SD%ZQ!q_k=9(D6e#} z;f#qQ)?9fDmHHgWWU-e(?WFO;Fs zx#L9UqxpE8#rbFb8=_2VG1O+(!f&n_O`oGkzlRx7344Qk=W5WM|J3N~dF}@f)1W3@ z8_vdCQrN3hWF7aU{Xg94XigedjPj=Oe~d`>W-e|F^`eHC_mDeiJA5xcfYOBm41PTw zmA^_6viTa6j%8v-bs-k;+2*Cxb@*=o3+c7pd5?V_S$z~J%C0NjKXL}=kNrj7TLaQk z)x`NH$M8*0g=Uz&5RcAhV0E-19W34tgW@OGk_F*i;n$r-bOY?cz?wZpg_U z65|p!35OXMV3aYKJ>a9!Q8d!KPu3JTUYvlIZ<|D4zc47hb>=ysZsF0n+7x0wScG+y ziRP{96x`BJ+-orr-QSq-Gi!$TRfn(Q-MYh2k?}x?nJliq$%V%8Dfn4$hBoaSOlAMW zrU_b**<;VUpY9aDp&6^sjYYqgHk7dsE1 zugn82l?>gZfEv3}_Pfsd8A4W<3EZ;PfbW_y8)U0HALzpL%L(7Nkca-5yPJI`?%~Rl;{}ZQ_ab154)|n z!}FPUtnM#PU~lMkDE0n;8|qOoDj@hvUchfXpZgV8z{TVb?p<5RY{0R+pX5G7Ngg(+ z#-VfW74FGbqta~;4EF9u%eg$*e^>_d<gb#%#$~?75hJ)fe~xxbY|QT4M4eB1ei{H;zH`XJ76EJ`>NjCgWe9K4kcAuvqPU6^8YEK5#Um z0QV6v8sv;JLw$NO)&M3SWH6%Mo(B9k41aq$;y|%3Ik7waw*Eal>~BuH7j&VjinBPw zOwkvve-PDLgI9?gh0rpk0jmv#(~7O4CYT-X8=^(i))F!7t_gYdFU&u)sVB{g94v01 zwgYv7lVk>7!j_lij)DPB}II#e+{<|b4n&Xh>uYraC*@$8B-h^^+X^ z*}jLJMFVN;=g%5zNs&m*Qr7GSF7Qn{WEqMo0D6(5q?bj4jJpNl-enc z;m2O0CC8Pb*O%j-$ulgiu_Vv@BJ4~4hHsv|Xk%dmf{I(<+Z>O)Tvv(CSY?vR+KOcg zpM4e;e?#$_BpCkOExva*#=i6MqJ@1X%6vzx>=Pt*ujBbL@7`V&$>GT}Me?`pN*mj2 zuVv7azFn z5NQe5vDUIx;yc(|cvouUci}%QS5{-Uls!6AlxW&gBT^leCq8k{ajCK@Rj>>6n8HGg z-_5)IzI+efJ`b^@tf`~WmR8R8K_#D6Qu~@wtZQFNU)`SuhxDXV8~c;xK`)B=q(vz< zo)p{Imof$iO7_gtr;Y@B?sm2vl)`#!TO0@~2a@+7() z>CXIj7wW~c!-EGkX{?eLO)|KK5vCS&@rFHj_o@)%7|PCT_8@lEqRi~&oEa4qpr=dA z{8r-Ee4a`DW5>7Ne{hkz2AzqrR6L(^65aP>f4mZfy&jH;8xL^1QGvGRY(f8=y&^r+ zjOMVP+$AqYWc2GrVUPBqgS}jX+5K41U`M~5Hj0HeRLK3EKCLTt6Jov+*=_4ZtL&mg z4Z8?5rtvfX=M&`dd+_>{dbG!ICiOG3RepWPa*Ina`{qgS|Be^gNqaHugc_+-sfzXN zCH3IE>G~ukiD5`2?94Ujfr1%bi_xb&%rYuB*P@BT%_-xy5!D^GrxL!m*F7+!r~^Ie zR_IzxJWq7{l?$E!nT}lvf|h%;zwP-vR8Jm3Q+;k=QE5EyW==wj`B4$u7T`>uGZ;7a z5_iDUamg0C>PW$d@6KxE<(4%1*xZ{@M=s795ywIy`v9_{{7QM zo&~=nPq*Rlq3&Wk_nPdp+F)?+hd3DNLU+^^X`Iw=q0sXQ-pVH7-gi%2$*#nu^?Ptj z<%(F??*n_SBA8MC5TCM4@S$d>FyUF&W+CNdPXB%99q`EG2)t!TxoyW`cwrY-C>zlN zn?&qgR*EjQ@8G;}1MdDcg8CQ!{@$L1v`s;%Re6jQ|J|H*?1>ROFCn_B5R-b1L}KS{ zOyBpMon%XK^K}N=Uf#t|lYXe+Tt>Q|1N~c?%>0-)q6hEWMQsjRwuXr1dEBj-xDyMF zy=m-3qIY&{;5oZL&2tH)=vADH&T%7$JTICtXBR@lo}#(8DmhGvlni@W1CKE>bS=oS zAoN=mq?sYLcs@ehqY*`&+9qtm{kg+pObdLsNOsgXqlbkZMeZpP{ldRVhLSJ!;hay1)%Ak) zj|Y*)6mvTA#avV`3Z%{vjeulFD^o(e&jzcdK>_wFRmaIrb}Voz$lFVa%HIl%clas?=lo zcSI$$ayP+}F1E?jAB!9x$B8A(i+KRwDWeNB?q(zNTOMR%dW#<$A7d$d6Qk3Zw^kqp z|D9*xxrMpRmu&IFposTP%kb)?0j3zG;`VSax_)mee*0TNwT~b5dKizYJX=^_6U?a^ zj+RN&@m?xaSQ{x&M)N4d+eV2O#a(H`ZGEH$Ul5B{YEj`cJ{LFG&{1Z3SL?(xTi$~n zZDZ$YTnx$t#Gui8kYGp3_RmFXRYD$nm^e*M+Wk zImvgtgD8y)AVX%F%o^T`=STIqBi#$f7AcYW9}Th_Y=xssT9K4!OQF_w_%t{U?SAIe zWgK&tdt||f@3NlKFWAqy8P})iv750N^M3I=BWwfbKA1p+x5a^i839yITD-4m>|YLhbmoFky|(uQ!`cQPylB^E1pBkvme!RE-8mI zbzeXCreD3;S5^0s`yS3zc#2u%XByD3R-62WzsKXPa+D=h$@cdZ*!63L!%rh-$PL5x zHZPhrUjcru_S}ydLM3~vgtxOF9;))3@Pokkp1O44b`fXSXF@4Xi&CGSMTl2#?6lJ- z!}hl@^P7n6cfxT->J>Xr%Z2e2A83@`L9_Z_5w~d;vp#-7a<)Mj8+b96SA`tg8#w>u zMKgD_=#qsbU=Hr0u9}5N8QGULbFVtQha+-PM?YR zA4g{yR^`@pVY<7!Q%bt`8j~)0I#*rH;fA{HW2f{3jk*oY{ih>3xLA|{A{ z@y+-9eXesUviI|>HRl-jJx|ez&dztFu#s+%cGsg7Q<(Wa%bz)Sf<|j<(Ghklk4f`k zS6CMcThC5n7d{*5zLXr8XF^eGUi3`bO5*xgkB;O^DBVUwC_HkcCCp=ZqILs@D^f8k zPL{kHZ{zWzR3rp7VAZY@xT}+kjsFy=spnI?v0$$7{=?WcpdCZ{{(<3;Q&9f<9DmhS z$obGFTo-n=koyeTP>dB%Q@Hd7od36qJxEs1-@;Jm5ro+GcU-korVPD`)>>DUge>ZGG zWRIO<(F1cb5t+EC^g)EIF`=R{JJ8Q9LF8w%!<4y0(QaErn!76X)-b1w$@P-2Rm`Ed z?@8wKcZi)i|6rNliMKB+c@N>w-p)^0!gqtS7S3ex`6pHu7GR}aAni@LCTXwRjdcFq ztN1-Gjn!C>nuqGNOzM&39^cRN4ocB@o>jQb zy@yL)L>^;aGWV=M+DNhn=HeXBWJ9#qh#w)G#k>(Jq}MzbftNR;?Dz?>$iV`Mr`F-* zI%l!m?uHneo&&>2T~N4WF{*3Xai=f=eHYGx&Ghcne~B}+p7Oi04BSC7qW+h|h1(7d zco*B!^KI)T-4?qsyWW`8J8nqstjiTvvo|9!eJq|U-4kn$?Lyf}C*(C32&3n@SZ_NW zJJqC7p1)OO2YJz}mkqc#Q~~q1^Y?1YSG2$XEisg_r}cOT7-&E@z6=kzGxT`0D}1@L zIV?PveY-!!mXp=&3|R`-+&V~Em?PyV^9qL6;?TaKIQ4-&0)CCCdD9i!7V~E@W;%O* zb*Oe2JH0r&9Dh=Wp35A z?hEI;`ukCjb)GnoHw>#BCP32cft1-BF;jO9vckuqCSW3b9SMbp>zVljnlG=1?)-WC z+TNW)>#R`59qx3wk36?Kdi+*eQ?|1ViUcODhoU+I0Km0wm$7Z79@q65fyny51 zttj^o^M7`&7Za82>B>xF+H@*kqPEtSYHmx@1K&XQV_VbLr`w?U`82l7F(UW4RLrTr zkAa-YUw0@EJ1R2~Y{JjHvkQ67qK`w3?VNEO4_Oh;_uV>7YuJqwkGt~y_yP{nRm`IS z?2_R32X7X?J9y$q%Vnq*Kf;wC3*o+15sG21)ZoLj#MTfj?c+i<+(Gu>v-FpV%nX>7 z$&N*D+K>=RdQFS)DA|Ry*!SDKX*Wtg29fm0C`ylAjrO`)1T0mf`nQ`UuY()Wl&D9! zr)CL@*W3dxk)rmxv69$TDPq#So4gB|IMEo~SpRC3W!t>>+IKJA1Znztw%yua5h3nE&-&TZ`-R3N*KFQK!#6{O(I=hUn z_xr`}sbWN(vcu$6y7a8u0iog+goOD9B&i)B8MxMlcNDfXw@ZoW_*5R!k$gBL^F_qsD2C+XNUBbG<33S z>ZCgSTszTG(@XNlQJyl5e__*_M-rb!A7MA=Gn#S-i+Md%Y0en-p3X9&OwI(EJIPSb zotk98@57NPe-RyJMfS-G6gE1H_9a}ypcTCTUEYrz`MlG2_*t0V_UAc69xfg)MT|-U z&u6TmlUj4~4&!4WRdHIFjMb!(2TdUtm?aLR>QK^;MKB%gL`s}lGj8NR z^P3ITOPSJY<)v`op6#U(+BEEFBBr|hll=GXq9}X+2C?x6MW4^wSo7vJVl(E6{(goc zLiH_r8`q0LYLCV6(Xq^wzbl5NI-ZS1%$`ixu*#aI>63k$%C20!G7s8KodpL5>d$5eI( zAH3^HedoW1w4FOiZ#E$Py`xKE=-OGGwx(6TkL8Wd5=o?HZ*;VbdRD%7wR3 z*Oww+<&R>dAxZkml#4A!JB8f*B%$cITXdgyNGw$yEJ?`Kf&Dg5y6Jlhf#Zn#?~Wv; zUWe?PDp zgJ{_`=)xq&3*zaFT9GFI0l!{2QbnOMHje(wvkxztcVe43zV8>hd0CLzDCS>QZ^ig? z8nmU0C#fXv#uT0{9`o>|G2WBWGfSP)%{17rK=kDxb2Un($!;UNbwB!3$|h6#QyxJj zQht>1&x36B?P%ne{t`(HXMtX_^Y70FvAQUf@3oHfJFfcT-X~2sL(D!7XvZ=O<{fbN zyS|$mPF#x8kS_H6 z<4nn9onv_Y!;Ct&3>0Qnr_kds-`yvElT=`&H49IdIVBc8g%7HI39MdLf;Wqw5ab6?kHr7-%H)7 z#wZ0*RWronq7YK10+jr{DawC1@?F!BrmfJ%$SN5+9_mcLv(F3rB2_XNW=e1Bw2^b- zCwKO%ps02g^v*7W`BvlvOfJg#QoR;OS->+=VI%aBhob! z;Lna*sV^BX$iU`pp>%nPHT7>R#TwZl@}0@~#dV2jtE^_$w#vYPFMe7O{NNaNPme}*Uup6F8|vl z217p~7ZRJ_c>lLWl$&HB&*7Hn74=JE7QF@wk`?ii^Hk$Y%{N zyxWW7{kep!?~FkE0Y}8T`q9--Q5ZdV2xp;tk==1sQuDebxjsW3>z1gI>60rGqn+tu z#Wm*t{C6y5bD)qn^4>iYZY}4nb?fs+)wWi?>yJz@DMqS=KIBiGpaZdWJ4Jf zWVl}zh@;Q=oP0@^;&%CzOmU|eQM4B^DS7o(w%hy^gt#aHS6=3<s#g$wn-DL8?v$dm%ALj@x+&5|Z(;wFKUI%ZrVq~D(b*UVBWF_@ z)Di@PDd3rEG4I>|U!9>=K2Bpf;>Jc~a?S?5Sbo+|r<+Zr2>Okc)&~L;VxFi1vJYO0*58Strklo;+iJ z83+n_^g8t6H9OjVJwsR(GygE5L#X^bBPm>T3#LEypg25HY|y@iTUWP;#OIB0J+e)5 zggdGyWxpbEOn}%I-+*a1|6(7cE)9AmMcX3{=`r)4y_fyuu7x38oMcXsZ(m~ga!Y#3 zU5*KAZ_sOSi74{kg`C!WgakB63TAD`zHZqFzELW6IXiN3mR%TMMc`)}T6-x1EsW3lS0+nxk24UT(3@(qH0dOF zrMk}VLpe!5vD-Kgx8F*{#jYDA4aQGlnj0n3zt~?|W&H^!W-Jm3HxijO`3(VmVkOt! z{1Csl+JvUrY!!Z?)>vYoT-sT1rX(v(108e6iL}F4Fx2OZL}e1YYhG8v_}{zIk0r|N;d8k++sg(!#>i2Q6!VP3q%muH4SFTmkY(#7 z{yaUzBWASBm|cLiyK6WrXhEq{p29uuAA6)1Lxr83(Y6}I_Ek*H1Z{Qy%KfZmxPLs7 zYL#Sh<3P1|tfoP?RfCu{Q6|dNm=UtNlUb1tm~xo+5eHN#e|Zz!*1J-@$4@v!yvNBQ z?(~cuty25{VU?T%*%vd5^9!HY87?Gn7n}Ro(R%Das3aOv z|6COEo%O0`Uy?S=7pIbV_Ww4L%5E#N(~H0RO_hjijDg0YE;LsA6ABVz5Ixt0Hfvtx z?x!A#!jhTseGwm1zezlH??cqRt9URjN4#Az8Wp`SqTyE$G2Yx$%>AfMr%p;y$f7kR z{=T~OCQy}vU2G-wbJXca_YYVxXfHy!8@DrkyO=V*7=8Ku_O|-F(DmDh7kzyxT((%a zF1Nz$Ia1`xJN)Jz!1Wqc8Zy(3E-ro`)_j$tm<|*ArQ}MN5AQ^O8O|*lccFJDW+MHu zG3m59P#yPGU+r?GlnCC3vFAK=nThBU)P>?k6Kx)`s`P}t3GLeGNh4>+NuK<@CXtJB zqdYmzlP^yaFQ0OL?~x~6+_8`pj9n|kD($eTe#KgBE?NTegWEY@zs4-2+@uH{GiV)O; zGj3NyC~MT#w)ILvz&dAhnhugvKlqmn{)n&&%^;56d37C0}thhWN$^Pz5Nxz0d+_> z2&7N!)g0yXikbZ`%<_DPd*Uo!ybPw35>xT8CIx$ARcY{*p``_9Qc+&4OWQ~E5%IIK zvG=kJJ;-w*2M+_9p=Coq`B~g;lnp&ev?t{r7Buj_8TDUmOCLE`;$Cq8OUGOaE$(E_ z-;ixMkuXIJ?P5t8&r7i^#I5A$#EY1BW-;P>on-sP{1f}B; zXUz^PW@3U#GvY5;P|At}*lj3F$yw4gdEH+8c5Yz@WIM9L62xa^p6x$=4%JWoqNmjv zBp#NciJ31&fMqdy&I=}`8y+~(i!;9?ds2sb7tU(VfZt7jO8eX&4tWc4IIb7w?hC{= zg8(Y#`&-h(uCTZo&Y1~9?71ZvpcYJejwZA*q(yRXa4=RkS<+{Z;`5UgJH)qmb|LDH zkW@xZhut2|?|*j1^g>N6Ojv|{XB)*nji2IM(q8=0v4KyN4nCexht+QG?Tv0nij!b& zjTg;7s6^3+8-&JP7n;Q9Pi6fm%;Q|3f$m1UjrV~6$WQ1NIfK0{)3~?w9J;>yQQUMF zdoFOVk~xOyC!V9eF%pmMJ!t%|`GxL20oBWIB+6m{v)Luu?TbfTB+o7#Em zpBVelp2qG{rUl8VVq8ZT&M72g@%jC@G}?u($>hR$?FMEmSdp3GPS{R3jT7H0nb$l5 zHKyutY`l)oy*I*ir5k28y@HiPHx%!7!Pbmo+*@6Y3YpQ+G}(jpEf-;TbrsqtlrgvI z77X`J!_2NN+@*7;E@!sl>~RGwZR6+6h$84FtQL-X{?sy){m+`9$(O=O)*uUmox0J? zlYMDoVJZ4-`05dwdH&ND)_Kv9 zcdOsF7Gc;9H;VBIg~?_98MZpnP>n`0aNBWIJ6hA=u`1N!<%z4-Bau<6Lq?xl#H-?N zXdR zv5k@?Sx)E`!I^%afY5%?PsLMtCn`QVNXQ@dqr>W9Lf_e&R`>CtH5DhtkqQS2%OY}^ znIgUn45X&o;iC7_D!Ap!3d_eQMbV5h+@7+p^zzQ`V*Z+Dw9YXQ9u{(RzpbmtYvZnH zuXa=o$``s-`ZSq&274oeM7BvMCf<~#a_-9Le{I39n=17EmNL0Ck>0u+_qGJya zW}uDKj>3LLC-iSBBVKOLb94T>8 zoFU?Cz47C}$>Phur^5XGU(tD@cj>;$0;uOjP>k7e_;YS_KR-kEPP_&GWvSQ_)|+!m z7x076xg%Wk$STbV{dmTxkz`LxHaVl4!cA zt$xaLqdSl>VSdTLCdAHcz!c+^FmVc_42?$^bYKj|ZtY7=7XPrIED0+^h}2*8g!Ib+ z$n;R6ywK@*=HUfRGtTku(n7YyNSt4-PBpFO)I+s{v#LaGygT^eCQUg9yy)!%4SLu5 z56XP@?0E4O`+Bj<=(!B%_3vZ0`D;vdm8C&9>-oH-NFTRrG8eF!8F&)XX}>A--`o&k z;XNs;TatM9@v&HPB!aBY>Z3BylYCr1LM^2y)HjEa%g=xKXEP07R=AMmzTeoX>44O# z;mj$&gHm@p$bL3Or1~{xl2~DW(ITiPH( z8$M$=P|KxlBIVCF%x|?1=na_Yp)B`&mNmJ7*Vv8U+G>m@^c zTqtCkFI`BBD4lmbko2C}(z0XQLp4gDWBurStf^C?`rr>}VrIjIEY4YR|6OnBE)4!E zO{3@;$_B~PwISOPdy1bE5431l*g|-&XhXw$88Rq21ewk3PM@B+XL3i`g;r)O(0tCKZy!{Q zlyDuYGjQg8M+M5aGB11{Q2IO#tFCL(k2kBap?sEb=J?5Iq$fx82@cL4pWa%8Wd41{9|TeZmkU6+j>SEHeU*vZEp}%`b+YlW(l4o$WhP- zE0VjROA8E5sNVrS8Y^W&w|=sp?Vkgs{kEc*AWJghJnsJDOmyQc?s49?z0Jyj?nDXI zuD7Q1@+D|d_MuA_$FRJ59d6HffNpn=V)dd}l*!-0o&S#WyK4vJ_0J$-x&iguaRtFM z8qrP7g5KvHM?HUz<9Y98aQOoKqCeuyQzPoNbCx1uEM&Z&XEjlahPEk!OnaOIx-a>m)86S^AR~<$_ z2hr;VZ_%8kOXcZZ>DJhP$ezodBYu_)&AE$>WJ(FVV_lLw2-A1;#)8hBxHm)#mzGb$ z@VY&agbc*a?jcABUI0(Or-+t1E}4JB9^vk77*_LGa&wplw)c2|XKt!uz~5n*FOvwr z?@gGl@ByQiFN2J1Ene|VV8-S{cv18d^LD+)`A8j#n5NFZ?*eh;uO%tg>yVS{Mj>g@ zq^`0u6kG)173xmL78%IDc?2b29GGog!pyW(Tvl+P_B9zuJ9!z|vzY0g{~u&@1b2}5 zPP6Vme7PymU%ndK4(>qL15xP6K8?nOWpLZE8dV3%QMT-8g*^O*A2hpl#f7qmTp}~Pc6yIeL z%K1gnZJiH&U*w5$nG~LpC{UW~eBm3Bi}Cq?&@1haMDbb%7M#!`iTr6%Gitan`E(Q~ zRtp-q=CF8^SdRMR{uYLMbnHaI{vg|Z@wx@ z9_!Iw>sh4^TV9B7z3iC?HB20N=SGWnzmhnux1_%N1L%kKCUNMY1LaM3r4q9QQC+D` zUzdX z?}S&I0ab@7VQY&&-cB*1xZ-G(B#e}(ChL-Kb{@R4M~J9j%G6_4DZKVf5@##eN3NfW zLZ-Du$WO_K<>p$bUff(-k$)ON2QFgHTJGviEJn=rN5~s( zL|uQ%Af%%SiwA45N4#9bs6K;GbEG#6bgr0PimX}(&St-W>7du#pR{I{_iL<^J%c1~ zGYX!42S+Dt4ItDaJ<++%juZET1c~7*Md(InPGGESxHVu@bzMjlJ9l+ia6@5DRk~7cyEGZ@O z9y1TWqt#f4-Y}2uV)<{B4pXIVy{qwJf*c*nGNZIz@^I4^^!$VdnjicYW%}W?!l*a; zHr^L^U3*c%qc)+}Y)<{$Z*!jB7!UOfnJ3x^Jw7|OZ?Wba*(3ZN6N4k#%iv(bS?KU9 zqB=5xy%r64aB8ykZ0*y->vSp}W z0-uM5Y+~17hGg8TKH}Yn5%_&5H55~JiwO_+!({Iy$)TugV(6-H7=3QWURNuc^ne+$ zfpR2&*qIJP9bd0}K~9D-?|cJj`>btvFoM}V2Rx~|dkS_;bs>XtKjw!`LjepZl%1=$ zU-*lNvOxM*&CD)zIQO z+GOljID|m{tYwTy!g2Ns78=`AfmtTn&z~0yk6F;s^7Z%^x?5x$`H;#pAF6o!PwXjI zp~)E%n)&jx(3}4Y)iDlKB|eFm!^)I2wg#T}?xKp%OUl=8VODB2x=&=rWcyuyhMz!U zVIZvu_$u7ZHo%Fw9Hg;H7_Z_^L>CPzJ#t%|NzGt()MqUCDFqH%v)wG1RKzaI6J!JYe|=V*@q0GwSR5t)wI>P>=Q#R`^}lN#cbw63B7xM8ujxw zA!c_0@*@kdqGC97^N(Wj$MbN?-iy`zp4}5^L}ec{k=p(fhk15(lRL{TJa60bPLU!* zm>H+phV{s!09c7_dd(eJHYxg{pA007-&M5Pcf+3T4kqbR410!# z06%PJkIcZ185r4WO8T38Df?71dW7(qvPn>DU=o7++tEYSE_BPV9uM+tsiWGSRQ`F)wC5`jlfT~ILl z8XWouQd^u4POo_jpP*2(8DNLn9h_VF8%(P|&4b#i?Ra;qjr-0Ik!PC>r^$gfCOuE19fBdT6uO#f+K6>?g3^z5oReR-#b<;=8PvYvUR{enc7>+amO+KRnH zbMVsImUIrS$K8Y5n8)r&-G1*yIM4PUzJ7sq7ng9C*#b>7|KLySFgP3y#PjGE7?YNU zTYkQ9UQ>eDINmWmi(%&5Jv_>Jh?2mmh*Hi*htG8!9qx_ka_)%D=b7!Po!B>dHlAK} zrLa%yFk-qrR(x@%-6dzSHo}9m2O-p)D8Zc<%oJcRLdvS;ShSIMz>=QKUh2lPpd;uO zr$h)xXlRQwV%f{9bX|)yl^+XLwVwE1Z$n{{AShoMfQ9x3WRfILh0QhS zec6~g0^2Zud=vBA?Z}LoEIGICz+lR6A){}^yxt8W!|t;%YjUJTX=P&aovt{w#*9KP zs*1e#?7DxYCl&>I(K%*XH$Ki6-tFAwoZ`>aQ4jI+unko#|0}ukwFGJ5zeKuEckz4B zO+?KaEpB}%FWn@4gL$~$M8!uH+B9jKSWquZV|yu4pQZWY?f4IPvHLH&UQ0Npov9pmTwp!|U)*sN&q%v#5J9mRVHJIG3Ek9P9=7{Yn}`UfhH6*dQ`p9mgIG zQ`B06(j3>}MW!~^ca&`0ioV?O-u>w@9vdCRb#Fm`+GS|UsRUGdcSuf5?JLG^d5G_+eMRv1 zF{RgT)v*_2wHUlPLqxkZ;(T4WcwP8j+?Medf1ia*w#~D|mi7C^wKsXf(A^wSC8s2D zif8dO*j+N+=O{cfn0;=zRy^^~gEr5DhRyv|`ckU|8qv=Xnxspu+Fg+l*NIM{Lw}cB z;(7l^*wf#Zwp{Uphr&s$xb8?7x-{@Rum&MTHr!8ahB`aNYR8&V+4cv_#eN6bh}n3~ z9khAbkMUv5eC*HcO|rUDWVB>CF83z-J;)t)J${Js^E&ay-4gE1ruSvPX`P2)9>GP? z`-%z`ndwo>a^~*M_opujvb68vBW(2drl&bZbfoGN8ZDWNSXY34lYihtDW8v+>ll<; zi*RXKQrmt8)w_5HwLyc*lQj^!G+1)}&dWGF7lIv_-@p zAGGa1hn&m)F#1HajhXpgvR!Ej^K{m?=u>f~Eq$r&LF09-srwU8+EJW}!?J_8TQpcW zoy|e#qMVSZu^WZ%=s8G#Z!Tsq8=}#zLn!rX!<+%QGD&b{@NF)5< zlepLJNPjLVQ0LZ2(L=DON!pLzJh7z#Qg4KmI-1mwTEqO*R8)H@Q&{ymVPcZM^Rc9Ms;(hInU+Mbx;Mg)m_MVo&|ocJj6Uj zW7=P~m-Dlyh4fEnTBCIV;lb_VS+6eCxMVZ#)~ylu8Usi%PlwWW2Ejw0&(js^B$d)3 z8t+Mw=X_>&-4B7bQ5_WbWa7;0eBRr7k!fWPo($N+F6ba?FkFq#+Y4ZIy(?{)!wf0; zWE3%P;CxGicu=zgOLu(4-oihU*D2dE&QhLYwY8}CUFI@6nv$x6A*IyXQ+293jp?FA zGmd%FHSVeRXm_FKMi^%!Y=3&p9qhDtm-L1ub0;f*qzUTDT)B1J|B}ux`OYlU9g3lx zso5fi4cDV?O56`y-WY1J%#3o5`_ZL)7X|e{fmI(z;p8nfk?fO?(KgncUzZjOa?j!T zod2-7>a0j@o`imP?5XJ(?h2H{KQfQIqjzTwtai3pL=Qj?%(#FmSzmc7O7HzjY z&?8fg+7fd)ugTxlogJ8zR)dFwTOnKQfE;${?4R)q9qHZSb5Ngj?tDbovjZ_j%AH20 zFT?fShBSuH<(t^Y@G#nvCcO$Hk7>IxX_Fouc+C8f{T0|a!juk=FsBIn&wTDSr)=&~ zIdYG=?S?b;{%k^9e|E=)cSD#dz6S9V`=f^Wps{J}M_gcuxfkc5uJ;~XK6#xv1^hWu z&_MO>YrN+Tpz$2p3tsVpx%z&zZAhCi5Aj5gy&Y(Htw?T;CU|?b1Hr=$=;?oBp(j3K zd5J3J%b3xZo1IwNdP5k$=eg&5&f)cL5ndj~UD)Axod8mZef8CC(ff;bUUyshBBjVMI zOK5-G43iDV#Ev1EnA86rj;%Z;>AAQ;a`^CBd|IX|wuw|B^C$z`i{^{+vIS!6;JXOL zrqF+!5mVbKg|y@0urITutEY6JsbYZG@tX8`QHt32$PeG0O=x(3d8(Rs9y?Z=QRcZH zP|mF3IlCjtdB{-Mvn>3(bzY2f^5tEAAT?ZlA+|m1$}{!u%&%94?o}UJKD;a0eDWgY z_2-4|4okXpMvz?KI?;5@jw*%(lA7EXk?=~FPHjvS2RXAn&9M)rSFaMfu@yYqSHZHb zMq=b+&iE^i#rgezV5Sft+56}t($^`FT&3-$W!)PPWYC0*dv=C)`*aV_yPMOy)tnW* z#ku=YU1%RWhb5l%m^s&ohVsvIZSXVf+T0}iM`iNo<`iD08$$ZtIgI)53}hYGic_iC zxVq;U##e2FFWN-t<_mZ^ZYgH;FA)7?D&dfyi0fGj+~YZf*`}U!Z&-i4uG}i-hdA<# zX9ZHyt_ou}S9&pv)xcTF66F`=;?!;}n*HXns2coB4COgQuPQn8wUxm$X2I2LT_7H$ z5%n=tp!m8}#DqoBMD{GmM;~KmQ!r`qT&U4L4Nl$I1D@U`wD*7-9Q*xI{D|)(lIAtS zV?8@6%(`D%!JV6=sa=KAke9;P`=R7{OqbI2&FToB7$ElgpBA-4O>x0`DxdGJq2)vb zJsi)>qqwtBXTD0nv~2FTo`Uq$7<%~U7~&SZ!g~`%a(SwOgxfM?VWL1i%B3;tYaNa^ z>XVD7J~l;VKt9@n0@Vxfk^9;s|LD>YyCS&tNk+^&bJ8iUz(F~7I_B+U22cb=ER>^= z;vJZ>sXx86(4myed%%SNvg@Xe!26%X*jZ}y*jm8k2;VD@8_`m&k7DN!S2*+cc0?Te ze$)S=JllsJ_0lGn6>7XYbs=lcFhtq6!QrbnZA!?2t2Fa3*_Uej@ifM4dWu60Qgqn- z6uym@Bb(oHBsW_Pzo%J;I2=7M5c65&&Y2_**{l$a9a8K`kc1BTZ36c}HnjipaoBbD z#9tF_x_{&(*42i>`=~Wd*>elazAK=vekyi;J`c@r-Jq(n8rNFxVMdBRV*0yc?v3j> zS=b99_8^TG1Nt(@hj}iXf94&7IORlFm-V8Vmi&G5^dxtgy-?jdOpMd(Dh7lX;disM zM0VnGp?zgJT&Ea`+1e|`+NtjZwzuQOCr?UDnWi@Tur~E$_vgji6ENq8IyEeFr*h*hINaTg+FG@#caKAoVnt7CFR>!qnqZMq?aJ>r zWq$vz6&F<8c<#Zj(L2s`%~TbBQ<*!g$hr6b?umxqzSJ+&jZ)4N?%KY_``xFY%3P+A zUF&f3^gcYVSER4Q?w}v%i64&S%wWSS$g4}y;BCxs@M*+aQ#tn6Cox;U6Ze<}ZS~+d z%qIqs{T3;4g8Q-Y1AMvvcv9>!FU8%&9&}`)su*~?8XFP|@pLh$ElHI^Qu0vIBbp8` zFrZ$KDquI3vjTPOds8yUqVx>B4RE6mwQ{i3yo`7acgl`cz-3O(P90)Ha@y&*=};m5 z*E1N^XBVzG#fw-+AF4Tjia8~1;)|>c>GG_2(pX;k?LNF%bqkiaGb!mHAW|sEki%S9mBb+<#syzqAe|ujR>D_MhbU z&4tLI z;F+D$zQu={R|U~Wy$j5I4x;z5b~JqJeb~LfZ;-u?|<5&$w|$ z33CVuxz2CVu*!^lR+r-@e@||CsZsTx5~!7RK<=0|J;*6Saijv(5Amcrj{vN-&4B^W z!lwLL2D>9Um|M_=`qaAOKbI}AOjO33PcA~UItfN9%4lC&F9|DU-@<5TjF@#=EZnvV z7Sk>9J}?1qwi$^0R~9(4bQ_-U=~;R$t`Brg)?n4`4I*rt27Pl5C&M2{MJRKfbQVU? zh3&T`pVu;YapLRFTUe_%94ZICh|iq&Rw=b$E(W`>qlaVf z7Dw{F`x8!S^P&CKgQQ~=X!+0{2n^%F8JGhp}6jXKOa*y%P3W4`>4(Ugd*Jgbj?8Av18 z71^)Yn9N;0$<8nl<_(6_KcgqJ!7Aa#GolD5TiTn~g#5Y8qdvns6SqQtx&?UGPZ!8sQV3{q}bYuU;a9xUsG^dVIc~TF! zFFJ=?k;gIxsxHZDo28=JoiW)2G8Q6rBf%o9K!-W=V%8=&9 zT>M#cO>{r#L%R$8DNla4=z77Q`+%HVu{32a9rIyq_)MP{Oo6)!#K<5!deza5ral=Z z1~_-28CyeWdYCHiUbdtI6$xU3dNn*FqLFuds94wb1^r68;fJlWu*klS!T!c@to(`6 za%qtn`wnSUs`NL}AoP3L3oKaHg!DBfl5x2=Fz=%^XG1*b^6K}v{?~%ORM=8??x_aB zg>JGl@kdWSM_-Y}2a{~{;^zV$Ti|#=1$1uRgzcomV$Fo(d=_UP#GqY>V;*c{(sd{u zN|;o``m*)lWQ8cuwxlrY3=#4!f@FM^X^B%hO2+jkvolIm zx#bv+Rz^_ydL#O^emhEX|A}*R#z<=ZK8B@6he$r!muJNv6s(r?1(3A?5H?n0Sz1a*1FJFL#*Sd|n`H5=Lil{6IO zM$^!->bS)`<~5tus8$|`{Qh1HXJ*|+tC!+$xD7UQ-g=dxHZ5Movv_yzoXW{iLI}@2 zo_kV9kv4a-G-;t6d*(Es;9Ra0&5)O+CCn+Rv6H58XFgzG!E@#isnJp9-R7s(No*d3 zQryV~u`J#|_~!H=&0Ps%V@H7KbB(C$S6l4aZbsb?)FTXvK#DPGMYqGU_dBuWg)JSK z--KoRy>R#9Se%Zl#&jiJWNsdXaG6GAb*~fK7CFOuX9dRAX<)!RTQTi_tiB$9F-)aL z=_p@$`qW2>%6wmiwu_fAp6~~I2XDiYQ6oa@D@RBkyh_LUsUs!zgLaFKyUVd`fzc)N zds>o7n!m)okY)rr*^_;tIc$bWk@j!SBDGu<-u#RYXymToLVr^2NXPvLru2TdALX9e z$M-T9_AmOg=Wi(%%`_sKtU%MhR!OeaGM_||-N5BJVv4UTozBpw@V9Pac79iy6&6G? zXKYFM`cVA0ZnTBDi4(Y6=EFO%xb>dwZS$m}S9Lu1;2Ej8H0AEU4u|W9P+-Z9wdiVQ zi8H5kGS7>D)#CJ3C0d`h7aJaZ!>awV^p%)3$SnQ-HR@z_Bmqi^j&$3{lcbKFL~ri& zJ&g&Z3jx*n>76(#@K{2ckrQ!2$XSy{)gSu#?!8$sC;{6SnkCla#IWDAH{1;V& z7b53p8GI}aX;8QEIGb)c{u}hJc#7G# z_cDu3ljA(9QwV0G0DW*j^dvcGaLaIFTGFa9F_jxoc)-OME$+lCL$ z>`ZFcqLJ<`yibb7F`i+`TT9c13NIAQD#L>Iqxh>j97BFR#KsPuTW_>y&iDh2G1!dg zJKW=56@!(WTNPK|;L-9JY&zydr6G@TZPRj`(9$8j+Y8|vYfh>ZM9UZN#Iw!%v?52) z+?06eJ+h$6MJ_a9&@aU7Goi99ds^}N8~ezdDebfq1-B@YriTT+yk<_Cd%e&yb2JL^ zAGW;gjlCA@;kI-%?#=51t@#ot_m9HO@bjGOF%t$JhPWjC6wk)E3u_Bm^seUp&d~;m zt^OeFS;_ORi!BIV`Vw9;nK;)^nw)aqk4bbq8dHLac{B+qOp-DDH`-adzXEitA{PmnSa$2s5!q88W&M?D&tA^`-}?u9 zonLTy;RT1a%GCKphOQemiugZ`NXk&9o3XBBa`z9Po%AVM%Z2Whzs8uAD$K|Wrpxu* z9SaW^Dy0cHdFU+uU3yy57PkSnp0gt@b*fm;`|$#oV<=RdkFMXD!7%s=&Y8ubb-z0% zea?k(V=S!FlyJrMA@oxu^!VgZbaeB@NnZCeJRh5WIy1TNUmuD!`HP+GBdd9#icimEM0(3c zNuMrm_=N~0a|>BK%sj%+bp)iNY^Cmh7-TNm*m=S!($`$?d zCvfFrJ?_jkqk)%9uwJnhg{@|^P42O<@u`F*`-!@)GDiA{3S>1q)3&~K@SOGmTl+9S z_tZ1!uDXTwKg`IK=kUAU{DW=AWLTARZgZd}vq)EBL^Y^m-B0d%PDN63G%Z%pMzj$l)}1R9o#%I7J9j}HCgu(_E$BHS7V-sv zX9uYx>oD0~gPHm+G(+|~db`dJ~s1^cDZyuWY2tFI1d z|54!Gi*ps{YZd9xPB}3|li6H1Wy!B{Xko=hMKV3vosPLBF@LXK;&pY6XmVMImfKZ6 zPkqyb&iO1HEYU2Or@sRb!Yq3_sn(bF zafi`7kv%06XBwip2BY67uz!)WGY8}G^sEt;TdT4+%a8VG`_T2jGL*J<2#sJy$>6R% zX!7y^W;eLdFLP&FHl2H7y=~tYRzK_z{63jo_n__*kxr0-Rq@AY4(EM z6TSLvWM@nb^bjjv-`a>7Q&mXsY)^^|G9V2tQ|esFZp@RGWPVDAISCeIyUmn_&v2kW z*Ud@O?l|^H`cea*`|JCq!DoUmZQ5>12@|j5S%MR-)6PNOwFnr;KE|3sdok_BaM)kE ziM6kCQb`<)+i7GjcdgJ z@^5-4jmi057Yb5x;`f^!Wk|Zx`_mp+A)b(Czz6GOnR0)e;s!p4&0s8_-jW4=lp;mS3l=Z^CUv>D_TSCTs zU+>>^4+EwO`ZjnH&mf~=lh=g?amK^eKNFtQr08Ch6z$m?fy27&#E6!olo{+7b(W^< zlaGrqRSWWA@B6Z|^TmJl+VtmDBhD4r2)$4z+P7m5K1C*@W4t{b;C%lKul4NrwI*ff z!#I+67LLpv$zQ(%N%=~6rd*CbhthG|4;ZUZ!+zWiSla5r_bYbc@mXZ?%On&JcnH}w zW$2@M7@PPtG4o$3;*;YsWy(b8Gsk3w$!g?;%}4GzbL#L(#yh_OyeD!amjj#d*V~Z> zpKzmn>wTd5sV|i-x25DQzWAEqK&#pP;?iRYwxy;+{#_GXjs=Qc(!9H~`GAh%NbleF z1$brMj+o;)Vk_V0q;%3?G0B^*O*k)BWN*Pnp0$YJjpB;JHLTt4PAY%dV~`dA-7A5R zzMw+h@hV8^VvY7G`egBGGM-HH!^_JC+{Kn5WJ7s{4QtVe~XpFcZml<=P>c{Msa`LMTyF^QVb2$WmxZ5 zvfpfUz>h(x1;l?8+iU?N~K1<$(zrk zXYK5|^@G;pQ)Q_HDZ3!51qio^mJmGv9@)h8^B$vvX8w z5V9JkVqVoM@km{tTspnEKek7#S*l9?cdFq_@;ebbPM^M=n1a|jy#I>q`acE$mfRUi z`&3#X)Gt6|RUj!9s?ro}MYX%1XxY~8vuW&G*f*t!5BCGa(2C!18gxM%GBYSF?qo*N z3O&hxI_)CNpdZ!oZbx*nL7J|F#-A}O7L=zF3gX-tIJ8FAh~Eestce0d)$o z_-Z|bUUX{}Gs>Qbd?!`vRO-UKFMC`#r$EPhb&S#thy9*hPkP{a}LptlGSS>{Ar*@=*~Wt?cqwWcv$I+$l)ETZ)tsifV3 zM!&YAbt_FI85{c2Zr+Peczaw@YU4y#vYg1U=c$6!b8qlZK8N3L3iLPk9t=Yd<5NHv z`th5ejW5oj>kS>!*j0`3qd42@lLQLvghhlj#oDJ}!o4c|w@jJzZ!d(IX)mHno>Uld z9*vFqbgM9seng%|-+D(X%k}2&P9d`A?!|;sTe`*^vgZfV;jzP(KJgxN%=$yvRO3m% zv-PReb{2E*b8+OEI(arqFz{CiT55XGd;a{4liLSMSD^tN(HIo6T{IY3(8+h)w~W;h z+wMBkhvHP`ndXQub~coDpJ&kbv|Pent2Q{FHlV*dzA<0qI7T%nko|9Yq|ph; zX_#@Ay$5F2C*V}2G_HEQl?>k!i@WT3iHleyf*THCwsO8G7#EpusLy*)>wXBbnTcih z+-T9`kqGJH2jyBT3Yt6(2GtTc-UKtn)M=SUg1Eo-rr6x1NXu#mi=(%_gz?HA6mHu? zOtJbd24-!=m_{G&^8FRVa*v{4?QqUJ8N)Px2Q2kux#w_KUa`YF3oF0eC zhs@~uk#-zu9)MX=s&uWa0dLdv5tsA<5C3JLZ1y0?xPC{z@->un<2>%m_ptrB4Q8|M zAXLf}Ll5$~zbAX^#swqAt~Uh)zeK8@K9ct6Q{u`A7_vZU!!H_J-(Fq;_x8UfziCZHDYfb)|*XA@ILGf_tq= zJX2u*d--zqO|bK5@&arKoW*X}rI=RDzOsT75~nMc%pzd!UWvbGTR8O_-`!@c3Zu-m6#G@GCQIGoS*O2|HMKV^1|9`%v=Gk<6I~@i);MQ*1!r z65Z&usXGde*;ALX9u&z8_@l-!9vL$U=(oIaedlJ|87@&Q|3!kdgnT)-8Ek%4{n6q^kYKjLOaHP z?}Y3$ZPDo?Pwo%j!Z&n}nA)!vM$1(xwIB0S?>9owBF-A1%>sV$9`b%DI)TF)^C$Z_0ix_l9i{1~6hvDfJ zl6u~o+&y*)$DV78=_7ndcY7tiJc}1PXL{2P`!`5@7$H)(1k#nabqJ~{k<4HDT@2`~W?VW}XCiM~g2FG?BIWddC|Sk)otSzd5)y?+ zrZT;%aKYj;yTtQT8sxt7i?B-jCo;y!k*l=|Jqvh;fv4=~SDpqfv1O-dl_|YT??MMk zKf<8inL0LK#JBJDknN&Ji){J1&S#BpBiPv;ehgPTpD+_#onnr8qb8rI`Helc{NQuT zGauTe$BxF6aiK;L&4v_oY$HP zsT*(c`1xz`@<;$)EtDb|ziP4QS3h_w+{4R1+2Y?OL8^v|B-_mWn%Df?zw-y5H+^Jg zm<#2dRibM-@??G@1&j_O^Zh2GQYQ{mcX-qFPbtFU6T7?@2#VEA5UU?MW8t(e)aR8N zy^ZXKdF+P$T+^K_&_O|Nd-XWaHj<@il{~Q|&`u=2Acb+?W%Ucy*z0fR4wb3L0t@iZ2ql@ri zj#l5k_B7GTQ>0&0qeET#(s!F@ddi(thm@n_&2~8cdWNBG#Yl=ii3cx#qvTNq zb{}rQ?lHTuds{gsos=c3nDtQm_7PUk|KL(v7Ub%TC}F4*$=$tzAkN}SID>52SiUQnukO$e*CqZ|EYXl`}{J6f(_)>eVt8kgCM#mhDsjI$@sC4Et`$;#tVHqNHoMgyp{BPvyJ%Rl-H=1_;J?br( zQUAe)(y0lz%`cBW;s?@4HHotJ|4)lK7od##9iiMm3A26T&ZKiy7 z=W|K-_j;uDQG-;VOBvosbMa`lp&mD8booL z_VlV_9|kM}b)RlW-*fsvUZW@W=C)}fX%V%HD>DU{;0%7c<+Y`&@OuI#p z(hH>YFH@18nv5k+`PU2d$Zz;CF~~#_iG!?2cD}ZF8e=Ge7v$DU-130D({>6kc?Xg?4*VW z@vkSf>owwFaaRmfFGHTg4s>4^h|zzmc?Yu(ruZyOZ+~T%a3p7hDzSZiFDy}E4rtm3 z{8`fvi&wFiWASqYRt(4FRo%&>GYlQw*(1BujUJ1=u<5EvOJ;F)b@?1Df8LY4rEKWA zQX677^r5@j&eUY}5p|{>^lX_04PXBO_t!g+MD(R@-WHtqTLqOZYZ04g2*a4e%tKp& z_rep$kIcsRg8~y3-sA0EK}jx#$XfmY`z?sZ?iz;A^E%*A=Ffd$D|8-+!-Y|w@LuaY zR#qJ6^VMriDn5?B?>1r7-N%r=a);SP<~%p&{p$GBBJ2{g82k4kTe;aHZoCz3HD)h$ ztDP9_WI{2!nS=M0`+-V^)WJV)pH$5L9`3qbVh-@^1W1i|hhy^>VXC_-9`A0zgr_5! z?bQz|mnyL7P9j!mnz1MI6qfnsqE>YP9__l0%}Z`!LB}M>IUa(`qXOiW*xY*$hTe3h z&a^V$W1%Vd>~ag?#QWPp`+fgKJwm&*I*BFQgn?frntA^JUHL$PiDHfT>{N`q>-vyu zP!|l*c#l;Dy=k!CQL(k`0^etCXwY9pp1=1-_L^wyI;>4mts$H--vj8Y(W#~;v0(fP z1U1W()6RChbts3QpCSdldX28XUSU;)CXG7XiU&sdIM!~6`F#vH(|l5l4D`mjnMSns zSEuAC=bciV%qdr|QN*b{k*sZ=?Q6E1_kYe?!~rFH(I?)Se8*@MILz!S`m-x#&c_zX z&W3A94l5OQC4)slP7xMl$O*&2`jWWn7kuvL%x>LkPdIF;EwsQqaRyhp#q+QpXStJ{tC~ z94cK7h-mo-s4Qp4&BJ8{*%mj^N9_jIL~UZ`c%=v)os06zigOp1hgp1DXK zzE>;=u7ykaDDh^dwZ#2d4Ya*7MQWdG;=`qSG~^{Si=aF6(@*#+w>t}~!Dd($vq8k# zb_&@JPjvJvzGl#J8t3%9|GzI?^Xwo-amFZm)nWKfNkjdr-ZaAWFji^TqlEqMsjZHP zDUzj!)@o$@Mjwx4z9U82g#NDTgXW@mcC8yzTkRuU7dcqlqE8NP)i`k>78_dBDex8d z7|ylAaeO!~UhyK8CS^*k-^A{XzI0|)sIuTDI{yb}^r- zGed!-OMl~?ekb=AyHG`KIduQZ)0xrikeWVQbWYgl)3k-X(mBh;T3Rk5PTm($Cqsq9 zm>fyRPzi#VgOPOp8T-O?aHZ6QKD7QoosdQ3d|Udy?+wf&`(yLi1$YI}+S!SlH(3Y^5_{g)jP0Vf?GUqvV z^8PB#F9u0z9Wd39r_bLw+poj@rVBnaI-ea}>hAPW2%6mM98U3b`th*=2&6~yg%E;Pu_ivK^)#h3xj z=)(88RjFshFLt5krE*`+K?Zq_ub`2Vj|4vV52<#g0}l7uO>qom%nXQZJIgs6jc}!o8aZ{y9JNTGLeZLwH=6r-(fuo3sTaV06MOts(lfWFyPCone$g-esAsXCU zGNU2dx@1wOMYnlg`CQ7BhCMide?cBJ>JIaPRIXr5Ezf*}ExA{wAhKUS`u*z(M(C`= z%)WQ9PNo7aM|j^@l8*_&SJCt~6qkvFg$wUHUy9q`C_AR=EJ#-y7W?3=tHwnXg0Hs*{i znx+MtO=&2~Ixph7O%_u(&&@ycJ}U?=`byMa_C34yhO<}9l>Sw<@cc&^zfB&ZNiGX> zi+OHR{T!9!V_@XV%wN|^j2nL*%XgRKijFy!mRL~q*Jl_pYdkLV*?jPVr?}weipi&3 z$!_p4+%9I8PM!mOO_+rzJq+pVG8ihR3*B{Qq&yX54^;Q*EZfRmc&+X8iU4tNH=5KoBVEoIk?5Au%XWRy) z94yD8Prs3Gtxhi2G^u=lz6ji}L!A=#L^Rh+_B8h(w+V)v|NAafvaRTX$`N=hPRHyF zL+Y-(i5XJqNY^o;!)=-Hn|~84=`)^X?ZV(&?nwM7OT&8ZfyVsaa7wJkXR~ySyFCS) ze_iDc*=;;mipK@ZJIFis5slJ&pn4`9eQF+JO2{epG31=RGRpuO^FOg#d#vr=^M ziutn)u8#;Lh_;$nwt1Q{6{I)A|d-X8(uO{{WB1N|*-Q=u@DfOzC zq5*a1F=Lz-wV8j1u&zSGoERZ{&xpF#8H)0KdxY0>Tbhu6RJ?p%BksI4p!Hr)@}mYj z)73{pTu`v4ecWMQTCzjz{GvyLnRQgOG*M{CxRAp3CF1aiB8;(V5Vp6E3j0$R&_{W= zXeo`qHfQI3^z*6^n|AU&r72f9-sZWq{U-#e6p9skN;Gd$9lqw=6up*yLfHB4RL&d$ zpQ1WkNz3EJ71zx$;4+jTS6nH@sa3EzS# z?>3`>eS#5jaeU8Zr}y7{tk}5_`r|#2AD@c>d?$Ob?7gTgd<=Qs>pb1Vo|_5!P;Hh_ z*WU^Ftum?OXroS*LyeAjZ;Dn^FO}(uRt>I z3t#PT!L^+U(C*?-6I<)>%>O8Kp9r#=p+FCVV-R%vx1`wChfd!82zT#;V#$F3_MbMR z?%pQu(L0i-{c9Yz?J6c*=>p}USrYF&&4LU=U6`$%Ev}zED=If@BmDYViOsINxHo*b zc-iv=R&uU9ylR2(?8{eh{ncsMOgt=Vzh%RA-*XJ&GtVnCTX?lr;@riaq;^jYcA^RM zIA3*Sh!#FfdWZoF?AaCY6tjB&M8_XTYCHN0S3MtNjzLcv!9BC+zm1p>u?z=49+R9d z`~|O%TbMDBEk+h8kcVz0CRi_&v|cc0?{S{6TP8;ydW!7H(?qZ9%(dI)jULe_#gOh= zRJ2x+J7*uD66(rqu|{MBzhfSi3w@fULOE|bpwGVBZ8F6ukNbudf zH+uZ36rXucQp^0_L5aN>IL`i24_R>k22Y29T!s$B&>24k)efTmcm4`b8GV{xz&q@> zE|9S|Bq{Fn*Pe62YTnPuMV7LkQyFn%SKx_WEoXol#U6|QFtt{i6t7C-Q>GsR=e@;t zKYipR9g}or59dc_&3gwGOD61SLDtXDu#NmxFv(AWOg71|2W3D0d+19?_C|>Eic9Qw z@Fb_%e43oF8Hp`kq*Y!aTvKczf3y=#k2PueI=-9ES0cH1BU(`UO?XWl)<^p7TAgl||Zh2>|+Q*kYo0BCox#w=b4zzW3PQi(y z9%K{kO>{ynOSgFPjDh`2c}MG z#g_UT7^Jxw!-l>`n#Dctg)*avc#iL=e{@IYl6*{==<&pyG^Po8wtM($LMo^Fj&Tjwq8GsgPapC+w})q zV*W^O&%c5Z7R|Uh=9E~yJ_}DLRbx_-yW~avS!}%BLO8IMNrzj_*Y#?@dCJr0~_fLhR+d!qsF~ zXlVWvXZ~FmcI?3UdhrK*@?%A)v?<*<)6Ds3bv|D(Ymm>zhkx)MTIV*5W^BcvTp*?Q zBa|FDfU)PE2%Wt(xE#L`#Y?J@HPasXuErEo^Z^wMLpV!qO}SQ882WSw;`uq)@N^FI zgLFt$&4cc3_zy3c&lIu9mPBSEl%MI)^T+J%jQ)+Kg}rGZe?PC6zeLDrOZIqrlB?Gj z&h2=S-$X0AblDw$rYy&sxMeWne5_h-BA*GEM_oS#g|;gYQR!x zG`-N0(BC9w9M65hvtn=3c*(rD6EWDi?Hi8t3!uv2U=6lR5OKjTs6*HF%z_Bb}oJh~b+9hX! z!T>~_*@JHqg<$@%H@QabMBJ*EqGl-b zM|u7ryV;AbE|8U&dq%@VE^DbcxVr$YaTe5A zu0Sc?rg-=x8fKF;X!gFZ!ukAcT87rf$Z}nY>%N#tzHcCkS`t82~$E%4A+) z<{VY(UdCCoH2$-mR*CvRJ$l_WU8KIQ5<9M1l6?4c5glTNPn-kkw`ZJ~m~caK^{b8K zVqa(Kl|58w8|)X6)y|aHTw1WTpiuJ6(2|~X28jE~cd&X-fe1Q~C{#8)!q<^z;=l6V z`KNk5#|@Wk?!?Q`d|w%{>tr{&K1YTWXI~IL-@DU{>JHc>bV_y)Xhi7D4=C+rNH12& zkfTR44lmZFcgo*TQq8`mf1Gh_lc$4Gv6A%%<7aAtgle*EQn~V5d6f4GGP^R9|E=Y3B5#G${U#-6px-YEB z=({4_ew56<7gxGmCP!AB{Zu<@LGpDvboFKmuIal9Z>xFYfL%ShuGlEn^qG^-yEUZC z>=jB%XT()qDbftTBerRG=UF@`Wvm`z;#`?6B?jS>ZV2EGj+Lp@e#fEno@iZQ)E?iBZrUbw7@`t znK!%fmRSQgdf$YnZzgW-G^Eu}tMPa|`xx!jNTa9_cjy0tyLuEZ9~Km8ro_CkSom4` z(lkfTU+OQy-^8JmH=7XP5P*vHa@1q`Wc=mM+BgeM3hAMaFQGHA#7Buz4NS>$>T3+% z?@jRm>hvt{4RbLhB)hd2*^U2zQ`;R#?-=`cmbPLCJ9`b*=HjPoJNAdm(-nTcd5l$J zzOWkQmi#X_4t_6Hh-G*F6JEcEF$3bNP#jnyf&=}i8|MJ5txTz^rV7*j=fLQrCEb4a z3s&m>uu?Xl0EauAFA0EDRSeA5+(F{$h2oOsSY$nZ2)hqsM5E(Yq!nGkrD;!v510Al zKB!PoSXXLW<5;k$L6y2LRwDaSdGU3zEDg|Y#QGb%*#YTKxfvUUOH4Y#6`42vvPMj5 zTg`nCK6hC7h%G;?VST$5$tp%X5AcIEzb8LyT9dllZ=rjG8E>DpXexKMuA6VcA4O(7 zx?9ryhT(8Lq)%}?SDfn^kHRN*bab#OeYohu^Z5@Fy)D+^ZK&-Y5VzD20q7He`sqvL4j8BGjxD-i$EJV%6 zY)Jp=%CpaM6x+l=md^;r3%+97gWveIb{{%YIVZiS3BNycRz>+YhTiE$nIaC`v`y&2 zTp#M!lb>On|1mh)hb**SLgBj(?Of2GUh&y#)tDVv`?wF?OwprR_p)%~6}uwV8ByDr zy;vD)Pj5dcQ{$Zt?1jIDzI&vZvB@kP=eO8DNP+Y)3O>)z!}PZlUB_+AtnsH*&L>ZQ zdm1f&1ITNzprBzTP*e}5?T;MUmBe4uj6Y)S4{aLOZb0{KD}+p=GCgCq$A?jWME`5b zGjw9?}c z_qPM6aHA>pGro=^T?f#{z7CWcmIT|eLuuKIQjD1~9TSe^F#|FKr^Of?^|^vc>?o^P z7K6fsbliKYO+{CZLZ5qhE7!6gW$gj9Br((0LY~(DO^4shCWw*JlzHvA@YgPY!F4Gb zHBkz?6pynD;tRawwlR0-DxY(G>C*{;xua&G*Sfwme%M5)<|m-qe<012>WR+<5qR}& zCi?vwjMX{%y9 z_}Y=P{y<7Mp9CwN%}Bp*#_qOiF{Ac4($4w9YHqohYOw?VML!T%?W#rKzJPW$EHUhCQE=yr`F8J;l_5nhhex-J=#)EvBXwV>&qhnP*oU1$?q@{MPf;+aEO zl=BGXizAtpqzl;}{8^AT0eyU3Ff8gG^tASHUz=;PaToEV{R&hEufqwCGJFbTF5t>2 z{FXfemGC?8Qrm$btC!-$aQ4sa-ihkKFfobmchUyQXm;s_sNUSy9-Il|sR7iUnC2V( z`Y^l>dsFQ$xxz~MGw^>jkcuA;6mp+AZ+EZ~;p{G*Y1^OQcdv0aL6s8nN0Z#rDhy_( zzDz+sifL^TTm4UAQ-60_yXce{U!KjgeD04F_e5uQAx>C1Qe01E>T2l3yv`-4@1;+F zR3_o#?P#p!oQ!{rG)%e;#d{OxfXDMQtK=zOX=u~ydrwizZ0&yv7X10vhC8G0;!?mB z@v3JJ3Yz;(5)fpFo6I`)*gajMusu)Q>SasEDjkLXY+rJy=p`oJ=tGV-{VDM50Wqb> zm*?!R)HQJjpZ#s7no8H*#Xd=tc~Z3lN^I@|2am zT;zP|M#1l;XtwQ1@$7jcO6{eno9THmtyPOoI7w0Tu%48@vK5<;%Ft0hzgMX?p<<^D zS>5bOD%;qlSYa*Rj?RO@sK@9#|53j8fU~&q@;WBc0a4dng1gDQd&`-LA%(qgc1$Tc zs>Z@c#~%-7*YY#QA7w{?%5Thid&WM7{*kzEnfWH0WwmVfUAq{YZ``>{OtsE_&?t5j1>Z zkw|r|67uZ((0rUOve#%}^&?;QyOaBS@(d_N$MqMC60)Tv_!`B@S1&{|SZqf84feuI5WnPkVT zPW0i-{QXOog=_4Tsl%}yrqR3k{y9SUzv(Mx@dY&}M_K6Fo)b2l9kll^l)O&ZDE>XH z7k!7yl53F``9{Qx)1f?n>#jm6U3*}hw+fAwHlkoRGiFaL!cfkjhwkFMZpsqecXgnm zXT8Wf%MJ53^Vw|`cT?8+@ENopou0z}DR*xg`Q3~D?bV|5FWjlAA7_sHC`b-j7}3r6 zzTCGyE_pG*k?*gr< zXkA@7KEHP2?T+=}psB=I}QjO@ppM11>E5p~*>?iX*y z=?mLMReT?kW@b>g728GAw(hik2s1l2rV8Wwf9N>hle88%iffgs?00E^UkL9yqdcf> z<3}j`tVZ)28#24|3vJ7?FQ3t~y%n0q*Wq9VQfXF@MHzp*c31J?bEk^Ilcakn@X zIX`Yng>C5n{pFHQ6NWJ+#UvNx}$E3H;a5;N8uh5~zV z?4K7EcvWWLo0PS<@b8__uUUnqZ_34Xg>LZPwisR`nk5^JK8Z6P2e3Q92qXPt5&fng zpHoL6hqmLh{9xLysE4iW$hn>D!~9P@TAtEV7&IGVvpx5ncfa=SwCan-v3eBLSTCu6 zR4T57CbAc58m|5<6$2+SBec#82Wqwo9p-Wk?YE9SBM-!%h+1*U(2c%CRB^xIfOu(S zNwe0x!}bFz_@nDa2?Z_CH&H>;qZ00v??*|pA0{Sqme+!rElyS9%*|K0YrYMiV|a%& zNeb3{2KVh>kC`LPxc6d3ACzn1mFtO3Bh|?BMKpGoXwt`*j`VL$I!rjLe&&ul^{L&8 zN6gk~)3KyEYotgcwioYld()BG&DdJdgAVZ7_NvlPysR|g=NU7dr8xtBdjdu{&cTmm ztN30r2N_!jVO+dDnxAY#)}eW*pYs|W|NMM6cQL|@&E+s?ek3tX7>qAtm}NLpN+LRS z@xX2~!qzuoMdcL?9laZ^F5l5T^BHtvQ}IpjFN!KMxsPl{#@v7Iw96DN106}-T$RiN zd_>|oGa?@|QZfw{xr%0-mE6yY-BSqEHzb`2nOM`DgfdeD3VV70`@C=CL_j@WNLDkC zRsk0We!w}!{rGs=9>1Bj=4UzxryscCm0v0RFXo~6@i<&R_Z+h?S0i^M=k>I*VL0Rl z_b??GHJ;!w--$veY{W|CXe?)zw$JWJNENwa{t;%j_DIFN$*wf+6Fa{Zmmp)LGmV|- zM#JtV^E1JZcFgr9m4C}{qt^}Y_e+yeWv*nv)fqodPp`|t!Y3y`rc#$V^Vy9#L zdmZvx>%p$Lo%q*9l{8+kQ)xm4ly_^;+gYD+VQmf6D)ea2YAH(N%=e;S#@z2=CZ78U z?wlF3@0UHg&trtrIe|6rHRxJ&f^T?&vTuHClJC2Jz3J=F=LONuT4MN5=FFz^d?(UR zxW90tQ6-Z^wZbhNFL);AAMq6L9~ASP+D)w6>LSq`_zWNZ$wB9oG_`--D{TPWpv9yVYV|t|I+IE1VJxsc32^b|f^j_uZb3k86gtt`hyo;hxE>pLkZ^ zj}F}~Vn6DAJX=4K`tuyO&#D3_M0n7mgXiI(SOkxEt1!KvF}uqP+0!=^E4*y+ZzcCY zJHxTVw;$GjdV;Q>eCUy6HZ)rGvG_07Swel`EFT1=%s@I5wFN;+W_bH7Q+(%)cEs{2 z2-x^o#4l2yQ4@4=(&($umeHaarptL}WJM_?PorMPAb+F>y(m?pNX2b9=hlmc+*PEz zB@+_ zEuAU)oMb0(_6pH?nX@rR`(xyHOImXHDJDGE#-3smddtq6r;;`iA8toKvfsje^-#?5 zU4V{+hwMXkz^&s>SgLm);-)$}KSbdN|J$&oE5>Alh7XeBZ0j$WTyP`Fb~!rdz+JGp zf%J*lgmd`*S;u!b-*o=WT&N+5G(U{?OmA{=O%x^;oWryV+ctA^CDXR(+tvk|*raG&N-AKI9;8SvMj&E0Ltdh2Eo8B;AG>Y3(ArGXCQ>gYh{ zF1XTU&j0MqH=qMi5}qN~GP^SwZx1QarGu{#S{RSo)>iCa_Y4mexCb&$g|xf8#MlGf z$oO44j&*jTm#sW0Yn();>3BlpmM^1NLE^dp(d=^DKVUAW!Bq zN=S9%InLGX^6 zgB_d=)}S(V2|wG45%yDux(95;ajj!wdWH>6;mgbP3$?;H-JBZkZHBg%flv){C6^XU z@;dumtexM?nQ2E#dfg_5y?TlIF8XvgtXS-qZ^hjO*O2H^fcN)Z=@zH6ZmOi?z#R#V zvCU#8?I}$EXTy2<0-wvwP|taSr}#iMBSENban4> zOfYT5VI}rZ=w`veRf=M7bN{!qOvHUUj44JM)K=FG`cF1MW??HbqsztqZz)(1e?o-q zFA~w=NeFryA(E>&9}$y=zE-IsY*?_6>zj$%Lw=~6JOo?n1mEjc;PJ~q^z1c=?22Y# z#B4L>18{$Bt}^-FRTIaLR)}}$x)h$2lAnI63l6?kq3ur(N&Gk`;orucs4IQowoeh~ z7N_$5{fm$Z?E!7(vaS2tAL=)Cc<#aMmVWxQ-0d^URYqav_FnW$u@ioK24Y;0A{AWw zfRX)_G0~d$^xv}}f37=B+MdI2@)mrH_2(IQCG!9-An;{5jwW*c=bZ&LkA8+@#lFz4 zaHFt_r&vF90;F~u(~8m|n9y65wk>m@vAfs8nwcxzxu>*o$#lHv(t~z&u_5iLRVX`X zL>1F*scbcS?!qjnX|*-o*>D%~Kds2t#*Rk3o{G;!Yw)o$1~=j&u;=`A?h}uJgVJiO zF4_;t%1tO-bO)Z9lOl$pG#vT5h zr%~tp4KM9~@jY_`4u?I0WLG0J0@bO(C42U~h!kvwOK)Cc>RJIXT4l#;j1YK+vq_9hP#k) z4`$=^J%ax-q-f9`Pmy`|0s?+EVBJeQ-?ZeDXzk0arSOYl%fmGBzj@NZ5^w4`ph=9- z+KESwUd)F|5+lx?MZpk9k}}dIjc7kCxn;|4J6n2M?#DY;YdorE{?7^Y-M567YrNs0&n$uQrRsT`mB8(3r@v~$Jh7{+Qm|Y7&eMqfAp!jciC05 znJMDWd28-Q>=LoE&Xn!uFUoxR=h0pivTD0fYo;WK`GrQAD?i2QHC=~CHe8P~#A8@lM z6~P0T6Pw+g>Jx)N)D&E6<`39w}$B=$X(8R9Y=#5znzMc6gaY^@~S6e?~ zNXkwYYXs8BD}UkWy;-C*IMKFoukk%tMi?-s+3?>{NoY=p5`BgM0lCizv@D)f6m zF)2hWc4n4S-gt2t$Jw=3!mq&H45xh=;8co8XI&_1iaft;=F64=uPI8n{>;t>%Lt;uJ?;w`* z*WN)3gZjyl{7glf_31r^ySwsErx6>{-=e&SD}@Rb(pcSsEo95@j|;dmM3&wKcA+;F zHz6~)4q=PfsgZV`^B^kBwpXR~Q3wBzqw@^txqaJsJK8&G4=U}wf7fvmlCt;A-ut$9 z3S~=)WMwxrkfMQ-ky&O!D5InjlE!oXpZE9urgD9+>pYL+^AXn{X^ZUdjl!&~Nc^W8 zz`H1cvlF)qJD1C%W$JVwSd6%Me(~xza|*dTd|13BHB)>__~H|GpdHhS5sy zGCf54&M=<&?!!0ENR^lQKyyS85=w6HygL|cboC@XEUjog`x6W&iULDF3tH=`OW*7V z2={^3v>{T1o||sOUGE5SzO4&zE`9E&CC$*~bjnp}y4NagZ50 zR^-}lO0mZRsC5WuGs1LfM#%u`#_y|`k9N!{;T>F-JF{i_(27z6QZA{MjF}orsV! zC^~b47`M0>CT?-;b?-|fkIT_nr`_Cp9!y;;)#%r<3_M9=C)pS2|B2-=%$Hph+9niW z;{&}vM^Vha)VDFKkW+LM5oX<}fxF=Cg_R;S)q&)WU1i@eRLMnHc1L6p4}Czlb%U!$Wv? z+-X7s$LUe;{GK!?L!BOd?na(=*35^uBehuOZTn;}2Sw0WOMeQJy@9T0dy{931r@$7 zX6CLh_1#l}qk5as^!O1P#@)i{fn(vh^(mA-KgPZ(yJ1pwnSEg9q|ZKrvqj8tFY3j; zf=kRfQ>GbHwMjet4(EHj(A+a}6c&5 zUg##6isAouVdNPt*jio}?TULb%Wjv@Y10xb$DM)qbqNBd#$&C5allmj=`h)N2>0~| z77S+J`miPY5%JYW?6cCRmnQzy)%~`J9brbf^IRxeN)|(&m@*gEkN1H?#jp~d*KMAH zt{(DuFS`#{Hrt}uFJFX>x`4NzmNTnf8`jrluyF0ZLqqnLeRHQ5rqbm4 zQ4!_b!#pgJq05F*LbI#~&1#Rp>nvUJ)+O?r${dF?hU~}nq}XP@pL;0NwW1(e994+b zNj)hv)s(y1AJ8q#j{FapQFH)1T*kPN!Y(~ZE$j=w&S3PLunY6!w6Xfj67*tcMEi{) zXs;ZEwOc2_?#Uf!zH_7X7hj2C9v^Uw9n1r?^2MT#YcSs9OM6e5!tTugo+Gwl<)9WU zY;l0MlN8A^u@wrm%z1~{%`97 zEXnNIF7#Uc2$p6PA*s-oMh`iI=w)Z|YNa*R7o5Pyeie9ry9U{Hb73yg#boztc+cC7 z!7>4OCffnc;Ul=`*c1D1K16Bvi&zr56}2g^5VW%t177aJ^b;4+UzK@tTed>$)m)rm zuJqE@aop-=Z|-028(d|Uc*a1!r+Cw}$t&@@(v3ESJJEjs8Qg7hr3fuSmvvX6l>Lzb zdmJh6+;|M$&bhSXpRiLqR>V&#MEloH^z7UvLfP5*Vbv4t{B>HQTD(~#zb?h%cY^Lk zzYw#T6R}EJLe58%MOt|U`$*i#AX}4e&y#}YQf(vzo6yRW+!eAnL=AN5;`jqXgMJB5 zIbGVZjU583N>H`Kibj@8QS|tGIP^`O{y6@_`3WC6!|_zi9_Yif`992#cVJGxJM)5w zmfw-(UQ=%xAU}le8wHVZm8Qs2bf6iZ2GIe{Yr^WFJLR#Dv*qBt{H zA9O8v9HL0FC%O0g%7NOH>M*TDYo_+BBxJ|Mb9d zaWd`(t{q85>31cuG(HBp9m3%%N#;D!i6kFb%+8-rg#KP{(f3A}3 z+VCE;S8d~+NJDutM*A2|u}Nc} zn55nxZOaV(-+h0I4IcezGrt2LKP`gQK~E~W!HkV(+!=QrMpuH4;BV4*>^h)CeH|6? zG=yEHUAj`b;U{t7$OnA)??!j}{1TVO@*VM?G5sBwhTyil(9hJRkYQ&aKR21#c!tz! z^8lwm%kjN>4oVO9qrdzf`H;UAhqw17i}p4c+AqT1yTd7ZtT7TdNx`>_zmd;8F*m7| zzkM3iuG=coN3&b@Wjp?ERHYf;n|QwJK~^W#=pN5Jl_uEHrg|xgN&bw)W>?Y~atql@ znxVoyuZew`5&DPSV@XOR8lEC>q&!(pRHeoy4dy8HrgI}(#gUt)Sftg59)I`3)Wny< zTO)*go;?)GmF#EZPTQr=fe5|ZjqewqFh_bUHjQPbk^Wtn{MAL6%pAC?-{XE^p;&!l zD^7+n`$Kjv^I~i;VFu5@9zGVTF505qL5>Pf{zQxE6aTDcDLTxIdY?u0lAb$jF>rG= z`uC4Pz@;wY$YE7Uw&!`MZ2Kr_a+oFx{g)s>ZI9&HW<#-q9Y(<(jhGV0&i2t&Vs1zq zq|%h>ZMqwbr%I93G!xGIyHI4f|p~y8A*f4ZIf(+zHBdGv|r%s{pvpmmhUSjjZZD>1Ei~Q1;sJqvdrq7ON zHqUG9G1sE~pO?WwyMaBA^2{Af!r-m8w3->-^Vm79YG6zUNBNV|)@$swaHS`aUSw_l z5O*%E<98-^SS>Utvg>Z{RbW zD{^>rLadRBht%yZR97f3EFNvclPC?c?9R-;g=a8>9kBCH@UG2Cnx?1f(@_;OI%fO} zL4LXv&2L_xdS&`sV?kj-J*g-u9MQ?WNv_j@rdiL0>+3;ep~f>;l@r`+?MJ)!rQ!YY zL+B{Gj)8nP*z#@_I^Ji(^k^Eql#>xt%q$B<9WrWRhVok(3N|yL`H~ZGR8?UQq8!zH zOU2b#S<3a0rG)J~M;@1pVI`kn6nj-NE$a%l70YpE{)TW&J%@lH9+GWE72@;eZJ6*+ zUG&|QEXvQFMDCk~V(K78?ujkOBFFA1*t`lhO+-$o#-sO;&2W$GPbcQ6;JVxa^kwec z)HWTmxG<3WIWp`|G$-F((vrD6KffoZMZ+I@iJwN1V&T6ns9s}^wXRao$~cC&QV)Jt za3<-e}H>B|$P2nXE@&m$*{l72Zuu z)TXf$EvWC_FBqMtL5rNcXx7ngSon7i=S7#|s)-Z6d+o)S%8jUVbAzVGI5;N?9MgFO zw*{advMTt>?A6T3LFCh43vv+!oT=$e0r7p2@gN2s-t8DY_cp3d$3kNbb5Iat7uwe zO?zJ@VQaus=7kv3*elEhGEIh6`C~k&UW~NOs>tZ^mAy!#p%-C;+QawpurdZEnign{ z%D~)+8(8#g6l64GA=HY|^Mf?-2Q?S-{qx*3=Ew$cSAgI6 zGKyHS-idOelhN+a`>r|6=!hMO!TSXb>SUym#RPoOcBXOCZlths8~!WI#Af#nWPR(+ z?72c%X~>ZNjG>gftO%L^eMQr@0D89ERwO0eM>>CpJ1zE$Gv8lh!WKKao^(B+-0%wT zuboMKjS5}tY6m;5g&32g&+N+y?DCm`8&;a6cJikPxzQgMlQe1AHyK)7S%UGLTMNIa zOyMtXVNWkDTH5gq3p!rFM7KmFvuD=BP+8=SP{Eo+T}qrcFaN^&=Yp82IiioM&{h|8 zDD1DK=%F*2z4fEaqQ0WF!-DSr>_g8cZW9%PyS{xS;?JGCuo<9^>(^X`e(rrF+U*x> zwyerCuzi8fOegH*4o0DQZyKqsK>p@kcrPDJKka{VCRK@UT^L9o_GbOY+2p`;=5pwVd($lC>6jyBf}t)Fsua5r zdeRB2e7wjO0o>>7k4JMph~Po$%)RcLxXSD$L+XTWTlC%xFu zd8%={xKrPU9=XX<{M-$kKk=vPp1*N^_IXGqZ4!@u?-%uXFVQ6+RG_gAGh=6^@LnuFl+snss2b9U8Eqkm{|Y)c{XM=uTflwc zU@{0(pw>P^a67Uu$>ey!`M5W<&$Z#$CmrZF&cgKrQZ(__SX2huWADnYq-1Ey{huoQ zyKE7VyFi0(&#gr8#@qR|M|x3T-_MZyDqV0#_anS||3lLj8S4435+858#jMK;R5$J& zioKZi_Cb~Zy)=s{w?J|Mm&EkD5n`)sFzt^$FK&hZ6ywJ9Czl?&c>dgl-Y0%RJ%-_7 z1N$Y23#asI#+2dchdT-g=0M9hT@`WPyOP+qj4>_&96{WDixbA43{P ztR_fI3}ngokv!R6SuH;P(4;SirRkI~ET~%EmA>V0r~C451f+%s^l5b#y<;c9fAZk~ zo3fq4S}_F%148n9nC}xcW8RAc*FV4^zXxsVYX{X~Es(6{pZm%k;_ZrWIDN^Gu8nk{ z1nC&8iBhF=zV>9XG#*KfMs)Uz4SkDSfD?U{Xe+ZU)~5N0!Tc6I%w95?y&J?N$6gej z-@$CnTyf#B1Eu|PpfTJLSig6vB=+pCuqt_#sSi&tA+PaH~%PX45UfD{~JW_QlygH7>wq=Q^6%!>f?P9+e=@dRflsg za~HEWi~BkuUNqz1Wwe}jroYTQ9C14j(ly+}b?-;kj+t1}FCI@U*e&o&n?Cp*z$~6; z#}{^|z>qW8RBcO*cjd_2X%^o7$V3n3I}Nt%4|%=AXkM&JzcYKIWkELPayPX?Argk= z5u#&853*i!1}Vu$MW8z~TU6I!eS)Qk;aHqCy$w^lR>**sTTQzC# z%o;HxQi`;hf4DiR6iWA9Y4*%(crBBP6@C0^?c7+b3%rVBYIZbQAxoHsFM{b*8TO{~ zZq+gpQvGYNTJE#tPrydh*2|IQOJj0)qe&YT4CwV13wk`PJI%4RrE5Ld;qlsl%5+u8 zW~436DCcgEw+9u@XV$q#0h-8@!ep3{qML?A4c_FXnu=7#Xk06LitXwraOK(<=6Kx4 zQ2s{5eu%;PRoQT4*5$rEw-7#}4h1mbeGEG;mHFJe{KXB>Wo!rj;5C^ZF z;$1H@AH4Lj`M@sh&rv4jBXW4Q_6+xoI)vT(Cz54N+p(ED3JJwiMaJ5AjJtYA{4LM+ z@7?<}cAEv^{?S>?wDF*t)nPp62*%GPeaK+fa17~`@VC{L4(e!fKWdMdmryFC0#zyb zXrKr@YAIH6=cs+OoXA@9U39cZ@vdEhHG6&vjh9JS`fn7LCUrwh!ft$CAO}U|GEuj{ z4Y|~vj7`5{Psu=pjP6R;O&gH>Xe4(2w4|%ATDkM1552e#K;~J_LAmfdvkD%{QK&i` z$Zx+|d|!MW3N!EEz&0yHZtp>NvR`B2T=w{}OMY8jInU<@W7!LPD(F868+K`tbi5-? zIlUJ2-H5!-FfVcZIxHT@UhUI<)PLD~v?ypZ|G=604{YJNnF&<{dQfcl5^P?~Y#~$b z@Og|u@t&dlPKd#CnMIgx83v7u3*gpfj)acIn4EABG0gMW;ZiIaxz!S@6`S#Ct_gc) zwXwJGF_x_uBu3PX#=&0^nB-dvJyC-pkCSk(TQknY*C6rOa?B1c#{-i;7`9Z0Tojb4 z%Zdx)(RWSaZZIi@{+2wJ>rQif>yyRcXkl^7j_{71-U*y#SYk;j(=Nh%{|(+jbEYlz zD!RVigPa9r@C{go^yPr*_$D~rn1;_gywJY<0Z#G$$}oIBta$b^^I{=}^^AlLcM9&B zyvESDC`dTp_+jQPBp+CX)tS+l#XI@s#}n{;J5ag$ag_I&jftG^ypn$rsiltW zHuhqVpD%ObY^m!8?&JJk%M7ppI(ONde%7#Se%@Z(PE({-so5gFX9{M-s8LwjMIn3k zAo8jjG2W|FVs~MIXgYnCUDkq{_0Nd+X18Fb;z2J13x!_q{aC2rN6Uk|QGb(Zcv##6 zl`x8 z$Kwrc>gDTAr$!J=cCpP?6&O6%o7T$JRq9Xk# z4$V9#E?wYE{em{wo_{Ry;h9gH-3Mgm?-BCbt5CULm9DAt_u+E`f?nuRSh52t$-T#v zDrI^ZrBAmNtB^G2l(^s-gW1e8bSSzmx$S-kbzjc_|DFlGQ|U+vy8_is+fZtqEOuQk z;Vf(z9R2?a4V@g^Oj-mV^RJTg&JVEst&Pa2a>NQ(E$ok)C3H;|U}mTTz8@Rt+gfje z6*v8OUiC-p=W}gZk0B^O^hab=Xi$lRF838&akaycZ2FDB-?1wsxl6e7#QewFT_eQ& z7n(FRV=u;UY?5dnlBXF)*Klc_o){n%z`cr>ocD_r_ci*HU;oe8eISN8z|Nd^c*kAO zV#znz3ej|`H|NtMMA;xYydY<{r3j*ylP@ocpKpJ#_Ty>4RcFn{cbV(oRRA$31z zPyX9oC~KQJ85miS%R27xq;l?Xcn`AqcoJ{IJ;Vg(VUlm-)21gp= zX-hBk?P%3xeTe_u=-nzlgE;%rZ?Zcz$2!oCDqqf|a@S;X8U|@7(9nBj=)ds@_ho*< z#<&VTdy*ig>(GqhweaZs2mki*oWFN1WcSKYh3-U{c}Uald$P1^@G+bY@FV5i_mWHI z)sT4=ObPqVg?sZ?yxQ(TPgji*?uqx{?VO3`u7cduddY`FA6`Xr4aqL6B zi>2s#%uwWxzKp#Od(y!oUG!a1h`n8Uk!pto^JEV3-~IoapwXOsssZj{aGu6tAyH5mQXiz&nNX^I9~hyjARWu0r*{PIUAN#>R&=7?^ww zE%8TT9^*xQG%um)&^DAb2hsuUQ~a$y!~6p$@(xK6Lx!x!V|!J)KYxcLH)IN~vis%a z;1~`!Rz^&X?J-53$*8GZv`c#~I#Xe@z&QUlEU>U0w*olmwhK zWWL21BU;d}2;b&^fwB&39YLhh7b~ne!7dm3u)k3H&o5bhM8}VY^MC=Z+r`CpV zQ0?9y-#;0X|B%1v`hEj~tX*l|2q}^*S3$$~@8}zxi=O}7F=$#n297XOEQ?Xc^uN?DxGyL3Me4^q`B z#>0`qmhVNEIbCR8c@UNO+=kZ=V_F($Np^e}=$OG-u!p)7821KqW7t2sqZd`%h4OtO z5b@*oV)&y`_^Z7FmKQhSU7IbAP9BXH<4$7;_ouFNPi~+6CsEp1gI3d~S>5%$s;V9VLqYjRZnMpcZecBOaU_T#|mQ;5D}$M^ix_K}4$SBZOzZo+5O zBe*<{5o_`X1+0|4j({<%Bx|nMNS+<2Kx-=nT=Ly4ifS)n-NK8Kn|(P0(7}$bHbC#C zK25lvF1$LeajDvfY+i8g{be_-;OwdGf9$R@y@f7s47h{bg2NMEVc;uE8Y0z|?ryvc z3!^TmOR^)msScE_`cEW&@u7R`-Dzx_fc)299WpSi+r> z#@p`n?h1cPMqCmh6Az)mvjFcyuW|m7*(-(V(5;sj%S!j6;btZ@KORNHt#@L|P+sAQ7bZEV94QU96GEHryVJ9;Y@7zMVv3)G+G45cBdm}doxc%Q@BjFr*R|qAmrl? zk@RVZNKdff98`|T+jC#cT5m#US2v0sF~LIfKT|q#MMXGsKRj4Qg?8;qL|bA%TK=Lt z)$8p?fBQkyHeQbG)}DeFyRmw89V`lrz9OopHhhhi3ggL&^sbu=+8R#<#8h*}QpT0@ zaT*x^@Uq1JPEp>gL%lG<$xo=P*d>nZ^+9QCj6~vo3Fp`PQ=CpX@*^)}_fco^S{sMZ z)hW=t5=5_*_A`g(4TgAV(AyDpqN2|$)IB$)Q$?~k-ued#$GcM7p4Xz~Y7&BZ7pA-} z3m=zc!^m5U^Q@(q_C1Qv8t!VN7#sR4(u^f*QC$^4fsZwL9vcVlAdn`z&WdjBMWQ^P z;mff%0Ks7~=JJG3dIu(S(e&%Fl za35=S=%ahoA}qgq1$NAI^Gxx>@QbDJe;Wcv=_rg|Q_QX*GfZkGa+sn+_3ND34bzX3 z*k@VupC`$k@StiZ?oyiC(@CRPR1}4Zl-Pb^n%!bV$NiJoz1t?nG4I@D-f)TZoG5W8 z`>!Ys{0ko+H@Y=Ng1WWx)R^W?sS#;niN6AU({(28yKXclIRcyFH0gglns$eISXrS= zlRccd{}qeFOPTG;j;>w(BP4n3gK4s|pf{f%O1`{hS0Cr1egCW$mzmwCddq{nGka3! znBVM+^Pq>kKf7+~iIwJ{09hmM5xo|29hJ~OpUG|mP12b7l)E`cF#WLtO)xFRmzl}1 znQlxEyH;b?h<2QJIf;RJ^=RoXMJr!##FbWQdOA^-p1)5+d5|aH)1FDLnm#}VGqJs2 zWQy@o#Yk&lKguE(QNX>PdeNa8@-F z#Va&%nmGsanlvao-W!kW{-ETQBE3|R!$IGAOf1x=>wf;w>so;>Ja>{Z$;3xhSNhl| z9KXynFs#FgBK|vt4825LwG5!J(o3RO@lLEVm8aK(UP`>@ZO6m;U1@E2sc2M>gVM=b zylm## i2ILVOY8dRyM){{>BG^3%+lIg$2k~U{6(URl#G+(y>SK&gJ`}CmfUKI%H zaG|DjHyX!p^m85(8fJeJN6sw4%)8at=bO*}?M_(tE`?h$XECJvz&zv?GBsIU6eIp3eVX+i`R*bF!LmW6Rq%p2<4Wmm~?|BF>(!kpetFTiTcUQ}UdKt3D4;uiB*{wkW%HMPO;l^uZ#!=fN_%>|*l zn-RA)9!^Cgv1I=;Ead!8Ui>|DS;@S-!%8Tsy@T72UAZS2j1;>X_#bztz+qp-guTxA z%X8IDqr1?j3Imw_`GsYib#)!ho`N3@xEi8Ft6m$?AcRuQ|k-J|J{S{rPq-5BaOMe?qt}Mjo~MD!uWbUO5ZUjDaRXb zmG6<}9|x)8+4!(Qiq>rygOnu%%ZNhWkEEhxY66~RJ;k2D`#3e?1TIeCnF6k%+$0LR ziybhxn;qrz-_L$?f{^l~am%BSZ4(B=YZjD|xeM{`fn>+L)w4Hta;8Dhv1CE#q!W1; zGo0#syHK4%G>*odhT^3+$o+Fz5YLNO1~=fn)^qWoIs>OoA7h~VD~ZzgyArj~YY2I! zE)OjXLZ>%POug55<)BrSu~mvr8f~vIj9dgXHIF!)~kr9jw|W z8r2@)*NIRVD!GauX*F=@F$XbAJ;d97caZ9=gI@uE;qN(85|Hwhc^?W?s&7@`J>whx zy#9vyzA+Nd2lw%JpDD>k*;4kzFR=Y!O4cUE)TQJpbL4wcPZLi%!*dB$uO^W@`Yev~ zzq3;Yn}vlpyFd$)q4c{`WL_^u#L`RLsgA?*57i=U)eU~%b8p+hkQr5H;XivlsvB>L z51ccUZ{_odc?>2UdxcCm=N4vizBl8Bn8&kug?xd5<)2b5Rz0iT2i=LICKqU<}366p2b4bGmy_WUCt8k#Oo=8=z&=Wb9K)$KkT>Y ztd*0z;|ylbU|B31*H>s?XhPgtJ@konDTuFqg5($v@#6J6@$c(KN#*1RBFb736?4By zRy_`sSZ&b2g%7hu?4mn(`@0{N^*DtY|MIXd#hX6Xr6c@PI(wamFdsJw^KX1aO@t!% z#8vPkrwT6dyx%Z0#XkcX`qeH+Xi-A&g3}n)&Y9EhX$UFK!t$T$3~IlGw?_`+%WPvh zZ*d<>DTdJIB;29my&96kQAogEg#C~AiWeHn8C?>XC9zgiRWr#_2a^Hu0dvl_$* zPyA_9BK>kZgeFxn`+)hqftt+gl%`v%?$mXZ3_V`_6R8Sr^r2LZ25?sBuBj_6kElZ3 zM|s+MQH9*Py~5Qmij-E&PQ{kH*!xJ8jaCdFTabdXzw$5 zl5LqRe&q*{mZvl=@NDJ&hcC~g^=X*bKv5a*OC>KnC{4?XYU|mBzJc8|Up$zN=)u{X z0J6+9qk!l@nq>5ynf#X#!hWlVhpO?|;~D0p@GMxr5;Ds!;B0yWETkHdZ`zfvS)W3; zx4$9jElpO|TR`zVtL>#kDPdWN8*EK-qd;pzlHp!p&ga4q@=raB^RL|KZnYq1sX|y! zIEXN3Cn_GGMxTq$<9L`KJ?_w=%sVIWVXGtUPwm77hi#bBtB761dNhGEM#^EFKg(68 zFFFfxSm7DuV@xTy#{o=#5H1pP&B#Q54=z03C`tr6N$lF5zBkGR<%O$Cnp-S$Enb!Yt1VqAXtf(T8U;Z%b`+HImSAwoO2{o6gH=uaaHGZxrcT>&_`n*x z-cW#*KLhAC_uF(rt|Drupo_id;wiHhw&*h(S2*I>^v!&?{fd<}FY&&FyQ0g!U{2Uw z7rVKD*dA z=IM;E6)6ZAUWEK(laO5-gT*}y;aB3%j-zb6hPbk*Ia>ymyQbXzaNJtT^H&(_k|q$hC`kT z5z^Ix*50rXqS>7sBmBr%5+x?`OlWBq?**>^C&GUBq&#s*BB{NJXOHx;%-gGA=IW=& z5AKFu;~q;+rx&4XRjOEaOrAN5sp898X?ouI3%mN&iOR=Hyzh{s-+|A>wP&BOzPlQY z({iG>Z>31%y&8RG=1J6U1=^abL&te8RF?J)Kh3s@Y<3H-H2#P+2F2plpi~SvQi7J7 zBP0iy59)0E2BqisahF>c-5=!O;lxdtaoY`J8c)J~`g-=|sKZF9456Qi43AI5@x^Xf zJ1CeO)(RA+Pr|yUK-zj_CpwqgAoX94xYk>d&K&v=?~infOWC^IHC+Z*i}^zDM_2A< z3}8l>D}{b(M!ysKi#7*$mMJtDalnw@vo)Cyao=ZZ_e zm|1yum}I)-Bt9(7#>5|51ulKAF*hz97lM|Ei_FL9JYJ5q7qscibp<4iZ^p1A+|Tv; z$=&)gSf6I5Oi35K34Vadi+a$*D$bKHe}4nyC53Z5-4l_`m zEhw?(3!3#;b2cE9WOv9>KZ{6o;~h~?=|Rl+k;aJ&QdGFc2R8No7^bU8HGZ=oYxztJ z&Qzt5C)gQu;1h1xICAGmiP_#CapQwEbxN%0tJHUP%3F~$_mA2OW$4WlC0dXc!euA-iCBGjW zcwYRFcSKz{XE;>+&mXX)zYL9t8X$5{zDJX2gI|}k0mapw@SLYi@>8N<`pj5T-PDJq zlOoY4ZdAdRt-aZ2%^CE2PyPFq4Wh)zPLch%4P|rfsrpJU_$MjzIqXO;H$M{ZZggUn zy&2D~z3371C@*GkcWQ<^Jy<>qE6$kHrDT8Fmva~iO`NZhlcnZers7p^H@d@aliowd zi?+4yB>kOpXQdfpp}sFIgbyvX?n(dSzZsUg(Vr4e8f4=|?)!*VL|M`^cAB|rR-#cV z8H<**!E@|w?4Hfe=!b7H_Qo@K3@t%ylsu_Ec?7ja&d-lI3h%k~s5z%XD=U)Wn)(8n zAGlZeZ3|x2+EVBVPcqte9kmO+$t1y#PvBxb~={}T=ew8?D~$Ar5LuDwAD%b;`yNm?-jwoR-9qZ``254Ct!Qz3297F)i$w{!C^^0dnZ2&!M^h5! zm9NBq!){=u`V|BpK8p>9PeI{;5#5Q+#;`+wA+OB2rn#KcsaB$!wd}thl7Z&6ZU1wM zlFW>=;>WKuDE!)$%4RFV!Qvq7Iq&T{c7u2todoSGSz_+L7%|XkD-QhahFtqMBHWwz zH_l(gsct&L?EGP<_zlC2g=3)FQ_%9{dDwd14@0+v(E8FvaQsMc^>(F}EEQ_gc`Vs} z^_Q4jsYBO$9nGJ2MIGxix=@IFp=9OODDkE62#n(GIeYt(y*_85rrRh6Wou&j>=joUGpb9P79O2{{Z*_Q0Bmpfulm%6Z1#NCp*RaleK7-hPVYD<+41~iO& zy~VQ5^i;);4E7I&j^zZL^I3ol&)Mj^c{FF~+;QbZApZF8;7t1(gipJJW7@r`PlqxKqvGGjO_T$G$Zu zN*G;)t6DbnUDclCZ#uD4(~G9wbEeVh<~)0`qIJ(KNqfq4_@|viP|r5#{B#foW;u{E zse}Kh+l*I-6dG- z($4E{$eI)kNeVOLIvYi8XHQI-&;A{qff#wZ59*w?C~kizKFF6rTUwn0Mt;KejE`9G zw>#bE{#!;tAqv05i^7{mWNs2Gnv^C8$pY^1f4d_V^;1MzST}l-7A^+HIFQ}vSz<@I z7p^MfL-VLmusyKMwSl?sZE-zElOAYq!pSm4)K4~}dS=4TDk~6cK4{YVfPS#(!+Vi_%rP}# zZtPAUGC5O)7qc!vX&=b>hdgBl#UMeUNV5K)LO|NFMnttQ7dIn=h2xfb4DY&DOrPGP zpxE>u%5~gD`>aMWTzH7q{ud-I-A$o$FiYr9%@Y*J>;&foNmxcII)VmBCLTPBCvt~y zp<znU^94nEV=_QY-|s?Cb#$q2j-fc6XU{VwX?oXo zQ|x-;KfT|&rWEMMcO|`~ zyO2a2z->J_3U04Q`Lc7^u}+U_R#jkMxjab?i(#)qJ5KTUJkRhj2HfXtCbK<@I7~Sp z&5CAR@up9%x$sOjq+7%Lk^bIXIQDfX<)?o1_DdemDGze~#hwPv)TXt}8G2CaPuZ%LV>92T((_8BJmCWQnai&Hj^&jgz8~aJ3xO$*I`Z_Yh<`qj+%I zS*Vzeho1ZsG~4UZP>WmW{QCu=jqFDhz;+|(ol z&QNAgOuGm=bOASt4Qb$4J=~VxjliKYXulsJp&WKpZ7_oQ*I03B?{=gm^51<#wZC4- zap;cj&HE2#`z3gi@3%QvkkT8!589K2J3O;4havh{U+R~rMSfAyBIo7-*IN)A{ zJw=mY^!PT$sBkZ5_*OjMxf6bCO(|<(JTm+5hVflzN?#F!rf)0ogU>4KE*X&i=0HwQ zd}toCM9SC+e`>NfxpDv9G}n)On3cRb))AA3??Yoh1vVZ?-{qtR8M)B$4NWH1WJw_c;{4GbC(RmnMrAPj2n6tCK4AWm)PcBzGpO~<2h{ScaJipr;G3@Yi@oV5GsB_Qxx4sokiDqW>yKbb}U`NistKeLq zNhRaE(X6WP2XP?EoPZzO3EeqGRb~4}Rl+cbY z#>JS;s4iDPW&qE-SIonfqZ7o@!ZQ3k;3%H>IpP*`1Ae9Ji$Bs!F~Z0Oljo^Q>*Lld(=qA>wOQ@i|9RF&!2~&9|=Id6%??agQFsDB`t^R^Kn?gzI z+h-zu57E4-PGWX%RrC%UM1yO$iX#RkVvB+o?bmvRpWy+bYU2ecH(Ww0Wm4FA`J8h1_lZ^C_Hq*BEU2OHk2Yr?i75BoZ-$tkW1 zv3t@nzuA~Z-zdTIkeBG;XHKZA;qHAsqE2mNmO1DG^Wn9B>_W{+zyIUtECZ^}zAj8i zH%NDPDJ5~w+KK^Uw_^9$-HH-|fQ4d#0iq~if<-DeA_gLgC}IE#7GfacyZ`UU`7jK_ zz4v#|*?X<$p|h(#;+Ic69On(9nJI6@*}(@yU7{N8o7N~Ed{IEbY9)&MY=tQygM|EE z9oko7eNnt2iB`P4Q&! z@Ll!{6P(NK=)Vf)MQv8$*S#C{ZG3^0Ys=uoU5z`-TafBX2)N;fH~hPe_jE+;>?kyP ze1?a%JSGqCMN!+kkijnIZff{aQJOscWVga-K94J|)uEoJl&IJGMELK$E$MF^Bet5c zFY-t4pl>ZZga$vyes6x8pSZq2tgo*W=Sv#-d0|hB6I7rp{SS}JyHOXJByoD|Z?v_T z67e42E^>yo4v7f9>eWZ1VgfU~x~ zG<$?GP5v587izpo?XNpInX)TBX_&-rp&Q?8-FW7-S&SMap&t$ow0g_h!kO70vD^Fv z_Kjv<)xfv7V0IFtNsSz}D>1J7A$W}7K62?7>|XZ^<0hwI-$f-Li#r|( z2=?ktY1)RAV7?pE@&o9rBKOJ~7Q<1I*`>2gsQ<17_|P>2=YFY^_lDlcd{~BbcMCel zXEY1NOfi%*B24SYIo%~9g!dQr15Ux;^1gTq2eR&r%`l z0wem)&%%*vKM-JWA61>DShmTPvYi@mZfqtJ`Te3Ua}goSb8%z61GTStBc54CqqAEY`ACuJ9gBVCHXBaffn-$c8)!Zyu?V@@;Rq|`3cBB-G+V3 z&T?ly4LT8np%DB6R;^~Vq;EbR|9TJKP<93IUO?wbGtND=r?kwo==J&^bgu9W{mU;g zCxvr-YU2Vc<^F}`z5WmhW7%OIMyCr@D1L2;BG7~Gbj`avwrJ_mTNPQFKi3Eo-ZjBWh-tFBov8 z0^{caQ+|AdJnvD1rZ%zr;+61K;y!!YSDgJ-D&i06lhfih9N1wCzZ~wE&YFhvq(L4| z-c4bGC8lig^;tKG2GNn?Hr zPa{ljBuTn(7UY&rCw|-fE{d8eO-Js2#xNBxVf64F{{GdYO&gp@>(~$M3;8bAUeyfx5uPl%-Lk~^R{f$2g|&qvUTEOsjp1V1yHfPf`5>`fdKjBy%5cT- zb&+9f4hn~4!)1S8aqrq`?9*sL*MsU5ZEKA2lip#>5KFpWYk{v_<>>7rHF{bogGu5M zQm0zcp`XR5Z+e0uw{+=A*%`#Ye25=E&FE;ybCxCiO0T*+7)4Y>17^(F@{CKIxT`7O=DHn;&i}iRn+b+D7Y;h=Al}cljDarmLY-3z# zy7~|14K-kqT1rb)dg4Cf!h?M|3wcT%UnrJsm9kQ5~v1R(G=_V z*fK_iUkl#fFZ}@LXEX6AejK(uYe0PcK-ko6M#RnU7%|%tXGXEpNK%dWNiJZDNMQ70 zSt?Rcp|W8^i(Sts(@{H3@=DhfDgV^zoC9ZJ^pns-t5I^-bAvGPJPyNp6m9=;Sq%TT z05Wwa{r4U@BC0Q66}8Ga&g`*+6RAubb%px+9+Z1+~3Tg=KJXtKl-p{Cyor$ zq`T})k&EXZK09NlurK}I>KzDvV?r(+YP3V6C%tG4px}2hyxST;H)OqOS&k8f>FC+}a4|0ht)vMVfTX>acg< zact)~`>mN(=sROK#%BNI`N(I)8UKNn(P4CJm!bVvx{%Aib6D!Y{7imbR&R+x-3*{=fOyB$Ov*U}8V!MNZ#|rF$~P??^{# zbGd~4-q}K#-LTJQ?ZBJs6GdICKV|biH6_*<%ieV&?7RUf*lR%PQ6r9bccv-TrQ+}g zCFV}v!{IO2G5fnaU66f-FcWqmOz2LjRj1HC;wGNPIMC?&2H~C?38@MhT5z#QbQ(lr z_`iBgS+P!h>>ULe1y$NzVNMy`v2M1uq$O7Nlr_kTY%*P`beAc;=kFffCIh;cX+s9x zGobgwj|R^7rrKeLQKKA0Pp_Mk&)EVfT6nOFBnKl4*5Tx&$C#aZ3EszcvD^A0R;%nm zcfBDvz`yJ3?q(F8a}I7x-XfBDv`$W^{{NnQ{*WexyJWz^{u6iIRoJPji2WaUceq)F zoD9u5Pre0>`8=gJLdV3!a+@P{^>6g)jtVMV@=9kVjxNf z3vsu^mOGv8iP3+e6Dm+!|^3% zRQ#Cx4v&agefp$f*ajC359BX)qOdYKDm-R{Sa<&H?8t|0kT)I~zsBT0e2#Pg_lG`0 z_rybdn$*A<{>S0~-!D!q|Ayg)r^I?sL&`S#g>p-MJYpVdQU6)ks-(&8wQdw}GY0dR z{Sa;eQn63KUOmp-IXY0$@5jthHKX@hHk82ZmWO4Iw6}?K0DC{6Qek)^$#)9f3SU5CdLewrqdfa6EZ&nsXO06;W{&8pBPiyOKDo1c|^3$ zv?cG$>>|vYDIDzesDN5IPiii%YI@Md-^nnWw2!?JuB3b6Br=^M*lEhyoY(tsdq^%0 zz2WT3*sZX=tBMN$htNB66r;uwQXbT!MzRs7HuZoc_W~|E=fh{|F4&DMM}WyA>`qBV zlI9_7;w-fFibQ5OkAuQQN17$K5>XL-aLUP#MoYzF$;Nr;8D~qAD>>tl=SrvddDEgY ze{?$gQ|ucj8v0EF)1Tb)JJ5r)d=_A@btZKu*N5QLyFlp8GHW_l$wo5dbTXZn%qbIL3{G!vr&9WKAx1A(&Rs%u)d=P!5+z? zGs}q%ef1|TX=P-lT9Qsk4@w`KETr7LsX?kQsi`wVHPcx546vem%sqPg-*NH!AM@tr z{i(gmN^)1nfV1ld#XW~I4CwYtJoifzqh?=3W65^$@nO5EU!Gn%o+?ueuy zU51{Vk|sHI)#8aAzfgJo9ad+DiJ8XDn4Q&?E}A$~Ejuw{Mp)48Ja_uKy%y_#=};tR z;&af5;Y+fF!QA~QVgFyn%u42H72tXAb2!=mhUDkj-B3Dl6mL$h=RD9gank<|cFkOd zcp5A=4}OJgqXFpcR4qa$UxM~7ADZ~t3m;AFn04$;bCchQPnJ&nUD<=ats98qsS_|{ z#u6bl(vWIi1|sVtclSdzs6|T+^8MZmuLb&)S3eHNf{A?gcA*#F(~x93kWROBAd$Zd zK7{t6Qs!9~PL0RW;$ZPN_Eq2(QHK$8;>6NzF5>QiHlB&)ib_qz;;UO9;ZLrqMCQ;R z(L%ZQ?+CC2ERJ%G$6KEU7ZYNFT8!;oKc4rR|KiU+~movM3; z#C};t*H#>d&#%{5`9hth$yvgDnG_j5Riugbs+d*y8g2ZHuAHa^tgXP@3QLM_DZy!_ zN=#5OBZaHA><@YbKRbO2U?0Hm>`uIhh{G>i{@hk-(C?jFk=X~NJ>e^w2XDr^Z=qy= zPjb`y@&H$WiroKqo6BSd4}7F=j_D1|I!qj^+4uV zZi|I>%vLKLNIBh#MO36IlH(-goMDcTt@>10cNI5oOn_XaKIv>ZjU5l2&|?qx0Vj*r&oCtH6KCh6(5&CWe~)sJK0BDQ8q|0O)Xd%^=8$@; z(L&CtO}#LHskEdRlY;0(yO`h)cC=MUR+v;$qwc;pw*VjS1v~d-=$V} zSd+&zJYW!)`i~e>rLJ61W>+ms$_+~DP5}e zryscogb_QUiatna->_foD0ZgkIqXTj_YlX}|ASt+Crz8A&CJjUo&$g#bNXbqHVzjqyU^sBiWL7S0?VTA zp=N~w$sC>y17;_s{!*g0!vpcu>ow|q)yT;>fxVa8g|Cq{tykF%R<&B%g>tsUh!Kl5NVCa@=JeYu_JqjN4`xB9UmPt$&nwc<-mMsfGMuE$o1TG$_fz@%Uj%73@4~n)YSc4jY;o}5D1?pHrr(;XMcGnE(Y^T} zo*!`KJP7}H42)><6*H>puSiLzR`e*Fg6(`ec0q8#867>qqu^njc1wXf=9WE=#UqagttI7m<5Wp4s9FVmkXQ=32h(VAH`&`1h~J z0xfl1+{R39x%aSL)(>$`I^-wQfJi+Xq|9@mP3*1HZK#(N!-^WbK>`heyn&7W|Qo4LsUWf&2Z{$+&-% zD7!vbTq(YTZikr#yU12Fe7nS1XiG}^H&4_i@cd(d9kuekT6McI-j&9oDO8tEP8ooE zekr&;P>VjSG=tH9;gD+MPUz=Ow6s3N2D z9UZ6Df~41$;$iV4TyvUQ{5vWS`?#~XYkG*dr+5{5bBm$hx&;5mTSMj6Rjlbh6jwrm z;P8(#kQf1!uGrNXONNStxd-_zA7RY_~#4un`) z(&`p<`tCaeqXTD2cBK^xQ_E^ZM%apX=e0z@yDB`Hr74EYP{#L{A7QzCub6xGkMP>l zRm3FT6{8w`;98R>xjxZR6!JV}lJsGrWOE#5S{9P~2M*$O=0Pl7*&eIW>#}c>s{fevxq-Csx)kTcbrrifI;Q@WZmFEwG|KX&)O^S{2EV+dRC5nv#{a; z!;B~`_AzP~%M{I<_6|iiq(~!5hEyiKgyk0*>a$3jjP})G{o^+XpD9hAXPd+dcJW#b zeImXLc_Z}7CA4H?cYHokB5tk@Vv$y`{OOxy*3nl%JG`2!`R@xb&ICSLp{_USK|1a6k2+pdcAXz5Jh zSfoWhsnX;edah`EqYBNCXp)c49vH0>l5#IyQLP$*u}j>Fa#!pZVQ(^#dNe}vd)9g3 zG3B?opWJ{JGkYq3w^Qt8_R-!_bK2i*hZN2`96#?w;Tk+2?y(bUb2Z4cs}0$For%^O zbsD_UgN{1v#gk=bq{FW-o@w_u~DPu~;E2y)8u(ZbMy^s6#xt-PM@_KCst!d%HK=BwBA;#kVpDbw!gB9G zTEUxAos%KA{~8SHc;@54{4DQNP;q3x2>WZ@y_R7M_pc?(ibTEHI;>+yk4w0g7-$=V z=g$<$>4YUUT;G zJOD}P#R=v!)EA(6r!V#1l+9iJIJn+t_Zp>QV9F#6&3b?X?CCkCoP^#<`EZTbqdbKY zyjb`i%O;r7;@tCa;LMl$CMELdSAbNVHolAN)7g3N#Ke6^prfeI`BNt}z1xZkZv`qp zRVp@(K8!Mt@8VBRanOu`(Xe^EPpp^HDSW`6^{>;FV6lIr*z|1^>~03*2*O@diEe%^v-`EWTqHVZRIwJqO3GLEj4NOjYy&L zZ-Dq!8_Vb0e&~GqS@?Kt#x@@nY%6UR^20MQv+q#UTeS*ZKN~!9Ga=o8j~JQS1HYMX z^7ElIeLEkD<`m9vP5rMsMt3C8+0ndhtGKN5V1$Xtdr(-H}Hu8q^e&*V0Xcrk%-%D0h-Dd-= zK5H|dAb|AR-@%=IQNiQgS-sbe!OYW~xS6@#i^}jS*NE9c&h&cX2&^{hk7HiDvBbp! zPfpK8r2HYgmrTRYGwkhnITz>3okQYj$j#t z@Sc5J{AWz}`vA9L=@|F=3uLsp`=hl9!xJAP(BcQaZ`GpDZR|>ZdR7>JGp1bbI~`=k z%u-2L8k#LdZx-|xJHy?1rgellxM>*G#g;Tn(xGf!$j^Isx^(3dmZj~%{1=aKH(?HQ z3WMO~{s0eNCPH@IG;FB(ib}Qr;Gf(BCKoQimuIjo(_%3rsR$t_>yg-zfLTdLpnU2E zs;B&i<4DB$684hMPDZl)Y&>G$n9w|hFCStdpYK9<{5B)+oC_(R;;d-z9vE)wOwBPp z=zI5Nm>=&)TbsOS+DdEoer&_!bMoYrzf@d&xF4J6e@A=yZ;8*!Baq?_@ynA>#LgN2 ziEqcb-#r(!>i#_uJzy*DpA4d!*mSX$nHc?&1L!vQ-@pGEj(2MvaF;W3i-%g^MIUu6 zA7nved{&}-v;nf&mDW@xM_mpVb1&7JGF=s@X74HJ7#k7Y{D$%wW%$!2mwS_j6gas` zA{+czWIkmlOaGB#%uzX*+%}{DlifuFYTW5cLX~81mKo`OcA?8zal$=9i<)=#pbsw= zGVhcB9b*@!`YIkScVpkiH^?d;5K(f{^kl*p ztb9}-qn$XD1GtPz` zE-4mOhP#<#bsniVRK&!&$#@%b8qS0GO!E0GOg^7R_JyrTtV|bYxl0-xHVfx%I4b$) z2DZ=Igr^>V#LlbCX3KRHVVRy7XK&0spnH-&=jWrkzz#q6-4BXbuZ%s3-rT`$6^l88 zIp=98y9|9$yHuAn`Ygrsx9wtFt~R|+*1*akLnZyrbfwF7*+|Bin5=sVpQHKO!T{iBQ? z5ULJp6kKe9hq|kUYOyYPup?#oI|YPrrgw{%1|{0QgL1SB*)>bk*nZ!+d*6+!*%=*s zsskHK9mr(j4Iro$wb`0fx8y3;WpIZ+l^rjfeH>EJj37;Y+8b+xthgXLbNjJ~{i((I z0Z>h;9UeGH;Y)ZZB|bPSB+}~iasETRJ>rB9GREwbeuHrn`=arQDs4IZ6vvYvi*wo& zFyi!0{JDEWd`w&lDfZ;-oVc8sIX0Nka1S2t4@A%zMeagM(fawV*e^A>Xys%XatQ81 zzgui24=#Q~e$_KHZPi|bGrH6HHe-s(=FYdX z2N~$P(m{TP2i_;zscOQVB!AARR3Z5K4xDL_<$l){9FgCT`yFx={j&;z9^0@e^#c-P ztDv$(k(_t$!6*5*FpO5A(TnF{GM~|V+bhr+E$%OQ+fm2hoS(C=W_Q@#Leb2Q>ywvq5S02_lmv5LXz>{i{~4DV{f_@8LW#GABHH96+b)1F1yYwjcznrs|hM6 znU}-toNcx*kR1CE**w#pCd2;4wJBKoNS@;BqD4sBaa``xiUY$WVq4ZO{4vs^TAuS4 zNlO#G)aN{e2N`*)kwS(gwH~q{ORay9O4Fdw&(?I-hxf&aMEjX(*~VPu-RlO^xmh;k z=$;0n_F!_ndK!O@>}F0Y`wNC;F?;7QrmOFT?B3IOwt5W?d$H@>n)iiEE+RbaAIe@S zP~`6%^y8WC#ka=HJIaRo0Tq&+Cr^tvhKY5>7f^Sl1?A;w5}&cBp}}YOwRShe&Y0u) z`*n}xo7EFBeP}Ea%C1QQ4DX8?#Q2ZF!0y%PGx;F0#@8dTPzC+Dzp8qr z5sJ%t!)uQg-TnQJnQz?0O}D2rrzi1jR+qDLe1F$rhi#!1rRdpEWAYX#?$@9XGrg#5 zY$xuN>a%018(nUdAwzc~GUHC)tCjU=$nQ$2Z@Q80P%UJS9*&R0|ATq43LMk6BI40h zgq?JRs+9z`cYL89&hF*~5()|Hg2=oUeC*wi{k>nshU6P~@S5m*i5Jp_Z^fASf9T0~ zcp24fNUf8mO@7(P;QUvQc%Dm~yA6{eRMnAdV@3viChqoQ znb^%80^j=WxTbswrs*am&$+q?`+H~_ZcR(q@8`SqLF`XszD2@Pn0GbB^+EU1hrLky zE(gGGTr;e7M&rL9J+ON18ASM9Md$u;@Seh5ZHF54m+6mN+xEcxc`45!y`eYA6XEYY z>5cw=W-%{BDDO*l?qK(EhbgAN@+8CFxp>8X_(AF2sNl{RJT_;aq?ITAJ-iJmRuU>_ z2K^z+VcaP@gY}1{sK(@iWHt9b6Wm)6(3lsrzU~5cDR-fR!G}dwfxV#C>)71cgND3F z7Z#aMF#n!Ab@bg|s5$61&YoxX#nS(|Bm(ildKPrXsMC?>s%XvXiM1Q_>14=obRRMj zONQyt7k^pyDBeP|xi;NPXoS_&M{pcvMru(~G;Ygz=7--9lR6D(o~y1Hm27|#=uyz6 zl%Nj2Pna@w6q%B?5uoXy|gzJ62@;zN<;OGI3-8BJC85NpD3 z@y}Di`pN;4!(VD)Te$Soe|P{8I!N= zMw}Q}D_R!o(y={;=)KU7rrrIEy3=te(GBL$N(-VN?uA!bFZ#Mtj>dgnh?K5BCE*qs zyjOVzo6p&zbml{GOY;+YACeQsbI(ew44)(5#VE0bUFAD7CW(TRgC#x563t!nM91yR zLj9LHvS*)>gk0v%=L89vMx?{2rT`aJ*tM5Y$Xvgp=<}){sh6L|c=o#89HL2)^VE69 zSA&$n7No`e!@U!vX^p7{X~$^de&ty-^s%JGi%pQd@BnkUHyB_010Rm$Ae{4uX^B4?rx1{ip`0~tqDF!=3g&O6D{ zTng~4kjRBEkE%5t^%YvWH7JI!g>>5rTfV>isdXXtiAhNdzX zd|Oow!u{l_m!~?#h1X)B<9mF4&z?N955lxPK;p6Kni!>!CZy|P#s1tlvCrY0SgSZO zsI*EKmmhmlT>Tx~`ofulFAm&&e$KotdCt6aBbOA;`0if`JJb1mc5A`YpAtMRipK7L z%=n8{LZ!VwOdH-J@1+;Ec=V*h+{aYepu%}?UrJQr_v5jy)T-Wx_C{)v)K5jaF`e_z zL9Zk?uf>Qp%QCTIaYtd1$5mzw&crXZz=BG(tzvWSV`06siJ!#|?8k~12AnIt&e`U1 z_FeENyd9^cJ;?ZfJ$1Dm$Wzha-jF9vjN6AbMrO3?peJn&_z%}NvKM%lI`yb03TX_a zZk$btF&;nzGySMM!${p4(B! zh?Kw!&zhl-a026x{)Vi56(*+~z_l>;Jr=*mL7!8QeZj6c=PwAj{0r~Sr=WJvXMAsv zrt_TBEOVD9&1X{NRGAKyn~vmre6d8UG6%NR-6^zdv*fbVEmSTrrvvCGcJw)qHN`v= zljis8UllTXydC%E_oiXY8FlhKfaKBunq(|RZ$p+KrJ@iS>r5zjaSzPkEbxKF+$-{5 zz&Qcl;f&QJ?~){Z?2#p6pSsdsaTepOip7*FJ9>D3CzeP}7xK3PsCuD2olDV#;U#8^ z*gDa`zE?!nDpg9zG9nXsHT3`QCp+Hn<8Ey!e7QGMozTD@j4b3jdyS|yLb*Ny%T}%aX6y&23sP`nMr6!4Sy`D_iA&xm}$kc zI_6ULG9?=?Tl%&|pLQkL(dyizu=4dK|B~*s$^1AL7fVQyowSqp6vFur--(%1?x+yO z&Y1^Dj6aTkb9TY|(FG*gALZF;I6nG5MW4-P6z`aitcMLyn`B2+xkjaA3h3KSN?9dc13PL7RKe?7SlVVMda+wC>blo z|F7r7+fC~+HR6IOa2qRmkdX#IEjMI62*EiiLF?p3U_pTudz`$;f7~F1{20o9?@;<| zs6t^bMUvUW)nL0{g$~$0kc_iGEF3=R(aQH9f}+p3qjgy<{Fe&2=C+9|JPWE^|5ME9 zUMtp5XFuYFao9Xi3QZGLVfEFDu7}9btrQ=g=X52zmz*K^(G6eA+~@%Fkk{H9A-uW? zt4ChIj6y=surIiMHWhp7yW{4$7P!hk!0$s%xU=H5c-LY_`hj2Y*yEb0zN<&Co7-^J z+!0gQnfq+{3{2qMLeB~>+N!+~(FbJMM-tM@rfJ0G-;VH1D4cy`0Ki z{UNrrug#rovM%DdoE?3L;`6Pa4|l!1;ooHwhTRRvoQ?gk@XP?-A*rJH$t?8o+6+0X zN9=j$oHaW!vYBbTrcgqG`Xf+!;UiL+{~vSO3e#*>VczF<9Nhf}>pYI2ZD}WCAOGTc z)>_E-=bj?p1@6r>rc^H(n$o#aq*vS1-$~4!oYh_IGuI>c7M|%3yc5)1;zm78ld-rx z6(f1p{oN=XIn3PLf5DL~@26qk!6@i32Z<84BT7pe@kd*+@b_kBSli-m_&qH5;`~^J z4Gi;hxo=pEfMe{`obeDxA3eZh`2+k+$VBe@dzkWl12SI^gJUCi!^ba&?_Ym*NcdBk z)fUX)_wO6tg}42T#s?)g8lK8b>N$fjKvTl^BNv)yF#_{UT-bZ#N9Ok;VYeX*5dq)P zI>b@Hsu*1^bfGno$3@AYLRe0Ji;iNoAg}&jB5Y6^rgC<0`|nB-JaG>@(){S&rOhJZ z`xSic?@5FDDbXbMwM5S8iR34Sl&aGmrPF+2lftg)d+M;R_Q9q`6W+ylA={*zm=WSg z_096MXX$xNy>3GHIzGc~eHGkpXNs&E=IGz^qXUanaIM{%Ecs{;o?bR8F&{yh;AlJm?a4oL%Ws(-ARtN)`74J2>OHT#O7Y!{TX6 z#K+?ElEjp9JSsIoA~m6-VV5NEs1)6CX4gkqFX65D8e`=Dpv~N**denMZ_EtXLFhzp zPPXBQr3H=i@uu|muVFe!pYo;5DSrMN#7(*+l1?UbUpEJ}&!mO5*acIaOnCOXA&v*+ zBBbvb7)wR)=b}uUzxV)NZOgDfTn+kTt{`jdV9tD3iNdPa{CDIzIRD)a@ec4ux*x4l zP{yCK{&3h5L|#9qU>q#5T{}|9ZDIyTurFdg+QqxcHgq0+pmZ}sc-+#UU7@ZBi6HvY z%2|VoBXHOjLarK`r0%&B7&DMO_1od$mID=5d4{*dN@PaW;KG#{F=}jr$X?%q?gkUZ z@4AknN3WW&_t-4S&<(9(jq|FYmDbgT%|%K$lNcqkZmbf|eNE6|6Ij@JLeUjKEW66LA15i;rgFWI3HP!vk&}ec!oN& zcN1ZgNi^YPGh&}EgYNe}RGux1E#n@GVW*hebcB5yMjjY5L4{Ud^TqSqrJ_faF6G&& zP>6Iha|T@~F-MC+F36BQlrjxt*egU&rzUX&QITOU%FAg?63!g0)>QOZ?yHQOg#2(s`GPDOJH#(v&2knWOC_ z)syN+HVCaLad6KKpoY$q;#FrT9(#VrlyVbFTQdwZ_~$qTJ5l5hUCiZM^AzF?Fchy z$LMcG=pA(&!#U?3I_x2CXl=&soLcVVzQovDvdla@0`pCOaF1P|NveGAh^b>|pfatA z;X8d#O?vj+g|2@%gC}cM>4c3vDXCRpK(;bD4Gp00C(gic*JjSb`jY2pJ$f?s5IlW6 z=!bzSg?-wI$wvHK>19II-{N3(;|88&FpqXa3>@B7pmBvdU2$2B{XXnbd;14dmK3u; zvq|EykXhMT+*|QV6F$qG>5($;h2(Wa@mC4|J=ldNcTcG0JY0^Q|q zeqe2?*qEz9(cvHQR{jlV6F~O{e?o=kXZ*)b*2^k&(AsvBXFj08*Y}H0d-ia)unW2S zWQbGFN7;$;5$X$~CBwp#u*pM_uCL_)%7d=d?CwZz_0F`Tw>h2uXu)$hJ(_vcly-BM z$#jGR4L3iC-?6Ph$=uCr*t-+P3eA!oPaK)sd>Q$+*9+^-ZozTQ6dYW09_@7}VA~Xi zRqP`U-*g{_Jg2_%Y(G*;bV%AV3x9&XLGG9w4Xru^+it(`d^qn!t{j0`qAdL~`3mcj zks`~W7_YsUWxih`Hl^ml$D$p62P=er!x^k=*ckM1xh^_gHe!I1X7RBJkAS!uum_L(%CyuCi_FumhXjJ zDMQNZ_P)^K!gL7!J=;fq3p6xs7gi_xv0LziSWu|Od7TKnY)lvSFN|>5yC3`pC}M?9 znmBYz4}VVi(D?B0?4}LyE$!xXD5o5eLng9kS(g^|;XZrmC|D^u)6I@?un#h# z`6a&O{WuyY##mFUGjm*ya2L*6kD{jaBuBn?1>M_*r^_qN zk!H?2h%uiE@%Y>g_?HbsySfYle^eklGZe=@6BN_9x3F>(R{Zx%BwkC0-P>HuaqW(_ zsYTGpEkUluK)ej)`PPnGn0-TnTTfmJ`>}51_-Y@9J(Y;h+jtg~n1;E%6mVsQ56$In zb>&4re%}kKD?ALJ0=`Qa1k%b!N1^>OhPsC^c9IV{nN+q(I+{G_N>L^#f%Uu*(b&f-yckC6b6exH= zf7m=(ihpu?w5NU&vVu0^a<~df^J2hs#TabQ(xCvUR_u6t8*?~2=xhHM2l`z`UU66Q z;l9VC#}D9pBS)O#UEX8PANyYYC(`9DXsvm4;Z@%g;^iV2(i)m5Vy6bsmgP;7A%#A) z?ssqID<+GHHthCP_M$l-r-&b)oM^+~e9=DtJ}lNoiIuB&h^6hfvA@AoGRGlL4C4H% zpKg{|zm8}B>D`5QHx+un;unhZi$z%%9h(39JI4L&E6Vvi9Hps9G5npd(m|Tuw5rmQ zCPnI>{{@xI4tjJ~o3<6GvF9jKn9a?Dtpc+EANWhue;?!S(`l%6Rtx#>1t^R-flJ%w z;~wt=58f}p)$CcYmG^}D`==OL6^5FD9(Zb#g%r8#60fMW5DSZilhF{7F={^?lR2IS^N<_sh?4dP}tWU%X9`&zhNIm_GlZtvE@uh^illXR~59mon6$4 zDQL}fFP8nu?Am2JQ2%F!P&l8%Ge*wKc(U*Op*zoIUZH0lyQ(yT*u5!5(+_o}`MLqf zm~#rzE9~gF-g~$$u3(Nd&-zwy?l>k5r|%e1gE8;pe7@rS{Y^;L?M+FQAMt|ElzT3Z zpa~fY)Y-|qpG|#8I(jsEn%s_p z=xoWs*;;ha;R{R#pOGx?ZcQIHF=Kl8gCgP?PxORNERbbCR#GLphu8D+SC&FHy~M>* zdGa`_L1*JXiP%083K)4ud^>Yj$oCB)&sEVPX=|Mr^C_4eRr>6k^rTRO59}WbLHzJQ zD$DxEOwKvz^3RD-_!sH#?UA@Q9NUlF$0}(X^gg@-i;wc2{?J#EX=Z{;x;Id7X$6z< z(dkDQ>8q<>|dD``i#H4C2L#c7=D-`Y(__L5{mUN!XM52rs5u()(57cfPPlsT4u*)Xq|49Td$b>;@|fF_6^;KMFA^#9 z>}X9z8U}4I5L0Tb=<=rod>^kT{+H+J7+_9=cI)7ovNSD|v7#(%O%!>&!aH?M>U=7L zniEpw-EtCf4tEey*PVEQ9^2tI}UY4PL?FC}v z!!=0sd51sG+a#0cu0@S%7aIE7f^tvkkaeRW-Rsn$b9si8Ue2?YW;=RT$n3eZ=9Hc3 zNH%+q;^KTC8p+O6`$2rZahK3E?u